kcp源码走读

kcp协议与tcp协议类似,是一种ARQ协议。他的优点在于比tcp的延迟更小30%-40%,但相应的会牺牲一部分的带宽,大该比tcp多浪费10%~20%。tcp的设计目标是增大网络利用率,而kcp的设计目标是增大网络传输速率。因此kcp与tcp对比,kcp有如下机制可以提高传输速度:

1.kcp的RTO每次是增加为1.5倍,相比tcp的2倍,具有更短的超时重传时间
2.无延迟ACK,通过配置让ack立即发送,而tcp为了增加网络利用率会尽量让ack与用户数据一起发送
3.快速重传门限可控制,可以适当缩小fastack门限,提高重传响应速度
4.earlyRetrans机制,无用户数据时立刻发送ack
5.拥塞控制可取消,取消拥塞控制后,用户的发包速率不受网络拥塞的影响

 

golang版本kcp源码下载地址:https://github.com/skywind3000/kcp

 

一个发送数据接收数据的基本流程如下

//用户有一段数据buf需要发送,于是调用send函数

kcp1.Send(buf.Bytes())//send函数将buffer分片成kcp的数据包格式,存在待发送队列中

kcp1.flush()//将发送队列中的数据通过下层协议(UDP)进行发送

//kcp2接收到下层协议(UDP)传进来的数据底层数据buffer

kcp2.Input(buffer[:hr], true, false)//调用Input函数将UDP接收的数据buffer转换成kcp的数据包格式

hr = int32(kcp2.Recv(buffer[:10]))//kcp2将接收的kcp数据包还原成kcp1发送的buffer数据

 在Send的时候用户数据长度不作限制,但在Recv的时候不一定能够一次性接收完Send的所有数据,用户在Recv后应该做校验。


 
func (kcp *KCP) Recv(buffer []byte) (n int)

    // merge fragment
    count := 0
    for k := range kcp.rcv_queue {
        seg := &kcp.rcv_queue[k]
        copy(buffer, seg.data)
        buffer = buffer[len(seg.data):]
        n += len(seg.data)
        count++
        kcp.delSegment(*seg)
        if seg.frg == 0 {
            break
        }
    }
    if count > 0 {
        kcp.rcv_queue = kcp.remove_front(kcp.rcv_queue, count)
    }

count = 0
    for k := range kcp.rcv_buf {
        seg := &kcp.rcv_buf[k]
        if seg.sn == kcp.rcv_nxt && len(kcp.rcv_queue) < int(kcp.rcv_wnd) {
            kcp.rcv_nxt++
            count++
        } else {
            break
        }
    }

    if count > 0 {
        kcp.rcv_queue = append(kcp.rcv_queue, kcp.rcv_buf[:count]...)
        kcp.rcv_buf = kcp.remove_front(kcp.rcv_buf, count)
    }
// fast recover
    if len(kcp.rcv_queue) < int(kcp.rcv_wnd) && fast_recover {
        // ready to send back IKCP_CMD_WINS in ikcp_flush
        // tell remote my window size
        kcp.probe |= IKCP_ASK_TELL
    }

recv函数将接收消息队列中的数据包还原成原来的消息格式,通过buffer返回给调用者

还会把rcv_buf中的与接收序号相匹配的数据拷贝到rcv_queue中。这里注意到在Input->parse_data函数中有同样的处理,这里之所以需要重复处理是因为kcp.rcv_queue的大小可能会发生改变,len(kcp.rcv_queue) < int(kcp.rcv_wnd)条件有可能重新成立。

fast_recover标识的意思是快速告知对端我又有窗口大小空出来了,因为在Input函数中有可能窗口会满了,此时发送给对端的是窗口满消息,而在recv过后,因为取走了消息,可用接收窗口又变大了,此时需要快速告知对端可以继续发消息了。

 

 

