KCP源码解析系列(四)滑动窗口

KCP 的滑动窗口TCP 大体相同。倘若深入探究源码,便会发觉之前那些难以理解的理论,在实现方面只有寥寥几行代码。接下来,看看 KCP 是怎样达成这些机制的。

TCP的可靠传输的实现–滑动窗口

倘若您未曾了解过滑动窗口,不妨观看以下 12 分钟的视频。我敢赌 5 块钱,您看完后肯定能理解滑动窗口机制。

https://www.bilibili.com/video/BV194411h71z/?spm_id_from=333.337.search-card.all.click

KCP中的滑动窗口

窗口大小

kcp和TCP一样,也是全双工的,所以不单有发送窗口,也有接收窗口。

发送窗口由远端窗口和拥塞窗口共同决定。

	//发送窗口,调用ikcp_wndsize配置,默认32,
    kcp->snd_wnd = IKCP_WND_SND;
    //接收窗口,调用ikcp_wndsize配置,默认128,同时必须要大于128
	kcp->rcv_wnd = IKCP_WND_RCV;
    //对端窗口,接收端ack时,会将wnd带过来
	kcp->rmt_wnd = IKCP_WND_RCV;
    //拥塞窗口,动态计算,后边会讲到
	kcp->cwnd = 0;

发送方的滑动窗口控制

  • 发送方维护一个 snd_buf 队列,保存已经发送但尚未确认的 Segment。

  • 当发送新的 Segment 时,会检查 snd_wndcwnd,确保在窗口范围内发送。

  • 每次发送数据时,发送窗口滑动,新的数据包进入窗口,并放入 snd_buf 中。

接收方滑动窗口控制

  • 接收方维护一个 rcv_buf 队列,保存接收到但尚未处理的 Segment。

  • 当接收到一个新的 Segment 时,首先检查其是否在接收窗口范围内 (rcv_wnd)。

  • 接收窗口滑动,将新的数据包放入 rcv_buf,并从窗口中移除已经确认的数据包。

窗口滑动的实现(以发送窗口为例)

滑动窗口犹如节假日时的高铁站。在安检处,有一位工作人员把控着人员的入场。安检处外,人员有序地排着队,而安检处内同样存在一个小队伍,大家都在有条不紊地进行安检。这个小队伍的最大长度,就是窗口的大小,倘若安检处里的人员过多,工作人员便会禁止人员入场;一旦有一部分人完成安检得以放行,工作人员就会再度允许人员进入。

我们把第一个进入高铁站的人设置编号为0,然后接下来排队的人编号递增,

定义 snd.una为下一个进入高铁站的人员编号

定义 snd.nxt 为下一个进入安检口的人员编号,

那么 snd.nxt - send.una 就是安检处的人数,也就是发送了但未收到确认的的数据包的个数,

snd.una + snd.wnd - snd.nxt 就是还可以进入安检的人数,也就是可用窗口的大小。

编号小于snd.una的人,意味着已经进入高铁站了,我们就不用管了,

具体可以结合如下图示理解。

请添加图片描述
接收发送窗口的基本实现和发送窗口一致,具体参考下图,

请添加图片描述

滑动窗口的源码解析

滑动窗口中的队列
名称作用说明
snd_buf存储已经发送但尚未收到 ACK 确认的数据段当一个数据包被发送后,它会被放入 snd_buf 中,直到收到对端的确认 (ACK)。KCP 通过这个队列来追踪未确认的数据包,并在需要时进行重传
rcv_buf存储接收到但尚未被应用层处理的数据段当 KCP 接收到一个数据段时,如果该段是乱序到达的(即序列号不连续),它会被放入 rcv_buf 中。接收窗口在等待正确的序列号时,乱序数据会保留在这个队列中,直到可以被应用层处理
snd_queue存储待发送的数据段应用层调用 ikcp_send() 将数据传递给 KCP 协议栈后,数据会被切分成多个数据段并放入 snd_queue 中。KCP 会根据窗口大小和拥塞控制机制,从这个队列中取出数据段进行发送
rcv_queue存储已经按照正确顺序接收的数据段,等待应用层处理一旦数据段的序列号符合接收窗口的预期(即接收方期望的下一个序列号),这些数据段会被从 rcv_buf 移动到 rcv_queue 中,并等待应用层读取
snd_nxt的更新

只要发送窗口有空闲,就把send_queue中的数据移动到snd_buf

