MRP: Multiplexed Reliable Packets, over UDP
这段本文用于指导Golang代码实现本协议。
代码应满足如下要求:
- 每个实例绑定1个UDP端口,可以接受外来连接,也可以对外发起连接。
- 对外提供如下对象:
- Socket,表示1个MRP实例,所有全局操作的载体,也可以创建Track;需要实现为无锁结构体,并且通过TrackId可以直接找到对应的track,而不是通过Remote。
- Track,主要的数据流操作对象;需要实现为无锁结构体。
- Remote,类似于连接对象,但是应该尽量弱化它的存在,以Track为核心。需要实现为无锁结构体,并且通过TrackId可以找到对应的track。
- Window,收发包窗口。每个Track上面要绑定2个窗口(Window)对象,分别处理该Track上的收包和发包。
- Frame,数据帧对象,是上层应用之间交互的最小单位批次。实际可能拆分成多个Packet处理。
- Packet,用于操作数据包,是实际收发的UDP数据包负载。
- 使用1个协程(即goroutine,下同)接收所有包。
- 使用1个协议,定时间隔扫描所有track的发包窗口,尝试进行发包。
- 对外提供的Send接口,在发包窗口期内没有堆积的包时,尝试直接进行发包。
- 外部可以注册一个对象,以处理如下事件:
- socket状态变化
- track状态变化
- 收到数据帧
- 数据帧发送结果
本协议(MRP,即“Multiplexed Reliable Packets”)与QUIC协议十分类似但更简单。
设计上,它与QUIC的主要不同之处在于不使用加密,因为我们的主要目标场景是用作内网RPC通讯。其余都是相似之处。
与QUIC一样,它完全基于UDP。
它以数据帧的形式向应用层交换数据,而不是字节流。但是超过MTU大小的数据帧实际会分成多个UDP包中发送。
它支持多流(这里我们称之为track,与QUIC中的stream对等),
相同的流内的数据包是有序的,不同的流之间完全没有干扰,甚至因为基于UDP所以也没有HTTP2上因为TCP导致的对头阻塞。
- socket: 绑定1个UDP端口,表示1个MRP实例。下面挂了若干track和若干remote。
- track: 流。数据包通过流进行传输。每个track在不同的实例上有不同的id。
- frame: 帧。是上层应用之间交互的最小单位批次。实际可能拆分成多个packet处理。
- packet: 包。用于操作数据包,是实际收发的UDP数据包负载。
- 握手: 两个MRP实例之间,需要通过握手建立track。
- 确认: 接收方告诉发送方“我已收到你发送的数据包”。
- 窗口: 暂存收发的数据包,分为发送窗口和接收窗口两种。
- 发送窗口:数据包还未发送、或发送后还未被接收方确认,就会留在发送窗口中。
- 接收窗口:数据包已接收但尚未调用外部回调进行处理,就会留在接收窗口中。
本协议所有数据都放在UDP包内部。其内容包括一个16字节的头部,之后都是负载数据(Payload Data)。
头部长度16字节,其中的所有字段都是小端的。其结构如下表所示:
| bytes | 0 | 1 | 2 | 3 |
|---|
| 0-3 | Flags | DataSize |
| 4-7 | TrackId |
| 8-11 | SeqNr | SafeNum |
| 12-15 | AckBase | AckExt |
| 16-(15+DataSize) | Payload Data |
每个字段具体定义如下:
- byte[0:1] 是 Flags 字段。
- byte[2:3] 是 DataSize 字段,是负载数据的字节数。
- byte[4:7] 是 TrackId 字段,是接收端的track的id。当发起握手时,此字段置0。
- byte[8:9] 是 SeqNr 字段,表示当前包的编号,是无符号数,溢出时回卷到0。当发起握手时,此字段存储发起端的TrackId的低16位。
- byte[10:11] 是 SafeNum 字段,用于结合SeqNr校验包体的合法性。当发起握手时,此字段存储发起端的TrackId的高16位。
- byte[12:13] 是 AckBase 字段,表示前面的SeqNr都已被接收到了。
- byte[14:15] 是 AckExt 字段,用于扩展AckBase的表达能力,可以表示AckBase之后的16个包是否已经收到。
Flags字段共16位,每个位的定义如下:
- bit[0] 是 Hello 标志,表示这是个握手包。
- bit[1] 是 Fin 标志,表示发送者已经关闭了此track。
- bit[2] 是 Ack 标志,表示带有Ack信息。
- bit[3] 是 Ctn 标志,表示该Frame尚未完结,还有后续Packet。
需要通过握手,才能创建track。支持0-RTT握手。
发送方实例,分配1个自己未使用的32位TrackId,填充到SeqNr和Secret字段中,并将TrackId字段置0、Hello标志置1,然后发出。
发送后,track进入HelloOut状态。
因为支持0-RTT握手,可以在发包时直接带上负载数据。
收到带有Hello标志时,就是一个握手包。
对端的TrackId存于SeqNr和Secret字段中,这个值应保存下来,后续所有发给该track的包,TrackId字段都应填充此值。
若TrackId字段为0,就是对端发起的握手。此时,track进入HelloIn状态。
若有负载数据,需要酌情通过回调呈现给应用层。
需要答复一个带Hello标志的包,TrackId设置成对端的TrackId,SeqNr和Secret字段设成本端分配的TrackId,
且需要按照确认机制设置AckBase和AckExt字段,若有负载数据也可以同时带上。
答复后,track进入Established状态,即握手已经完成。
若TrackId字段非0,就是本端发起的握手、对端进行了答复。此时,track进入Established状态,即握手已经完成。
若有负载数据,需要酌情通过回调呈现给应用层。
收到数据包后,应该进行确认(即Acknowledge或简称Ack),以使发送方知晓数据包已经收到。
Ack信息通过头部的AckBase和AckExt字段发送给对方。
头部的Flags字段中的Ack标志为1时,表示AckBase和AckExt是有效的。
AckBase表示发送端发送的所有SeqNr在AckBase之前的包,都已收到。
因为SeqNr和AckBase是16位无符号整数,必须注意超过65535后回滚到0的情况。
AckExt表示AckBase之后的16个包的接收情况,每1位表示1个包,为1的位表示对应的包已经收到。
发送方收到Ack后应立刻根据AckExt表示的情况,将尚未收到的包进行重发。
暂存收发的数据包,分为发送窗口和接收窗口两种。
- 发送窗口:数据包还未发送、或发送后还未被接收方确认,就会留在发送窗口中。
- 接收窗口:数据包已接收但尚未调用外部回调进行处理,就会留在接收窗口中。