func (kcp *KCP) Send(buffer []byte) int {
    var count int
    if len(buffer) == 0 {
        return -1
    }

    // append to previous segment in streaming mode (if possible)
    if kcp.stream != 0 {
        n := len(kcp.snd_queue)
        if n > 0 {
            seg := &kcp.snd_queue[n-1]
            if len(seg.data) < int(kcp.mss) {
                capacity := int(kcp.mss) - len(seg.data)
                extend := capacity
                if len(buffer) < capacity {
                    extend = len(buffer)
                }

                // grow slice, the underlying cap is guaranteed to
                // be larger than kcp.mss
                oldlen := len(seg.data)
                seg.data = seg.data[:oldlen+extend]
                copy(seg.data[oldlen:], buffer)
                buffer = buffer[extend:]
            }
        }

        if len(buffer) == 0 {
            return 0
        }
    }

    if len(buffer) <= int(kcp.mss) {
        count = 1
    } else {
        count = (len(buffer) + int(kcp.mss) - 1) / int(kcp.mss)
    }

    if count > 255 {
        return -2
    }

    if count == 0 {
        count = 1
    }

    for i := 0; i < count; i++ {
        var size int
        if len(buffer) > int(kcp.mss) {
            size = int(kcp.mss)
        } else {
            size = len(buffer)
        }
        seg := kcp.newSegment(size)
        copy(seg.data, buffer[:size])
        if kcp.stream == 0 { // message mode
            seg.frg = uint8(count - i - 1)
        } else { // stream mode
            seg.frg = 0
        }
        kcp.snd_queue = append(kcp.snd_queue, seg)
        buffer = buffer[size:]
    }
    return 0
}

send函数主要功能是把用户需要发送的字符数组转化成kcp的数据包。如果用户的数据超过一个MSS,还会对数据进行分片。这里有两种分片的方式,消息方式和流方式。消息方式把用户数据分片,为每个分片设置ID,将分片后的数据一个一个地存入发送队列种,接收方通过id解析回原来的包,消息方式一个分片的数据量可能不能达到MSS(最大分片大小)。流方式则是会检测每个发送队列里的分片是否达到最大mss,如果没有达到就会用新的数据填充分片。流方式的网络速度优于消息方式,但是流方式接收方接收时是一个分片一个分片地接收,而消息方式kcp接收函数会自己把原本属于一个数据的分片重组回来。

 

func (kcp *KCP) flush(ackOnly bool)

flush函数大致功能如下:

发送ack

发送探测窗口消息

将发送队列中的消息存入缓存队列(缓存队列实际上就是发送窗口)

检查缓存队列中当前需要发送的数据(包括新传数据与重传数据)

根据重传数据更新发送窗口大小

if change > 0 {
        inflight := kcp.snd_nxt - kcp.snd_una
        kcp.ssthresh = inflight / 2
        if kcp.ssthresh < IKCP_THRESH_MIN {
            kcp.ssthresh = IKCP_THRESH_MIN
        }
        kcp.cwnd = kcp.ssthresh + resent
        kcp.incr = kcp.cwnd * kcp.mss
    }

在发生快速重传的时候,会将慢启动阈值调整为当前发送窗口的一半,并把拥塞窗口大小调整为kcp.ssthresh + resent,resent是触发快速重传的丢包的次数,resent的值代表的意思在被弄丢的包后面收到了resent个数的包的ack。这样调整后kcp就进入了拥塞控制状态。

 

    if lost > 0 {
        kcp.ssthresh = cwnd / 2
        if kcp.ssthresh < IKCP_THRESH_MIN {
            kcp.ssthresh = IKCP_THRESH_MIN
        }
        kcp.cwnd = 1
        kcp.incr = kcp.mss
    }

如果发生的超时重传,那么就重新进入慢启动状态。

 

