KCP[基于UDP的可靠信息传输]——对TCP的深入理解

引言

       因为自己对游戏还比较感兴趣,之前在学UDP的时候也听闻:在游戏中常使用:UDP实现可靠信息传输。但它究竟如何实现在我眼里却一直都是个迷。
       在一次学习中,偶然遇到了KCP的代码。虽然代码不长,但逻辑非常清晰,一下子就让我对基于UDP的可靠数据传输拨云见雾,同时竟然也对TCP的理解也愈加深刻了,实在是比较开心,因此在这里做一次简单分享聊以慰藉。
       最后是希望可以写一个小demo模拟一下UDP可靠传输的过程,当然,仅仅是模拟过程,不会有真正的udp等传输过程(那个看kcp就行),等到后面空闲下来,会自己实现一个udp可靠传输的过程。

知识预备——TCP

       如果大家对TCP的可靠信息传输有过了解,那对于滑动窗口——SR(select repeat)肯定也不会太陌生。KCP或者UDP实现可靠信息传输说的主要思想其实就是基于TCP的思想的基础上进行延伸和扩展的。
       正所谓大道三千殊途同归,若是我们能够对TCP有比较清晰的认识,对于我们揭开UDP可靠信息传输的面纱会有很大的帮助。
       所以在深入了解基于UDP的可靠信息传输之前,我们先来看看TCP是怎么做的。

TCP怎么保证数据可靠传输

       在讨论TCP是如何保证可靠消息传输之前,我们肯定要对所谓“可靠”有一个比较清晰的认知。
       在我看来,所谓可靠无异于三个方面:不丢不错不乱
        什么情况下会丢失?信道丢失分组或者网太卡等。
        什么情况下会错误?在传输过程中因为信道的问题发生了位错误等问题。
        什么情况下会乱序?由于信道的不可预知和丢包重发的机制,都会导致乱序的发生。

        如何解决上述存在的问题——RDT3.0
        1、解决包错误:通过校验和以及重传的机制可以保证包不错。
        2、解决包乱序:通过序列号和ACK应答的方式保证乱序、重传等问题。
        3、解决包丢失:通过超时重传的机制保证包丢失时可以通过一定手段重新获得。

       现在虽然保证了信息的可靠传输,但是上述是一个一个发包的,这样太慢了。如果想要追求信息传输的速度,就势必要引入流水线的机制,但这样又会存在问题:主要就是乱序到达的分组和重传分组如何解决的问题。

       简单点说就是:乱序到达的分组,我们是丢弃呢?还是通过某种手段把它存起来呢?如果存起来我又怎么知道和前面的分组对应起来呢?
       重传分组我怎么知道是重传呢?接收到重传分组怎么处理呢?

       为了应对流水线引入的问题,TCP引入了滑动窗口的概念,比如GBN、SR等。
       这里重点研究一下SR——接收方和发送方都设置缓存窗口,接收方对每个分组单独进行确认。 这正是KCP中实现udp可靠数据传输的思想。

滑动窗口协议——SR

请添加图片描述
       先看一下SR简单示意图,结合之前对SR的介绍:接收方和发送方都设置缓存窗口,接收方对每个分组单独进行确认。我们简单分析一下SR究竟是如何处理数据的。详见:中国大学mooc

       发送方将窗口内的包发送,如果接受的包是窗口最左边期望的,就可以移动窗口,由于乱序的问题,可能后面的包早已接受到了ACK,所以在移动的窗口的时候并不是一个一个的移动,而是根据接收包的情况进行移动。

       接收方和发送方类似,缓存在窗口内的包,直到是期望的包后才进行窗口滑动。

       具体细节这里不多做介绍,大家记住这两个队列即可,后面的KCP代码分析中会对这两个队列着重分析。(SR更多细节详见:中国大学mooc)

UDP的可靠信息传输——结合KCP源码分析

KCP发送方

