1 个不稳定版本
| 0.1.0 | 2023年8月8日 |
|---|
#1715 in 编码
340KB
5K SLoC
解析和序列化(网络)数据包。
packet是一个库,用于帮助解析和序列化嵌套数据包。网络数据包是最常见的用例,但它支持任何带有头部、尾部和嵌套的数据包结构。
模型
packet的核心组件是各种缓冲区特质(XxxBuffer 和 XxxBufferMut)。缓冲区是一个带有前缀、主体和尾部的字节数据缓冲区。缓冲区的大小被称为其“容量”,主体的大小被称为其“长度”。根据实现了哪些特质,缓冲区的主体可能能够在解析或序列化数据包时根据容量进行收缩或扩展。
解析
在解析数据包时,缓冲区的主体存储下一个要解析的数据包。从一个缓冲区解析出数据包时,任何头部、尾部和填充都会从缓冲区中“消耗”掉。因此,在解析完一个数据包后,缓冲区的主体等于数据包的主体,下一个对parse的调用将从前一个调用停止的地方继续,解析下一个封装的数据包。
数据包对象——Rust对象,是成功解析操作的结果——建议简单地保持对头部、尾部和主体的引用,以避免任何不必要的复制。
例如,考虑以下数据包结构,其中TCP段封装在IPv4数据包中,IPv4数据包又封装在以太网帧中。在这个例子中,我们省略了以太网帧校验序列(FCS)尾部。如果有任何尾部,它们将被当作头部处理,只是它们是从尾部开始消费,并朝着头部方向处理,而头部则是从头部开始消费,并朝着尾部方向处理。
还要注意,为了满足以太网的最小主体大小要求,IPv4数据包之后添加了填充。IPv4数据包和填充一起被认为是以太网帧的主体。如果在这个例子中包含以太网FCS尾部,它将放在填充之后。
|-------------------------------------|++++++++++++++++++++|-----| TCP segment
|-----------------|++++++++++++++++++++++++++++++++++++++++|-----| IPv4 packet
|++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++| Ethernet frame
|-----------------|-------------------|--------------------|-----|
Ethernet header IPv4 header TCP segment Padding
最初,缓冲区的主体将等于以太网帧的字节(尽管根据缓冲区如何初始化,它可能还有额外的容量)
|-------------------------------------|++++++++++++++++++++|-----| TCP segment
|-----------------|++++++++++++++++++++++++++++++++++++++++|-----| IPv4 packet
|++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++| Ethernet frame
|-----------------|-------------------|--------------------|-----|
Ethernet header IPv4 header TCP segment Padding
|----------------------------------------------------------------|
Buffer Body
首先,对以太网帧进行解析。这会产生一个假设的EthernetFrame对象(这个库不提供任何具体的解析实现),该对象引用缓冲区,并更新缓冲区的主体以等于以太网帧的主体
|-------------------------------------|++++++++++++++++++++|-----| TCP segment
|-----------------|++++++++++++++++++++++++++++++++++++++++|-----| IPv4 packet
|++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++| Ethernet frame
|-----------------|----------------------------------------------|
Ethernet header Ethernet body
| |
+--------------------------+ |
| |
EthernetFrame { header, body }
|-----------------|----------------------------------------------|
buffer prefix buffer body
EthernetFrame对象可以可变借用缓冲区。只要它存在,就不能直接使用缓冲区(尽管可以使用EthernetFrame对象来访问或修改缓冲区的内容)。为了解析以太网帧的主体,我们必须释放EthernetFrame对象,这样我们才能再次调用缓冲区的方法。[1]
释放EthernetFrame对象后,解析IPv4数据包。回想一下,以太网主体包含IPv4数据包和一些填充。由于IPv4数据包编码了自己的长度,IPv4数据包解析器能够检测到它正在处理的某些字节是填充字节。消耗和丢弃这些字节的责任在解析器,这样它们就不会在后续解析中被错误地视为IPv4数据包主体的部分。
这种解析会产生一个假设的Ipv4Packet对象,该对象包含对缓冲区的引用,并更新缓冲区的主体以等于IPv4数据包的主体
|-------------------------------------|++++++++++++++++++++|-----| TCP segment
|-----------------|++++++++++++++++++++++++++++++++++++++++|-----| IPv4 packet
|++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++| Ethernet frame
|-----------------|-------------------|--------------------|-----|
IPv4 header IPv4 body
| |
+-----------+ |
| |
Ipv4Packet { header, body }
|-------------------------------------|--------------------|-----|
buffer prefix buffer body buffer suffix
我们可以一直这样进行下去,重复解析后续的数据包主体,直到没有更多数据包可以解析。
[1] 也可以将EthernetFrame的body字段视为缓冲区并直接从其中解析。然而,这种方法有一个缺点,即如果解析分散在多个函数中,解析内部数据包的函数只能看到缓冲区的一部分,因此如果他们希望稍后重新使用缓冲区进行序列化(请参阅本文档的“序列化”部分),他们将限于在更小的缓冲区中进行,这会增加需要分配新缓冲区的可能性。
序列化
在本节中,我们将使用与解析相同的包结构来展示序列化 - 以太网帧中的IPv4数据包中的TCP段。
序列化包括两个任务
- 首先,给定一个具有足够容量的缓冲区,以及已经序列化的一部分数据包,序列化数据包的下一层。例如,给定一个已经序列化TCP段的缓冲区,序列化IPv4头部,从而生成包含TCP段的自包含IPv4数据包。
- 其次,给定一个嵌套数据包序列的描述,确定缓冲区必须满足哪些约束才能容纳整个序列,并分配一个满足这些约束的缓冲区。然后,使用前一个项目所述的方法分层次序列化缓冲区。
将序列化到缓冲区中
PacketBuilder特质由能够将数据包的新层序列化到现有缓冲区的类型实现。例如,我们可以定义一个Ipv4PacketBuilder类型,它描述源IP地址、目的IP地址以及生成IPv4头部所需的其他元数据。重要的是,一个PacketBuilder不定义任何封装的数据包。为了在IPv4数据包中构建TCP段,我们需要一个单独的TcpSegmentBuilder来描述TCP段。
PacketBuilder通过constraints方法公开它需要的头部、尾部以及最小和最大主体长度。它通过serialize方法进行序列化。
为了序列化一个PacketBuilder,必须首先构建一个SerializeTarget。一个SerializeTarget是对用于序列化的缓冲区的视图,它使用正确的字节数初始化头部、尾部和主体。这些所需的字节数通过调用PacketBuilder的constraints方法来发现。
PacketBuilder的serialize方法将数据包的头部和尾部序列化到缓冲区中。它期望SerializeTarget已初始化为与将要封装的主体相等的主体。例如,假设我们正在尝试将TCP段序列化到IPv4数据包的以太网帧中,到目前为止,我们只序列化了TCP段
|-------------------------------------|++++++++++++++++++++|-----| TCP segment
|-----------------|++++++++++++++++++++++++++++++++++++++++|-----| IPv4 packet
|++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++| Ethernet frame
|-------------------------------------|--------------------|-----|
TCP segment
|-------------------------------------|--------------------|-----|
buffer prefix buffer body buffer suffix
注意,缓冲区的主体目前等于TCP段,主体的内容已初始化为段的内容。
给定一个Ipv4PacketBuilder,我们调用相应的方法来发现它需要20个字节来存储头部。因此,我们通过扩展主体20个字节来修改缓冲区,并构建一个SerializeTarget,其头部引用新添加的20个字节,而主体引用主体旧的内容,对应于TCP段。
|-------------------------------------|++++++++++++++++++++|-----| TCP segment
|-----------------|++++++++++++++++++++++++++++++++++++++++|-----| IPv4 packet
|++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++| Ethernet frame
|-----------------|-------------------|--------------------|-----|
IPv4 header IPv4 body
| |
+-----------+ |
| |
SerializeTarget { header, body }
|-----------------|----------------------------------------|-----|
buffer prefix buffer body buffer suffix
然后我们将SerializeTarget传递给Ipv4PacketBuilder的serialize方法,它将IPv4头部序列化到提供的空间中。当serialize方法返回时,SerializeTarget和Ipv4PacketBuilder已被丢弃,而缓冲区的主体现在是IPv4数据包的字节。
|-------------------------------------|++++++++++++++++++++|-----| TCP segment
|-----------------|++++++++++++++++++++++++++++++++++++++++|-----| IPv4 packet
|++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++| Ethernet frame
|-----------------|----------------------------------------|-----|
IPv4 packet
|-----------------|----------------------------------------|-----|
buffer prefix buffer body buffer suffix
现在,我们准备重复与数据包的以太网层相同的过程。
构建序列化缓冲区
既然我们已经知道如何给定一个包含已序列化数据包子集的缓冲区,我们可以序列化数据包的下一层,我们首先需要弄清楚如何构建这样的缓冲区。
这里的主要挑战是我们需要在实际序列化之前对我们的序列化内容做出承诺。例如,考虑将TCP段发送到网络。从我们代码的TCP模块的角度来看,我们不知道缓冲区需要多大,因为我们不知道TCP段将被封装在哪些数据包层中。如果IP层决定通过以太网链路路由我们的段,那么我们需要一个足够大的缓冲区来容纳以太网段中的IPv4数据包中的TCP段。另一方面,如果IP层决定通过GRE隧道路由我们的段,那么我们需要一个足够大的缓冲区来容纳以太网段中的IP数据包中的GRE数据包中的IPv4数据包中的TCP段。
我们通过Serializer特质来实现这种“先承诺再序列化”。一个Serializer描述了一个可以将来序列化的数据包,但尚未序列化。与PacketBuilder不同,一个Serializer描述了数据包的所有层,直到某个点。例如,一个Serializer可能描述一个TCP段,或者它可能描述一个IP数据包中的TCP段,或者它可能描述一个以太网帧中的IP数据包中的TCP段,等等。
构建一个 Serializer
Serializer 是递归的 - 一个 Serializer 与一个 PacketBuilder 结合会产生一个新的 Serializer,该 Serializer 描述了在新的数据包层中封装原始的 Serializer。例如,一个描述 TCP 数据段的 Serializer 与一个 Ipv4PacketBuilder 结合会产生一个描述 IPv4 数据包中 TCP 数据段的 Serializer。具体来说,给定一个 Serializer,s,和一个 PacketBuilder,b,可以通过调用 s.encapsulate(b) 构造一个新的 Serializer。该方法通过值消耗了 Serializer 和 PacketBuilder,并返回一个新的 Serializer。
请注意,虽然 Serializer 通过值传递,但它们在内存中的大小仅与它们所构建的 PacketBuilder 相当,而那些在大多数情况下应该相当小。如果大小是一个问题,可以为引用类型(例如,&Ipv4PacketBuilder)实现 PacketBuilder 特性,并传递引用而不是值。
从 Serializer 构建缓冲区
如果通过从最内层的包层开始并向外添加包层来构建 Serializer,那么为了将 Serializer 转换为缓冲区,它们是通过从最外层的包层开始并朝内消耗来构建的。
为了构建缓冲区,提供了一个 Serializer::serialize 方法。它接受一个 NestedPacketBuilder,它描述了一个或多个封装的包层。例如,当在以太网帧中序列化 IP 包中的 TCP 数据段时,IP 包 Serializer 的 serialize 调用将提供一个描述以太网帧的 NestedPacketBuilder。然后,这个调用将计算一个新的 NestedPacketBuilder,描述组合的 IP 包和以太网帧,并将其传递给 TCP 数据段 Serializer 上 serialize 的调用。
当到达最内层的 serialize 调用时,该调用的责任是生成满足其传递的约束条件的缓冲区,并用其数据包的内容初始化该缓冲区的主体。例如,前一个示例中的 TCP 数据段 Serializer 需要生成一个具有 38 字节前缀的缓冲区,用于 IP 和以太网头部,其主体被初始化为 TCP 数据段的字节。
现在我们可以看到Serializer和PacketBuilder是如何组合的——从serialize方法调用返回的缓冲区满足PacketBuilder::serialize方法的要求——其主体被初始化为要封装的报文,并且有足够的头和尾空间来序列化这一层的头部和尾部。例如,对TCP段序列化器上的Serializer::serialize的调用将返回一个前缀为38字节的缓冲区,主体被初始化为TCP段的字节。然后,对IP数据包上的Serializer::serialize的调用会将这个缓冲区传递给其Ipv4PacketBuilder上的PacketBuilder::serialize调用,从而得到一个前缀为18字节、主体被初始化为整个IP数据包字节的缓冲区。这个缓冲区随后将适合从Serializer::serialize的调用中返回,允许以太网层继续在缓冲区上操作,依此类推。
特别需要注意的是,在整个构建Serializer和PacketBuilder并消费它们的过程中,缓冲区只分配一次,并且每个数据包的字节只序列化一次。不需要临时缓冲区或缓冲区之间的复制。
重用缓冲区
Serializer特质的另一个重要属性是它可以由缓冲区实现。由于缓冲区包含前缀、主体和后缀,并且由于Serializer::serialize方法按值消耗Serializer并按值返回一个缓冲区,因此缓冲区本身就是一个有效的Serializer。当调用serialize时,只要它已经满足请求的约束,它可以简单地按值返回自身。如果约束不满足,它可能需要通过某种用户定义的机制生成不同的缓冲区(有关详细信息,请参阅BufferProvider特质)。
这使得在许多情况下可以重用现有的缓冲区。例如,考虑在缓冲区中接收一个数据包,然后对此数据包进行响应,使用一个新的数据包。可以重复使用存储原始数据包的缓冲区来序列化新数据包,从而避免不必要的分配。
依赖关系
~1MB
~14K SLoC