1 KCP协议header
conv: 会话id,UDP是无连接的,conv用于表示来自于哪个客户端
cmd: 命令
- IKCP_CMD_PUSH:推送数据
- IKCP_CMD_ACK:数据确认
- IKCP_CMD_WASK:接收窗口大小询问命令
- IKCP_CMD_WINS:接收窗口大小告知命令
frg: 分片,用户数据可能会被分成多个KCP包,发送出去
wnd: 接收窗口大小,发送方的发送窗口不能超过接收方给出的数值
ts: 时间序列,计算RTT
sn: 序列号
una: 下一个待接收的的序列号,假如收到了序列号为3的包,una就为4
len: 数据长度
data: 用户数据
2 KCP 源码流程图
kcp_create:创建kcp对象
kcp_update:按照一定的频率调用kcp_update来更新 kcp状态,内部调用kcp_flush,
kcp_send:发送数据,应用层的数据先放到kcp内部,直到调用kcp_update才真正发送数据,最终调用output会调用sendto函数
kcp_recv:接收数据
在kcp协议中,一条连接对应了一个kcp对象
ikcp_send和ikcp_recv是一一对应的
3 KCP源码
3.1 KCP数据包结构体
ikcp.h中
struct IKCPSEG
{
struct IQUEUEHEAD node;
IUINT32 conv;
IUINT32 cmd;
IUINT32 frg;
IUINT32 wnd;
IUINT32 ts;
IUINT32 sn;
IUINT32 una;
IUINT32 len;
IUINT32 resendts;
IUINT32 rto;
IUINT32 fastack;
IUINT32 xmit;
char data[1];
};
注意,这里只是数据包的结构体,而不是header本身,真实的数据按照KCP协议的header存放。
resendts:下次超时重传时间戳
rto:超时等待时间
fastack:收到ack时计算该分片被跳过的累计次数,此字段用于快速重传,自定义需要几次确认开始快速重传
xmit:发送分片的次数,每发一次加1
最后的data大小为1,这是零长度数组(柔性数组),放在结构体最末尾,可动态设定大小,而不需要一个指针指向一块内存区域
3.2 KCP连接控制结构体
struct IKCPCB
{
IUINT32 conv;
IUINT32 mtu; // 最大传输单元,默认为1400,最小为50
IUINT32 mss; // 最大分片大小,不大于mtu
IUINT32 state; // 连接状态
IUINT32 snd_una; // 第一个未确认的包
IUINT32 snd_nxt; // 下一个即将发送的KCP数据包序列号
IUINT32 rcv_nxt; // 待接收消息序号
IUINT32 ts_recent;
IUINT32 ts_lastack;
IUINT32 ssthresh; // 拥塞窗口的阈值
IINT32 rx_rttval; // RTT的变化量,代表连接的抖动情况
IINT32 rx_srtt; // smoothed round trip time,平滑后的RTT;
IINT32 rx_rto; // 收ACK接收延迟计算出来的重传超时时间
IINT32 rx_minrto; // 最小重传超时时间
IUINT32 snd_wnd; // 发送窗口大小
IUINT32 rcv_wnd; // 接收窗口大小
IUINT32 rmt_wnd; // 远端接收窗口大小
IUINT32 cwnd; // 拥塞窗口大小, 动态变化
IUINT32 probe; // 探查变量, IKCP_ASK_TELL表示告知远端窗口大小。IKCP_ASK_SEND表示请求远端告知窗口大小;
IUINT32 current; // 当前时间戳
IUINT32 interval; // 内部flush刷新间隔,对系统循环效率有非常重要影响, 间隔小了cpu占用率高, 间隔大了响应慢
IUINT32 ts_flush; // 下次flush刷新的时间戳
IUINT32 xmit; // 发送segment的次数
IUINT32 nrcv_buf; // 接收缓存中的消息数量
IUINT32 nsnd_buf; // 发送缓存中的消息数量
IUINT32 nrcv_que; // 接收队列中消息数量
IUINT32 nsnd_que; // 发送队列中消息数量
IUINT32 nodelay; // 是否启动无延迟模式。无延迟模式rtomin将设置为0,拥塞控制不启动;
IUINT32 updated; //是 否调用过update函数的标识;
IUINT32 ts_probe; // 下次探查窗口的时间戳;
IUINT32 probe_wait; // 探查窗口需要等待的时间;
IUINT32 dead_link; // 最大重传次数,被认为连接中断;
IUINT32 incr; // 可发送的最大数据量;
struct IQUEUEHEAD snd_queue; //发送消息的队列
struct IQUEUEHEAD rcv_queue; //接收消息的队列, 是已经确认可以供用户读取的数据
struct IQUEUEHEAD snd_buf; //发送消息的缓存
struct IQUEUEHEAD rcv_buf; //接收消息的缓存, 还不能直接供用户读取的数据
IUINT32 *acklist; //待发送的ack的列表 当收到一个数据报文时,将其对应的 ACK 报文的 sn 号以及时间戳 ts
//同时加入到acklist 中,即形成如 [sn1, ts1, sn2, ts2 …] 的列表
IUINT32 ackcount; //是当前计数, 记录 acklist 中存放的 ACK 报文的数量
IUINT32 ackblock; //是容量, acklist 数组的可用长度,当 acklist 的容量不足时,需要进行扩容
void *user; // 指针,可以任意放置代表用户的数据,也可以设置程序中需要传递的变量;
char *buffer;
int fastresend; // 触发快速重传的重复ACK个数;
int fastlimit;
int nocwnd; // 取消拥塞控制
int stream; // 是否采用流传输模式
int logmask; // 日志的类型,如IKCP_LOG_IN_DATA,方便调试
int (*output)(const char *buf, int len, struct IKCPCB *kcp, void *user);//发送消息的回调函数
void (*writelog)(const char *log, struct IKCPCB *kcp, void *user); // 写日志的回调函数
};
3.2 ikcp_create & ikcp_release
ikcp_create创建一个kcp对象,同一个连接两端的conv需一致
// 创建kcp,并初始化,最终返回kcp
ikcpcb* ikcp_create(IUINT32 conv, void *user)
{
ikcpcb *kcp = (ikcpcb*)ikcp_malloc(sizeof(struct IKCPCB));
kcp->conv = conv;
kcp->user = user;
...
kcp->writelog = NULL;
return kcp;
}
ikcp_release则是释放相关资源
3.3 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 (!iqueue_is_empty(&kcp->snd_queue))
{
// 取出发送队列的最后一个报文
IKCPSEG *old = iqueue_entry(kcp->snd_queue.prev, IKCPSEG, node);
// 如果该报文数据长度小于mss,即该报文还可以继续容纳数据
if (old->len < kcp->mss)
{
int capacity = kcp->mss - old->len;
int extend = (len < capacity)? len : capacity; // 需拷贝的数据长度
// 创建一个新的seg,先放入原报文中的数据,之后拼上buffer中的数据
seg = ikcp_segment_new(kcp, old->len + extend);
assert(seg);
if (seg == NULL) {
return -2;
}
iqueue_add_tail(&seg->node, &kcp->snd_queue);
memcpy(seg->data, old->data, old->len);
if (buffer) {
memcpy(seg->data + old->len, buffer, extend);
buffer += extend;
}
seg->len = old->len + extend;
seg->frg = 0;
len -= extend;
iqueue_del_init(&old->node);
ikcp_segment_delete(kcp, old); //删除old的segment
}
}
}
// 计算数据可以被最多分成多少个frag
if (len <= (int)kcp->mss) count = 1;
else count = (len + kcp->mss - 1) / kcp->mss;
//mss = mtu(1400) - 24 = 1376
if (count >= (int)IKCP_WND_RCV) { // 超过对方的初始接收窗口
if (kcp->stream != 0 && sent > 0)
return sent;
return -2;
}
if (count == 0) count = 1;
for (i = 0; i < count; i++)
{
int size = len > (int)kcp->mss ? (int)kcp->mss : len;
seg = ikcp_segment_new(kcp, size); //分配一个新的kcp segment
assert(seg);
if (seg == NULL) {
return -2;
}
if (buffer && len > 0) {
memcpy(seg->data, buffer, size); // 从buffer中拷贝到seg->data
}
seg->len = size;
// frg编号
//报文模式:假如count = 4, 则编号分别为3 2 1 0
//流模式:编号为0 0 0 0
seg->frg = (kcp->stream == 0)? (count - i - 1) : 0;
iqueue_init(&seg->node);
iqueue_add_tail(&seg->node, &kcp->snd_queue); // 发送队列
kcp->nsnd_que++; //发送队列长度加1
if (buffer) { //偏移size位后进行下一轮循环
buffer += size;
}
len -= size;
}
return 0;
}
在ikcp_send中,有两种模式
(1)报文模式 kcp->stream = 0
- 计算数据可以被分为多少个mss,计入count中
- 将数据多次(如果需要的话)拷贝到seg->data中,并放入到kcp的发送队列中
(2)流模式 kcp->stream != 0 - 将发送队列中的最后一个包取出来,并放入一部分数据,后续处理与报文模式相同
3.4 ikcp_update
ikcp_update由应用层,定时调用,如 10ms调用一次
伪代码大概如下:
while(1)
{
//sleep 10 ms
for(kcp : kcps)
ikcp_update(kcp, current)
}
void ikcp_update(ikcpcb *kcp, IUINT32 current)
{
IINT32 slap;
kcp->current = current; // 当前时间戳
if (kcp->updated == 0)
{
kcp->updated = 1; // 标记调用了ikcp_update
kcp->ts_flush = kcp->current; // 下次flush刷新的时间戳
}
slap = _itimediff(kcp->current, kcp->ts_flush); // 上一次flush的时间与现在的间隔
if (slap >= 10000 || slap < -10000) // 至少10ms的间隔触发一次
{
kcp->ts_flush = kcp->current;
slap = 0;
}
if (slap >= 0)
{
kcp->ts_flush += kcp->interval; // 先按interval叠加获取下一次要刷新的时间
// 如果下一次要刷新的时间已经经过了
if (_itimediff(kcp->current, kcp->ts_flush) >= 0)
{
kcp->ts_flush = kcp->current + kcp->interval; //更新为当前时间 + interval
}
ikcp_flush(kcp);
}
}
- 更新时间戳相关信息
- 强制至少10ms刷新一次
- 调用ikcp_flush
3.6 ikcp_output
在看flush之前,先看一下output
static int ikcp_output(ikcpcb *kcp, const void *data, int size)
{
assert(kcp);
assert(kcp->output);
if (ikcp_canlog(kcp, IKCP_LOG_OUTPUT)) {
ikcp_log(kcp, IKCP_LOG_OUTPUT, "[RO] %ld bytes", (long)size);
}
if (size == 0) return 0;
return kcp->output((const char*)data, size, kcp, kcp->user);
}
在官网中提到,需设置回调函数:
// KCP的下层协议输出函数,KCP需要发送数据时会调用它
// buf/len 表示缓存和长度
// user指针为 kcp对象创建时传入的值,用于区别多个 KCP对象
int udp_output(const char *buf, int len, ikcpcb *kcp, void *user)
{
....
}
// 设置回调函数
kcp->output = udp_output;
数据并没有在ikcp_send发送,而是在ikcp_output中通过kcp->output(最后一行)发送数据。
3.7 ikcp_flush
每个报文(seg)的 fastack 记录了该报文被跳过了几次,由函数 ikcp_parse_fastack 更新。
需注意区分kcp和seg,有一些字段二者共有。
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;
if (kcp->updated == 0) return;
// 先处理ack包,因此命令是IKCP_CMD_ACK
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;
// acklist存放的是待应答的包
// 逐一获取acklist中的sn和ts,编码成segment
count = kcp->ackcount; // 需要应答的分片数量
for (i = 0; i < count; i++)
{
size = (int)(ptr - buffer);
//下一个ack的header放不下,先将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); // 应答包 把时间戳发回去可以计算RTT
// ack 的报文没有 data
ptr = ikcp_encode_seg(ptr, &seg); //按照header格式存入数据,并将指针偏移到data处
}
kcp->ackcount = 0;
// probe window size (if remote window size equals zero)
if (kcp->rmt_wnd == 0)
{
// 初始化探测间隔和下一次探测时间
if (kcp->probe_wait == 0) // probe_wait: 探查窗口需要等待的时间
{
kcp->probe_wait = IKCP_PROBE_INIT; //7s
kcp->ts_probe = kcp->current + kcp->probe_wait; // 下一次探测时间
}
else //远端窗口为0,发送过探测请求,但是已经超过下次探测的时间
{
if (_itimediff(kcp->current, kcp->ts_probe) >= 0) // 检测是否到了探测时间
{
if (kcp->probe_wait < IKCP_PROBE_INIT)
kcp->probe_wait = IKCP_PROBE_INIT;
kcp->probe_wait += kcp->probe_wait / 2; // 每次探测间隔增长 0.5 倍
if (kcp->probe_wait > IKCP_PROBE_LIMIT)
kcp->probe_wait = IKCP_PROBE_LIMIT; // 最大120s
kcp->ts_probe = kcp->current + kcp->probe_wait;
kcp->probe |= IKCP_ASK_SEND; // 请求远端告知窗口大小
}
}
}
else
{
// 远端窗口正常,则不需要探测
// 远端窗口不等于0,更新下次探测时间与探测窗口等待时间为0,不发送窗口探测
kcp->ts_probe = 0;
kcp->probe_wait = 0;
}
if (kcp->probe & IKCP_ASK_SEND)
{
seg.cmd = IKCP_CMD_WASK; // 询问对方窗口size
size = (int)(ptr - buffer);
if (size + (int)IKCP_OVERHEAD > (int)kcp->mtu) {
ikcp_output(kcp, buffer, size);
ptr = buffer;
}
ptr = ikcp_encode_seg(ptr, &seg);
}
if (kcp->probe & IKCP_ASK_TELL)
{
seg.cmd = IKCP_CMD_WINS; // 告诉对方我方窗口size
size = (int)(ptr - buffer);
if (size + (int)IKCP_OVERHEAD > (int)kcp->mtu) {
ikcp_output(kcp, buffer, size);
ptr = buffer;
}
ptr = ikcp_encode_seg(ptr, &seg);
}
kcp->probe = 0; //清空标识
// 计算拥塞窗口大小:min(发送窗口,远端窗口)
cwnd = _imin_(kcp->snd_wnd, kcp->rmt_wnd);
// kcp->nocwnd == 1 则关闭流控(拥塞控制)
if (kcp->nocwnd == 0) cwnd = _imin_(kcp->cwnd, cwnd); // 进一步控制cwnd大小, kcp->cwnd拥塞窗口
// 如果 snd_nxt(下一个要从 send_que 发到 send_buf 的包序列号) 在发送窗口内
while (_itimediff(kcp->snd_nxt, kcp->snd_una + cwnd) < 0)
{
IKCPSEG *newseg;
if (iqueue_is_empty(&kcp->snd_queue)) break;
newseg = iqueue_entry(kcp->snd_queue.next, IKCPSEG, node);
iqueue_del(&newseg->node);
// 插入到snd_buf 发送窗口
iqueue_add_tail(&newseg->node, &kcp->snd_buf);
kcp->nsnd_que--;
kcp->nsnd_buf++;
//设置数据分片的属性
newseg->conv = kcp->conv;
newseg->cmd = IKCP_CMD_PUSH;
newseg->wnd = seg.wnd; // 告知对方当前的接收窗口
newseg->ts = current; // 当前时间
newseg->sn = kcp->snd_nxt++; // 序号
newseg->una = kcp->rcv_nxt; // 告诉对方可以发送的下一个包序号
newseg->resendts = current; // 当前发送的时间
newseg->rto = kcp->rx_rto; // 超时重传的时间, 重传间隔的时间
newseg->fastack = 0; // 是否快速重传
newseg->xmit = 0; // 重传次数
}
// 快重传:fastresend
resent = (kcp->fastresend > 0)? (IUINT32)kcp->fastresend : 0xffffffff;
// nodelay: 0表示关闭快速模式,rtomin 被设置为 kcp->rx_rto 的八分之一,即使用一个较大的最小重传时间间隔
// 1表示开启快速模式,rtomin 被设置为 0,减少重传间隔
rtomin = (kcp->nodelay == 0)? (kcp->rx_rto >> 3) : 0;
//发送snd_buf的数据
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; // 下一次要发送的时间
}
// 当前时间达到了重发时间,但并没有新的ack到达,出现丢包, 重传
else if(_itimediff(current, segment->resendts) >= 0)
{
needsend = 1;
segment->xmit++;
kcp->xmit++;
if (kcp->nodelay == 0) { // RTO翻倍
segment->rto += _imax_(segment->rto, (IUINT32)kcp->rx_rto);
} else { // RTO * 1.5
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) // segment超过设定的快重传的ack次数
{
// fastlimit默认为5, 超过这个次数后走正常重传方式
if ((int)segment->xmit <= kcp->fastlimit ||
kcp->fastlimit <= 0) {
needsend = 1;
segment->xmit++; // 重传次数
// printf("req resend: sn:%u, fastack:%d, %d\n", segment->sn, segment->fastack, resent);
segment->fastack = 0; // fastack 这个值是发送方收到应答的其他序号时进行统计的,跳过的序号
segment->resendts = current + segment->rto; // 下一次重传的时间
change++;
}
}
if (needsend) {
int need;
segment->ts = current;
segment->wnd = seg.wnd; // 剩余接收窗口大小(接收窗口大小-接收队列大小), 告诉对方目前自己的接收能力
segment->una = kcp->rcv_nxt;
size = (int)(ptr - buffer);
need = IKCP_OVERHEAD + segment->len;
if (size + need > (int)kcp->mtu) {
ikcp_output(kcp, buffer, size);
ptr = buffer;
}
ptr = ikcp_encode_seg(ptr, segment); // 把segment封装成线性buffer发送 头部+数据
if (segment->len > 0) {
memcpy(ptr, segment->data, segment->len);
ptr += segment->len;
}
if (segment->xmit >= kcp->dead_link) {
kcp->state = (IUINT32)-1;
}
}
}
// 将缓冲区剩余部分发送出去
size = (int)(ptr - buffer);
if (size > 0) {
ikcp_output(kcp, buffer, size);
}
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;
}
}
该代码分为多个部分
(1)取出acklist,发送ack确认包,这里是多个ack合并发送
(2)探测远端窗口
(3)发送snd_buf数据分片,需考虑数据是第一次发送、重传还是快重传
(4)更新拥塞窗口大小
3.8 ikcp_recv
int ikcp_recv(ikcpcb *kcp, char *buffer, int len)
{
struct IQUEUEHEAD *p;
int ispeek = (len < 0)? 1 : 0; //ispeek只是为了拿数据看看,不用将数据从queue删除
int peeksize;
int recover = 0;
IKCPSEG *seg;
assert(kcp);
if (iqueue_is_empty(&kcp->rcv_queue))
return -1;
if (len < 0) len = -len;
peeksize = ikcp_peeksize(kcp); //计算当前一帧数据的总大小(一个或多个分片组成的数据帧)
if (peeksize < 0) // 没有数据可读
return -2;
if (peeksize > len) // 可读数据大于用户传入的长度,每次读取需要一次性读取完毕,类似udp报文的读取
return -3;
if (kcp->nrcv_que >= kcp->rcv_wnd) // 接收队列segment数量大于等于接收窗口,窗口可以恢复
recover = 1;
for (len = 0, p = kcp->rcv_queue.next; p != &kcp->rcv_queue; )
{
int fragment;
seg = iqueue_entry(p, IKCPSEG, node);
p = p->next;
if (buffer) {
memcpy(buffer, seg->data, seg->len); // 把queue的数据就放入用户buffer
buffer += seg->len;
}
len += seg->len;
fragment = seg->frg;
if (ikcp_canlog(kcp, IKCP_LOG_RECV)) {
ikcp_log(kcp, IKCP_LOG_RECV, "recv sn=%lu", (unsigned long)seg->sn);
}
if (ispeek == 0) {
iqueue_del(&seg->node);
ikcp_segment_delete(kcp, seg); // 删除节点
kcp->nrcv_que--; // nrcv_que接收队列-1
}
if (fragment == 0) // 收到完整的数据,send的时候:3 2 1 0
break;
}
assert(len == peeksize);
// move available data from rcv_buf -> rcv_queue
while (! iqueue_is_empty(&kcp->rcv_buf))
{
seg = iqueue_entry(kcp->rcv_buf.next, IKCPSEG, node);
// 条件1 序号是该接收的数据
// 条件2 接收队列nrcv_que < 接收窗口rcv_wnd;
if (seg->sn == kcp->rcv_nxt && kcp->nrcv_que < kcp->rcv_wnd) {
iqueue_del(&seg->node);
kcp->nrcv_buf--;
iqueue_add_tail(&seg->node, &kcp->rcv_queue);
kcp->nrcv_que++; // 接收队列 有多少个分片 + 1
kcp->rcv_nxt++; // 接收序号 + 1
} else {
break;
}
}
//接收端有空间继续接收数据
if (kcp->nrcv_que < kcp->rcv_wnd && recover) {
// ready to send back IKCP_CMD_WINS in ikcp_flush
// tell remote my window size
kcp->probe |= IKCP_ASK_TELL;
}
return len;
}
ikcp_recv中主要有3件事
(1)接收一帧数据,需要组装
(2)将数据从缓冲区移到接收队列中
(3)修改probe为IKCP_ASK_TELL,以告知对方我方窗口大小
3.9 ikcp_input
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
IUINT32 prev_una = kcp->snd_una;
IUINT32 maxack = 0, latest_ts = 0;
int flag = 0;
if (ikcp_canlog(kcp, IKCP_LOG_INPUT)) {
ikcp_log(kcp, IKCP_LOG_INPUT, "[RI] %d bytes", (int)size);
}
// 数据异常
if (data == NULL || (int)size < (int)IKCP_OVERHEAD) {
printf("%s conv:%u, data:%p, size:%ld\n", __FUNCTION__, kcp->conv, data, size);
return -1;
}
while(1) {
IUINT32 ts, sn, len, una, conv;
IUINT16 wnd;
IUINT8 cmd, frg;
IKCPSEG *seg;
if (size < (int)IKCP_OVERHEAD) break;
// 校验数据分片
data = ikcp_decode32u(data, &conv); // 获取segment头部信息
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, &len);
size -= IKCP_OVERHEAD;
if ((long)size < (long)len || (int)len < 0) return -2; // 数据不足或者没有真正的数据存在
if (cmd != IKCP_CMD_PUSH && cmd != IKCP_CMD_ACK &&
cmd != IKCP_CMD_WASK && cmd != IKCP_CMD_WINS)
return -3;
kcp->rmt_wnd = wnd; // 远端的接收窗口
ikcp_parse_una(kcp, una); // 删除snd_buf中小于una的segment
ikcp_shrink_buf(kcp); // 更新snd_una为snd_buf中seg->sn或kcp->snd_nxt
if (cmd == IKCP_CMD_ACK)
{
if (_itimediff(kcp->current, ts) >= 0)
{
//更新rx_srtt,rx_rttval,计算kcp->rx_rto
ikcp_update_ack(kcp, _itimediff(kcp->current, ts));
}
ikcp_parse_ack(kcp, sn); // 将已经ack的分片删除
ikcp_shrink_buf(kcp); // 更新控制块的 snd_una
if (flag == 0) {
flag = 1;
maxack = sn; // 记录最大的 ACK 编号
latest_ts = ts;
}
else {
if (_itimediff(sn, maxack) > 0) {
#ifndef IKCP_FASTACK_CONSERVE
maxack = sn; // 记录最大的 ACK 编号
latest_ts = ts;
#else
if (_itimediff(ts, latest_ts) > 0) {
maxack = sn;
latest_ts = ts;
}
#endif
}
}
if (ikcp_canlog(kcp, IKCP_LOG_IN_ACK)) {
ikcp_log(kcp, IKCP_LOG_IN_ACK,
"input ack: sn=%lu rtt=%ld rto=%ld", (unsigned long)sn,
(long)_itimediff(kcp->current, ts),
(long)kcp->rx_rto);
}
}
else if (cmd == IKCP_CMD_PUSH) { //接收数据
if (ikcp_canlog(kcp, IKCP_LOG_IN_DATA)) {
ikcp_log(kcp, IKCP_LOG_IN_DATA,
"input psh: sn=%lu ts=%lu", (unsigned long)sn, (unsigned long)ts);
}
if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) < 0) { //在接收窗口内
ikcp_ack_push(kcp, sn, ts); // 对该报文的确认 ACK 报文放入 ACK 列表中
// 判断接收的数据分片编号是否符合要求,即:在接收窗口(滑动窗口)范围之内
if (_itimediff(sn, kcp->rcv_nxt) >= 0) {
seg = ikcp_segment_new(kcp, len);
seg->conv = conv;
seg->cmd = cmd;
seg->frg = frg;
seg->wnd = wnd;
seg->ts = ts;
seg->sn = sn;
seg->una = una;
seg->len = len;
if (len > 0) {
memcpy(seg->data, data, len);
}
//1. 丢弃sn > kcp->rcv_nxt + kcp->rcv_wnd的segment;
//2. 逐一比较rcv_buf中的segment,若重复丢弃,非重复,新建segment加入;
//3. 检查rcv_buf的包序号sn,如果是待接收的序号rcv_nxt,且可以接收(接收队列小于接收窗口),转移segment到rcv_buf
ikcp_parse_data(kcp, seg);
}
}
}
else if (cmd == IKCP_CMD_WASK) {
kcp->probe |= IKCP_ASK_TELL; // 收到对方请求后标记自己要告诉对方自己的窗口
if (ikcp_canlog(kcp, IKCP_LOG_IN_PROBE)) {
ikcp_log(kcp, IKCP_LOG_IN_PROBE, "input probe");
}
}
else if (cmd == IKCP_CMD_WINS) {
// do nothing 如果是tell me 远端窗口大小,什么都不做
if (ikcp_canlog(kcp, IKCP_LOG_IN_WINS)) {
ikcp_log(kcp, IKCP_LOG_IN_WINS,
"input wins: %lu", (unsigned long)(wnd));
}
}
else {
return -3;
}
data += len;
size -= len;
}
if (flag != 0) {
ikcp_parse_fastack(kcp, maxack, latest_ts);
}
// 如果snd_una增加了那么就说明对端正常收到且回应了发送方发送缓冲区第一个待确认的包
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; //并设置相应数据量,该数据量以字节数
}
}
}
return 0;
}
(1)获取kcp包,解析数据
(2)删除snd_buf中小于una(已确认)的segment,并更新snd_una
(3)解析命令
- IKCP_CMD_ACK:计算RTT、RTO时间,并做一个标记,后续判断是否有需要快重传的包
- IKCP_CMD_PUSH:接收数据,并转移的rcv_buf中
- IKCP_CMD_WASK:标记告知对方窗口大小的标识
(4)判断是否需要快重传
(5)流量控制:慢启动、拥塞避免
参考链接:
[原创] KCP 源码分析(上) - hellozhangjz - 博客园 (cnblogs.com)
[原创] KCP 源码解析(下) - hellozhangjz - 博客园 (cnblogs.com)
https://xxetb.xetslk.com/s/1QH6AQ