设计分析

       在了解源码之前,还是一样思考几个问题:
       1、如果包很大怎么办?比如说一个包10M。
       2、窗口是怎么知道哪些包需要发送,哪些包已经收到了ack不需要发送,哪些包需要超时重传呢?
       3、我们是如何得知包在窗口内还是在窗口外呢?

       带着这几个问题,我们便能很自然的得倒我们的解决方案。
       第一个问题:可以借鉴TCP分片的思想,既然包很大,那我就把他切片,一次发一个片就可以解决了。
       第二个问题:可以设置两个队列——发送队列和窗口队列。发送队列记录的是待发送的新数据,窗口队列是真正的滑动窗口,并用send_next和window_size记录窗口大小(即窗口左边界+窗口大小);超时可以用定时器或者时间戳解决;是否收到ack用标志位记录即可,快速重传使用segment->xmit(包的发送次数)即可。
       第三个问题:窗口队列是真正的滑动窗口,并用send_next和window_size记录窗口大小;只有序列号在[send_next,send_next+windows_size]之间才认为在窗口内。

       下面结合之间我们的分析,得出发送方发送数据的流程图:
请添加图片描述

源码分析

       在分析源码之前,其实是要分析kcp的头部的数据结构的,但这部分一来网上已经很多博主进行相关的分享,二来我们讨论的只是它的一个局部,也就是说我们只需要知道它的部分含义即可,所以这里就不多做说明,感兴趣的同学可以去看看kcp源码:KCP C版本
       在KCP中,和上述过程对应的就是send和flush两个函数,下面我们分别看看这两个函数的具体内容,揭开我们对UDP可靠数据传输的神秘面纱。

ikcp_send
int ikcp_send(ikcpcb *kcp, const char *buffer, int len)
{
   
    IKCPSEG *seg;
    int count, i;

    assert(kcp->mss > 0);
    if (len < 0) return -1;

    //流模式,先不用关注
    if (kcp->stream != 0) {
   
        //...
    }

	//****** 第一部分 *********
    if (len <= (int)kcp->mss) count = 1;
    else count = (len + kcp->mss - 1) / kcp->mss;

	//关于拥塞控制的,先不用关注
    if (count >= (int)IKCP_WND_RCV) return -2;
    
	if (count == 0) count = 1;

	//******* 第二部分 *********
    // fragment
    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;
}

       前面if (kcp->stream != 0)是关于流方式的逻辑,可以先不用关注,直接看到第一部分:
       第一部分是在干嘛呢?其实就是在做我们流程中提到的分片过程,每一个分片大小是mss,那么要分片数量就是count。——至此我们算出了要分多少片

       第二部分又是在做什么呢?
       (1)第一个size是控制最后一片或者是出现长度不到一个mss的情况。
       (2)之后通过ikcp_segment_new新建一个segment对象,再之后memcpy(seg->data, buffer, size);把buffer的size个字节的内容copy到data中。
       (3)然后就是对kcp头部的说明:seg->len记录这个片的长度,seg->frg记录这个片是第几个片,方便后面组装,同时要注意的是这个片最后一片是0。
       (4)最后就是通过iqueue_add_tail函数将这个片放到发送队列的队尾,然后让发送队列长度+1,即kcp->nsnd_que++。
       第二部分总结一下就是流程图里叙述道的:将数据封装到segment里面,然后加入到发送队列中去

       细心的同学可能会比较疑惑:怎么send把数据加到队列里面去就不管了呢?那到底是由谁发送的呢?

       真正的发送工作是由flush这个函数完成,这个函数会被周期性的调用,进行数据的发送。下面我们来看看这个函数的具体细节。

ikcp_flush
void ikcp_flush(ikcpcb *kcp)
{
   
	//********** 第一部分 ************
    //初始化定义下面用到的数据结构:略

    // 'ikcp_update' haven't been called.
    if (kcp->updated == 0) return;

	//初始化赋值部分:略
    //...


    //********** 第二部分 ************
    // 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;
        }
        ikcp_ack_get(kcp, i, &seg.sn, &seg.ts);
        ptr = ikcp_encode_seg(ptr, &seg);
    }

    kcp->ackcount = 0;


    //********** 第三部分 ************
    if 
  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值