KCP是一个非常简单的可做流控的可靠传输协议,他的实现很多地方都借鉴了TCP的协议实现。通过学习KCP源码,能够更加熟悉ACK、重传、流控等实现方法,对TCP的理解也能有很大帮助
目录
序
1. KCP简介
2. 使用方法
2.1 客户端
2.2 服务端
3. 基本收发逻辑
3.1 数据封装
3.2 数据发送逻辑
3.3 数据接收逻辑
4. 重传
4.1 RTT、RTO计算
4.2 选择性重传
4.3 快速重传
5. UNA + ACK
5.1 ACK
5.1.1 产生ACK
5.1.2 发送ACK
5.1.3 处理ACK
5.2 UNA
5.3 非延迟ACK
6. 流量控制
6.1 阻塞窗口
KCP是一个快速可靠协议,底层通过不可靠数据传输,通过浪费带宽的代价来实现降低延迟的效果。纯算法实现的数据协议,可以通过任何形式(UDP、TCP等)发送数据,但其作用是提供低延迟、可靠、流控机制,加到TCP等已经提供相关功能的传输方法而言纯属画蛇添足。所以目前看来,基于UDP来实现KCP的应用是最为合适的一种方法。即拥有UDP简单、快速的传输效果,又能够提供可靠的传输机制。
具体的介绍可以移步:https://github.com/skywind3000/kcp,整个工程其实就两个文件,相对比较简单。
简单列举下KCP特性:
后面会从源码的角度对以上特性一一分析来了解KCP的工作机制。
在后面的讨论中,都会使用UDP作为底层的传输方式。
使用方法非常简单,最简单的demo中,客户端 + 服务端代码不超过100行就能搞定。
客户端中需要先创建UDP的socket并定义UDP的数据包发送方法,然后初始化KCP。
客户端初始化逻辑:
static int m_fd;
static sockaddr_in server;
static ikcpcb *kcp;
int udp_output(const char *buf, int len, ikcpcb *kcp, void *user) {
::sendto(m_fd, buf, len, 0, (struct sockaddr *) &server, sizeof(server));
return 0;
}
void initSocket() {
static const std::string i_host = "127.0.0.1";
static const int i_port = 1120;
m_fd = ::socket(AF_INET, SOCK_DGRAM, 0));
hostent* he = gethostbyname(i_host.c_str());
char ip[32];
inet_ntop(he->h_addrtype, he->h_addr, ip, sizeof(ip));
int flags;
flags = fcntl(m_fd, F_GETFL, NULL));
fcntl(m_fd, F_SETFL, flags | O_NONBLOCK);
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
inet_aton(ip, &server.sin_addr);
server.sin_port = htons(i_port);
}
void initKcp() {
*kcp = ikcp_create(0x11223344, NULL);
kcp->output = udp_output;
}
非常简单,不用多讲。注意倒数第二行:kcp->output = udp_output,这里给KCP定义了数据的传输方式
客户端收发逻辑:
char buff[2048];
struct sockaddr_in peer;
IUINT32 tLastSend = iclock();
const int TIME_SEND_INTERVAL = 3 * 1000;
int cntSend = 0;
while(1) {
IUINT32 now = iclock();
if (now - tLastSend > TIME_SEND_INTERVAL) {
sprintf(buff, "send %d", ++cntSend);
tLastSend = iclock();
int res_send = ikcp_send(kcp, buff, strlen(buff) + 1);
printf("send: <%s>, res:%d, strlen:%d\n", buff, res_send, strlen(buff));
}
ikcp_update(kcp, now);
socklen_t len = sizeof(peer);
ssize_t r_len = ::recvfrom(m_fd, buff, sizeof(buff), 0, (struct sockaddr*) &peer, &len);
if (r_len > 0) {
int res_input = ikcp_input(kcp, buff, r_len);
int res_recv = ikcp_recv(kcp, buff, r_len);
printf("receive: <%s>, res_input:%d, res_recv:%d, r_len:%d\n", buff, res_input, res_recv, r_len);
}
}
这段代码的含义是每3秒向服务端发送一个字符串,并接收服务端传回的字符串。因为在初始化时设置了recv为非阻塞方式,所以,这里会一直循环不会停。
与普通UDP收发方式不同的是,发送数据只用了了ikcp_send方法;接受数据依然使用UDP的接受方式,但是在收到数据之后必须调用ikcp_input和ikcp_recv。
此外,在每次循环里都调用了ikcp_update,这个方法非常重要,需要不断循环调用。随所是需要不断调用,其实也没有必要每次循环都要调用这个方法,可以使用ikcp_check来获取下次调用ikcp_update方法的时间。
与客户端类似,先进行UDP的socket初始化和数据包发送方法,然后初始化KCP
服务端初始化逻辑:
static int m_fd = 0;
static ikcpcb *kcp;
int udp_output(const char *buf, int len, ikcpcb *kcp, void *user) {
::sendto(m_fd, buf, len, 0, (struct sockaddr*)&client, sizeof(client));
return 0;
}
void initSocket() {
m_fd = ::socket(AF_INET, SOCK_DGRAM, 0));
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(i_host.c_str());
server.sin_port = htons(i_port);
::bind(m_fd, (struct sockaddr*)&server, sizeof(server));
int flags;
flags = fcntl(m_fd, F_GETFL, NULL));
fcntl(m_fd, F_SETFL, flags | O_NONBLOCK);
}
void initKcp() {
kcp = ikcp_create(0x11223344, NULL);
kcp->output = udp_output;
}
与客户端类似,不解释了
服务端收发逻辑:
struct sockaddr_in client;
char buff[2048] = {0};
int cntSend = 0;
while (1) {
socklen_t len = sizeof(client);
ssize_t r_len = ::recvfrom(m_fd, buff, sizeof(buff), 0, (sockaddr *) &client, &len);
if (r_len > 0) {
int res_input = ikcp_input(kcp, buff, r_len);
int res_recv = ikcp_recv(kcp, buff, r_len);
if (res_recv > 0) {
printf("receive: <%s>, res_input:%d, res_recv:%d\n", buff, res_input, res_recv);
sprintf(buff, "back send: %d", ++cntSend);
ikcp_send(kcp, buff, strlen(buff) + 1);
}
}
ikcp_update(kcp, iclock());
}
与客户端类似,这段代码含义是一直等待接收数据,收到数据后变向客户端返回一个数据。
同样,发送数据使用ikcp_send,收到数据后需要调用ikcp_input和ikcp_recv,也需要不断调用ikcp_update。
比较简单,以上就完成了整个利用KCP的客户端服务端收发逻辑
下面来看下KCP最基本的收发逻辑
简介中说过,KCP实际上是一个传输协议,他定义了一套自己的报文格式。
因为kcp有自己的一套报文封装格式,所以在上面使用的时候,要发送的数据是不能通过udp直接发送的,要通过调用ikcp_send来进行发送,发送前会对要发送的数据进行分组和封装。同理,udp收到的数据实际上也是不能直接用的,要通过ikcp_input、ikcp_recv进行解封装装、重组和其他一些控制逻辑才能拿到真实的发送数据。
我们先说一下这个封装体的各个字段的作用:
数据报头的最大大小是MTU,真实数据的最大大小是MSS,实际上MSS就是MTU - 24。这在初始化的代码中能够看到:
const IUINT32 IKCP_OVERHEAD = 24;
ikcpcb* ikcp_create(IUINT32 conv, void *user)
{
...
kcp->mss = kcp->mtu - IKCP_OVERHEAD;
...
return kcp;
}
对应到相应的代码中,封装有独立的方法:
//---------------------------------------------------------------------
// ikcp_encode_seg
//---------------------------------------------------------------------
static char *ikcp_encode_seg(char *ptr, const IKCPSEG *seg)
{
ptr = ikcp_encode32u(ptr, seg->conv);
ptr = ikcp_encode8u(ptr, (IUINT8)seg->cmd);
ptr = ikcp_encode8u(ptr, (IUINT8)seg->frg);
ptr = ikcp_encode16u(ptr, (IUINT16)seg->wnd);
ptr = ikcp_encode32u(ptr, seg->ts);
ptr = ikcp_encode32u(ptr, seg->sn);
ptr = ikcp_encode32u(ptr, seg->una);
ptr = ikcp_encode32u(ptr, seg->len);
return ptr;
}
/* encode 32 bits unsigned int (lsb) */
static inline char *ikcp_encode32u(char *p, IUINT32 l)
{
#if IWORDS_BIG_ENDIAN
*(unsigned char*)(p + 0) = (unsigned char)((l >> 0) & 0xff);
*(unsigned char*)(p + 1) = (unsigned char)((l >> 8) & 0xff);
*(unsigned char*)(p + 2) = (unsigned char)((l >> 16) & 0xff);
*(unsigned char*)(p + 3) = (unsigned char)((l >> 24) & 0xff);
#else
*(IUINT32*)p = l;
#endif
p += 4;
return p;
}
解封装是在ikcp_input里实现的,没有单独的方法:
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
...
while (1) {
IUINT32 ts, sn, len, una, conv;
...
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, &len);
...
data += len;
size -= len;
}
return 0;
}
每个数据报是由一个单独的结构体来维护的,这个结构体是一个双向链表:
struct IQUEUEHEAD {
struct IQUEUEHEAD *next, *prev;
};
//=====================================================================
// SEGMENT
//=====================================================================
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];
};
先不要管流量控制、超时重传、丢包这些东西,这些会在下面讲到。我们只来讨论最基本的发送逻辑。
//---------------------------------------------------------------------
// user/upper level send, returns below zero for error
//---------------------------------------------------------------------
int ikcp_send(ikcpcb *kcp, const char *buffer, int len)
{
IKCPSEG *seg;
int count, i;
...
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;
}
前面说了,当你要发送数据的时候需要调用ikcp_send方法,他会对你的数据进行分组和封装。
count 是发送传入的数据需要使用的数据报个数。传如的数据可能很大,一个数据报可能装不下(前面说了最大数据报是mss),第9~14行就是来计算数据报的数量,分组的数据报序号被倒序写入frg里面(28行)。
19行创建了一个报文数据结构,25行将要发送的数据拷贝到数据结构里面,30行将数据报文添加到待发送队列队尾。
kcp->snd_queue是待发送队列,强调一下是“待发送队列”。是一个按找SN号排列的有序队列,31行的kcp->nsnd_que是待发送队列的长度。
看到这里ikcp_send其实已经完了,但是并没有实际发送数据,而只是将数据放到了待发送队列里面。
最关键的在ikcp_update里面:
//---------------------------------------------------------------------
// update state (call it repeatedly, every 10ms-100ms), or you can ask
// ikcp_check when to call it again (without ikcp_input/_send calling).
// 'current' - current timestamp in millisec.
//---------------------------------------------------------------------
void ikcp_update(ikcpcb *kcp, IUINT32 current)
{
IINT32 slap;
kcp->current = current;
...
if (slap >= 0) {
kcp->ts_flush += kcp->interval;
if (_itimediff(kcp->current, kcp->ts_flush) >= 0) {
kcp->ts_flush = kcp->current + kcp->interval;
}
ikcp_flush(kcp);
}
}
current是当前的时钟变量。KCP将时钟的定义交给了使用者,使用者只要调用这个方法传入当前平台的系统时钟即可。
kcp->ts_flush是下次需要执行ikcp_flush的时间,kcp→interval表示执行ikcp_flush的时间间隔。
最最重要的部分是在ikcp_update的最后调用了ikcp_flush,ikcp_flush是真正去发送数据的方法。以上也说明了在主逻辑里面为什么要不断的调用ikcp_update方法,他为整个KCP的运行提供了时钟并调用flush清空待发送队列。
来看ikcp_flush:
//---------------------------------------------------------------------
// ikcp_flush
//---------------------------------------------------------------------
void ikcp_flush(ikcpcb *kcp)
{
...
// calculate window size
cwnd = _imin_(kcp->snd_wnd, kcp->rmt_wnd);
if (kcp->nocwnd == 0) cwnd = _imin_(kcp->cwnd, cwnd);
// move data from snd_queue to snd_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);
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;
}
// calculate resent
resent = (kcp->fastresend > 0)? (IUINT32)kcp->fastresend : 0xffffffff;
rtomin = (kcp->nodelay == 0)? (kcp->rx_rto >> 3) : 0;
// flush data segments
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;
}
...
if (needsend) {
int size, 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);
if (segment->len > 0) {
memcpy(ptr, segment->data, segment->len);
ptr += segment->len;
}
if (segment->xmit >= kcp->dead_link) {
kcp->state = -1;
}
}
}
// flash remain segments
size = (int)(ptr - buffer);
if (size > 0) {
ikcp_output(kcp, buffer, size);
}
...
}
// output segment
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);
}
第8行计算阻塞窗口的大小(后面流量控制再讨论)
第12行的循环意思是:当对端接收窗口允许的情况下,将待发送队列的数据报复制到kcp→snd_buf中,kcp→snd_buf表示已发送队列待确认队列,重复“已发送队列待确认队列”
第40行的循环表示:对所有的已发送待确认队列,如果是第一次发送(未重传),则构造真实的数据报到ptr中。ptr和buffer这两个也是很重要的变量,之前没讲。buffer是一个固定大小的内存数组,大小是MTU的3倍。ptr算是buffer的pc指针。ptr开始指向buffer,每写一些数据,ptr遍向后移动一些距离,ptr - buffer也就表示了已经写入buffer的数据大小。
第81行,将所有需要发送的数据发出去。61行也有同样的调用,意思是,如果buffer满了,先发送一批。在ikcp_output方法中,最终调用了kcp->output方法。可以回到最上面“客户端使用方法”代码中的第33行,他实际上就是调用了udp的接口将数据发出去。
还有一些变量需要补充一下:
第43行segment->xmit,表示该数据报被发送的次数,包括后面要说的重传在内,每发送一次这个变量自增1。
第47行segment->resendts表示如果该数据报未被确认时数据重传的时间kcp->rx_rto,是重传超时时间,后面会讨论,rtomin是重传延迟,可以通过ikcp的接口进行设置。
第55行的kcp->rcv_nxt,表示当前端下一个要接收的数据报的序号。他跟减少确认报文的策略有关,后面再讨论。
前面有说,数据接收时数据是用udp的接口进行接收的。但是收到的数据是不能用的,因为他包含了KCP协议的报文头。需要执行ikcp_input、ikcp_recv将数据解开报文头,如果数据太大还要进行数据重组。那么先来看下ikcp_input的工作:
//---------------------------------------------------------------------
// input data
//---------------------------------------------------------------------
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
IUINT32 una = kcp->snd_una;
IUINT32 maxack = 0;
int flag = 0;
if (ikcp_canlog(kcp, IKCP_LOG_INPUT)) {
ikcp_log(kcp, IKCP_LOG_INPUT, "[RI] %d bytes", size);
}
if (data == NULL || (int)size < (int)IKCP_OVERHEAD) return -1;
while (1) {
...
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", sn, ts);
}
if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) < 0) {
ikcp_ack_push(kcp, sn, ts);
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);
}
ikcp_parse_data(kcp, seg);
}
}
}
...
}
...
return 0;
}
第16行,一个循环来对data进行解包,因为data可能包含不止一个数据报,所以用循环来执行。直到解完最后一个数据报跳出循环。
第18行,cmd就是数据报头的cmd字段(前面“数据解包”部分代码已经展示了是怎么拿到这个变量的),如果cmd是IKCP_CMD_PUSH,也就是用户发送的真实数据,那么会执行:
其他的:
_itimediff方法传入两个参数A,B,可以理解为A-B
第23行:判断收到的数据报是否小于接收窗口尾部,在则处理,不在丢弃数据报。kcp->rcv_nxt表示下一个要接收的数据报序号,可以理解为接收窗口头部。kcp->rcv_wnd表示接收窗口。
第25行:判断收到的数据报是否大于接收窗口头部,在则处理,不在丢弃。
要知道,因为涉及到丢包重传的机制,而且ACK包也有可能会出现丢包现象。所以很有可能会出现接收端已经正确接收了某个数据报,但是发送端没有收到这个数据报的ACK,所以发送端可能会重传一次,这种情况(当然这只是其中一种情况)可能就会出现接收端收到了小于接收窗口头部的包。这种情况就把收到的包直接丢掉。
第24行的ikcp_ack_push(kcp, sn, ts);暂且不讲,这里就先记住,收到的所有PUSH数据报都会产生一个ACK,并放到ACK队列中。
下面来看ikcp_parse_data。前面说,收到的数据会先放到队列里面,但是还不确定是那个队列。为什么不知道是那个队列,因为收到的这个数据报可能是后发送的数据报。因为网络链路或者丢包等原因,发送端是按序发送数据报,但是接收端不一定是按序收到数据报,如果接收端先收到了后面的数据报,那就先将他暂存起来。
KCP的接收有也有两个队列分别是:
//---------------------------------------------------------------------
// parse data
//---------------------------------------------------------------------
void ikcp_parse_data(ikcpcb *kcp, IKCPSEG *newseg)
{
struct IQUEUEHEAD *p, *prev;
IUINT32 sn = newseg->sn;
int repeat = 0;
if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) >= 0 ||
_itimediff(sn, kcp->rcv_nxt) < 0) {
ikcp_segment_delete(kcp, newseg);
return;
}
for (p = kcp->rcv_buf.prev; p != &kcp->rcv_buf; p = prev) {
IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node);
prev = p->prev;
if (seg->sn == sn) {
repeat = 1;
break;
}
if (_itimediff(sn, seg->sn) > 0) {
break;
}
}
if (repeat == 0) {
iqueue_init(&newseg->node);
iqueue_add(&newseg->node, p);
kcp->nrcv_buf++;
} else {
ikcp_segment_delete(kcp, newseg);
}
#if 0
ikcp_qprint("rcvbuf", &kcp->rcv_buf);
printf("rcv_nxt=%lu\n", kcp->rcv_nxt);
#endif
// move available data from rcv_buf -> rcv_queue
while (! iqueue_is_empty(&kcp->rcv_buf)) {
IKCPSEG *seg = iqueue_entry(kcp->rcv_buf.next, IKCPSEG, node);
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++;
kcp->rcv_nxt++;
} else {
break;
}
}
#if 0
ikcp_qprint("queue", &kcp->rcv_queue);
printf("rcv_nxt=%lu\n", kcp->rcv_nxt);
#endif
#if 1
// printf("snd(buf=%d, queue=%d)\n", kcp->nsnd_buf, kcp->nsnd_que);
// printf("rcv(buf=%d, queue=%d)\n", kcp->nrcv_buf, kcp->nrcv_que);
#endif
}
前面说过,KCP里面的队列基本都是按SN的有序队列,第16 ~ 26行就是把收到的数据报插入到rcv_buf队列中,并保证有序。
第42 ~ 53行中遍历所有rcv_buf队列中的数据报,如果数据报的SN(数据报序号)与下一个要接收的数据报序号相同则将数据报放入待反队列。再说一下各个变量的含义,kcp->rcv_nxt表示下一个要接收的数据报序号,也可以理解为接收窗口头部,kcp->nrcv_que表示待反队列中的数据报个数,kcp->rcv_wnd表示待反队列的窗口大小(就理解为待反队列的最大长度吧)。如果满足条件,就将数据报从rcv_buf转移到kcp->rcv_queue(都是按序的)。如果不满足条件跳出循环,此时rcv_buf可能还有一些数据报,因为可能会出现乱序到达的情况,乱序到达时由rcv_buf先暂失序的数据报。
第20行的repeat表示该数据报已经接收过了,因为可能网络的一些原因发送端没有收到该数据报的ACK,所以又重传了一次。如果有repeat在第28行丢弃他。
第10 ~ 14行也是判断数据报是否在接收窗口内,不在,丢弃他。不用担心未处理的数据报被丢弃,因为不会产生这种数据报的ACK,所以发送端会重传这些数据报。
下面到了最后一个步骤,将收到的所有数据返给调用者:
//---------------------------------------------------------------------
// user/upper level recv: returns size, returns below zero for EAGAIN
//---------------------------------------------------------------------
int ikcp_recv(ikcpcb *kcp, char *buffer, int len)
{
struct IQUEUEHEAD *p;
int ispeek = (len < 0)? 1 : 0;
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)
return -3;
if (kcp->nrcv_que >= kcp->rcv_wnd)
recover = 1;
// merge fragment
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);
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", seg->sn);
}
if (ispeek == 0) {
iqueue_del(&seg->node);
ikcp_segment_delete(kcp, seg);
kcp->nrcv_que--;
}
if (fragment == 0)
break;
}
assert(len == peeksize);
...
return len;
}
比较简单了,一个循环遍历待反队列,并将数据拷贝到调用者传入的buffer里面。还是要注意,数据大的时候是要分组的,而且分组数(frg)在数据报中是倒序排放的,所以到每个frg为0的时候就跳出循环。
如果待反队列里面的数据比较多,看似是要不断的调用ikcp_recv以获取数据。
保证可靠传输的很重要一个策略就是超时重传,超时重传的一个关键问题是如何计算RTT及RTO。
RTO(Retransmission TimeOut):重传超时时间,即从数据发送时刻起,超过这个时间将进行数据报重传。
RTT(Round Trip Time):往返时间,即数据报发送到收到确认的时差。
在“数据封装”中提到过,数据报的第8 ~ 11字节为ts字段,ts表示数据报发送时刻的时钟,他在计算RTT中起到了关键作用。
先看下计算RTT的时机:
//---------------------------------------------------------------------
// input data
//---------------------------------------------------------------------
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
...
while (1) {
...
if (cmd == IKCP_CMD_ACK) {
if (_itimediff(kcp->current, ts) >= 0) {
ikcp_update_ack(kcp, _itimediff(kcp->current, ts));
}
ikcp_parse_ack(kcp, sn);
ikcp_shrink_buf(kcp);
if (flag == 0) {
flag = 1;
maxack = sn;
} else {
if (_itimediff(sn, maxack) > 0) {
maxack = sn;
}
}
if (ikcp_canlog(kcp, IKCP_LOG_IN_ACK)) {
ikcp_log(kcp, IKCP_LOG_IN_DATA,
"input ack: sn=%lu rtt=%ld rto=%ld", sn,
(long)_itimediff(kcp->current, ts),
(long)kcp->rx_rto);
}
}
...
}
...
return 0;
}
如果收到的数据报为ACK数据报,那么就会在第11行执行ikcp_update_ack,这个方法就是计算RTT、RTO的。ts是数据报中的ts字段,表示数据报发送是的时钟,不过注意ts不是ACK这个数据报发送的时钟,而是ACK要确认的PUSH数据报发送的时钟。
来看下如何计算RTT和RTO的:
//---------------------------------------------------------------------
// parse ack
//---------------------------------------------------------------------
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);
}
有兴趣的同学可以去翻阅《计算机网络 自顶向下方法 原书第七版》中文版中的第158页“3.5.3 往返时间的估计与超时”。这里的计算方法和书上讲的计算方法一模一样。
kcp->rx_srtt就是当前计算出的RTT时间。
kcp->rx_rttval是当前RTT偏差。
kcp->rx_rto就是之后用来重传的超时时间(在普通模式下,这个超时时间还会加一个延迟,在后面“非延迟ACK”中会有提到)。
RTT的计算,就是纯数学计算,真要探究为啥这么算可能要去研究其数学模型了。
后面“处理ACK”的部分会讨论
快速重传即不等待超时,当探测到达丢包的可能行达到一定程度之后,直接对未确认的数据报机型重传。KCP中设置kcp->fastresend可以启动快速重传模式。
//---------------------------------------------------------------------
// input data
//---------------------------------------------------------------------
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
...
while (1) {
...
if (cmd == IKCP_CMD_ACK) {
if (_itimediff(kcp->current, ts) >= 0) {
ikcp_update_ack(kcp, _itimediff(kcp->current, ts));
}
ikcp_parse_ack(kcp, sn);
ikcp_shrink_buf(kcp);
if (flag == 0) {
flag = 1;
maxack = sn;
} else {
if (_itimediff(sn, maxack) > 0) {
maxack = sn;
}
}
if (ikcp_canlog(kcp, IKCP_LOG_IN_ACK)) {
ikcp_log(kcp, IKCP_LOG_IN_DATA,
"input ack: sn=%lu rtt=%ld rto=%ld", sn,
(long)_itimediff(kcp->current, ts),
(long)kcp->rx_rto);
}
}
...
}
if (flag != 0) {
ikcp_parse_fastack(kcp, maxack);
}
...
return 0;
}
static void ikcp_parse_fastack(ikcpcb *kcp, IUINT32 sn)
{
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) {
seg->fastack++;
}
}
}
发送端收到ACK数据报后还会去检查失序数据报(第16行和第33行)
在不断调用ikcp_input的过程中,seg->fastack自增,该值表示了数据报失序到达的次数。
//---------------------------------------------------------------------
// ikcp_flush
//---------------------------------------------------------------------
void ikcp_flush(ikcpcb *kcp)
{
...
// calculate resent
resent = (kcp->fastresend > 0)? (IUINT32)kcp->fastresend : 0xffffffff;
rtomin = (kcp->nodelay == 0)? (kcp->rx_rto >> 3) : 0;
// flush data segments
for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = p->next) {
IKCPSEG *segment = iqueue_entry(p, IKCPSEG, node);
...
else if (segment->fastack >= resent) {
needsend = 1;
segment->xmit++;
segment->fastack = 0;
segment->resendts = current + segment->rto;
change++;
}
...
}
...
}
在ikcp_flush中,如果设置了快速重传失序数(第八行的kcp->fastresend),一旦超过这个失序数,失序的数据报便会不等待超时时间,直接执行重传操作。
在“数据接收逻辑”的代码第24行,调用了ikcp_ack_push。每当收到PUSH数据报时,都会通过这个方法产生数据报的ACK,并放到ACK队列中
//---------------------------------------------------------------------
// ack append
//---------------------------------------------------------------------
static void ikcp_ack_push(ikcpcb *kcp, IUINT32 sn, IUINT32 ts)
{
size_t newsize = kcp->ackcount + 1;
IUINT32 *ptr;
if (newsize > kcp->ackblock) {
IUINT32 *acklist;
size_t newblock;
for (newblock = 8; newblock < newsize; newblock <<= 1);
acklist = (IUINT32*)ikcp_malloc(newblock * sizeof(IUINT32) * 2);
if (acklist == NULL) {
assert(acklist != NULL);
abort();
}
if (kcp->acklist != NULL) {
size_t x;
for (x = 0; x < kcp->ackcount; x++) {
acklist[x * 2 + 0] = kcp->acklist[x * 2 + 0];
acklist[x * 2 + 1] = kcp->acklist[x * 2 + 1];
}
ikcp_free(kcp->acklist);
}
kcp->acklist = acklist;
kcp->ackblock = newblock;
}
ptr = &kcp->acklist[kcp->ackcount * 2];
ptr[0] = sn;
ptr[1] = ts;
kcp->ackcount++;
}
kcp->ackcount是还没发送的ACK的数量,kcp→ackblock当前ACK队列的容量。只不过ACK队列是用一个数组来表示的,这个数组的 X * 2 + 0位是数据报的序号,X * 2 + 1位是收到的PUSH数据报发送时的时钟。 强调是”收到的PUSH数据报发送时的时钟”,这个时钟就厉害了,他是计算RTT和RTO的重要参数,后面再讨论。
现在且记下会在ACK队列里面产生一个ACK,后面再讨论ACK的发送。
前面已经介绍了,接收端收到了PUSH的数据报之后会产生ACK放到ACK队列里面,kcp->acklist是以数组形式表示的ACK队列,kcp->ackcount表示了ACK队列中的元素个数。下面我们就来看下ACK是如何发送的。
位置还是在ikcp_flush里面,前面“数据发送逻辑”里面也介绍过,ikcp_flush是由ikcp_update调用的,ikcp_update需要不断的被调用这调用以便更新时钟以及清空发送队列。
//---------------------------------------------------------------------
// ikcp_flush
//---------------------------------------------------------------------
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;
// 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;
...
// flash remain segments
size = (int)(ptr - buffer);
if (size > 0) {
ikcp_output(kcp, buffer, size);
}
...
}
static void ikcp_ack_get(const ikcpcb *kcp, int p, IUINT32 *sn, IUINT32 *ts)
{
if (sn) sn[0] = kcp->acklist[p * 2 + 0];
if (ts) ts[0] = kcp->acklist[p * 2 + 1];
}
比较简单了,不再介绍。
//---------------------------------------------------------------------
// input data
//---------------------------------------------------------------------
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
...
while (1) {
...
if (cmd == IKCP_CMD_ACK) {
if (_itimediff(kcp->current, ts) >= 0) {
ikcp_update_ack(kcp, _itimediff(kcp->current, ts));
}
ikcp_parse_ack(kcp, sn);
ikcp_shrink_buf(kcp);
if (flag == 0) {
flag = 1;
maxack = sn;
} else {
if (_itimediff(sn, maxack) > 0) {
maxack = sn;
}
}
if (ikcp_canlog(kcp, IKCP_LOG_IN_ACK)) {
ikcp_log(kcp, IKCP_LOG_IN_DATA,
"input ack: sn=%lu rtt=%ld rto=%ld", sn,
(long)_itimediff(kcp->current, ts),
(long)kcp->rx_rto);
}
}
...
}
...
return 0;
}
static void ikcp_parse_ack(ikcpcb *kcp, IUINT32 sn)
{
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 (sn == seg->sn) {
iqueue_del(p);
ikcp_segment_delete(kcp, seg);
kcp->nsnd_buf--;
break;
}
if (_itimediff(sn, seg->sn) < 0) {
break;
}
}
}
static void ikcp_shrink_buf(ikcpcb *kcp)
{
struct IQUEUEHEAD *p = kcp->snd_buf.next;
if (p != &kcp->snd_buf) {
IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node);
kcp->snd_una = seg->sn;
} else {
kcp->snd_una = kcp->snd_nxt;
}
}
还是在ikcp_input方法中,当收到ACK后第18行调用ikcp_parse_ack处理ACK。
第37 ~ 57行展示了ikcp_parse_ack处理ACK的过程,比较简单,就是找到ACK所代表数据报序号,将已发送待确认队列数据报删掉。
处理完ACK后,可以看到第19行调用了ikcp_shrink_buf。第60 ~ 69行展示了ikcp_shrink_buf的过程。他是将una向后移动,即发送窗口头部向后移动。
如何实现的快速重传:
结合“数据发送逻辑”中讨论的数据报发送的过程。所有的已发送但是还没有确认的数据报会被放到kcp->snd_buf这个队列当中,当收到ACK时,会在kcp->snd_buf队列中删掉已经确认过得数据报,然后在通过ikcp_shrink_buf方法移动发送窗口。同样“数据接收逻辑”中也讨论过,收到的数据报会在接收端先放到kcp->rcv_buf队列中,直到收到完整且连续的数据报后才会转移到kcp->rcv_queue并传给调用者。 当接收端出现失序或丢包情况时,发送端可能会先收到后面数据报的ACK,发送端就会先把kcp->snd_buf中后面的数据报删掉,不再重新发送。如果前面的数据报接收ACK超时,会只重发前面未收到ACK的数据报。当发送窗口前端连续的数据报都收到ACK后,便会向后移动发送窗口。这种逻辑就是前面说过的“选择性重传”,与之对应的是回退式的重传。
KCP在使用ACK确认的同时,也使用了捎带式的UNA确认。大概思路是:在收发的数据报中添加了UNA字段,该字段表示的是对端下一个要接收的数据报序号。这个字段也表示,所有序号小于UNA的数据报已经被对端正确接收。那么也就是说,所有未收到ACK的数据报如果SN小于UNA,都可以直接丢弃了,因为这些数据报已经被对端正确接收,不用再等待ACK进行重传了。
还是来看ikcp_input方法:
//---------------------------------------------------------------------
// input data
//---------------------------------------------------------------------
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
IUINT32 una = kcp->snd_una;
IUINT32 maxack = 0;
int flag = 0;
if (ikcp_canlog(kcp, IKCP_LOG_INPUT)) {
ikcp_log(kcp, IKCP_LOG_INPUT, "[RI] %d bytes", size);
}
if (data == NULL || (int)size < (int)IKCP_OVERHEAD) return -1;
while (1) {
...
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, &len);
...
ikcp_parse_una(kcp, una);
ikcp_shrink_buf(kcp);
...
}
...
return 0;
}
第29行ikcp_parse_una(kcp, una);就是丢弃una之前的所有待发送数据报,代码如下:
static void ikcp_parse_una(ikcpcb *kcp, IUINT32 una)
{
struct IQUEUEHEAD *p, *next;
for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = next) {
IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node);
next = p->next;
if (_itimediff(una, seg->sn) > 0) {
iqueue_del(p);
ikcp_segment_delete(kcp, seg);
kcp->nsnd_buf--;
} else {
break;
}
}
}
之后执行ikcp_shrink_buf,前面“处理ACK”部分也已经提到过了,是为了更新本地的una将发送窗口头部向后移动。
这是一个设置项,在kcp->nodelay中设置:
//---------------------------------------------------------------------
// ikcp_flush
//---------------------------------------------------------------------
void ikcp_flush(ikcpcb *kcp)
{
...
// calculate resent
resent = (kcp->fastresend > 0)? (IUINT32)kcp->fastresend : 0xffffffff;
rtomin = (kcp->nodelay == 0)? (kcp->rx_rto >> 3) : 0;
// flush data segments
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 += kcp->rx_rto;
} else {
segment->rto += kcp->rx_rto / 2;
}
segment->resendts = current + segment->rto;
lost = 1;
}
else if (segment->fastack >= resent) {
needsend = 1;
segment->xmit++;
segment->fastack = 0;
segment->resendts = current + segment->rto;
change++;
}
...
}
...
}
如上代码segment->resendts表示的是如果为收到ACK,数据报重传的时间点。
第9行,如果设置了无延迟模式,即kcp->nodelay不是0,那么rtomin为0。rtomin实际上是代表了超时重传的延迟时间,这个数值会影响后面的重传时间。
第19行,如果是第一次发送数据报,超时时间直接设置到RTO之后并加上rtomin的时间点,非延迟模式下rtomin为0,超时时间就是RTO时间。
第25行,如果是已经出现重传的数据报,非延迟模式下,下次重传的时间是RTO/2。重传延迟是普通模式下的1/2。
其实可以看到,但出现数据报重传后非延迟模式下减小了数据重传的超时时间,网络中可能会出现多个重传的数据报,是一种用带宽换速度的策略。具体效果只能看实测数据,至少其github上写的是有效的。
kcp->cwnd这个变量记录了当前发送的窗口大小,其实这个就是我们所指的阻塞窗口。他是用来调节发送端的发送速度的。
//---------------------------------------------------------------------
// ikcp_flush
//---------------------------------------------------------------------
void ikcp_flush(ikcpcb *kcp)
{
...
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;
...
// calculate window size
cwnd = _imin_(kcp->snd_wnd, kcp->rmt_wnd);
if (kcp->nocwnd == 0) cwnd = _imin_(kcp->cwnd, cwnd);
// move data from snd_queue to snd_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);
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;
}
// calculate resent
resent = (kcp->fastresend > 0)? (IUINT32)kcp->fastresend : 0xffffffff;
rtomin = (kcp->nodelay == 0)? (kcp->rx_rto >> 3) : 0;
// flush data segments
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 += kcp->rx_rto;
} else {
segment->rto += kcp->rx_rto / 2;
}
segment->resendts = current + segment->rto;
lost = 1;
}
else if (segment->fastack >= resent) {
needsend = 1;
segment->xmit++;
segment->fastack = 0;
segment->resendts = current + segment->rto;
change++;
}
if (needsend) {
int size, 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);
if (segment->len > 0) {
memcpy(ptr, segment->data, segment->len);
ptr += segment->len;
}
if (segment->xmit >= kcp->dead_link) {
kcp->state = -1;
}
}
}
// flash remain segments
size = (int)(ptr - buffer);
if (size > 0) {
ikcp_output(kcp, buffer, size);
}
// update ssthresh
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;
}
}
static int ikcp_wnd_unused(const ikcpcb *kcp)
{
if (kcp->nrcv_que < kcp->rcv_wnd) {
return kcp->rcv_wnd - kcp->nrcv_que;
}
return 0;
}
第68行的lost表示了丢包的个数。
第75行的change表示了进行快速重传的包的个数。
第112 ~ 133行中展示了,一旦出现丢包或者有快速重传的现象存在,那么就要逐渐的减小kcp->cwnd发送窗口大小。
代码中在第21行将kcp->snd_queue的数据报放入kcp->snd_buf时,通过cwnd来限制转移的数据报数量。通过控制转移入kcp→snd_buf的数据报数量,来控制整个发送端的发送。
//---------------------------------------------------------------------
// input data
//---------------------------------------------------------------------
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
...
if (_itimediff(kcp->snd_una, 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) {
kcp->cwnd++;
}
}
if (kcp->cwnd > kcp->rmt_wnd) {
kcp->cwnd = kcp->rmt_wnd;
kcp->incr = kcp->rmt_wnd * mss;
}
}
}
return 0;
}
kcp->rmt_wnd表示对端的接收窗口大小,这个数值是怎么来的?可以看上面flush方法里面的第10行,在每次flush时都会计算自己端的接收窗口,并通过数据报传到对端。具体的计算方法在上面代码中的最后ikcp_wnd_unused展示了。
在收到数据时判断kcp->snd_una与una的大小。kcp->snd_una是发送端下一个未收到ACK的数据报,una是已经收到的ACK。如果una大于kcp->snd_una,也就是说收到的ACK的una比下一个要确认的snd_una大,那么此时可能会有丢包现象的存在。相反,如果kcp->snd_una比una大,那么此时网络可能比较畅通。当网络畅通时,逐渐增加kcp->cwnd,这样在flush的时候就能够从kcp->snd_queue放入更多的数据报到kcp->snd_buf中。这样就实现了逐渐扩大发送流量。