func (kcp *KCP) Input(data []byte, regular, ackNoDelay bool) int {
    una := kcp.snd_una
    if len(data) < IKCP_OVERHEAD {
        return -1
    }

    var maxack uint32
    var lastackts uint32
    var flag int
    var inSegs uint64

    for {
        var ts, sn, length, una, conv uint32
        var wnd uint16
        var cmd, frg uint8

        if len(data) < 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, &length)
        if len(data) < int(length) {
            return -2
        }

        if cmd != IKCP_CMD_PUSH && cmd != IKCP_CMD_ACK &&
            cmd != IKCP_CMD_WASK && cmd != IKCP_CMD_WINS {
            return -3
        }

        // only trust window updates from regular packets. i.e: latest update
        if regular {

            kcp.rmt_wnd = uint32(wnd)

        }
        kcp.parse_una(una)
        kcp.shrink_buf()

        if cmd == IKCP_CMD_ACK {
            kcp.parse_ack(sn)
            kcp.shrink_buf()
            if flag == 0 {
                flag = 1
                maxack = sn
            } else if _itimediff(sn, maxack) > 0 {
                maxack = sn
            }
            lastackts = ts
        } else if cmd == IKCP_CMD_PUSH {
            if _itimediff(sn, kcp.rcv_nxt+kcp.rcv_wnd) < 0 {
                kcp.ack_push(sn, ts)
                if _itimediff(sn, kcp.rcv_nxt) >= 0 {
                    seg := kcp.newSegment(int(length))
                    seg.conv = conv
                    seg.cmd = cmd
                    seg.frg = frg
                    seg.wnd = wnd
                    seg.ts = ts
                    seg.sn = sn
                    seg.una = una
                    copy(seg.data, data[:length])
                    kcp.parse_data(seg)
                } else {
                    atomic.AddUint64(&DefaultSnmp.RepeatSegs, 1)
                }
            } else {
                atomic.AddUint64(&DefaultSnmp.RepeatSegs, 1)
            }
        } 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
        } else if cmd == IKCP_CMD_WINS {
            // do nothing
        } else {
            return -3
        }

        inSegs++
        data = data[length:]
    }
    atomic.AddUint64(&DefaultSnmp.InSegs, inSegs)

    if flag != 0 && regular {
        kcp.parse_fastack(maxack)
        current := currentMs()
        if _itimediff(current, lastackts) >= 0 {
            kcp.update_ack(_itimediff(current, lastackts))
        }
    }

    if _itimediff(kcp.snd_una, una) > 0 {
        if kcp.cwnd < kcp.rmt_wnd {
            mss := kcp.mss
            if kcp.cwnd < kcp.ssthresh {
                kcp.cwnd++
                kcp.incr += mss
            } else {
                if kcp.incr < mss {
                    kcp.incr = mss
                }
                kcp.incr += (mss*mss)/kcp.incr + (mss / 16)
                if (kcp.cwnd+1)*mss <= kcp.incr {
                    kcp.cwnd++
                }
            }
            if kcp.cwnd > kcp.rmt_wnd {
                kcp.cwnd = kcp.rmt_wnd
                kcp.incr = kcp.rmt_wnd * mss
            }
        }
    }

    if ackNoDelay && len(kcp.acklist) > 0 { // ack immediately
        kcp.flush(true)
    } else if kcp.rmt_wnd == 0 && len(kcp.acklist) > 0 { // window zero
        kcp.flush(true)
    }
    return 0
}

input函数接收udp协议传过来的报文,把udp报文解码成kcp报文进行缓存。

kcp报文分为ack报文,数据报文,探测窗口报文,响应窗口报文四种。

kcp报文的una字段表示对端希望接收的下一个kcp包序号,也就是说明接收端已经收到了所有小于una序号的kcp包。解析una字段后需要把发送缓冲区里面包序号小于una的包全部丢弃掉。 ack报文则包含了对端收到的kcp包的序号,接到ack包后需要删除发送缓冲区中与ack包中的发送包序号(sn)相同的kcp包。上述una和ack处理完后,需要更新kcp.snd_una(发送端第一个未被确认的包),如果snd_una增加了那么就说明对端正常收到且回应了发送方发送缓冲区第一个待确认的包,此时需要更新cwnd(拥塞窗口)

收到数据报文时,需要判断数据报文是否在接收窗口内,如果是则保存ack,如果数据报文的sn正好是待接收的第一个报文rcv_nxt,那么就更新rcv_nxt(加1)。如果配置了ackNodelay模式(无延迟ack)或者远端窗口为0(代表暂时不能发送用户数据),那么这里会立刻fulsh()发送ack。

 

posted on 2017-05-10 18:21  skyer1992  阅读(5854)  评论(0编辑  收藏  举报

导航