feat(packet): implement zstd compression and refactor packet decoding

This commit is contained in:
2026-06-26 20:20:34 +08:00
parent c7c6ee9a61
commit d31ec68f3c
4 changed files with 125 additions and 38 deletions

View File

@@ -1,17 +1,20 @@
#pragma once #pragma once
#include "Cubed/tools/compression.hpp"
#include "packet.pb.h" // IWYU pragma: keep #include "packet.pb.h" // IWYU pragma: keep
#include <array> #include <concepts>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include <netinet/in.h> #include <netinet/in.h>
#include <span>
#include <stdexcept> #include <stdexcept>
#include <type_traits> #include <type_traits>
#include <utility> #include <utility>
namespace Cubed { namespace Cubed {
constexpr int HEADER_LEN = 12; constexpr size_t HEADER_LEN =
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t) + sizeof(uint32_t);
constexpr size_t PACKET_COMPRESSION_THRESHOLD = 100;
using Packet = std::shared_ptr<std::vector<uint8_t>>; using Packet = std::shared_ptr<std::vector<uint8_t>>;
enum class CompressType : uint16_t { enum class CompressType : uint16_t {
NONE = 0, NONE = 0,
@@ -27,17 +30,17 @@ inline CompressType get_compress_type(uint16_t id) {
case to(ZSTD): case to(ZSTD):
return ZSTD; return ZSTD;
} }
throw std::runtime_error(std::format("Unkown CompressType {}", id)); throw std::runtime_error(std::format("Unknown CompressType {}", id));
} }
struct PacketHeader { struct PacketHeader {
uint16_t cmd; uint16_t cmd{};
CompressType compress_type; // 0=none 1=zlib CompressType compress_type{}; // 0=none 1=zlib
uint32_t uncompressed_size; uint32_t uncompressed_size{};
uint32_t compressed_size; uint32_t compressed_size{};
}; };
enum class PacketEnum { enum class PacketEnum : uint16_t {
LOGIN_REQ = 1001, LOGIN_REQ = 1001,
LOGIN_RSP = 1002, LOGIN_RSP = 1002,
LOGOUT_REQ = 1003, LOGOUT_REQ = 1003,
@@ -91,20 +94,40 @@ template <typename T> constexpr uint16_t get_packet_id() {
} else if constexpr (is_same_v<U, Pong>) { } else if constexpr (is_same_v<U, Pong>) {
return to_num(PONG); return to_num(PONG);
} else { } else {
static_assert(always_false<U>::value, "Unkonw Type"); static_assert(always_false<U>::value, "Unknown Type");
} }
} }
template <typename T> Packet make_packet(const T& msg) { template <typename T>
requires std::derived_from<T, google::protobuf::Message>
Packet make_packet(const T& msg) {
PacketHeader header{}; PacketHeader header{};
header.cmd = get_packet_id<T>(); header.cmd = get_packet_id<T>();
uint32_t size = static_cast<uint32_t>(msg.ByteSizeLong()); uint32_t raw_size = static_cast<uint32_t>(msg.ByteSizeLong());
header.uncompressed_size = size; std::vector<uint8_t> raw(raw_size);
header.compressed_size = size;
header.compress_type = CompressType::NONE;
auto packet = std::make_shared<std::vector<uint8_t>>( if (!msg.SerializeToArray(raw.data(), raw_size)) {
HEADER_LEN + header.compressed_size); return {};
}
std::vector<uint8_t> payload;
if (raw_size >= PACKET_COMPRESSION_THRESHOLD) {
std::vector<uint8_t> compressed = compress_data(raw);
if (compressed.size() < raw.size()) {
payload = std::move(compressed);
header.compress_type = CompressType::ZSTD;
} else {
payload = std::move(raw);
header.compress_type = CompressType::NONE;
}
} else {
payload = std::move(raw);
header.compress_type = CompressType::NONE;
}
header.uncompressed_size = raw_size;
header.compressed_size = static_cast<uint32_t>(payload.size());
auto packet =
std::make_shared<std::vector<uint8_t>>(HEADER_LEN + payload.size());
uint16_t cmd_net = htons(header.cmd); uint16_t cmd_net = htons(header.cmd);
uint16_t compress_type_net = uint16_t compress_type_net =
@@ -120,16 +143,14 @@ template <typename T> Packet make_packet(const T& msg) {
sizeof(uncompressed_size_net)); sizeof(uncompressed_size_net));
std::memcpy(packet->data() + 8, &compressed_size_net, std::memcpy(packet->data() + 8, &compressed_size_net,
sizeof(compressed_size_net)); sizeof(compressed_size_net));
if (!msg.SerializeToArray(packet->data() + HEADER_LEN, std::memcpy(packet->data() + HEADER_LEN, payload.data(), payload.size());
static_cast<int>(size))) {
return {};
}
return packet; return packet;
} }
inline PacketHeader inline PacketHeader decode_packet_header(std::span<const uint8_t> header) {
decode_packet_header(const std::array<char, HEADER_LEN>& header) { if (header.size() < HEADER_LEN)
throw std::runtime_error("Invalid header");
uint16_t cmd_net; uint16_t cmd_net;
uint16_t compress_type_net; uint16_t compress_type_net;
uint32_t uncompressed_size_net; uint32_t uncompressed_size_net;
@@ -145,5 +166,31 @@ decode_packet_header(const std::array<char, HEADER_LEN>& header) {
return {ntohs(cmd_net), get_compress_type(ntohs(compress_type_net)), return {ntohs(cmd_net), get_compress_type(ntohs(compress_type_net)),
ntohl(uncompressed_size_net), ntohl(compressed_size_net)}; ntohl(uncompressed_size_net), ntohl(compressed_size_net)};
} }
template <typename T>
requires std::derived_from<T, google::protobuf::Message>
bool decode_packet(T& message, std::span<const uint8_t> data,
const PacketHeader& header) {
if (data.size() != header.compressed_size) {
return false;
}
if (header.compress_type == CompressType::NONE &&
header.uncompressed_size != header.compressed_size) {
return false;
}
switch (header.compress_type) {
case CompressType::NONE: {
return message.ParseFromArray(
data.data(), static_cast<int>(header.uncompressed_size));
}
case CompressType::ZSTD: {
auto raw = decompress_data(data, header.uncompressed_size);
return message.ParseFromArray(raw.data(), static_cast<int>(raw.size()));
}
default:
return false;
}
}
} // namespace Cubed } // namespace Cubed

