注:特性来自github.com/skywind3000… 的 README.md 源码来自此仓库的代码
技术特性
TCP是为流量设计的(每秒内可以传输多少KB的数据),讲究的是充分利用带宽。而 KCP是为流速设计的(单个数据包从一端发送到一端需要多少时间),以10%-20%带宽浪费的代价换取了比 TCP快30%-40%的传输速度。TCP信道是一条流速很慢,但每秒流量很大的大运河,而KCP是水流湍急的小激流。KCP有正常模式和快速模式两种,通过以下策略达到提高流速的结果:
RTO翻倍vs不翻倍:
TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,十分恐怖,而KCP启动快速模式后不x2,只是x1.5(实验证明1.5这个值相对比较好),提高了传输速度。
static void ikcp_update_ack(ikcpcb *kcp, IINT32 rtt)
{
IINT32 rto = 0;
if (kcp->rx_srtt == 0) {
kcp->rx_srtt = rtt;
kcp->rx_rttval = rtt / 2;
} else {
long delta = rtt - kcp->rx_srtt;
if (delta < 0) delta = -delta;
kcp->rx_rttval = (3 * kcp->rx_rttval + delta) / 4;
kcp->rx_srtt = (7 * kcp->rx_srtt + rtt) / 8;
if (kcp->rx_srtt < 1) kcp->rx_srtt = 1;
}
rto = kcp->rx_srtt + _imax_(kcp->interval, 4 * kcp->rx_rttval);
kcp->rx_rto = _ibound_(kcp->rx_minrto, rto, IKCP_RTO_MAX);
}
//rto计算
void ikcp_flush(ikcpcb *kcp){
// skip .......
else if (_itimediff(current, segment->resendts) >= 0) {
needsend = 1;
segment->xmit++;
kcp->xmit++;
if (kcp->nodelay == 0) {
segment->rto += _imax_(segment->rto, (IUINT32)kcp->rx_rto);
} else {
IINT32 step = (kcp->nodelay < 2)?
((IINT32)(segment->rto)) : kcp->rx_rto;
segment->rto += step / 2; //1.5倍增加
}
segment->resendts = current + segment->rto;
lost = 1;
}
// skip .......
}
选择性重传 vs 全部重传:
TCP丢包时会全部重传从丢的那个包开始以后的数据,KCP是选择性重传,只重传真正丢失的数据包。
//1. 在接收到ack时,将不对应的包设置fastack+1
static void ikcp_parse_fastack(ikcpcb *kcp, IUINT32 sn, IUINT32 ts)
{
struct IQUEUEHEAD *p, *next;
if (_itimediff(sn, kcp->snd_una) < 0 || _itimediff(sn, kcp->snd_nxt) >= 0)
return;
for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = next) {
IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node);
next = p->next;
if (_itimediff(sn, seg->sn) < 0) {
break;
}
else if (sn != seg->sn) {
#ifndef IKCP_FASTACK_CONSERVE
seg->fastack++;
#else
if (_itimediff(ts, seg->ts) >= 0)
seg->fastack++;
#endif
}
}
}
//2.在flush中,fastack大于某值时,发送
void ikcp_flush(ikcpcb *kcp){
// skip .......
for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = p->next) {
IKCPSEG *segment = iqueue_entry(p, IKCPSEG, node);
int needsend = 0;
if (segment->xmit == 0) {
needsend = 1;
segment->xmit++;
segment->rto = kcp->rx_rto;
segment->resendts = current + segment->rto + rtomin;
}
else if (_itimediff(current, segment->resendts) >= 0) {
needsend = 1;
segment->xmit++;
kcp->xmit++;
if (kcp->nodelay == 0) {
segment->rto += _imax_(segment->rto, (IUINT32)kcp->rx_rto);
} else {
IINT32 step = (kcp->nodelay < 2)?
((IINT32)(segment->rto)) : kcp->rx_rto;
segment->rto += step / 2;
}
segment->resendts = current + segment->rto;
lost = 1;
}
else if (segment->fastack >= resent) {
if ((int)segment->xmit <= kcp->fastlimit ||
kcp->fastlimit <= 0) {
needsend = 1;
segment->xmit++;
segment->fastack = 0;
segment->resendts = current + segment->rto;
change++;
}
}
if (needsend) {
// skip .......
}
快速重传:
发送端发送了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是否延迟发送可以调节。
void ikcp_flush(ikcpcb *kcp)
{
IUINT32 current = kcp->current;
char *buffer = kcp->buffer;
char *ptr = buffer;
int count, size, i;
IUINT32 resent, cwnd;
IUINT32 rtomin;
struct IQUEUEHEAD *p;
int change = 0;
int lost = 0;
IKCPSEG seg;
// 'ikcp_update' haven't been called.
if (kcp->updated == 0) return;
seg.conv = kcp->conv;
seg.cmd = IKCP_CMD_ACK;
seg.frg = 0;
seg.wnd = ikcp_wnd_unused(kcp);
seg.una = kcp->rcv_nxt;
seg.len = 0;
seg.sn = 0;
seg.ts = 0;
.....................
//其实就是每次update时,都ack
UNA vs ACK+UNA:
ARQ模型响应有两种,UNA(此编号前所有包已收到,如TCP)和ACK(该编号包已收到),光用UNA将导致全部重传,光用ACK则丢失成本太高,以往协议都是二选其一,而 KCP协议中,除去单独的 ACK包外,所有包都有UNA信息。 目测就是ack重传那里
非退让流控:
KCP正常模式同TCP一样使用公平退让法则,即发送窗口大小由:发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。但传送及时性要求很高的小数据时,可选择通过配置跳过后两步,仅用前两项来控制发送频率。以牺牲部分公平性及带宽利用率之代价,换取了开着BT都能流畅传输的效果。
int ikcp_input(ikcpcb *kcp, const char *data, long size){
// skip .....
if (_itimediff(kcp->snd_una, prev_una) > 0) {
if (kcp->cwnd < kcp->rmt_wnd) {
IUINT32 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) {
#if 1
kcp->cwnd = (kcp->incr + mss - 1) / ((mss > 0)? mss : 1);
#else
kcp->cwnd++;
#endif
}
}
if (kcp->cwnd > kcp->rmt_wnd) {
kcp->cwnd = kcp->rmt_wnd;
kcp->incr = kcp->rmt_wnd * mss;
}
}
}
}
void ikcp_flush(ikcpcb *kcp){
//skip ....
cwnd = _imin_(kcp->snd_wnd, kcp->rmt_wnd);
if (kcp->nocwnd == 0) cwnd = _imin_(kcp->cwnd, cwnd);
//skip ....
if (change) {
IUINT32 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;
}
if (lost) {
kcp->ssthresh = cwnd / 2;
if (kcp->ssthresh < IKCP_THRESH_MIN)
kcp->ssthresh = IKCP_THRESH_MIN;
kcp->cwnd = 1;
kcp->incr = kcp->mss;
}
if (kcp->cwnd < 1) {
kcp->cwnd = 1;
kcp->incr = kcp->mss;
}
附:主要数据结构注释
struct IKCPSEG
{
struct IQUEUEHEAD node;
IUINT32 conv; //用来标记这个seg属于哪个kcp
IUINT32 cmd;//这个包的指令是: // 数据 ack 询问/应答窗口大小
IUINT32 frg; //分包时,分包的序号,0为终结
IUINT32 wnd;//发送这个seg的这个端的 窗口大小--> 远端的接收窗口大小
IUINT32 ts; //我不知道为什么要用时间轴,这个都1秒,有什么用 ??
IUINT32 sn;//相当于tcp的ack
IUINT32 una;//una 远端等待接收的一个序号
IUINT32 len; //data的长度
IUINT32 resendts;//重发的时间轴
IUINT32 rto;//等于发送端kcp的 rx_rto->由 计算得来
IUINT32 fastack;//ack跳过的次数,用于快速重传 ??
IUINT32 xmit;// fastack resend次数
char data[1];//当malloc时,只需要 malloc(sizeof(IKCPSEG)+datalen) 则,data长=数据长度+1 刚好用来放0
};
struct IKCPCB
{
//会话ID,最大传输单元,最大分片大小,状态 mss=mtu-sizeof(IKCPSEG)
IUINT32 conv, mtu, mss, state;
//第一个未接收到的包,待发送的包(可以认为是tcp的ack自增),接收消息的序号-> 用来赋seg的una值
IUINT32 snd_una, snd_nxt, rcv_nxt;
//前两个不知道干嘛 拥塞窗口的阈值 用来控制cwnd值变化的
IUINT32 ts_recent, ts_lastack, ssthresh;
//这几个变量是用来更新rto的
// rx_rttval 接收ack的浮动值
// rx_srtt 接收ack的平滑值
// rx_rto 计算出来的rto
// rx_minrto 最小rto
IINT32 rx_rttval, rx_srtt, rx_rto, rx_minrto;
//发送队列的窗口大小
//接收队列的窗口大小
//远端的接收队列的窗口大小
//窗口大小
//probe 用来二进制标记 要干嘛
IUINT32 snd_wnd, rcv_wnd, rmt_wnd, cwnd, probe;
//时间轴 时间间隔 下一次flush的时间 xmit发射多少次? 看不到有什么地方用到
IUINT32 current, interval, ts_flush, xmit;
//接收到的数据seg个数
//需要发送的seg个数
IUINT32 nrcv_buf, nsnd_buf;
//接收队列的数据 seg个数
//发送队列的数据 seg个数
IUINT32 nrcv_que, nsnd_que;
//是否为nodelay模式:如果开启,rto计算范围更小
//updated 在调用flush时,有没有调用过update
IUINT32 nodelay, updated;
//请求访问窗口的时间相关 当远程端口大小为0时
IUINT32 ts_probe, probe_wait;
IUINT32 dead_link, incr;
//发送队列
struct IQUEUEHEAD snd_queue;
//接收队列
struct IQUEUEHEAD rcv_queue;
//待发送队列
struct IQUEUEHEAD snd_buf;
//待接收队列
struct IQUEUEHEAD rcv_buf;
//用来缓存自己接收到了多少个ack
IUINT32 *acklist;
IUINT32 ackcount;
IUINT32 ackblock;
//用户信息
void *user;
//好像就用来操作数据的中转站
char *buffer;
//快速重传的阈值
int fastresend;
//快速重传的上限
int fastlimit;
//是否无视重传等其它设置窗口
//steam模式的话,会将几个小包合并成大包
int nocwnd, stream;
int logmask;
int (*output)(const char *buf, int len, struct IKCPCB *kcp, void *user);
void (*writelog)(const char *log, struct IKCPCB *kcp, void *user);
};