引言
UDP与TCP如何选择?
选项 | UDP | TCP |
是否连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输,使用流量控制和拥塞控制 |
连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 |
传输方式 | 面向报文 | 面向字节流 |
首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
适用场景 | 适用于实时应用(IP电话、视频会议、直播等)游戏行业、物联网行业 | 适用于要求可靠传输的应用,例如文件传输 |
udp场景总结:
- 实时性要求,例如音视频通话、游戏
- 节省资源的要求:嵌入式设备(电池供电),手机日志上报
如果做到可靠性传输?
- ACK机制
- 重传机制
- 序号机制
- 重排机制
- 窗口机制
tcp不需要我们管,因为tcp自带这些机制,保证数据可靠性。
upd要实现这5种机制必须要用户层处理。
kcp简介
KCP是⼀个快速可靠协议,能以⽐ TCP浪费10%-20%的带宽的代价,换取平均延迟降低30%-40%,且最⼤延迟降低三倍的传输效果。纯算法实现,并不负责底层协议(如UDP)的收发,需要使⽤者⾃⼰定义下层数据包的发送⽅式,以 callback的⽅式提供给 KCP。 连时钟都需要外部传递进来,内部不会有任何⼀次系统调⽤。
整个协议只有 ikcp.h, ikcp.c两个源⽂件,可以⽅便的集成到⽤户⾃⼰的协议栈中。也许你实现了⼀个P2P,或者某个基于UDP的协议,⽽缺乏⼀套完善的ARQ可靠协议实现,那么简单的拷⻉这两个⽂件到现有项⽬中,稍微编写两⾏代码,即可使⽤。
名词说明(源码字段):
- ⽤户数据(mss):应⽤层发送的数据,如⼀张图⽚2Kb的数据
- MTU:最⼤传输单元。即每次发送的最⼤数据
- RTO:Retransmission TimeOut,重传超时时间。
- cwnd:congestion window,拥塞窗⼝,表示发送⽅可发送多少个KCP数据包。与接收⽅窗⼝有关,与⽹络状况(拥塞控制)有关,与发送窗⼝⼤⼩有关。
- rwnd:receiver window,接收⽅窗⼝⼤⼩,表示接收⽅还可接收多少个KCP数据包
- snd_queue:待发送KCP数据包队列
- snd_nxt:下⼀个即将发送的kcp数据包序列号
- snd_una:下⼀个待确认的序列号
技术特性
TCP是为流量设计的(每秒内可以传输多少KB的数据),讲究的是充分利⽤带宽。⽽ KCP是为流速设计的(单个数据包从⼀端发送到⼀端需要多少时间),以10%-20%带宽浪费的代价换取了⽐ TCP快30%-40%的传输速度。TCP信道是⼀条流速很慢,但每秒流量很⼤的⼤运河,⽽KCP是⽔流湍急的⼩激流。KCP有正常模式和快速模式两种,通过以下策略达到提⾼流速的结果:
- RTO翻倍vs不翻倍:
TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,⼗分恐怖,⽽KCP启动快速模式后不x2,只是x1.5(实验证明1.5这个值相对⽐较好),提⾼了传输速度
- 选择性重传 vs 全部重传:
TCP丢包时会全部重传从丢的那个包开始以后的数据,KCP是选择性重传,只重传真正丢失的数据包。(TCP同样有选择重传SACK,但有区别,后续⽂章再介绍)。
- 快速重传(跳过x个包立刻重传):
发送端发送了1,2,3,4,5⼏个包,然后收到远端的ACK: 1,3,4,5,当收到ACK3时,KCP知道2被跳过1次,收到ACK4时,知道2被跳过了2次,此时可以认为2号丢失,不⽤等超时,直接重传2号包,⼤⼤改善了丢包时的传输速度。
- 延迟ACK vs ⾮延迟ACK:
TCP为了充分利⽤带宽,延迟发送ACK(NODELAY都没⽤),这样超时计算会算出较⼤ RTT时间,延⻓了丢包时的判断过程。KCP的ACK是否延迟发送可以调节。kcp的ack是一个list。
- UNA vs ACK+UNA:
ARQ模型响应有两种,UNA(此编号前所有包已收到,如TCP)和ACK(该编号包已收到),光⽤UNA将导致全部重传,光⽤ACK则丢失成本太⾼,以往协议都是⼆选其⼀,⽽ KCP协议中,除去单独的 ACK包外,所有包都有UNA信息。
- ⾮退让流控:
KCP正常模式同TCP⼀样使⽤公平退让法则,即发送窗⼝⼤⼩由:发送缓存⼤⼩、接收端剩余接收缓存⼤⼩、丢包退让及慢启动这四要素决定。但传送及时性要求很⾼的⼩数据时,可选择通过配置跳过后两步,仅⽤前两项来控制发送频率。以牺牲部分公平性及带宽利⽤率之代价,换取了开着BT都能流畅传输的效果
逻辑流程图
使用方式
- 创建 KCP对象: ikcpcb *kcp = ikcp_create(conv, user);
- 设置传输回调函数(如UDP的send函数)
kcp->output = udp_output;
真正发送数据需要调用sendto
- 循环调用 update: ikcp_update(kcp, millisec);
- 输入一个应用层数据包(如UDP收到的数据包)
ikcp_input(kcp,received_udp_packet,received_udp_size);
我们要使用recvfrom接收,然后扔到kcp里面做解析
- 发送数据: ikcp_send(kcp1, buffer, 8); 用户层接口
- 接收数据: hr = ikcp_recv(kcp2, buffer, 10);
发送流程::kcp_send -> kcp_update(loop) -> sendto(kcp分片header + 用户数据)
接受流程:recvfrom -> kcp_input(放到snd_buf)-> kcp_recv(真正的用户数据)
KCP的配置模式
这部分KCP⽂档有介绍,理解KCP协议⽆需过于关注。协议默认模式是⼀个标准的 ARQ,需要通过配置打开各项加速开关:
⼯作模式
int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc)
- nodelay :是否启⽤ nodelay模式,0不启⽤;1启⽤。
- interval :协议内部⼯作的 interval,单位毫秒,⽐如 10ms或者 20ms
- resend :快速重传模式,默认0关闭,可以设置2(2次ACK跨越将会直接重传)
- nc :是否关闭流控,默认是0代表不关闭,1代表关闭。
example:
// 普通模式 ikcp_nodelay(kcp, 0, 40, 0, 0); // 极速模式 ikcp_nodelay(kcp, 1, 10, 2, 1);
最⼤窗⼝
int ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd);
该调⽤将会设置协议的最⼤发送窗⼝和最⼤接收窗⼝⼤⼩,默认为32/128. 这个可以理解为 TCP的 SND_BUF和 RCV_BUF,只不过单位不⼀样 SND/RCV_BUF 单位是字节,这个单位是包。
最⼤传输单元
纯算法协议并不负责探测 MTU,默认 mtu是1400字节,可以使⽤ikcp_setmtu来设置该值。该值将会影响数据包归并及分⽚时候的最⼤传输单元。
最⼩RTO
不管是 TCP还是 KCP计算 RTO时都有最⼩ RTO的限制,即便计算出来RTO为40ms,由于默认的 RTO是100ms,协议只有在100ms后才能检测到丢包,快速模式下为30ms,可以⼿动更改该值:
kcp->rx_minrto = 10;
KCP数据结构
数据包结构(IKCPSEG)
KCP中只有⼀种数据包格式,不管是数据还是信令,都使⽤相同的结构与头。KCP头中每⼀个字段的意义如下:
- Conv,32bit,4Byte
为⼀个表示会话编号的整数,和TCP的 conv⼀样,通信双⽅需保证 conv相同,相互的数据包才能够被接受。conv唯⼀标识⼀个会话,但通信双⽅可以同时存在多个会话。
- cmd,8bit,1Byte
⽤来区分分⽚的作⽤。
- IKCP_CMD_PUSH:数据分⽚;
- IKCP_CMD_ACK:ack分⽚;
- IKCP_CMD_WASK:请求告知窗⼝⼤⼩;
- IKCP_CMD_WINS:告知窗⼝⼤⼩。
- frag,8bit,1Byte
⽤户数据可能会被分成多个KCP包发送,frag标识segment分⽚ID(在message中的索引,由⼤到⼩,0表示最后⼀个分⽚)。
- wnd,16bit,2Byte
剩余接收窗⼝⼤⼩(接收窗⼝⼤⼩-接收队列⼤⼩),发送⽅的发送窗⼝不能超过接收⽅给出的数值。
- ts,32bit,4Byte
message发送时刻的时间戳
- sn,32bit,4Byte
message分⽚segment的序号,按1累次递增。
- una,32bit,4Byte
待接收消息序号(接收滑动窗⼝左端)。对于未丢包的⽹络来说,una是下⼀个可接收的序号,如收到sn=10的包,una为11。
- len,32bit,4Byte
数据⻓度。
除了上述的包结构的字段外,还定义了⼏个⾮常重要的变量:
- resendts
下次超时重传的时间戳。
- rto
该分⽚的超时重传等待时间,其计算⽅法同TCP。
- fastack
收到ack时计算的该分⽚被跳过的累计次数,此字段⽤于快速重传,⾃定义需要⼏次确认开始快速重传。
- xmit
发送分⽚的次数,每发送⼀次加⼀。发送的次数对RTO的计算有影响,但是⽐TCP来说,影响会⼩⼀些,计算思想类似
IKCPCB结构
IKCPCB是KCP中最重要的结构,也是在会话开始就创建的对象,代表着这次会话,所以这个结构体体现了⼀个会话所需要涉及到的所有组件。
- conv:标识这个会话;
- mtu:最⼤传输单元,默认数据为1400,最⼩为50;
- mss:最⼤分⽚⼤⼩,不⼤于mtu;
- state:连接状态(0xFFFFFFFF表示断开连接);
- snd_una:第⼀个未确认的包;
- snd_nxt:下⼀个待分配的包的序号;
- rcv_nxt:待接收消息序号。为了保证包的顺序,接收⽅会维护⼀个接收窗⼝,接收窗⼝有⼀个起始序
- rcv_nxt(待接收消息序号)以及尾序号 rcv_nxt + rcv_wnd(接收窗⼝⼤⼩);
- ssthresh:拥塞窗⼝阈值,以包为单位(TCP以字节为单位);
- rx_rttval:RTT的变化量,代表连接的抖动情况;
- rx_srtt:smoothed round trip time,平滑后的RTT;
- rx_rto:由ACK接收延迟计算出来的重传超时时间;
- rx_minrto:最⼩重传超时时间;
- snd_wnd:发送窗⼝⼤⼩;
- rcv_wnd:接收窗⼝⼤⼩;
- rmt_wnd:远端接收窗⼝⼤⼩;
- cwnd:拥塞窗⼝⼤⼩;
- probe:探查变量,IKCP_ASK_TELL表示告知远端窗⼝⼤⼩。IKCP_ASK_SEND表示请求远端告知窗⼝⼤⼩;
- interval:内部flush刷新间隔,对系统循环效率有⾮常重要影响;
- ts_flush:下次flush刷新时间戳;
- xmit:发送segment的次数,当segment的xmit增加时,xmit增加(第⼀次或重传除外);
- rcv_buf:接收消息的缓存;
- nrcv_buf:接收缓存中消息数量;
- snd_buf:发送消息的缓存;
- nsnd_buf:发送缓存中消息数量;
- rcv_queue:接收消息的队列
- nrcv_que:接收队列中消息数量;
- snd_queue:发送消息的队列;
- nsnd_que:发送队列中消息数量;
- nodelay:是否启动⽆延迟模式。⽆延迟模式rtomin将设置为0,拥塞控制不启动;
- updated:是否调⽤过update函数的标识;
- ts_probe:下次探查窗⼝的时间戳;
- probe_wait:探查窗⼝需要等待的时间;
- dead_link:最⼤重传次数,被认为连接中断;
- incr:可发送的最⼤数据量;
- acklist:待发送的ack列表;
- ackcount:acklist中ack的数量,每个ack在acklist中存储ts,sn两个量;
- ackblock:2的倍数,标识acklist最⼤可容纳的ack数量;
- user:指针,可以任意放置代表⽤户的数据,也可以设置程序中需要传递的变量;
- buffer:存储消息字节流;
- fastresend:触发快速重传的重复ACK个数;
- nocwnd:取消拥塞控制;
- stream:是否采⽤流传输模式;
- logmask:⽇志的类型,如IKCP_LOG_IN_DATA,⽅便调试;
- output udp:发送消息的回调函数;
- writelog:写⽇志的回调函数。
kcp发送/接收数据过程
发送
引出问题:
- 为什么需要send_queue?
- 为什么需要snd_buf?
send_queue用于告诉kcp要上传哪些分片,kcp_update就是从send_queue获取分片,然后执行send callback。
snd_buf主要用于暂时保存发送的数据,数据发送时从send_queue放入buf中。发送完成后获取ack时,如果需要重传,从snd_buf中找到指定的分片,进行重传。已经确认被收到的分片数据会从snd_buf删除。btw,ack确认的优先级是先确认una,再确认ack。
接收
引出问题:
- 为什么需要rcv_buf?
- 为什么需要rcv_queue?
rcv_buf保存的数据是从recv_from中获取的数据,这个数据是原始数据,kcp通过将接收的数据暂时保存在rcv_buf中,方便做数据过滤与清洗,以及重排序等操作。
rcv_queue主要保存已经排好序且过滤好以后的数据。是返回给用户的真实数据。
引申问题
- sendto每次发送多长的数据?
mtu为一帧数据最大传输单元,一般为1400。但是kcp发送数据的时候是kcp分片header(24字节) + 用户数据(mss)。所以一帧可以发送的数据为 mtu - header = 1376字节
- ikcp_send可以发送多大长度的数据?
发送窗口(cwnd) * (mtu - kcp header) = 最大可发送数据,例如128 * 1376 = 176,12
代码分析
https://download.youkuaiyun.com/download/u012173846/80494675