View File

@@ -0,0 +1,40 @@
#pragma once
#include <cstdint>
#include <format>
#include <span>
#include <stdexcept>
#include <vector>
#include <zstd.h>
namespace Cubed {
constexpr int DEFAULT_ZSTD_LEVEL = 3;
inline std::vector<uint8_t> compress_data(std::span<const uint8_t> data) {
size_t max_size = ZSTD_compressBound(data.size());
std::vector<uint8_t> compressed_data(max_size);
size_t compressed_bytes =
ZSTD_compress(compressed_data.data(), max_size, data.data(),
data.size(), DEFAULT_ZSTD_LEVEL);
if (ZSTD_isError(compressed_bytes)) {
throw std::runtime_error(std::format(
"Compress Fail {}", ZSTD_getErrorName(compressed_bytes)));
}
compressed_data.resize(compressed_bytes);
return compressed_data;
}
inline std::vector<uint8_t> decompress_data(std::span<const uint8_t> data,
uint32_t original_size) {
std::vector<uint8_t> decompressed_data(original_size);
size_t decompressed_bytes = ZSTD_decompress(
decompressed_data.data(), original_size, data.data(), data.size());
if (ZSTD_isError(decompressed_bytes)) {
throw std::runtime_error(std::format(
"Decompress Fail {}", ZSTD_getErrorName(decompressed_bytes)));
}
if (decompressed_bytes != original_size) {
throw std::runtime_error("Unexpected decompressed size");
}
return decompressed_data;
}
} // namespace Cubed

