28

玩转KCP(3)-流量控制 – 萌叔

 3 years ago
source link: http://vearne.cc/archives/39328
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | http://vearne.cc

KCP协议的很多东西都是脱胎于TCP协议,所以他们在思想和实现上是完全相通的。xtaci/kcp-go 包含FEC也不过4000多行代码,skywind3000/kcp 主要是C/C++的代码,也就2000多行,萌叔建议大家都去阅读下源码。
慢启动、拥塞避免、拥塞发生、快速重传,这些概念都非常唬人,但看完代码你会发现不过尔尔。

在开始正式的文章之前,萌叔打算问几个问题?

  • 流控是为了保护谁?
  • 在实现中如何体现?

2. 流控是为了保护谁?

TCP是全双工,这里简化一下,我们只看半双工的情况

94218e18-9662-11ea-8639-784f43a6cab8.jpeg

Sender发送数据给Receiver

1) Sender中的应用程序把数据写入到本机的发送缓冲区
2) 数据从发送缓冲区写入到链路中,链路可能是由实际的光缆、电缆、多个路由器节点组成。
3)数据从链路转交到Receiver的接收缓冲区
4)数据从接收缓冲区交给Receiver的应用程序
  • 发送缓冲区大小是有限的,它必须被保护起来
  • SenderReceiver之间的链路的收发能力也是有限的,且是与网络中的其它节点共享的,因此Link也必须受到保护
  • 接收缓冲区大小也是受限的,它也应该受到保护

3. 在实现中如何体现?

在实际实现中每一个需要保护的点,都有与之对应的参数,先上结论

3.1 使用发送端的发送窗口(snd_wnd)保护本机的发送缓冲区

3.2 使用拥塞窗口(cwnd)来保护发送端与接收端之间的链路

cwnd是动态变化的值, 算法与TCP协议基本相同

d4abb900-98d7-11ea-9364-784f43a6cab8.jpg

3.3 使用接收端的接收窗口(rmt_wnd, 表示接收窗口的空闲大小)保护接收端的接收缓冲区

rmt_wnd对应KCP协议的wnd, 由接收端汇报

回顾一下KCP协议

0               4   5   6       8 (BYTE)
+---------------+---+---+-------+
|     conv      |cmd|frg|  wnd  |
+---------------+---+---+-------+   8
|     ts        |     sn        |
+---------------+---------------+  16
|     una       |     len       |
+---------------+---------------+  24
|                               |
|        DATA (optional)        |
|                               |
+-------------------------------+

4. 分析一次完整的写入动作

4.1 发送窗口和接收窗口对写入的影响

此处输入图片的描述

在KCP中,数据被拆分成Segment

step1:先写入snd_queue

step2:从snd_queue移动到snd_buf

step3:snd_buf中的Segment,通过UDP套接字发出(flushBuffer())

完整代码见sess.go
func (s *UDPSession) WriteBuffers

此处输入图片的描述

补充说明 waitsnd的计算方法如下

// WaitSnd gets how many packet is waiting to be sent
func (kcp *KCP) WaitSnd() int {
    return len(kcp.snd_buf) + len(kcp.snd_queue)
}

从上面的流程图可以看出,如果在写入函数中,KCP首先尝试写发送缓冲区,如果发送缓冲区已经满了或者设置了

sess.SetWriteDelay(true)

KCP还会尝试立马执行flush操作,以释放发送缓冲区的空间。如果还是无法写入,那么只能等待写超时。
注意 除了WriteBuffers函数会执行flush操作,KCP内部还有一个定时器,周期性的调用flush函数。

// sess updater to trigger protocol
func (s *UDPSession) updater() {
    timer := time.NewTimer(0)
    for {
        select {
        case <-timer.C:
            s.mu.Lock()
            interval := time.Duration(s.kcp.flush(false)) * time.Millisecond
            waitsnd := s.kcp.WaitSnd()
            if waitsnd < int(s.kcp.snd_wnd) && waitsnd < int(s.kcp.rmt_wnd) {
                s.notifyWriteEvent()
            }
            s.uncork()
            s.mu.Unlock()
            timer.Reset(interval)
        case <-s.die:
            timer.Stop()
            return
        }
    }
}

4.2 拥塞窗口对写入的影响

注意:在KCP协议中,拥塞控制可以被关闭

让我们把注意力集中到snd_buff

snd_buff

某一时刻,snd_buf的情况如上图

  • snd_una 表示未被ack的Segment的最小编号
  • snd_nxt 表示下一个待发送的Segment的编号

可见 snd_unasnd_nxt之间的橘色区域就是所有没有被确认的Segment(粗略的认为)。这部分Segment目前正在发送端和接收端之间的链路上”飞”,为了避免链路拥塞,KCP希望限制这部分的区域应该小于cwnd

在实际的KCP实现中,

// 拥塞窗口已满
if(kcp.snd_nxt - kcp.snd_una > cwnd){
    // 就不再把`snd_queue`的Segment移动到`snd_buf`
}

详细代码见kcp.go
func (kcp *KCP) flush()

    // sliding window, controlled by snd_nxt && sna_una+cwnd
    newSegsCount := 0
    for k := range kcp.snd_queue {
        if _itimediff(kcp.snd_nxt, kcp.snd_una+cwnd) >= 0 {
            break
        }
        newseg := kcp.snd_queue[k]
        newseg.conv = kcp.conv
        newseg.cmd = IKCP_CMD_PUSH
        newseg.sn = kcp.snd_nxt
        kcp.snd_buf = append(kcp.snd_buf, newseg)
        kcp.snd_nxt++
        newSegsCount++
    }

显然对于上图的情况,有cwnd是5,那么还可以从snd_queue中移动2个Segmen到snd_buf

1.KCP协议简介


请我喝瓶饮料

微信支付码

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK