[原创] 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;
}

热门相关:新幽灵鬼屋   他们在岛屿写作:我记得   新无主之花   大周仙吏   不负荣光,不负你