Skip to content

Commit 4b0b795

Browse files
authored
server: Implement packet compression (#380)
* server: Implement packet compression This should resolve a number of performance issues in debug mode. * server: Add option to use zlib-ng
1 parent f95dc0f commit 4b0b795

8 files changed

Lines changed: 127 additions & 10 deletions

File tree

Cargo.lock

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

feather/protocol/src/codec.rs

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ use cfb8::{
55
stream_cipher::{NewStreamCipher, StreamCipher},
66
Cfb8,
77
};
8-
use std::io::Cursor;
8+
use flate2::{
9+
bufread::{ZlibDecoder, ZlibEncoder},
10+
Compression,
11+
};
12+
use std::io::{Cursor, Read};
913

1014
type AesCfb8 = Cfb8<Aes128>;
1115
pub type CompressionThreshold = usize;
@@ -24,8 +28,10 @@ pub struct MinecraftCodec {
2428

2529
/// A buffer of received bytes.
2630
received_buf: BytesMut,
27-
/// Auxilary buffer for use with compression.
31+
/// Auxilary buffer.
2832
staging_buf: Vec<u8>,
33+
/// Another auxilary buffer.
34+
compression_target: Vec<u8>,
2935
}
3036

3137
impl MinecraftCodec {
@@ -56,6 +62,7 @@ impl MinecraftCodec {
5662
compression: self.compression,
5763
received_buf: BytesMut::new(),
5864
staging_buf: Vec::new(),
65+
compression_target: Vec::new(),
5966
}
6067
}
6168

@@ -76,8 +83,38 @@ impl MinecraftCodec {
7683
self.staging_buf.clear();
7784
}
7885

79-
fn encode_compressed(&mut self, _output: &mut Vec<u8>, _threshold: CompressionThreshold) {
80-
todo!()
86+
fn encode_compressed(&mut self, output: &mut Vec<u8>, threshold: CompressionThreshold) {
87+
let (data_length, data) = if self.staging_buf.len() >= threshold {
88+
self.data_compressed()
89+
} else {
90+
self.data_uncompressed()
91+
};
92+
93+
const MAX_VAR_INT_LENGTH: usize = 5;
94+
let mut buf = [0u8; MAX_VAR_INT_LENGTH];
95+
let mut data_length_bytes = Cursor::new(&mut buf[..]);
96+
VarInt(data_length as i32)
97+
.write_to(&mut data_length_bytes)
98+
.unwrap();
99+
100+
let packet_length = data_length_bytes.position() as usize + data.len();
101+
VarInt(packet_length as i32).write(output, ProtocolVersion::V1_16_2);
102+
VarInt(data_length as i32).write(output, ProtocolVersion::V1_16_2);
103+
output.extend_from_slice(data);
104+
105+
self.compression_target.clear();
106+
}
107+
108+
fn data_compressed(&mut self) -> (usize, &[u8]) {
109+
let mut encoder = ZlibEncoder::new(self.staging_buf.as_slice(), Compression::default());
110+
encoder
111+
.read_to_end(&mut self.compression_target)
112+
.expect("compression failed");
113+
(self.staging_buf.len(), self.compression_target.as_slice())
114+
}
115+
116+
fn data_uncompressed(&mut self) -> (usize, &[u8]) {
117+
(0, self.staging_buf.as_slice())
81118
}
82119

83120
fn encode_uncompressed(&mut self, output: &mut Vec<u8>) {
@@ -114,11 +151,23 @@ impl MinecraftCodec {
114151
&self.received_buf
115152
[length_field_length..length_field_length + length.0 as usize],
116153
);
154+
155+
if self.compression.is_some() {
156+
let data_length = VarInt::read(&mut cursor, ProtocolVersion::V1_16_2)?;
157+
if data_length.0 != 0 {
158+
let mut decoder =
159+
ZlibDecoder::new(&cursor.get_ref()[cursor.position() as usize..]);
160+
decoder.read_to_end(&mut self.compression_target)?;
161+
cursor = Cursor::new(&self.compression_target);
162+
}
163+
}
164+
117165
let packet = T::read(&mut cursor, ProtocolVersion::V1_16_2)?;
118166

119167
let bytes_read = cursor.position() as usize + length_field_length;
120168
self.received_buf = self.received_buf.split_off(bytes_read);
121169

170+
self.compression_target.clear();
122171
Some(packet)
123172
} else {
124173
None

feather/protocol/src/io.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::{
1313
borrow::Cow,
1414
collections::BTreeMap,
1515
convert::{TryFrom, TryInto},
16-
io::{Cursor, Read},
16+
io::{self, Cursor, Read, Write},
1717
iter,
1818
num::TryFromIntError,
1919
};
@@ -204,8 +204,8 @@ impl From<i32> for VarInt {
204204
}
205205
}
206206

207-
impl Writeable for VarInt {
208-
fn write(&self, buffer: &mut Vec<u8>, _version: ProtocolVersion) {
207+
impl VarInt {
208+
pub fn write_to(&self, mut writer: impl Write) -> io::Result<()> {
209209
let mut x = self.0 as u32;
210210
loop {
211211
let mut temp = (x & 0b0111_1111) as u8;
@@ -214,12 +214,19 @@ impl Writeable for VarInt {
214214
temp |= 0b1000_0000;
215215
}
216216

217-
buffer.write_u8(temp).unwrap();
217+
writer.write_all(&[temp])?;
218218

219219
if x == 0 {
220220
break;
221221
}
222222
}
223+
Ok(())
224+
}
225+
}
226+
227+
impl Writeable for VarInt {
228+
fn write(&self, buffer: &mut Vec<u8>, _version: ProtocolVersion) {
229+
self.write_to(buffer).expect("write to Vec failed");
223230
}
224231
}
225232

feather/server/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ common = { path = "../common", package = "feather-common" }
2323
crossbeam-utils = "0.8"
2424
ecs = { path = "../ecs", package = "feather-ecs" }
2525
fern = "0.6"
26+
flate2 = "1"
2627
flume = "0.10"
2728
futures-lite = "1"
2829
hematite-nbt = { git = "https://github.com/PistonDevelopers/hematite_nbt" }
@@ -52,6 +53,9 @@ libcraft-core = { path = "../../libcraft/core" }
5253
[features]
5354
default = [ "plugin-cranelift" ]
5455

56+
# Use zlib-ng for faster compression. Requires CMake.
57+
zlib-ng = [ "flate2/zlib-ng-compat" ]
58+
5559
# Use Cranelift to JIT-compile plugins. Pure Rust
5660
# but produces slower code than LLVM.
5761
plugin-cranelift = [ "plugin-host/cranelift" ]

feather/server/src/config.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ impl Config {
4747
} else {
4848
self.server.online_mode
4949
},
50+
compression_threshold: if self.network.compression_threshold <= 0 {
51+
None
52+
} else {
53+
Some(self.network.compression_threshold as usize)
54+
},
5055
view_distance: self.server.view_distance,
5156
max_players: self.server.max_players,
5257
default_gamemode: self.server.default_gamemode,
@@ -64,7 +69,7 @@ impl Config {
6469
pub struct Network {
6570
pub address: Ipv4Addr,
6671
pub port: u16,
67-
pub compression_threshold: u32,
72+
pub compression_threshold: i32,
6873
}
6974

7075
#[derive(Debug, Deserialize)]

feather/server/src/connection_worker.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,15 @@ impl Worker {
113113
pub fn enable_compression(&mut self, threshold: usize) {
114114
self.reader.codec.enable_compression(threshold);
115115
self.writer.codec.enable_compression(threshold);
116+
117+
log::debug!("Enabled compression");
116118
}
117119

118120
pub fn enable_encryption(&mut self, key: CryptKey) {
119121
self.reader.codec.enable_encryption(key);
120122
self.writer.codec.enable_encryption(key);
123+
124+
log::debug!("Enabled encryption");
121125
}
122126

123127
pub async fn read<P: Readable>(&mut self) -> anyhow::Result<P> {

feather/server/src/initial_handler.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ use protocol::{
1111
codec::CryptKey,
1212
packets::{
1313
client::{HandshakeState, Ping},
14-
server::{DisconnectLogin, EncryptionRequest, LoginSuccess, Pong, Response},
14+
server::{
15+
DisconnectLogin, EncryptionRequest, LoginSuccess, Pong, Response, SetCompression,
16+
},
1517
},
1618
ClientHandshakePacket, ClientLoginPacket, ClientPlayPacket, ClientStatusPacket,
1719
ServerLoginPacket, ServerPlayPacket, ServerStatusPacket,
@@ -292,6 +294,8 @@ async fn finish_login(
292294
worker: &mut Worker,
293295
response: AuthResponse,
294296
) -> anyhow::Result<InitialHandling> {
297+
enable_compression(worker).await?;
298+
295299
let success = LoginSuccess {
296300
uuid: response.id,
297301
username: response.name.clone(),
@@ -310,3 +314,14 @@ async fn finish_login(
310314
log::debug!("Completed initial handling for {}", new_player.username);
311315
Ok(InitialHandling::Join(new_player))
312316
}
317+
318+
async fn enable_compression(worker: &mut Worker) -> anyhow::Result<()> {
319+
if let Some(threshold) = worker.options().compression_threshold {
320+
let packet = ServerLoginPacket::SetCompression(SetCompression {
321+
threshold: threshold as i32,
322+
});
323+
worker.write(&packet).await?;
324+
worker.enable_compression(threshold);
325+
}
326+
Ok(())
327+
}

feather/server/src/options.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ pub struct Options {
3232
pub proxy_mode: Option<ProxyMode>,
3333
// HMAC key used with Velocity IP forwarding.
3434
pub velocity_secret: String,
35+
36+
/// Packet size threshold at which to compress data
37+
pub compression_threshold: Option<usize>,
3538
}
3639

3740
#[derive(Debug, Copy, Clone, PartialEq, Eq)]

0 commit comments

Comments
 (0)