Show HN: Hexi - 现代 C++ header-only 网络二进制序列化库
Navigation Menu
Header-only,轻量级的 C++ 库,用于二进制数据流处理。网络数据处理变得简单易用!
License
发现 Apache-2.0, MIT 许可
Licenses found
Apache-2.0 LICENSE MIT LICENSE-MIT 65 stars 2 forks Branches Tags Activity
EmberEmu/Hexi
Folders and files
Name | Name | Last commit message | Last commit date ---|---|---|--- .github/workflows| .github/workflows docs| docs include| include single_include| single_include tests| tests tools/amalgamate| tools/amalgamate .editorconfig| .editorconfig .gitignore| .gitignore CMakeLists.txt| CMakeLists.txt LICENSE| LICENSE LICENSE-MIT| LICENSE-MIT README.md| README.md
Latest commit
History
Hexi 是一个轻量级的、header-only 的 C++23 库,用于安全地处理来自任意来源的二进制数据(但主要是网络数据)。它介于手动从网络缓冲区 memcpy
字节和成熟的序列化库之间。
设计目标是易于使用,在处理不受信任的数据时保证安全,具有合理的灵活性,并将开销保持在最低限度。
Hexi 不提供:版本控制、不同格式之间的转换、基于文本格式的处理、以及卸载洗碗机。
将 Hexi 集成到你的项目中很简单!最简单的方法是直接从 single_include
中复制 hexi.h
到你自己的项目中。如果你只想包含你使用的部分,你可以将 include
添加到你的 include 路径中,或者使用 target_link_library
将其集成到你自己的 CMake 项目中。要构建单元测试,请使用 ENABLE_TESTING
运行 CMake。
以下是一些库可能称之为非常简单的动机示例:
#include <hexi.h>
#include <array>
#include <vector>
#include <cstddef>
struct UserPacket {
uint64_t user_id;
uint64_t timestamp;
std::array<uint8_t, 16> ipv6;
};
auto deserialise(std::span<const char> network_buffer) {
hexi::buffer_adaptor adaptor(network_buffer); // wrap the buffer
hexi::binary_stream stream(adaptor); // create a binary stream
// deserialise!
UserPacket packet;
stream >> packet;
return packet;
}
auto serialise(const UserPacket& packet) {
std::vector<uint8_t> buffer;
hexi::buffer_adaptor adaptor(buffer); // wrap the buffer
hexi::binary_stream stream(adaptor); // create a binary stream
// serialise!
stream << packet;
return buffer;
}
默认情况下,如果满足直接复制字节的安全要求,Hexi 将尝试序列化基本结构,例如我们的 UserPacket
。 但是,出于可移植性的考虑,除非你确定数据布局在写入数据的系统上是相同的,否则不建议这样做。 不用担心,这很容易解决。 另外,我们没有做任何错误处理。 一切都会水到渠成。
你主要处理的两个类是 buffer_adaptor
和 binary_stream
。
binary_stream
接受一个容器作为其参数,并用于进行读取和写入。 它对底层容器的细节知之甚少。
为了支持未编写用于 Hexi 的容器,buffer_adaptor
用作 binary_stream
可以与之交互的包装器。 与 binary_stream
一样,它也提供读写操作,但级别较低。
buffer_adaptor
可以包装任何提供 data
和 size
成员函数的连续容器或视图,并且可以选择提供 resize()
用于写入支持。 从标准库中,这意味着以下内容可以直接使用:
- std::array
- std::span
- std::string_view
- std::string
- std::vector
只要它们提供大致相似的 API,许多非标准库容器也可以直接使用。
容器的值类型必须是字节类型(例如 char
、std::byte
、uint8_t
)。 如果这造成问题,可以使用 std::as_bytes
作为解决方法。
Hexi 支持自定义容器,包括非连续容器。 事实上,库中包含一个非连续容器。 你只需提供一些函数,例如 read
和 size
,以允许 binary_stream
类能够使用它。
static_buffer.h
提供了一个可以直接与 binary_stream
一起使用的自定义容器的简单示例。
正如所提到的,Hexi 旨在即使在处理不受信任的数据时也能安全使用。 例如,可能存在被操纵的网络消息,试图欺骗你的代码读取越界。
binary_stream
执行边界检查以确保它永远不会读取超过缓冲区可用数据量的数据,并且可以选择允许你指定要读取的数据量的上限。 当你在缓冲区中有多个消息并且想要限制反序列化可能侵入下一个消息时,这非常有用。
buffer_t buffer;
// ... read data
hexi::binary_stream stream(buffer, 32); // will never read more than 32 bytes
默认的错误处理机制是异常。 遇到读取数据的问题时,将抛出一个从 hexi::exception
派生的异常。 这些是:
hexi::buffer_underrun
- 尝试读取越界hexi::stream_read_limit
- 尝试读取超过施加的限制
可以通过指定 no_throw
作为模板参数来禁用 binary_stream
中的异常,如下所示:
hexi::binary_stream<buf_type, hexi::no_throw> stream(...);
虽然这可以防止 binary_stream
本身抛出异常,但它并不能阻止异常从较低级别传播。 例如,如果写入时分配失败,包装的 std::vector
仍然可能抛出 std::bad_alloc
。
无论你使用哪种错误处理机制,都可以按如下方式检查 binary_stream
的状态:
hexi::binary_stream<buf_type, hexi::no_throw> stream(...);
// ... assume an error happens
// simplest way to check whether any errors have occurred
if (!stream) {
// handle error
}
// or we can get the state
if (auto state = stream.state(); state != hexi::stream_state::ok) {
// handle error
}
在第一个示例中,只有当写入数据的程序以与我们自己的程序相同的方式布局所有内容时,读取我们的 UserPacket
才能按预期工作。 由于架构差异、编译器标志等原因,情况可能并非如此。
以下是相同的示例,但以可移植的方式进行操作。
#include <hexi.h>
#include <span>
#include <string>
#include <vector>
#include <cstddef>
#include <cstdint>
struct UserPacket {
uint64_t user_id;
std::string username;
uint64_t timestamp;
uint8_t has_optional_field;
uint32_t optional_field; // pretend this is big endian in the protocol
// deserialise
auto& operator>>(auto& stream) {
stream >> user_id >> username >> timestamp >> has_optional_field;
if (has_optional_field) {
stream >> optional_field;
hexi::endian::big_to_native_inplace(optional_field);
}
// we can manually trigger an error if something went wrong
// stream.set_error_state();
return stream;
}
// serialise
auto& operator<<(auto& stream) const {
stream << user_id << username << timestamp << has_optional_field;
if (has_optional_field) {
stream << hexi::endian::native_to_big(optional_field);
}
return stream;
}
};
// pretend we're reading network data
void read() {
std::vector<char> buffer;
const auto bytes_read = socket.read(buffer);
// ... logic for determing packet type, etc
bool result {};
switch (packet_type) {
case packet_type::user_packet:
result = handle_user_packet(buffer);
break;
}
// ... handle result
}
auto handle_user_packet(std::span<const char> buffer) {
hexi::buffer_adaptor adaptor(buffer);
hexi::binary_stream stream(adaptor);
UserPacket packet;
stream >> packet;
if (stream) {
// ... do something with the packet
return true;
} else {
return false;
}
}
由于 binary_stream
是一个模板,因此最简单的方法是允许编译器执行类型推导。
如果你希望函数体位于源文件中,建议你为你自己的 binary_stream
类型提供你自己的 using
别名。 另一种方法是使用多态等效项 pmc::buffer_adaptor
和 pmc::binary_stream
,它们允许你在运行时更改底层缓冲区类型,但代价是虚拟调用开销,并且缺少一些与多态性不太一致的功能。
如何构建你的代码取决于你,这只是一种方法。
使用 binary_stream
时,字符串始终被视为 null 结尾。 写入 char*
、std::string_view
或 std::string
始终会将终止字节写入流。 如果你需要其他操作,请使用其中一个 put
函数。
同样,读取到 std::string
假定缓冲区包含 null 终止符。 如果不是,则将返回一个空字符串。 如果你知道字符串的长度或者需要支持自定义终止/标记值,请使用 get()
和 find_first_of()
。
以下是一些包含的额外功能的快速概述。
hexi::file_buffer
- 用于处理二进制文件。 简单。
hexi::static_buffer
- 固定大小的网络缓冲区,用于在你知道一次需要发送或接收的数据量的上限时使用。 本质上是
std::array
的包装器,但添加了状态跟踪。 如果你需要分多个步骤进行反序列化(读取数据包标头,调度,读取数据包正文),则非常方便。
- 固定大小的网络缓冲区,用于在你知道一次需要发送或接收的数据量的上限时使用。 本质上是
hexi::dynamic_buffer
- 可调整大小的缓冲区,用于在你想要处理偶尔的大型读/写操作而无需预先分配空间时使用。 在内部,它会添加额外的分配以适应额外的数据,而不是像
std::vector
那样请求更大的分配并复制数据。 它重用尽可能多的已分配块,并支持 Asio (Boost 或 standalone)。 实际上,它是一个链表缓冲区。
- 可调整大小的缓冲区,用于在你想要处理偶尔的大型读/写操作而无需预先分配空间时使用。 在内部,它会添加额外的分配以适应额外的数据,而不是像
hexi::tls_block_allocator
- 允许许多
dynamic_buffer
实例共享更大的预分配内存池,每个线程都有自己的池。 当你需要处理许多网络套接字并且想要避免通用分配器时,这很有用。 需要注意的是,必须由进行分配的同一线程进行释放,从而将对缓冲区的访问限制为单个线程(但有一些例外)。
- 允许许多
hexi::endian
- 提供用于处理积分类型的字节序的功能。
我们已经到了概述的结尾,但如果你决定尝试 Hexi,还有更多内容需要发现。 以下是一些美味的食物:
- 当底层缓冲区支持时,
binary_stream
允许你在流中执行写入查找。 例如,如果你需要使用在写入消息的其余部分之前可能不知道的信息来更新消息头(校验和、大小等),则此方法非常好。 binary_stream
提供了重载的put
和get
成员函数,允许进行细粒度的控制,例如读取/写入特定数量的字节。- 只要底层容器是连续的,
binary_stream
允许使用view()
和span()
写入std::string_view
和std::span
。 这允许你创建到缓冲区数据的视图,从而提供了一种快速、零拷贝的方式来从流中读取字符串和数组。 如果你这样做,则应避免在持有数据视图的同时写入同一缓冲区。 buffer_adaptor
提供了一个模板选项space_optimise
。 默认情况下启用此功能,并且允许它避免在流已读取所有数据的情况下调整容器的大小。 禁用它允许你即使在读取数据后也能保留数据。 此选项仅在正在写入和读取单个缓冲区的情况下才相关。buffer_adaptor
提供了find_first_of
,使其易于在缓冲区中查找特定的标记值。
要了解更多信息,请查看 docs/examples
中的示例!
About
Header-only,轻量级的 C++ 库,用于二进制数据流处理。网络数据处理变得简单易用!
Topics
serialization header-only buffer-management serialization-library serialisation binary-stream
Resources
License
发现 Apache-2.0, MIT 许可
Licenses found
Apache-2.0 LICENSE MIT LICENSE-MIT Activity Custom properties
Stars
Watchers
Forks
Releases
Packages 0
No packages published
Contributors 2
Languages
Footer
GitHub © 2025 GitHub, Inc.
Footer navigation
- Terms
- [Privacy](https://github.com/EmberEmu/<https