国内外c2c网站有哪些,开工作室做网站怎样找资源,有创意的文创产品,wordpress 禁止1、kcp 的协议特点
1.1、RTO 不翻倍
RTO(Retransmission TimeOut)#xff0c;重传超时时间。tcp x 2#xff0c;kcp x 1.5#xff0c;提高传输速度 1.2、选择重传 TCP丢包时会全部重传从该包开始以后的数据#xff0c;而KCP选择性重传#xff0c;只重传真正丢失的数据包…
1、kcp 的协议特点
1.1、RTO 不翻倍
RTO(Retransmission TimeOut)重传超时时间。tcp x 2kcp x 1.5提高传输速度 1.2、选择重传 TCP丢包时会全部重传从该包开始以后的数据而KCP选择性重传只重传真正丢失的数据包。
1.3、快速重传 tcp 重传模式 超时重传超过规定的时间 RTO 则重传 快速重传收到三个冗余ACK不去等待RTO 直接重传 这里指的是收到fastresend个失序报文后不等待超时直接重传减少丢包等待时间。
1.4、非延迟 ACK tcp 为充分利用带宽延迟发送 ACKRTT 时间较大延长了丢包时的判断过程。而 kcp 的 ACK 是否延迟发送可以调节。 1.5、ACK UNA ARQ 自动重传请求Automatic Repeat-reQuest模型响应有两种方式
UNA此编号前所有包已收到tcp ACK该编号包已收到 只用 UNA 将导致全部重传只用 ACK 则丢失成本太高以往协议都是二选其一。而 kcp 协议中除去单独的 ACK 包精确外所有包都有 UNA 信息。
1.6、非退让流控 KCP正常模式同TCP一样使用公平退让法则即发送窗口大小由发送缓存大小、接收端剩余接收缓存大小、丢包退让、慢启动这四要素决定。但传送及时性要求很高的小数据时可选择仅用前两项来控制发送频率。以牺牲部分公平性及带宽利用率之代价换取了流畅传输的效果。
KCP 实时性好但带宽利用率较低因为
非退让流控不断尝试发送数据有效包不多 每个包应答占用一定的带宽
2.kcp 实现
UDP收到的报文通过kcp_input传递给KCPKCP会对数据进行解包重新封装成应用层用户数据应用层通过kcp_recv获取。应用层通过kcp_send发送数据KCP会把用户数据拆分kcp报文通过kcp_output以UDP(send)的方式发送。 2.1、kcp 数据结构
kcp 报文结构 conv会话编号通信双方必须一致。cmd报文类型IKCP_CMD_ACK 确认命令IKCP_CMD_PUSH 数据推送命令IKCP_CMD_WASK 接收窗口询问大小命令IKCP_CMD_WINS 接收窗口大小告知命令wnd: 己方可用接收窗口大小接收窗口大小 - 接收队列大小frgsegmen t分片。0最后一个分片。3 2 1 0snsegment 报文的序列号。ts发送时间戳用于计算RTO和RTTuna待接收的序列号其实确认号表示该序列号之前的所有报文都收到了可以删除len数据长度DATA的长度DATA: 用户数据 kcp 使用的 Segment 定义如下
struct IKCPSEG
{struct IQUEUEHEAD node; // 用来串接多个 KCP segment即前向后向指针IUINT32 conv; // 会话编号IUINT32 cmd; // 报文类型IUINT32 frg; // 分片 IUINT32 wnd; // 可用接收窗口大小(接收窗口大小-接收队列大小)IUINT32 ts; // 发送时刻的时间戳IUINT32 sn; // 分片 segment 的序号IUINT32 una; // 待接收消息序号IUINT32 len; // 数据长度IUINT32 resendts; // 下次超时重传该报文的时间戳IUINT32 rto; // 重传超时时间//发送端在发送过程中携带着RTO该发送端会启动一个定时器进行定时如果超过RTO就会重传IUINT32 fastack; // 收到ack时该分片被跳过的次数用于快速重传IUINT32 xmit; // 记录了该报文被传输了几次char data[1]; // 实际传输的数据 payload
};每一个 KCP 用户都需要调用 ikcp_create 创建一个 kcp 控制块 ikcpcb。ikcpcb 结构用来实现整个 KCP 协议。
struct IKCPCB
{IUINT32 conv; // 标识会话IUINT32 mtu; // 最大传输单元,默认数据为1400,最小为50IUINT32 mss; // 最大分片大小,不大于mtuIUINT32 state; // 连接状态(0xffffffff表示断开连接)IUINT32 snd_una; // 第一个未确认的包IUINT32 snd_nxt; // 下一个待分配包的序号IUINT32 rcv_nxt; // 待接收消息序号.为了保证包的顺序,接收方会维护一个接收窗口,接收窗口有一个起始序号rcv_nxt 以及尾序号rcv_nxt rcv_wnd(接收窗口大小)IUINT32 ts_recent; IUINT32 ts_lastack;IUINT32 ssthresh; // 拥塞窗口的阈值IINT32 rx_rttval; // RTT的变化量,代表连接的抖动情况IINT32 rx_srtt; // smoothed round trip time平滑后的RTTIINT32 rx_rto; // 收ACK接收延迟计算出来的重传超时时间IINT32 rx_minrto; // 最小重传超时时间IUINT32 snd_wnd; // 发送窗口大小IUINT32 rcv_wnd; // 接收窗口大小本质上而言如果接收端一直不去读取数据则rcv_queue就会满(达到rcv_wnd)IUINT32 rmt_wnd; // 远端接收窗口大小IUINT32 cwnd; // 拥塞窗口大小, 动态变化IUINT32 probe; // 探查变量, IKCP_ASK_TELL表示告知远端窗口大小。IKCP_ASK_SEND表示请求远端告知窗口大小IUINT32 current;IUINT32 interval; // 内部flush刷新间隔对系统循环效率有非常重要影响, 间隔小了cpu占用率高, 间隔大了响应慢IUINT32 ts_flush; // 下次flush刷新的时间戳IUINT32 xmit; // 发送segment的次数, 当segment的xmit增加时xmit增加(重传除外)IUINT32 nrcv_buf; // 接收缓存中的消息数量IUINT32 nsnd_buf; // 发送缓存中的消息数量IUINT32 nrcv_que; // 接收队列中消息数量IUINT32 nsnd_que; // 发送队列中消息数量IUINT32 nodelay; // 是否启动无延迟模式。无延迟模式rtomin将设置为0拥塞控制不启动IUINT32 updated; //是否调用过update函数的标识IUINT32 ts_probe; // 下次探查窗口的时间戳IUINT32 probe_wait; // 探查窗口需要等待的时间IUINT32 dead_link; // 最大重传次数被认为连接中断IUINT32 incr; // 可发送的最大数据量struct IQUEUEHEAD snd_queue; //发送消息的队列 struct IQUEUEHEAD rcv_queue; //接收消息的队列, 确认过用户可读取struct IQUEUEHEAD snd_buf; //发送消息的缓存struct IQUEUEHEAD rcv_buf; //接收消息的缓存IUINT32 *acklist; //待发送的ack的列表 当收到一个数据报文时将其对应的 ACK 报文的 sn 号以及时间戳 ts //同时加入到acklist 中即形成如 [sn1, ts1, sn2, ts2 …] 的列表IUINT32 ackcount; // 记录 acklist 中存放的 ACK 报文的数量 IUINT32 ackblock; // acklist 数组的可用长度当 acklist 的容量不足时需要进行扩容void *user; // 指针可以任意放置代表用户的数据也可以设置程序中需要传递的变量char *buffer; // 存储字节流信息 int fastresend; // 触发快速重传的重复ACK个数int fastlimit;int nocwnd; // 取消拥塞控制int stream; // 是否采用流传输模式int logmask; // 日志的类型如IKCP_LOG_IN_DATA方便调试int (*output)(const char *buf, int len, struct IKCPCB *kcp, void *user);//发送消息的回调函数void (*writelog)(const char *log, struct IKCPCB *kcp, void *user); // 写日志的回调函数
};2.2、kcp 报文发送
KCP 中数据发送流程分为
上层应用调用 ikcp_send 将数据写入 snd_queue下层函数 ikcp_flush 决定将多少数据从 snd_queue 移动到 snd_buf进行发送 ikcp_send ikcp_send ikcp_send 的功能把用户发送的数据根据MSS分片成KCP的数据包格式插入待发送队列
分片方式
流模式检测每个发送队列⾥的分片是否达到 MSS没有达到则用新的数据填充分片。 消息模式将用户数据的每个分片设置 sn 和 frag将分片后的数据存入发送队列接收方通过 sn 和 frag 解包。即使⼀个分片的数据量可能不能达到MSS也会作为⼀个包发送出去。
int ikcp_send(ikcpcb *kcp, const char *buffer, int len)
{// 1、如果KCP开启流模式if (kcp-stream ! 0) { if (!iqueue_is_empty(kcp-snd_queue)) {// 取出 snd_queue 中的最后一个报文将其填充到 mss 的长度设置frg为0IKCPSEG *old iqueue_entry(kcp-snd_queue.prev, IKCPSEG, node);// 旧分片内数据长度小于mssif (old-len kcp-mss) {int capacity kcp-mss - old-len; // 还能容纳的数据长度int extend (len capacity)? len : capacity; // 需要填充的长度seg ikcp_segment_new(kcp, old-len extend); // 新建segmentassert(seg);if (seg NULL) {return -2;}// 新分片添加到发送队列尾部iqueue_add_tail(seg-node, kcp-snd_queue); // 拷贝旧分片的数据到新分片memcpy(seg-data, old-data, old-len); // 将buffer中的数据也拷贝到新分片if (buffer) {memcpy(seg-data old-len, buffer, extend);buffer extend; // buffer指向剩余数据的开头}seg-len old-len extend;seg-frg 0;len - extend; // 更新len为剩余数据长度iqueue_del_init(old-node); // 删除oldikcp_segment_delete(kcp, old);}}if (len 0) {return 0;}}// 2、计算数据需要分成多少段报文if (len (int)kcp-mss) count 1; // mss 1376 head 24 mtu 1400else count (len kcp-mss - 1) / kcp-mss;if (count (int)IKCP_WND_RCV) return -2; // 超过对方的初始接收窗口if (count 0) count 1; // fragment// 3、将数据全部新建 segment 插入发送队列尾部队列计数递增, frag 递减for (i 0; i count; i) {int size len (int)kcp-mss ? (int)kcp-mss : len;seg ikcp_segment_new(kcp, size);assert(seg);if (seg NULL) {return -2;}if (buffer len 0) { // 仍有待发送的数据memcpy(seg-data, buffer, size);}seg-len size;// 分片编号逆序。流模式情况下分片编号不用填写seg-frg (kcp-stream 0)? (count - i - 1) : 0;iqueue_init(seg-node);iqueue_add_tail(seg-node, kcp-snd_queue); // 加入到 snd_queue 中kcp-nsnd_que;if (buffer) {buffer size;}len - size;}
}应用层调用 ikcp_send 之后将用户数据置入 snd_queue 中当 KCP 调用 ikcp_flush 时才将数据从 snd_queue 中 移入到 snd_buf 中然后调用 kcp-output() 发送。
检查 kcp-update 是否更新未更新直接返回。kcp-update 由 ikcp_update 更新上层应用需要每隔一段时间10-100ms调用 ikcp_update 来驱动 KCP 发送数据
// ikcp_update havent been called.
if (kcp-updated 0) return;准备将 acklist 中记录的 ACK 报文发送出去即从 acklist 中填充 ACK 报文的 sn 和 ts 字段
// flush acknowledges
// 逐一获取 acklist 中的 sn 和 ts编码成 segment以流的方式凑够 MTU 发送
count kcp-ackcount; // 需要应答的分片数量
for (i 0; i count; i) {size (int)(ptr - buffer);// 超过 MTU 大小直接发送if (size (int)IKCP_OVERHEAD (int)kcp-mtu) {ikcp_output(kcp, buffer, size);ptr buffer; // 新建分片}ikcp_ack_get(kcp, i, seg.sn, seg.ts); // 应答包ptr ikcp_encode_seg(ptr, seg); // 编码segment协议头
}kcp-ackcount 0; 检查当前是否需要对远端窗口进行探测。由于 KCP 流量控制依赖于远端通知其可接受窗口的大小一旦远端接受窗口 kcp-rmt_wnd 为0那么本地将不会再向远端发送数据因此就没有机会从远端接受 ACK 报文从而没有机会更新远端窗口大小。在这种情况下KCP 需要发送窗口探测报文到远端待远端回复窗口大小后后续传输可以继续
// probe window size (if remote window size equals zero)
// 1、远端窗口大小为0需要发送窗口探测报文
if (kcp-rmt_wnd 0) {// 初始化探测间隔和下一次探测时间if (kcp-probe_wait 0) { kcp-probe_wait IKCP_PROBE_INIT; // 默认7秒探测kcp-ts_probe kcp-current kcp-probe_wait; // 下一次探测时间} else {//远端窗口为0发送过探测请求但是已经超过下次探测的时间 // 检测是否到了探测时间if (_itimediff(kcp-current, kcp-ts_probe) 0) { // 更新探测间隔probe_waitif (kcp-probe_wait IKCP_PROBE_INIT) kcp-probe_wait IKCP_PROBE_INIT;kcp-probe_wait kcp-probe_wait / 2;if (kcp-probe_wait IKCP_PROBE_LIMIT)kcp-probe_wait IKCP_PROBE_LIMIT;// 更新下次探测时间ts_probekcp-ts_probe kcp-current kcp-probe_wait;// 更新探测变量probe为IKCP_ASK_SEND发送探测消息 kcp-probe | IKCP_ASK_SEND;}}
}
// 2、远端窗口正常则不需要发送窗口探测
else {kcp-ts_probe 0; // 更新下次探测时间为0kcp-probe_wait 0; // 更新探测窗口等待时间为0
}将窗口探测报文和窗口回复报文发送出去
// flush window probing commands
if (kcp-probe IKCP_ASK_SEND) {seg.cmd IKCP_CMD_WASK; // 窗口探测[询问对方窗口size]size (int)(ptr - buffer);if (size (int)IKCP_OVERHEAD (int)kcp-mtu) {ikcp_output(kcp, buffer, size);ptr buffer;}ptr ikcp_encode_seg(ptr, seg);
}// flush window probing commands
if (kcp-probe IKCP_ASK_TELL) {seg.cmd IKCP_CMD_WINS; // 窗口告知[告诉对方我方窗口size]size (int)(ptr - buffer);if (size (int)IKCP_OVERHEAD (int)kcp-mtu) {ikcp_output(kcp, buffer, size);ptr buffer;}ptr ikcp_encode_seg(ptr, seg);
}kcp-probe 0; //清空标识计算本次发送可用的窗口大小这里 KCP 采用了可以配置的策略正常情况下KCP 的窗口大小由发送窗口 snd_wnd远端接收窗口 rmt_wnd 以及根据流控计算得到的 kcp-cwnd 共同决定但是当开启了 nocwnd 模式时窗口大小仅由前两者决定
// calculate window size
// 若没有流控取发送窗口和远端接收窗口最小值
cwnd _imin_(kcp-snd_wnd, kcp-rmt_wnd);
// 若存在流控则取当前拥塞窗口、发送窗口和远端接收窗口三者最小值
if (kcp-nocwnd 0) cwnd _imin_(kcp-cwnd, cwnd); 将缓存在 snd_queue 中的数据移到 snd_buf 中等待发送
// move data from snd_queue to snd_buf
// 从snd_queue移动到snd_buf的数量不能超出对方的接收能力发送符合拥塞范围的分片
while (_itimediff(kcp-snd_nxt, kcp-snd_una cwnd) 0) {IKCPSEG *newseg;if (iqueue_is_empty(kcp-snd_queue)) break;newseg iqueue_entry(kcp-snd_queue.next, IKCPSEG, node);iqueue_del(newseg-node);iqueue_add_tail(newseg-node, kcp-snd_buf); // 添加到发送缓存kcp-nsnd_que--;kcp-nsnd_buf;//设置数据分片的属性newseg-conv kcp-conv;newseg-cmd IKCP_CMD_PUSH;newseg-wnd seg.wnd; // 告知对方当前的接收窗口newseg-ts current; // 当前时间newseg-sn kcp-snd_nxt; // 序号newseg-una kcp-rcv_nxt; // 告诉对方可以发送的下一个包序号newseg-resendts current; // 当前发送的时间newseg-rto kcp-rx_rto; // 超时重传的时间newseg-fastack 0; // 是否快速重传newseg-xmit 0; // 重传次数
}在发送数据之前先设置快重传的次数和重传间隔KCP 允许设置快重传的次数即 fastresend 参数。例如设置 fastresend 为2并且发送端发送了1,2,3,4,5几个包收到远端的ACK: 1, 3, 4, 5当收到ACK3时KCP知道2被跳过1次收到ACK4时知道2被**“跳过”**了2次此时可以认为2号丢失不用等超时直接重传2号包每个报文的 fastack 记录了该报文被跳过了几次由函数 ikcp_parse_fastack 更新。于此同时KCP 也允许设置 nodelay 参数当激活该参数时每个报文的超时重传时间将由 x2 变为 x1.5即加快报文重传
// calculate resent
// 是否设置快重传次数
resent (kcp-fastresend 0)? (IUINT32)kcp-fastresend : 0xffffffff;
// 是否开启nodelay
rtomin (kcp-nodelay 0)? (kcp-rx_rto 3) : 0;将 snd_buf 中的数据发送出去
// flush data segments
// 发送snd buf的分片只要数据还在snd_buf 说明对方还没有应答
// 1、新的报文正常发送
// 2、超时重传
// 3、快速重传如果有
for (p kcp-snd_buf.next; p ! kcp-snd_buf; p p-next) {IKCPSEG *segment iqueue_entry(p, IKCPSEG, node);int needsend 0;// 1、如果该报文是第一次传输那么直接发送if (segment-xmit 0) { needsend 1;segment-xmit; // 分片发送次数 1segment-rto kcp-rx_rto; // 超时时间间隔segment-resendts current segment-rto rtomin; // 下一次要发送的时间}// 2、当前时间达到了该报文的重传时间但并没有新的ack到达出现丢包, 重传 else if (_itimediff(current, segment-resendts) 0) { needsend 1;segment-xmit;kcp-xmit;// 根据 nodelay 参数更新重传时间if (kcp-nodelay 0) {segment-rto _imax_(segment-rto, (IUINT32)kcp-rx_rto);} else {IINT32 step (kcp-nodelay 2)? ((IINT32)(segment-rto)) : kcp-rx_rto;segment-rto step / 2; //报文超时等待时间更新控制RTO1.5 }segment-resendts current segment-rto; //下一次发送的时间lost 1; // 丢包反应到拥塞控制策略去了}// 3、该报文的的被跳过次数超过设置的快速重传次数需要重传 else if (segment-fastack resent) { if ((int)segment-xmit kcp-fastlimit || kcp-fastlimit 0) {needsend 1;segment-xmit;segment-fastack 0; // 重置该分片被跳过的次数segment-resendts current segment-rto;change; // 标识快速重传的发生}}// 需要发送数据if (needsend) {int need;segment-ts current;segment-wnd seg.wnd; // 己方可用接收窗口大小segment-una kcp-rcv_nxt; // 待接收的下一个包序号size (int)(ptr - buffer);need IKCP_OVERHEAD segment-len;// 小包封装成大包发送if (size need (int)kcp-mtu) { ikcp_output(kcp, buffer, size);ptr buffer;}// 把segment封装成线性buffer发送 头部数据ptr ikcp_encode_seg(ptr, segment); if (segment-len 0) {memcpy(ptr, segment-data, segment-len);ptr segment-len;}if (segment-xmit kcp-dead_link) {kcp-state (IUINT32)-1;}}
}// flash remain segments
size (int)(ptr - buffer); // 剩余的数据
// 最终只要有数据要发送一定发出去
if (size 0) {ikcp_output(kcp, buffer, size);
}根据设置的 lost 和 change 更新窗口大小注意 快重传和丢包时的窗口更新算法不一致这一点类似于 TCP 协议的拥塞控制和快恢复算法
// update ssthresh
//如果发生了快速重传拥塞窗口阈值降低为当前未确认包数量的一半或最小值
if (change) { IUINT32 inflight kcp-snd_nxt - kcp-snd_una;kcp-ssthresh inflight / 2;if (kcp-ssthresh IKCP_THRESH_MIN)kcp-ssthresh IKCP_THRESH_MIN;kcp-cwnd kcp-ssthresh resent; // 动态调整拥塞控制窗口kcp-incr kcp-cwnd * kcp-mss;
}
// 如果发生了丢包阈值减半, cwd 窗口保留为 1
if (lost) { kcp-ssthresh cwnd / 2;if (kcp-ssthresh IKCP_THRESH_MIN)kcp-ssthresh IKCP_THRESH_MIN;kcp-cwnd 1; // 动态调整拥塞控制窗口 kcp-incr kcp-mss;
}if (kcp-cwnd 1) {kcp-cwnd 1; kcp-incr kcp-mss;
}2.3、kcp 报文接收 ikcp_recv 应用层接收函数为 ikcp_recv主要做三件事
读取组好包的数据 rcv_queue - 用户 buffer 将接收缓存 rcv_buf 的分片转移到接收队列 rcv_queue 如果有接收空间则将 kcp-probe | IKCP_ASK_TELL ; 以在update的时候告知对方可以发送数据了。 首先检测一下本次接收数据之后是否需要进行窗口恢复。在前面的内容中解释过KCP 协议在远端窗口为0的时候将会停止发送数据此时如果远端调用 ikcp_recv 将数据从 rcv_queue 中移动到应用层 buffer 中之后表明其可以再次接受数据为了能够恢复数据的发送远端可以主动发送 IKCP_ASK_TELL 来告知窗口大小
if (kcp-nrcv_que kcp-rcv_wnd)recover 1; // 标记可以开始窗口恢复开始将 rcv_queue 中的数据根据分片编号 frg merge 起来然后拷贝到用户的 buffer 中。
// merge fragment
// 将属于同一个消息的各分片重组完整数据并删除rcv_queue中segmentnrcv_que减少
// 经过 ikcp_send 发送的数据会进行分片分片编号为倒序序号因此frg为0的数据包标记着完整接收到了一次 send 发送过来的数据
for (len 0, p kcp-rcv_queue.next; p ! kcp-rcv_queue; ) {int fragment;seg iqueue_entry(p, IKCPSEG, node);p p-next;if (buffer) {memcpy(buffer, seg-data, seg-len); // 把queue的数据就放入用户bufferbuffer seg-len;}len seg-len;fragment seg-frg;if (ikcp_canlog(kcp, IKCP_LOG_RECV)) {ikcp_log(kcp, IKCP_LOG_RECV, recv sn%lu, (unsigned long)seg-sn);}if (ispeek 0) {iqueue_del(seg-node);ikcp_segment_delete(kcp, seg); // 删除节点kcp-nrcv_que--; // nrcv_que接收队列-1}// frg 0完整的数据接收到, 本次数据接收结束if (fragment 0) // break;
}下一步将 rcv_buf 中的数据转移到 rcv_queue 中这个过程根据报文的 sn 编号来确保转移到 rcv_queue 中的数据一定是按序的
// move available data from rcv_buf - rcv_queue
// 将 rcv_buf 中的数据转移到 rev_queue
// 根据报文的sn来确保转移到 rcv_queue 中的数据一定是按序的
while (! iqueue_is_empty(kcp-rcv_buf)) {seg iqueue_entry(kcp-rcv_buf.next, IKCPSEG, node);// 1、根据 sn 确保数据是按序转移到 rcv_queue 中// 2、接收队列nrcv_que 接收窗口rcv_wnd; if (seg-sn kcp-rcv_nxt kcp-nrcv_que kcp-rcv_wnd) {iqueue_del(seg-node);kcp-nrcv_buf--;iqueue_add_tail(seg-node, kcp-rcv_queue);kcp-nrcv_que; // 接收队列 有多少个分片 1kcp-rcv_nxt; // 接收序号 1} else {break;}
}最后进行窗口恢复。此时如果 recover 标记为1表明在此次接收之前可用接收窗口为0如果经过本次接收之后可用窗口大于0将主动发送 IKCP_ASK_TELL 数据包来通知对方已可以接收数据
// fast recover
// nrcv_que小于rcv_wnd, 说明接收端有空间继续接收数据了
if (kcp-nrcv_que kcp-rcv_wnd recover) {// ready to send back IKCP_CMD_WINS in ikcp_flush// tell remote my window sizekcp-probe | IKCP_ASK_TELL;
}kcp_input ikcp_recv 仅为上层调用的接口KCP 协议需要从底层接受数据到 rcv_buf 中这是通过函数 ikcp_input 实现。ikcp_input 中的所有功能都在一个外层的循环中实现
首先将接收到的数据包进行解码并进行基本的数据包长度和类型校验KCP 协议只会接收到前文中所介绍的四种数据包
调用 ikcp_parse_una 来确定已经发送的数据包有哪些被对方接收到。KCP 中所有的报文类型均带有 una 信息。发送端发送的数据都会缓存在 snd_buf 中直到接收到对方确认信息之后才会删除。当接收到 una 信息后表明 sn 小于 una 的数据包都已经被对方接收到因此可以直接从 snd_buf 中删除。同时调用 ikcp_shrink_buf 来更新 KCP 控制块的 snd_una 数值。
// 删除小于snd_buf中小于una的segment
ikcp_parse_una(kcp, una);
// 更新snd_una为snd_buf中seg-sn或kcp-snd_nxt 更新下一个待应答的序号
ikcp_shrink_buf(kcp); 处理 IKCP_CMD_ACK 报文
if (cmd IKCP_CMD_ACK) {if (_itimediff(kcp-current, ts) 0) { // 根据应答判断rtt//更新rx_srttrx_rttval计算kcp-rx_rtoikcp_update_ack(kcp, _itimediff(kcp-current, ts));}//遍历snd_buf中snd_una, snd_nxt将sn相等的删除直到大于sn ikcp_parse_ack(kcp, sn); // 将已经ack的分片删除ikcp_shrink_buf(kcp); // 更新控制块的 snd_unaif (flag 0) {flag 1; //快速重传标记maxack sn; // 记录最大的 ACK 编号latest_ts ts;} else {if (_itimediff(sn, maxack) 0) {maxack sn; // 记录最大的 ACK 编号 latest_ts ts;}}处理 IKCP_CMD_PUSH 报文
else if (cmd IKCP_CMD_PUSH) { //接收到具体的数据包if (_itimediff(sn, kcp-rcv_nxt kcp-rcv_wnd) 0) {// 对该报文的确认 ACK 报文放入 ack 列表中ikcp_ack_push(kcp, sn, ts);// 判断接收的数据分片编号是否符合要求即在接收窗口滑动窗口范围之内if (_itimediff(sn, kcp-rcv_nxt) 0) { // 是要接受起始的序号seg ikcp_segment_new(kcp, len);seg-conv conv;seg-cmd cmd;seg-frg frg;seg-wnd wnd;seg-ts ts;seg-sn sn;seg-una una;seg-len len;if (len 0) {memcpy(seg-data, data, len);}// 将该报文插入到 rcv_buf 链表中ikcp_parse_data(kcp, seg); }}
}对于接收到的 IKCP_CMD_WASK 报文直接标记下次将发送窗口通知报文而对于报文 IKCP_CMD_WINS 无需做任何特殊操作;
else if (cmd IKCP_CMD_WASK) { // ready to send back IKCP_CMD_WINS in ikcp_flush // tell remote my window size // 如果是探测包添加相应的标识位kcp-probe | IKCP_ASK_TELL;
}
else if (cmd IKCP_CMD_WINS) {// do nothing如果是 tell me 远端窗口大小什么都不做
}据记录的最大的 ACK 编号 maxack 来更新 snd_buf 中的报文的 fastack这个过程在介绍 ikcp_flush 中提到过对于 fastack 大于设置的 resend 参数时将立马进行快重传
最后根据接收到报文的 una 和 KCP 控制块的 una 参数进行流控