View File

@@ -45,7 +45,7 @@ asio::awaitable<void> NetworkClient::connect(std::string ip, int port) {
asio::awaitable<void> NetworkClient::read_loop() { asio::awaitable<void> NetworkClient::read_loop() {
try { try {
while (true) { while (true) {
std::array<char, HEADER_LEN> header_buffer; std::array<uint8_t, HEADER_LEN> header_buffer;
co_await asio::async_read(m_socket, asio::buffer(header_buffer), co_await asio::async_read(m_socket, asio::buffer(header_buffer),
asio::use_awaitable); asio::use_awaitable);
auto header = decode_packet_header(header_buffer); auto header = decode_packet_header(header_buffer);
@@ -66,7 +66,7 @@ asio::awaitable<void> NetworkClient::read_loop() {
case to_num(PacketEnum::LOGIN_RSP): { case to_num(PacketEnum::LOGIN_RSP): {
LoginRsp rsp; LoginRsp rsp;
Logger::info("Client: Receive Login rsp"); Logger::info("Client: Receive Login rsp");
if (rsp.ParseFromArray(body_data.data(), body_data.size())) { if (decode_packet(rsp, body_data, header)) {
if (rsp.success()) { if (rsp.success()) {
m_world.start_client_thread(rsp.uuid()); m_world.start_client_thread(rsp.uuid());
} else { } else {
@@ -76,34 +76,34 @@ asio::awaitable<void> NetworkClient::read_loop() {
} break; } break;
case to_num(PacketEnum::CHUNK_DATA_RSP): { case to_num(PacketEnum::CHUNK_DATA_RSP): {
ChunkDataRsp rsp; ChunkDataRsp rsp;
// Logger::info("Client: Receive Chunk Data rsp, size {}mb", Logger::info("Client: Receive Chunk Data rsp, size {}mb",
// body_data.size() / 1024.0f / 1024); body_data.size() / 1024.0f / 1024);
if (rsp.ParseFromArray(body_data.data(), body_data.size())) { if (decode_packet(rsp, body_data, header)) {
m_world.receive_chunk(rsp); m_world.receive_chunk(rsp);
} }
} break; } break;
case to_num(PacketEnum::BLOCK_CHANGE_RSP): { case to_num(PacketEnum::BLOCK_CHANGE_RSP): {
BlockChangeRsp rsp; BlockChangeRsp rsp;
Logger::info("Client: Receive Block Change rsp"); Logger::info("Client: Receive Block Change rsp");
if (rsp.ParseFromArray(body_data.data(), body_data.size())) { if (decode_packet(rsp, body_data, header)) {
m_world.receive_block_change(rsp); m_world.receive_block_change(rsp);
} }
} break; } break;
case to_num(PacketEnum::UPDATE_TIME): { case to_num(PacketEnum::UPDATE_TIME): {
UpdateTime rsp; UpdateTime rsp;
if (rsp.ParseFromArray(body_data.data(), body_data.size())) { if (decode_packet(rsp, body_data, header)) {
m_world.receive_time(rsp); m_world.receive_time(rsp);
} }
} break; } break;
case to_num(PacketEnum::PLAYER_INFO_RSP): { case to_num(PacketEnum::PLAYER_INFO_RSP): {
PlayerInfoRsp rsp; PlayerInfoRsp rsp;
if (rsp.ParseFromArray(body_data.data(), body_data.size())) { if (decode_packet(rsp, body_data, header)) {
m_world.receive_other_player(rsp); m_world.receive_other_player(rsp);
} }
} break; } break;
case to_num(PacketEnum::LOGOUT_RSP): { case to_num(PacketEnum::LOGOUT_RSP): {
LogoutRsp rsp; LogoutRsp rsp;
if (rsp.ParseFromArray(body_data.data(), body_data.size())) { if (decode_packet(rsp, body_data, header)) {
m_world.receive_player_logout(rsp); m_world.receive_player_logout(rsp);
} }
} break; } break;

View File

@@ -37,7 +37,7 @@ const std::string& Session::uuid() const { return m_uuid; }
asio::awaitable<void> Session::read_loop() { asio::awaitable<void> Session::read_loop() {
try { try {
while (true) { while (true) {
std::array<char, HEADER_LEN> header_buffer; std::array<uint8_t, HEADER_LEN> header_buffer;
co_await asio::async_read(m_socket, asio::buffer(header_buffer), co_await asio::async_read(m_socket, asio::buffer(header_buffer),
asio::use_awaitable); asio::use_awaitable);
@@ -58,14 +58,14 @@ asio::awaitable<void> Session::read_loop() {
if (cmd_id == to_num(PacketEnum::LOGIN_REQ)) { if (cmd_id == to_num(PacketEnum::LOGIN_REQ)) {
LoginReq req; LoginReq req;
Logger::info("Session: Receive Login req"); Logger::info("Session: Receive Login req");
if (req.ParseFromArray(body_data.data(), body_data.size())) { if (decode_packet(req, body_data, header)) {
m_server_world.handle_player_login(req.name(), m_server_world.handle_player_login(req.name(),
shared_from_this()); shared_from_this());
} }
} }
if (cmd_id == to_num(PacketEnum::PLAYER_POS)) { if (cmd_id == to_num(PacketEnum::PLAYER_POS)) {
PlayerPos pos; PlayerPos pos;
if (pos.ParseFromArray(body_data.data(), body_data.size())) { if (decode_packet(pos, body_data, header)) {
m_server_world.sync_player_pos(pos.uuid(), pos.pos().x(), m_server_world.sync_player_pos(pos.uuid(), pos.pos().x(),
pos.pos().y(), pos.pos().y(),
pos.pos().z()); pos.pos().z());
@@ -74,7 +74,7 @@ asio::awaitable<void> Session::read_loop() {
if (cmd_id == to_num(PacketEnum::CHUNK_DATA_REQ)) { if (cmd_id == to_num(PacketEnum::CHUNK_DATA_REQ)) {
ChunkDataReq req; ChunkDataReq req;
// Logger::info("Session: Receive Chunk Data req"); // Logger::info("Session: Receive Chunk Data req");
if (req.ParseFromArray(body_data.data(), body_data.size())) { if (decode_packet(req, body_data, header)) {
m_server_world.handle_chunk_req( m_server_world.handle_chunk_req(
req.uuid(), ChunkPos(req.pos().x(), req.pos().z())); req.uuid(), ChunkPos(req.pos().x(), req.pos().z()));
} }
@@ -82,13 +82,13 @@ asio::awaitable<void> Session::read_loop() {
if (cmd_id == to_num(PacketEnum::BLOCK_CHANGE_REQ)) { if (cmd_id == to_num(PacketEnum::BLOCK_CHANGE_REQ)) {
BlockChangeReq req; BlockChangeReq req;
Logger::info("Session: Receive Block Change req"); Logger::info("Session: Receive Block Change req");
if (req.ParseFromArray(body_data.data(), body_data.size())) { if (decode_packet(req, body_data, header)) {
m_server_world.handle_block_change(req); m_server_world.handle_block_change(req);
} }
} }
if (cmd_id == to_num(PacketEnum::LOGOUT_REQ)) { if (cmd_id == to_num(PacketEnum::LOGOUT_REQ)) {
LogoutReq req; LogoutReq req;
if (req.ParseFromArray(body_data.data(), body_data.size())) { if (decode_packet(req, body_data, header)) {
m_server_world.handle_player_exit(req.uuid()); m_server_world.handle_player_exit(req.uuid());
} }
} }