KCP协议的几个核心函数为ikcp_create, ikcp_send,ikcp_recv,ikcp_update,ikcp_input,ikcp_flush,这些函数可以构造整个数据收发的流程。其中ikcp_create比较简单,主要为创建KCP对象,ikcp_update主要是根据内部刷新事件判断是否开始调用ikcp_flush,所以这两个函数在此不再描述。
为了方便理解,部分注释直接写在代码中,函数的代码也尽量保持原有的样子,减少分拆。
该函数的功能非常简单,把用户发送的数据根据MSS分片成KCP的数据包格式,插入待发送队列中。当用户的数据超过一个MSS(最大分片大小)的时候,会对发送的数据进行分片处理。通过frg进行排序区分,frg即message中的segment分片ID,在message中的索引,由大到小,0表示最后一个分片。分成4片时,frg为3,2,1,0。
如用户发送1900字节的数据,MTU为1400byte。因此,该函数会把1900byte的用户数据分成两个包,一个数据大小为1400,头frg设置为1,len设置为1400;第二个包,头frg设置为0,len设置为500。切好KCP包之后,放入到名为snd_queue的待发送队列中。
分片方式共有两种。流模式情况下,检测每个发送队列里的分片是否达到最大MSS,如果没有达到就会用新的数据填充分片。接收端会把多片发送的数据重组为一个完整的KCP包。消息模式下,将用户数据分片,为每个分片设置sn和frag,将分片后的数据一个一个地存入发送队列,接收方通过sn和frag解析原来的包,消息方式一个分片的数据量可能不能达到MSS,也会作为一个包发送出去。
MTU,数据链路层规定的每一帧的最大长度,超过这个长度数据会被分片。通常MTU的长度为1500字节,IP协议规定所有的路由器均应该能够转发(512数据+60IP首部+4预留=576字节)的数据。MSS,最大输出大小(双方的约定),KCP的大小为MTU-kcp头24字节。IP数据报越短,路由器转发越快,但是资源利用率越低。传输链路上的所有MTU都一致的情况下效率最高,应该尽可能的避免数据传输的工程中,再次被分。UDP再次被分以后,只要丢失其中的任意一份,两份都要重新传输。因此,合理的MTU应该是保证数据不被再分的前提下,尽可能的大。
以太网的MTU通常为1500字节-IP头(20字节固定+40字节可选)-UDP头8个字节=1472字节。KCP会考虑多传输协议,但是在UDP的情况下,设置为1472字节更为合理。如果在发送数据前进行了MTU探测,那么可以更准确,但探测需要在应用层来完成。
int ikcp_send(ikcpcb *kcp, const char *buffer, int len)
{
IKCPSEG *seg;
int count, i;
// append to previous segment in streaming mode (if possible)
if (kcp->stream != 0) {
if (!iqueue_is_empty(&kcp->snd_queue)) {
IKCPSEG *old = iqueue_entry(kcp->snd_queue.prev, IKCPSEG, node);
//节点内数据长度小于mss,计算还可容纳的数据大小,以及本次占用的空间大小,以此新建segment,将新建segment附加到发送队列尾,将old节点内数据拷贝过去,然后将buffer中也拷贝其中,如果buffer中的数据没有拷贝完,extend为拷贝数据,开始frg计数。更新len为剩余数据,删除old
if (old->len < kcp->mss) {
int capacity = kcp->mss - old->len;
int extend = (len < capacity)? len : capacity;
seg = ikcp_segment_new(kcp, old->len + extend);
assert(seg);
if (seg == NULL) {
return -2;
}
iqueue_add_tail(&seg->node, &kcp->snd_queue);
memcpy(seg->data, old->data, old->len);
if (buffer) {
memcpy(seg->data + old->len, buffer, extend);
buffer += extend;
}
seg->len = old->len + extend;
seg->frg = 0;
len -= extend;
iqueue_del_init(&old->node);
ikcp_segment_delete(kcp, old);
}
}
if (len <= 0) {
return 0;
}
}
//计算数据可以被最多分成多少个frag
if (len <= (int)kcp->mss) count = 1;
else count = (len + kcp->mss - 1) / kcp->mss;
if (count >= IKCP_WND_RCV) return -2;
if (count == 0) count = 1;
// fragment
// 将数据全部新建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);
kcp->nsnd_que++;
if (buffer) {
buffer += size;
}
len -= size;
}
return 0;
}
ikcp_recv需要通过轮询的方式去调用,如果有数据,将返回完整的消息,如果没有就返回错误。输入参数中len需要是一个较大的值,buffer也需要预先进行空间分配,这些在应用层确定。
首先检测一下本次接收数据之后,是否需要进行窗口恢复。KCP 协议在远端窗口为0的时候将会停止发送数据,此时如果远端调用 ikcp_recv 将数据从 rcv_queue 中移动到应用层 buffer 中之后,表明其可以再次接受数据,为了能够恢复数据的发送,远端可以主动发送 IKCP_ASK_TELL 来告知窗口大小。
开始将 rcv_queue 中的数据根据分片编号 frg merge 起来,然后拷贝到用户的 buffer 中。这里 ikcp_recv 循环遍历 rcv_queue,按序拷贝数据,当碰到某个 segment 的 frg 为 0 时跳出循环,表明本次数据接收结束。经过 ikcp_send 发送的数据会进行分片,分片编号为倒序序号,因此 frg 为 0 的数据包标记着完整接收到了一次 send 发送过来的数据。
下一步将 rcv_buf 中的数据转移到 rcv_queue 中,这个过程根据报文的 sn 编号来确保转移到 rcv_queue 中的数据一定是按序的。
最后进行窗口恢复。此时如果 recover 标记为1,表明在此次接收之前,可用接收窗口为0,如果经过本次接收之后,可用窗口大于0,将主动发送 IKCP_ASK_TELL 数据包来通知对方已可以接收数据。
int ikcp_recv(ikcpcb *kcp, char *buffer, int len)
{
struct IQUEUEHEAD *p;
int ispeek = (len < 0)? 1 : 0;
int peeksize;
int recover = 0;
IKCPSEG *seg;
//计算当前接收队列中的属于同一个消息的数据总长度,这个长度应该比参数中的len小,如果大于,导致数据不能导出
peeksize = ikcp_peeksize(kcp);
// 接收队列segment数量大于等于接收窗口,标记窗口可以恢复
if (kcp->nrcv_que >= kcp->rcv_wnd)
recover = 1;
// merge fragment 将属于同一个消息的各分片重组完整数据,并删除rcv_queue中segment,nrcv_que减少
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);
buffer += seg->len;
}
len += seg->len;
fragment = seg->frg;
if (ispeek == 0) {
iqueue_del(&seg->node);
ikcp_segment_delete(kcp, seg);
kcp->nrcv_que--;
}
if (fragment == 0)
break;
}
assert(len == peeksize);
// move available data from rcv_buf -> rcv_queue
while (! iqueue_is_empty(&kcp->rcv_buf)) {
IKCPSEG *seg = iqueue_entry(kcp->rcv_buf.next, IKCPSEG, node);
// 1. 根据 sn 确保数据是按序转移到 rcv_queue 中
// 2. 根据接收窗口大小来判断是否可以接收数据
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++;
kcp->rcv_nxt++;
} else {
break;
}
}
// fast recover
if (kcp->nrcv_que < kcp->rcv_wnd && recover) {
// ready to send back IKCP_CMD_WINS in ikcp_flush
// tell remote my window size
kcp->probe |= IKCP_ASK_TELL;
}
return len;
}
KCP报文分为ACK报文、数据报文、探测窗口报文、响应窗口报文四种。
kcp报文的una字段(snd_una:第一个未确认的包)表示对端希望接收的下一个kcp包序号,也就是说明接收端已经收到了所有小于una序号的kcp包。解析una字段后需要把发送缓冲区里面包序号小于una的包全部丢弃掉。
ack报文则包含了对端收到的kcp包的序号,接到ack包后需要删除发送缓冲区中与ack包中的发送包序号(sn)相同的kcp包。
收到数据报文时,需要判断数据报文是否在接收窗口内,如果是则保存ack,如果数据报文的sn正好是待接收的第一个报文rcv_nxt,那么就更新rcv_nxt(加1)。如果配置了ackNodelay模式(无延迟ack)或者远端窗口为0(代表暂时不能发送用户数据),那么这里会立刻flush()发送ack。
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
IUINT32 una = kcp->snd_una;
IUINT32 maxack = 0;
int flag = 0;
if (data == NULL || (int)size < (int)IKCP_OVERHEAD) return -1;
while (1) {
IUINT32 ts, sn, len, una, conv;
IUINT16 wnd;
IUINT8 cmd, frg;
IKCPSEG *seg;
if (size < (int)IKCP_OVERHEAD) break;
data = ikcp_decode32u(data, &conv);
...
size -= IKCP_OVERHEAD;
if ((long)size < (long)len || (int)len < 0) return -2;
if (cmd != IKCP_CMD_PUSH && cmd != IKCP_CMD_ACK &&
cmd != IKCP_CMD_WASK && cmd != IKCP_CMD_WINS)
return -3;
kcp->rmt_wnd = wnd;
ikcp_parse_una(kcp, una); //删除小于snd_buf中小于una的segment
ikcp_shrink_buf(kcp); //更新snd_una为snd_buf中seg->sn或kcp->snd_nxt
if (cmd == IKCP_CMD_ACK) {
if (_itimediff(kcp->current, ts) >= 0) {
//更新rx_srtt,rx_rttval,计算kcp->rx_rto
ikcp_update_ack(kcp, _itimediff(kcp->current, ts));
}
//遍历snd_buf中(snd_una, snd_nxt),将sn相等的删除,直到大于sn
ikcp_parse_ack(kcp, sn);
ikcp_shrink_buf(kcp);
if (flag == 0) {
flag = 1; //快速重传标记
maxack = sn;
} else {
if (_itimediff(sn, maxack) > 0) {
maxack = sn;
}
}
}
else if (cmd == IKCP_CMD_PUSH) {
if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) < 0) {
ikcp_ack_push(kcp, sn, ts); //新segment的sn及ts放在acklist中
if (_itimediff(sn, kcp->rcv_nxt) >= 0) {
seg = ikcp_segment_new(kcp, len);
...
if (len > 0) {
memcpy(seg->data, data, len);
}
//1. 丢弃sn > kcp->rcv_nxt + kcp->rcv_wnd的segment;
//2. 逐一比较rcv_buf中的segment,若重复丢弃,非重复,新建segment加入;
//3. 检查rcv_buf的包序号sn,如果是待接收的序号rcv_nxt,且可以接收(接收队列小 于接收窗口),转移segment到rcv_buf,nrcv_buf减少,nrcv_que增加,rcv_nxt增加;
ikcp_parse_data(kcp, seg);
}
}
}
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
}
else {
return -3;
}
data += len;
size -= len;
}
if (flag != 0) {
ikcp_parse_fastack(kcp, maxack); //sn 大于snd_buf中包序号,可能有丢包发生
}
// 如果snd_una增加了那么就说明对端正常收到且回应了发送方发送缓冲区第一个待确认的包,此时需要更新cwnd(拥塞窗口)
if (_itimediff(kcp->snd_una, una) > 0) {
if (kcp->cwnd < kcp->rmt_wnd) {
IUINT32 mss = kcp->mss;
if (kcp->cwnd < kcp->ssthresh) {
kcp->cwnd++;
kcp->incr += mss;
} else {
if (kcp->incr < mss) kcp->incr = mss;
kcp->incr += (mss * mss) / kcp->incr + (mss / 16);
if ((kcp->cwnd + 1) * mss <= kcp->incr) {
kcp->cwnd++;
}
}
if (kcp->cwnd > kcp->rmt_wnd) {
kcp->cwnd = kcp->rmt_wnd;
kcp->incr = kcp->rmt_wnd * mss;
}
}
}
return 0;
}
ikcp_update_ack 会更新RTT和RTO等参数,该算法与TCP保持一致:
static void ikcp_update_ack(ikcpcb *kcp, IINT32 rtt)
{
IINT32 rto = 0;
if (kcp->rx_srtt == 0) { //rx_srtt初始为0时
kcp->rx_srtt = rtt;
kcp->rx_rttval = rtt / 2;
}
else { //rx_srtt 已经有值时
long delta = rtt - kcp->rx_srtt;
if (delta < 0) delta = -delta;
kcp->rx_rttval = (3 * kcp->rx_rttval + delta) / 4;
kcp->rx_srtt = (7 * kcp->rx_srtt + rtt) / 8;
if (kcp->rx_srtt < 1) kcp->rx_srtt = 1;
}
rto = kcp->rx_srtt + _imax_(kcp->interval, 4 * kcp->rx_rttval);
kcp->rx_rto = _ibound_(kcp->rx_minrto, rto, IKCP_RTO_MAX);
}
检查 kcp->update 是否更新,未更新直接返回。kcp->update 由 ikcp_update 更新,上层应用需要每隔一段时间(10-100ms)调用 ikcp_update 来驱动 KCP 发送数据;
准备将 acklist 中记录的 ACK 报文发送出去,即从 acklist 中填充 ACK 报文的 sn 和 ts 字段;
检查当前是否需要对远端窗口进行探测。由于 KCP 流量控制依赖于远端通知其可接受窗口的大小,一旦远端接受窗口 kcp->rmt_wnd 为0,那么本地将不会再向远端发送数据,因此就没有机会从远端接受 ACK 报文,从而没有机会更新远端窗口大小。在这种情况下,KCP 需要发送窗口探测报文到远端,待远端回复窗口大小后,后续传输可以继续。
在发送数据之前,先设置快重传的次数和重传间隔;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,即加快报文重传:
void ikcp_flush(ikcpcb *kcp)
{
// 逐一获取acklist中的sn和ts,编码成segment,以流的方式凑够mtu再发送
// flush acknowledges
count = kcp->ackcount;
for (i = 0; i < count; i++) {
size = (int)(ptr - buffer);
if (size + (int)IKCP_OVERHEAD > (int)kcp->mtu) {
ikcp_output(kcp, buffer, size);
ptr = buffer; //新起一个segment
}
ikcp_ack_get(kcp, i, &seg.sn, &seg.ts);
ptr = ikcp_encode_seg(ptr, &seg);
}
kcp->ackcount = 0;
// probe window size (if remote window size equals zero)
if (kcp->rmt_wnd == 0) {
if (kcp->probe_wait == 0) {
kcp->probe_wait = IKCP_PROBE_INIT;
kcp->ts_probe = kcp->current + kcp->probe_wait;
}
else {
//远端窗口为0,发送过探测请求,但是已经超过下次探测的时间
//更新probe_wait,增加为IKCP_PROBE_INIT+ probe_wait /2,但满足KCP_PROBE_LIMIT
//更新下次探测时间 ts_probe与 探测变量 为 IKCP_ASK_SEND,立即发送探测消息
if (_itimediff(kcp->current, kcp->ts_probe) >= 0) {
if (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;
kcp->ts_probe = kcp->current + kcp->probe_wait;
kcp->probe |= IKCP_ASK_SEND;
}
}
} else {
// 远端窗口不等于0,更新下次探测时间与探测窗口等待时间为0,不发送窗口探测
kcp->ts_probe = 0;
kcp->probe_wait = 0;
}
// flush window probing commands
if (kcp->probe & IKCP_ASK_SEND) {
seg.cmd = IKCP_CMD_WASK;
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 = (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;
// calculate window size
// 如果没有流控,窗口为发送窗口与远程窗口的最小值
// 如果存在流控,窗口为当前拥塞窗口、发送窗口,远程接收窗口三者最小值
cwnd = _imin_(kcp->snd_wnd, kcp->rmt_wnd);
if (kcp->nocwnd == 0) cwnd = _imin_(kcp->cwnd, cwnd);
// 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;
...
}
// calculate resent
resent = (kcp->fastresend > 0)? (IUINT32)kcp->fastresend : 0xffffffff;
rtomin = (kcp->nodelay == 0)? (kcp->rx_rto >> 3) : 0;
// flush data segments
for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = p->next) {
IKCPSEG *segment = iqueue_entry(p, IKCPSEG, node);
int needsend = 0;
//该segment 第一次发送
if (segment->xmit == 0) {
needsend = 1;
segment->xmit++;
segment->rto = kcp->rx_rto;
segment->resendts = current + segment->rto + rtomin;
}
//当前时间达到了重发时间,但并没有新的包到达,出现丢包, 重传
else if (_itimediff(current, segment->resendts) >= 0) {
needsend = 1;
segment->xmit++;
kcp->xmit++;
if (kcp->nodelay == 0) {
segment->rto += kcp->rx_rto; //rto 增加 1 * rto
} else {
segment->rto += kcp->rx_rto / 2; //rto 增加 1/2 * rto
}
segment->resendts = current + segment->rto;
lost = 1;
}
//segment的累计被跳过次数大于快速重传设定,需要重传
else if (segment->fastack >= resent) {
needsend = 1;
segment->xmit++;
segment->fastack = 0; //重置
segment->resendts = current + segment->rto; //新的resendts
change++; // 标识快重传发生
}
if (needsend) {
int size, 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;
}
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 = -1;
}
}
}
// flash remain segments
size = (int)(ptr - buffer);
if (size > 0) {
ikcp_output(kcp, buffer, size);
}
// 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;
}
if (lost) {
//丢失则阈值减半, cwd 窗口保留为 1
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;
}
}