[原创] KCP 源码解析(下)
ikcp_input
先从下层协议将数据读出来,并将对应的包头信息解析出来,根据不同的包头命令进入不同的处理逻辑。
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
IUINT32 prev_una = kcp->snd_una;
IUINT32 maxack = 0, // 收到的这组包里最大的ack
latest_ts = 0; // 以及最近的时间戳
int flag = 0; // 接收到的数据包里有CMD_ACK则为1
if (ikcp_canlog(kcp, IKCP_LOG_INPUT)) {
ikcp_log(kcp, IKCP_LOG_INPUT, "[RI] %d bytes", (int)size);
}
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;
// 数据报文长度小于 kcp 包头大小, 头部信息不完整, 退出
if (size < (int)IKCP_OVERHEAD) break;
// 取得所有包头信息
data = ikcp_decode32u(data, &conv);
if (conv != kcp->conv) return -1;
data = ikcp_decode8u(data, &cmd);
data = ikcp_decode8u(data, &frg);
data = ikcp_decode16u(data, &wnd);
data = ikcp_decode32u(data, &ts);
data = ikcp_decode32u(data, &sn);
data = ikcp_decode32u(data, &una);
data = ikcp_decode32u(data, &len);
// 减去包头大小
size -= IKCP_OVERHEAD;
// 数据报文长度小于包头指定的数据长度,数据被破坏,退出
if ((long)size < (long)len || (int)len < 0) return -2;
// 非法cmd, 退出
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);
ikcp_shrink_buf(kcp);
...
}
}
先看一下最后两行的 ikcp_parse_una 和 ikcp_shrink_buf,kcp 收到对方发送的 una,说明 una 之前的包已经收到,那么接收方就可以将 snd_buf里的 una 之前的包删掉,ikcp_parse_una 就是做这个的。
// 从snd_buf(双向循环链表)中删除小于una的包
static void ikcp_parse_una(ikcpcb *kcp, IUINT32 una)
{
struct IQUEUEHEAD *p, *next;
for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = next) {
IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node);
next = p->next;
// 如果该sendbuf的序列号小于una, 则说明该包已经被确认, 从snd_buf中删除
if (_itimediff(una, seg->sn) > 0) {
iqueue_del(p); // 把seg里边的node指针断了
ikcp_segment_delete(kcp, seg);
kcp->nsnd_buf--;
} else {
break;
}
}
}
上边的函数把 snd_buf 里的数据包删了,对应的 snd_una 也需要更新,ikcp_shrink_buf 就是更新 snd_una 的。
// 调用这个函数之前删除了一些被确认的包, 更新snd_una为当前发送缓冲里的最小序列号
static void ikcp_shrink_buf(ikcpcb *kcp)
{
struct IQUEUEHEAD *p = kcp->snd_buf.next;
if (p != &kcp->snd_buf) {
IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node); // 发送缓冲区第一个数据包
kcp->snd_una = seg->sn;
} else {
kcp->snd_una = kcp->snd_nxt;
}
}
前期工作处理完,然后根据不同的命令来处理包。
IKCP_CMD_ACK
当收到的命令为IKCP_CMD_ACK,说明该包是一个 ACK,需要将被ACK的包从 snd_buf 里删除,然后更新 snd_una,用的还是上边那两个函数。然后是更新maxack(这组包里最大的ack)和latest_ts(最近的时间戳),后边用于计算snd_buf里哪些包被跳过了(失序),决定是否快速重传。
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
...
// 循环将底层收到的数据读取并解析
while (1) {
IUINT32 ts, sn, len, una, conv;
IUINT16 wnd;
IUINT8 cmd, frg;
IKCPSEG *seg;
...
if (cmd == IKCP_CMD_ACK) {
// ts为该数据包发送时间, current为当前时间(收到该数据包的ack), 两者之差为rtt
if (_itimediff(kcp->current, ts) >= 0) {
// 计算超时重传时间
ikcp_update_ack(kcp, _itimediff(kcp->current, ts));
}
ikcp_parse_ack(kcp, sn);
ikcp_shrink_buf(kcp);
// 下边的代码就是更新maxack和latest_ts,
if (flag == 0) { // 只有在解析第一个IKCP_CMD_ACK包进入这个分支
flag = 1;
maxack = sn;
latest_ts = ts;
} else {
if (_itimediff(sn, maxack) > 0) { // 这里不能用max函数,因为有序号回绕的问题
#ifndef IKCP_FASTACK_CONSERVE
maxack = sn;
latest_ts = ts;
#else
if (_itimediff(ts, latest_ts) > 0) {
maxack = sn;
latest_ts = ts;
}
#endif
}
}
}
...
}
}
// 计算超时重传时间, 和计网自顶向下3.5.3节的公式一样
static void ikcp_update_ack(ikcpcb *kcp, IINT32 rtt)
{
// rtt:该数据包的往返时间
// rx_srtt: 平滑的rtt,近8次rtt平均值
// rx_rttval: 近4次rtt和srtt的平均差值,反应了rtt偏离srtt的程度
// rx_rto: 重传超时时间
IINT32 rto = 0;
if (kcp->rx_srtt == 0) {
kcp->rx_srtt = rtt;
kcp->rx_rttval = rtt / 2;
} else {
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); // rx_srtt + 4*rx_rttval表示一种比较坏的情况
kcp->rx_rto = _ibound_(kcp->rx_minrto, rto, IKCP_RTO_MAX);
}
IKCP_CMD_PUSH
当收到的命令为IKCP_CMD_ACK,说明是数据包。新建一个 IKCPSEG,将头信息和data 拷贝进去。然后调用 ikcp_ack_push 将该数据包加入 acklist,以便发送 ack 。
然后检查该 seg 是否已经接受过,如果接收过则忽略,如果没有接收过则将其加入 rcv_buf,之后将 rcv_buf 里连续序号的包转移到 rcv_que供上层读取,通过检查rcv_nxt即可判断数据包是否连续。这个检查过程是 ikcp_parse_data 实现的。
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
...
// 循环将底层收到的数据读取并解析
while (1) {
IUINT32 ts, sn, len, una, conv;
IUINT16 wnd;
IUINT8 cmd, frg;
IKCPSEG *seg;
...
else if (cmd == IKCP_CMD_PUSH) {
if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) < 0) { // 判断是否超出(大于)接收窗口
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) {
// (to , from, len)
memcpy(seg->data, data, len); // 拷贝数据
}
// 检查newseg数据包,如果没有接受过,就将其放在接收缓存内,
// 并将缓存内连续的数据放到接受队列里,可供上层应用读取。
ikcp_parse_data(kcp, seg);
}
}
}
...
}
}
// 收到对方发送的sn号包,更新acklist,acknode的格式为(sn, ts), sn这个包的发出时间为ts
// 注意可能收到多个sn相同的包
static void ikcp_ack_push(ikcpcb *kcp, IUINT32 sn, IUINT32 ts)
{
IUINT32 newsize = kcp->ackcount + 1;
IUINT32 *ptr;
// 申请更大的acklist空间
if (newsize > kcp->ackblock) {
IUINT32 *acklist;
IUINT32 newblock;
for (newblock = 8; newblock < newsize; newblock <<= 1); // 倍增的方式扩容
acklist = (IUINT32*)ikcp_malloc(newblock * sizeof(IUINT32) * 2); // 申请newblock个acknode空间
if (acklist == NULL) {
assert(acklist != NULL);
abort();
}
if (kcp->acklist != NULL) { // 将旧的acklist拷贝到新的acklist中
IUINT32 x;
for (x = 0; x < kcp->ackcount; x++) { // 从这里看出来每个acknode占用2个IUINT32
acklist[x * 2 + 0] = kcp->acklist[x * 2 + 0];
acklist[x * 2 + 1] = kcp->acklist[x * 2 + 1];
}
ikcp_free(kcp->acklist);
}
kcp->acklist = acklist;
kcp->ackblock = newblock;
}
ptr = &kcp->acklist[kcp->ackcount * 2];
ptr[0] = sn;
ptr[1] = ts;
kcp->ackcount++;
}
// 检查newseg数据包,如果没有接受过,就将其放在接收缓存内,并将缓存内连续的数据放到接受队列里,可供上层应用读取。
void ikcp_parse_data(ikcpcb *kcp, IKCPSEG *newseg)
{
struct IQUEUEHEAD *p, *prev;
IUINT32 sn = newseg->sn;
int repeat = 0;
// 新数据包在接收窗口之外, 直接丢弃
if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) >= 0 ||
_itimediff(sn, kcp->rcv_nxt) < 0) {
ikcp_segment_delete(kcp, newseg);
return;
}
// 判断是否接受过sn包
for (p = kcp->rcv_buf.prev; p != &kcp->rcv_buf; p = prev) {
IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node);
prev = p->prev;
if (seg->sn == sn) {
repeat = 1;
break;
}
// 当前包序号大于被检查的包,之后就不可能有 seg->sn 了,因为 rcv_buf 的链表 sn 是递增的。
if (_itimediff(sn, seg->sn) > 0) {
break;
}
}
if (repeat == 0) { // 没有接受过
iqueue_init(&newseg->node);
iqueue_add(&newseg->node, p);
kcp->nrcv_buf++;
} else {
ikcp_segment_delete(kcp, newseg);
}
// 将收到的连续数据包从rcv_buf 转移到 rcv_queue 供上层应用读取
while (! iqueue_is_empty(&kcp->rcv_buf)) {
IKCPSEG *seg = iqueue_entry(kcp->rcv_buf.next, IKCPSEG, node);
// 通过检查rcv_nxt即可判断数据包是否连续
if (seg->sn == kcp->rcv_nxt && kcp->nrcv_que < kcp->rcv_wnd) { //数据包连续 且 rcv_queue 不满
iqueue_del(&seg->node);
kcp->nrcv_buf--;
iqueue_add_tail(&seg->node, &kcp->rcv_queue);
kcp->nrcv_que++;
kcp->rcv_nxt++;
} else {
break;
}
}
...
}
IKCP_CMD_WASK
当收到的命令为 IKCP_CMD_WASK,说明对方想探测你的窗口大小。只需要在本地标记一下,等到发送的时候会检查这个标记然后发送窗口大小。
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;
}
IKCP_CMD_WINS
当收到的命令为 IKCP_CMD_WASK,说明对方回答了你的窗口探测,在这里不需要做处理。
检查失序(冗余)ACK
当收完数据包,KCP 会检查哪些数据包被跳过了,然后标记其被跳过的次数。不过有人会发现通过 maxack 去检查哪个数据包被跳过会不会有问题?因为小于 maxack 的包可能也收到了,但也要记录一次被跳过。答案是不会的,因为在收到IKCP_CMD_ACK的时候会调用ikcp_parse_ack; ikcp_shrink_buf ; 这两个函数,而这两个函数会把所有收到ack 的包从 snd_buf 删除掉,那么在ikcp_parse_fastack 中也就不会检查到这个包了。
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
...
// 循环将底层收到的数据读取并解析
while (1) {
...
if (cmd == IKCP_CMD_ACK) ...
else if(cmd == IKCP_CMD_PUSH) ...
else if(cmd == IKCP_CMD_WASK) ...
else if(cmd == IKCP_CMD_WINS) ...
else return -3;
...
}
// 只要收到了IKCP_CMD_PUSH,flag 就为 true
if (flag != 0) {
ikcp_parse_fastack(kcp, maxack, latest_ts);
}
}
// 计算快速重传的参数fastack(发送缓存里的包被跳过的总次数,冗余ACK)
static void ikcp_parse_fastack(ikcpcb *kcp, IUINT32 sn, IUINT32 ts)
{
struct IQUEUEHEAD *p, *next;
if (_itimediff(sn, kcp->snd_una) < 0 || _itimediff(sn, kcp->snd_nxt) >= 0)
return;
// 统计maxack之前的有几个没有被ack包
for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = next) {
IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node);
next = p->next;
if (_itimediff(sn, seg->sn) < 0) {
break;
}
else if (sn != seg->sn) { // 被跳过一次
#ifndef IKCP_FASTACK_CONSERVE
seg->fastack++; // 该数据包被跳过一次
#else
if (_itimediff(ts, seg->ts) >= 0)
seg->fastack++;
#endif
}
}
}
计算拥塞窗口
上边的工作做完后,需要计算并更新拥塞窗口,
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
...
/*
接收时,拥塞窗口增加
拥塞窗口控制, 慢启动:每收到一个ACK,拥塞窗口就加1,一个RTT拥塞窗口会翻倍增长。
拥塞避免:每收到一个ACK,拥塞窗口就加1/cwnd, 一个RTT拥塞窗口加1
下边发送时,拥塞窗口减少
*/
if (_itimediff(kcp->snd_una, prev_una) > 0) {
if (kcp->cwnd < kcp->rmt_wnd) {
IUINT32 mss = kcp->mss;
if (kcp->cwnd < kcp->ssthresh) { // 拥塞窗口小于慢启动阈值,快速增加拥塞窗口
kcp->cwnd++; // 慢启动
kcp->incr += mss;
// 达到阈值的时候 cwnd*mss = incr
} else { // 拥塞窗口大于慢启动阈值,拥塞避免模式
if (kcp->incr < mss) kcp->incr = mss;
// 经过计算,差不多是每个RTT增加一个mss, 间隔略小于一个RTT,也就是更快地增加拥塞窗口
// incr = k*mss , 拥塞窗口等于floor(k)
kcp->incr += (mss * mss) / kcp->incr + (mss / 16);
if ((kcp->cwnd + 1) * mss <= kcp->incr) {
#if 1
kcp->cwnd = (kcp->incr + mss - 1) / ((mss > 0)? mss : 1);
#else
kcp->cwnd++;
#endif
}
}
if (kcp->cwnd > kcp->rmt_wnd) {
kcp->cwnd = kcp->rmt_wnd;
kcp->incr = kcp->rmt_wnd * mss;
}
}
}
}
ikcp_recv
ikcp_recv 是用户调用的接口,期待收到长度最多为 len 的消息。
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;
assert(kcp);
if (iqueue_is_empty(&kcp->rcv_queue))
return -1;
if (len < 0) len = -len;
peeksize = ikcp_peeksize(kcp); // 查看用户消息大小,主要检查buffer大小能不能装下这个消息
if (peeksize < 0)
return -2;
// 用户的 buffer 不够装
if (peeksize > len)
return -3;
// 这里发现接收队列里数据太多,则标记一下,将会向对方发送一个窗口大小防止对面太快的发数据
if (kcp->nrcv_que >= kcp->rcv_wnd)
recover = 1;
// 将一个数据包拷贝进用户 buffer
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--;
}
// 非字节流模式 : 读到最后一段,已经组装出一个完整的上层数据包
// 字节流模式:每个包的frg都为0,每次recv只会读到一个包
// 也有可能遇不到fragment为0的包,消息的其他部分还在接收缓存或者网络中
if (fragment == 0)
break;
}
assert(len == peeksize);
// move available data from rcv_buf -> rcv_queue
// ikcp_input里已经转移过一次了, 用户把数据读走后rcv_que变小了, 这里再转移一次
while (! iqueue_is_empty(&kcp->rcv_buf)) {
seg = iqueue_entry(kcp->rcv_buf.next, IKCPSEG, node);
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包的大小
int ikcp_peeksize(const ikcpcb *kcp)
{
struct IQUEUEHEAD *p;
IKCPSEG *seg;
int length = 0;
assert(kcp);
if (iqueue_is_empty(&kcp->rcv_queue)) return -1;
seg = iqueue_entry(kcp->rcv_queue.next, IKCPSEG, node);
if (seg->frg == 0) return seg->len;
// 如果接收队列里的数据不足一个包,返回-1
if (kcp->nrcv_que < seg->frg + 1) return -1;
for (p = kcp->rcv_queue.next; p != &kcp->rcv_queue; p = p->next) {
seg = iqueue_entry(p, IKCPSEG, node);
length += seg->len;
if (seg->frg == 0) break;
}
return length;
}