EmberEmu/Hexi
GitHub: EmberEmu/Hexi
一个轻量级、仅头文件的 C++23 二进制流处理库,专注于安全高效地序列化和反序列化网络数据。
Stars: 286 | Forks: 8
将 Hexi 整合到你的项目中非常简单!最简单的方法是直接将 `single_include` 中的 `hexi.h` 复制到你自己的项目中。如果你只想包含你使用的内容,可以将 `include` 添加到你的包含路径中,或者通过 `target_link_library` 将其整合到你自己的 CMake 项目中。要构建单元测试,请使用 `ENABLE_TESTING` 运行 CMake。
以下是一些库可能称之为非常简单的入门示例:
```
#include
你主要会打交道的两个类是 `buffer_adaptor` 和 `binary_stream`。
`binary_stream` 接受一个容器作为参数,用于进行读取和写入。它不太了解底层容器的细节。
为了支持非专为 Hexi 编写的容器,`buffer_adaptor` 被用作 `binary_stream` 可以对接的包装器。与 `binary_stream` 一样,它也提供读取和写入操作,但级别更低。
`buffer_adaptor` 可以包装任何提供 `data` 和 `size` 成员函数的连续容器或视图。对于标准库而言,这意味着以下容器可以直接使用:
- [x] std::array
- [x] std::span
- [x] std::string_view
- [x] std::string
- [x] 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` - 尝试读取超过 imposed limit 的数据
通过指定 `no_throw` 作为参数,可以禁用来自 `binary_stream` 的异常。此参数在编译时消除了异常分支,因此零运行时开销。
```
hexi::binary_stream stream(buffer, hexi::no_throw);
```
无论你使用哪种错误处理机制,都可以按如下方式检查 `binary_stream` 的状态:
```
hexi::binary_stream stream(buffer, hexi::no_throw);
// ... 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
}
```
在第一个示例中,读取我们的 `LoginPacket` 只有在写入数据的程序将所有内容以与我们自己的程序相同的方式布局时,才能按预期工作。
由于架构差异、编译器标志等原因,情况可能并非如此。
这是同一个示例,但以可移植的方式进行。
```
#include
处理字符串需要一点思考。`std::string` 和 `std::string_view` 允许包含嵌入式空字符,即使很少这样做。
为了遵守最小惊讶原则,Hexi 默认使用长度前缀来读取和写入这些类型。这确保写入这样的字符串并将其读回将为你提供正确的结果,无论内容如何。
在大多数情况下,你会希望以 null 结尾的方式读/写字符串。为此,请使用字符串适配器,如下所示:
```
hexi::binary_stream stream(...);
std::string foo { "No surprises here!" };
// write it
stream << hexi::null_terminated(foo);
// read it back
stream >> hexi::null_terminated(foo);
```
这不是默认设置,因为写入可能包含嵌入式空字节的字符串会导致读回时字符串被截断。这是 Hexi 让你郑重承诺你对字符串数据处理方式正确的方式。
`const char*` 字符串*总是*以 null 结尾的字符串形式写入,因为这种类型中的嵌入式空字符几乎没有意义。使用 `null_terminated` 适配器读回它们。
其他适配器(如 `prefixed_varint`)也可用。有关用法示例,请参见 `docs/examples/string_handling.cpp`。
以下是对一些包含的额外功能的非常简明的介绍。
- `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::null_buffer`
- Hexi 的 /dev/null 等价物。如果你想知道序列化后类型的确切大小,通常用于分配或保留精确数量的内存,则此缓冲区很有用。运行两次序列化可能听起来效率低下,但编译器通常可以在编译时计算序列化到 `null_buffer` 的最终结果,或者将其提炼为几条算术指令。
我们已到达概述的结尾,但如果你决定尝试 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` 中的示例!
标签:Bash脚本, C++23, DNS, Emulator, Header-only, NAT穿透, Socket通信, STUN, 二进制安全, 二进制流, 序列化, 游戏开发, 游戏服务器模拟, 缓冲区管理, 网络安全, 网络编程, 轻量级库, 隐私保护