void ikcp_flush(ikcpcb *kcp)
{
    .....
	// calculate window size
	cwnd = _imin_(kcp->snd_wnd, kcp->rmt_wnd);
	if (kcp->nocwnd == 0) cwnd = _imin_(kcp->cwnd, cwnd);
    //当发送窗口有空闲,把kcp->snd_queue的数据移动到kcp->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++;
		...
        //snd_nxt++,表示发送但未确认的包+1
		newseg->sn = kcp->snd_nxt++;
        ...
	}
    ......
}

snd_una的更新

snd_una的更新主要依赖ikcp_shrink_buf函数,

static void ikcp_shrink_buf(ikcpcb *kcp)
{
    //获取snd_buf的首个包
	struct IQUEUEHEAD *p = kcp->snd_buf.next;

  //如果首个包和头节点不一致,说明snd_buf不为空,snd_una更新为该段的序列号 seg->sn。这表示当前最早未确认的段的序列号。
    if (p != &kcp->snd_buf) {
		IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node);
		kcp->snd_una = seg->sn;
    //如果首个包和头节点一致,说明snd_buf为空,所有的包都得到了ack,snd_una = snd_nxt,
	}	else {
		kcp->snd_una = kcp->snd_nxt;
	}
}

ikcp_shrink_buf执行的时机有两个,

  • ikcp_parse_una 调用后的 ikcp_shrink_buf

    • ikcp_parse_una(kcp, una); 解析了对端传来的 UNA(未确认的最小序列号),这个步骤会将所有序列号小于 una 的数据包从 snd_buf 中移除。因此需要调用 ikcp_shrink_buf 来更新 snd_una
  • ikcp_parse_ack 调用后的 ikcp_shrink_buf

    • ikcp_parse_ack(kcp, sn); 解析了对端传来的 ACK 确认包,确认了某些已经发送的包。这可能会导致 snd_buf 中的更多数据包被移除,尤其是当一个数据段的确认号正好匹配 snd_buf 中某个数据段的序列号时。此时 snd_buf 的状态可能再次变化,因此再次调用 ikcp_shrink_buf 来确保 snd_una 反映最新的发送缓冲区状态。
rcv_nxt 的更新

当接收到一个正确序列号的数据包后,rcv_nxt会递增,所以重点在于针对接收到的序列号做不同的处理,

检查数据包的序列号 sn 是否在接收窗口的范围内 (rcv_nxtrcv_nxt + rcv_wnd)。

如果 sn 小于 rcv_nxt,表示该包已经接收并处理过,直接丢弃。

如果 sn 在窗口范围内,但不等于 rcv_nxt,表示该包是乱序到达,先放入 rcv_buf,等待正确序列的数据包到达后再处理。

//---------------------------------------------------------------------
// parse data
//---------------------------------------------------------------------
void ikcp_parse_data(ikcpcb *kcp, IKCPSEG *newseg)
{
	struct IQUEUEHEAD *p, *prev;
	IUINT32 sn = newseg->sn;
	  //用于标识是否已经在接收缓冲区中找到相同序列号的数据包。
	int repeat = 0;
	//检查数据包的序列号 `sn` 是否在接收窗口的范围内 (`rcv_nxt` 到 `rcv_nxt + rcv_wnd`)。
	if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) >= 0 ||
		_itimediff(sn, kcp->rcv_nxt) < 0) {
		ikcp_segment_delete(kcp, newseg);
		return;
	}
	//rcv_buf是用于乱序数据包的处理的,所以可能存的是序号为1,3,5,7这样的数据,所以这里是遍历整个rcv_buf,把收到的包找到一个合适的位置插入
	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;
		}
		 //sn小于seg->sn,说明找到了合适的位置,结束循环
		if (_itimediff(sn, seg->sn) > 0) {
			break;
		}
	}

	if (repeat == 0) {
	 	//添加到p节点的后面
		iqueue_init(&newseg->node);
		iqueue_add(&newseg->node, p);
		kcp->nrcv_buf++;
	}	else {
		ikcp_segment_delete(kcp, newseg);
	}


	// 检查接收缓冲区中是否有按序到达的包,如果有,将其移到接收队列中
	while (! iqueue_is_empty(&kcp->rcv_buf)) {
		//取出接收缓冲区的第一个包
		IKCPSEG *seg = iqueue_entry(kcp->rcv_buf.next, IKCPSEG, node);
		if (seg->sn == kcp->rcv_nxt && kcp->nrcv_que < kcp->rcv_wnd) {
			//从rcv_buf中删除
			iqueue_del(&seg->node);
			kcp->nrcv_buf--;
			//添加到接收队列中
			iqueue_add_tail(&seg->node, &kcp->rcv_queue);
			kcp->nrcv_que++;
			 //接收窗口右移
			kcp->rcv_nxt++;
		}	else {
			break;
		}
	}

#endif
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值