Note
[toc]
1. TCP协议
传输控制协议(TCP,Transmission Control Protocol)是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。是传输层的协议。
TCP是为流量设计的(每秒内可以传输多少KB的数据),讲究的是充分利用带宽。而 KCP是为流速设计的(单个数据包从一端发送到一端需要多少时间),以10%-20%带宽浪费的代价换取了比 TCP快30%-40%的传输速度。TCP信道是一条流速很慢,但每秒流量很大的大运河,而KCP是水流湍急的小激流。
超时重传机制
TCP协议要求:发送端每发送一个报文段,就会启动一个定时器等待确认接收信息。接收端收到报文段后就要返回确认信息ACK。在定时器超时前数据没被确认,TCP就认为“丢包”。接着会重传这个包,直到接收端确认为止。这也是TCP可靠性的来源。
RTO RTT
重传超时时间(RTO,Retransmission TimeOut),TCP使用自适应算法动态调整RTO的值。
往返时延 (RTT,Round Trip Time)也就是数据包从发出去到收到对应 ACK 的时间。RTT 是针对连接的,每一个连接都有各自独立的 RTT。
TCP中对于RTO、RTT的控制算法有很多种,但都比较极端,比如Karn/Partidge Algorithm的算法采用是若超时则直接将RTO翻倍。如果还没收到就再 * 2... 也有如Jacobson/ Karels算法,是一种加权平均的经验算法。但其中的参数确实有点神秘魔法含义,反正能用就行。
快速重传机制
发送端收到来自接收端的三个相同的ACK时就会触发快速重传,(其实就算报文没丢失,也有可能因为报文到达的顺序不一致导致ACK的重复发送,而报文丢失必定会出现3个ACK,分别来自N-1和后边的两个报文)。
普通的超时重传太过笨重。丢失一个报文后就会重发之后的所有报文(为了确保可靠性而过分的慎重)。但快速重传机制下,发送端收到某个报文的三次ACK后就会立即重传,而接收端会将之前收到的报文先缓存起来,然后在收到丢失的报文后,继续正常工作。
延迟 ACK
接收端收到数据时要发送一个ACK(期待的下一个报文编号)表示确认收到。但只发送确认对带宽有所浪费,所以会延迟等待看是否有响应数据需要发送,若有则附着其中发送回去。一般等待最多200ms左右。这里的200ms指的是一个内部的计时器,每隔200ms检查有没有ACK要发送。例如有一个数据段在185ms到达,那么它最迟在200ms发送ACK,而不是等待到385ms看没有响应数据再发送。(这对网络延迟造成了更严重的影响)
ACK,UNA
ARQ响应模型有两种,即UNA(当前未收到确认信息的包编号,或理解成:此编号前所有包已收到,如TCP)和ACK(该编号包已收到),光用UNA将导致全部重传,光用ACK则丢失成本太高,二者取长补短会更好。
滑动窗口
一般来说,发送端的数据可以分为四类
-
Sent and Acknowledged
:数据已发送,且收到来自接收端的ACK。 -
Sent , But Not yet Acknowledged
:数据已发送,但尚未收到来自接收端的ACK。 -
Not Sent, Recipient Ready to Receive
:数据未发送,且接收端已经准备好接收 -
Not Sent, Recipient Not Ready to Recieve
: 数据未发送,且接收端也尚未准备好接收。
而所谓的滑动窗口,就是指的一个动态的 发送/接收 队列。如上图,发送端的滑动窗口可以分为两部分,即发送窗口(已经发送但未收到ACK)和可用窗口(接收端还能接收但发送端尚未发送)。
而接收端的滑动窗口也是类似的,其响应的WIN值所表示的就是窗口大小(剩余可以接收的大小),这样发送端在收到后可以动态调整发送的数据大小,甚至是暂停发送(让接收端先把之前的数据消化掉),以此达到流量控制,减少堵塞的目的。
2. KCP协议
KCP是一个纯粹的ARQ协议,通过重传机制实现UDP数据包的可靠传输,是一个在UDP之上SESSION之下的协议。以比 TCP 浪费 10%-20% 的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。是纯算法实现,所以要自己定义下层数据包的收发方式。
KCP 提高流速
KCP启动快速模式后RTO超时x1.5(实验证明1.5这个值相对比较好),提高了传输速度。相较于TCP,若三次超时则变成RTO * 8。
KCP是选择性重传,只重传真正丢失的数据包。KCP也有基本的超时重传机制与快速重传机制。KCP会直接重传失序次数过多的报文,而非等待其超时。
假设发送方依次发送了 1, 2, 3, 4 号报文, 随后收到 1, 3, 4 号 ACK. 收到 3 号 ACK 时, 我们知道 2 号 ACK 失序了一次, 收到 4 号 ACK 时, 我们知道 2 号失序了两次. ACK 失序次数越多说明它丢包的概率越大。
- KCP的ACK是否延迟发送是可以调节的。KCP中除了单独的ACK包之外,其他所有包都包含有UNA信息。
KCP 报文结构
KCP中共有四种报文,分别是
- 数据报文
IKCP_CMD_PUSH
- 确认报文
IKCP_CMD_ACK
- 窗口探测报文
IKCP_CMD_WASK
- 窗口通知报文
IKCP_CMD_WINS
报文结构如下:
0 4 5 6 8 (BYTE)
+---------------+---+---+-------+
| conv |cmd|frg| wnd |
+---------------+---+---+-------+ 8
| ts | sn |
+---------------+---------------+ 16
| una | len |
+---------------+---------------+ 24
| |
| DATA (optional) |
| |
+-------------------------------+
- cmd:域用来区分是具体哪种报文。
- conv:用来表示连接标识。
- frg:其后报文数量。
- wnd:剩余接收窗口大小。
- ts:时间戳。
- sn:报文编号
- una:尚未收到ACK的报文编号(小于una的都接收了)
- len:数据长度
- data:数据部分
利用ts计算出RTT(往返时间)从而判断是否需要超时重传。同时还有一个MSS(最大报文段大小)用来限制数据包的大小,超过了就要切片成多个报文。因此引入frg表示报文数量。
KCP 队列与缓冲区
注意到struct IKCPCB
里有四个struct IQUEUEHEAD
,是表示队列,也即发送队列,接收队列,发送缓冲区,接收缓冲区,都是手动实现的双向循环链表。且用大量的宏定义实现了队列的相关操作。这些队列有一个初始的头结点Head(不存数据),随后可在头尾进行插入删除等操作。
https://luyuhuang.tech/2020/12/09/kcp.html
KCP 发送 接收 重传
KCP 的整个发送, 接收与重传的流程大体如下:
- 调用 ikcp_send 发送数据, 创建报文段实例, 加入 snd_queue 中.
- ikcp_update 会在合适的时刻调用 ikcp_flush.
- ikcp_flush 会做:
- 发送 ACK 列表中所有的 ACK;
- 检查是否需要发送窗口探测和通知报文, 如果需要就发送相应的报文;
- 根据发送窗口大小, 将适量的报文段从 snd_queue 移入到 snd_buf 中;
- 将 snd_buf 中满足条件的报文段都发送出去. 这里的条件有:
- 新加入 snd_buf 中, 从未发送过的报文直接发送出去;
- 发送过的, 但是在 RTO 内未收到 ACK 的报文, 需要重传;
- 发送过的, 但是 ACK 失序若干次的报文, 需要执行快速重传.
- 根据丢包情况计算 ssthresh 和 cwnd.
- 这样, 刚才调用 ikcp_send 传入的数据就在 ikcp_flush 中被发送出去了.
- 报文到达对端.
- ikcp_input 会被调用, 解析收到的数据:
- 所有的报文都有 una 字段, 根据 una 将相应的报文标记为已送达;
- 如果是 ACK 报文, 就将相应的报文标记为已送达;
- 如果是数据报文, 就将它放入 rcv_buf, 然后将 rcv_buf 中顺序正确的报文移入 rcv_queue; 接着将相关信息插入 ACK 列表, 在稍后的 ikcp_flush 调用中会发送相应的 ACK;
- 如果是窗口探测报文, 就标记 "需要发送窗口通知". 在稍后的 ikcp_flush 调用中会发送窗口通知报文;
- 包括窗口通知报文在内的所有报文都有 wnd 字段, 据此更新 rmt_wnd;
- 根据 ACK 失序情况决定快速重传;
- 计算 cwnd.
- 调用 ikcp_recv 接收数据, 从 rcv_queue 中读取数据.
KCP 拥塞控制
与TCP相同,KCP也有拥塞窗口、慢启动、快速恢复等拥塞控制措施。
开始时cwnd=1,(congestion window,拥塞窗口,即发送端的一个可滑动窗口)。每经过一个RTT时间后就翻倍,但也不会无限指数增长。超过阈值后就是线性增长,这个阶段叫拥塞避免,这个阈值叫做慢启动阈值(ssthresh,Slow Start Threshold)。整个过程就叫慢启动。
随着cwnd的增大,网络最终被填满,就会出现丢包。此时说明cwnd应该减小,即丢包退让。KCP对此的处理是:如果发生超时重传, 就进入慢启动(将ssthresh置为cwnd的一半,然后cwnd重新从1开始慢启动); 如果发生快速重传, 就进入快速恢复(将ssthresh置为cwnd的一半,cwnd设置比ssthresh略微大一点,再以拥塞避免的方式线性增长).
[图片上传失败...(image-4153eb-1628154473200)]
KCP 源码
KCP的代码是比较精炼的,只有ikcp.c
和 ikcp.h
两个文件。
其中 ikcp.h
提供了结构体定义,队列操作,接口函数声明等。ikcp.c
是对各函数方法的具体实现。
ikcp_create
创建一个KCP的实例,将其中的各项成员初始化后返回该实例的指针。其中IUINT32 conv
参数用于标识该KCP连接。通信双方必须协商相同的conv才可以交流。该连接发出的每个报文段都会带上conv,同理也只接受包含conv的报文段。