Analysis of the principle of network transmission protocol kcp

1 Overview

For game development, especially MOBA (Multiplayer Online Arena) games, latency needs to be controlled. But for traditional TCP (network friendly, great), it is not conducive to the real-time transmission of packets, because its timeout retransmission and congestion control are network-friendly, and there is no advantage for the real-time performance of our packets. Therefore, it is generally necessary to implement a set of own network protocols based on UDP to ensure real-time and reliable packets. In fact, it sacrifices the friendliness of TCP, sacrifices bandwidth, and trades space for time. Based on UDP, there are some excellent protocols on the Internet, such as KCP.

2. Implementation principle

KCP is just a simple algorithm implementation and does not involve any underlying calls. We only need to register the KCP callback function when the UDP system is called, and then it can be used. So it can be understood as an application layer protocol. Compare TCP:

  • Double the RTO of TCP. The concept is terrifying. KCP is 1.5 times.

  • Selective retransmission, where only lost packets are transmitted.

  • Fast retransmission without waiting for a timeout. Default several retransmissions

  • TCP will delay sending ACK. KCP can be set

  • Non-concession flow control. The send window can only depend on the size of the send buffer and the remaining receive buffer size at the receiver.

In order to implement selective retransmission (ARQ), KCP maintains a receive window (sliding window). If ordered data is received, it will be placed in the receive queue for consumption by the application layer. If there is packet loss, it will be judged. If it exceeds the set number of times, it will choose to retransmit the corresponding packet. In fact, it is through an rcv_nxt (the current offset of the receiving window) to determine the current data packets that need to be accepted. If the received packet is in window scope, but not rcv_nxt. Save first, and wait for the packets to be continuous before putting continuous data packets into the accept queue for consumption by the application layer. In the same case of poor network, KCP will also implement congestion control to limit the packets of the sender.

Private message me to receive the latest and most complete C++ audio and video learning and improvement materials, including ( C/C++ , Linux , FFmpeg , webRTC , rtmp , hls , rtsp , ffplay , srs )

 

3. Source code analysis

First of all, we should go to github to see how to use it before analyzing. In fact, it is very simple. Initializing the kcp object and then implementing the callback function is actually implementing its own underlying UDP system call. Every time we send a packet through KCP, he will call this callback. After UDP receives the packet, call the ikcp_input function. We finally only need to send and receive data through ikcp_send and ikcp_recv.

Before looking at the code, let's take a look at the structure of the kcp packet, Segement


struct IKCPSEG
{
   struct IQUEUEHEAD node;
   IUINT32 conv;     //会话编号,两方一致才会通信
   IUINT32 cmd;      //指令类型,四种下面会说
   IUINT32 frg;      //分片编号 倒数第几个seg。主要就是用来合并一块被分段的数据。
   IUINT32 wnd;      //自己可用窗口大小    
   IUINT32 ts;
   IUINT32 sn;       //编号 确认编号或者报文编号
   IUINT32 una;      //代表编号前面的所有报都收到了的标志
   IUINT32 len;
   IUINT32 resendts; //重传的时间戳。超过当前时间重发这个包
   IUINT32 rto;      //超时重传时间,根据网络去定
   IUINT32 fastack;  //快速重传机制,记录被跳过的次数,超过次数进行快速重传
   IUINT32 xmit;     //重传次数
   char data[1];     //数据内容
};
1234567891011121314151617

Kcp realizes stable communication through these fields of the data packet, and can be optimized for different points. From the above fields, it can also be seen that kcp implements selective retransmission with the help of UNA and ACK.

First look at the logic of packet sending, we will call the ikcp_send method.

This method will first judge the kcp stream. And try to append the package to the previous paragraph, if possible. Otherwise, fragment transmission is performed.

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;

In the above code logic, count is actually the number of shards of the packet. Then loop to create a segment. The data structure of the segment is mainly to save the packet information of the fragment. For example eg->frg saves the number of the current shard. After finishing, call the iqueue_add_tail method to pass the segment to the send queue. These methods are implemented through macro definitions. In fact, it is a linked list operation. A queue is a doubly linked list. The logic is simple. Then at this step, the data shards are put into the queue. Where is the specific sending logic implemented? Continue to look down.

Let's take a look at the logic of the callback, which is actually the ikcp_output method, which will be called in ikcp_flush. That is, what ikcp_output does is the final data transmission. How is that driven? I'll take a look at this method first.

1. This method first sends an ack. Iterate over all acks. Call the ikcp_output method to send.

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;

 2. Determine whether window detection is currently required, because if the window is 0, data cannot be sent, so window detection must be performed. After the detection, if necessary, set the detection window time. Send a request to probe the window or a request to restore the window. The main thing is to request the peer window size and inform the remote window size.

    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;
     }

Put the result in a seg when you're done.

3. Calculate the window size available for this transmission, which is determined by multiple factors, and KCP can be selectively configured. You can choose not to incorporate a flow control window.

4. Put the message in the send queue into the send buffer, which is actually the send window. That is to say, all sent data will be in this buffer. Before sending data, you also need to set the corresponding retransmission times and interval.

    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;
     }

This logic is relatively simple, in fact, a seg is taken from the sending window queue. Then set the corresponding parameters. And update the buffer queue. and the size of the buffer queue. If nodelay is set, the retransmission time has *2 become 1.5

5. Traverse the send window queue. Determine whether there is data that needs to be sent (including retransmission). In fact, it is to get the corresponding segment, and then logically determine whether it needs to be retransmitted according to the information. Or need to send. After the judgment is completed, retransmission is performed.

The logic is also very simple

  • If the packet is transmitted for the first time, send it directly.

  • If the retransmission time of the packet is reached, transmit it again and record the loss flag

  • If the number of skips exceeds the fastack, retransmit.

In fact, lost and change are fields used to update the window size. And the two update algorithms are different.

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) {
   if ((int)segment->xmit <= kcp->fastlimit || 
      kcp->fastlimit <= 0) {
      needsend = 1;
      segment->xmit++;
      segment->fastack = 0;
      segment->resendts = current + segment->rto;
      //用来标示发生了快速重传  
      change++;
   }
}

Basically all the logic of fast retransmission and timeout retransmission is in this method. If there is a timeout retransmission (packet loss), it will enter the slow start, the congestion window will be halved, and the sliding window will become 1. The congestion window is also updated if fast retransmissions occur. See the code for the specific algorithm.

After reading this flush method, we basically understand the logic of sending data. Then see where he calls it.

In fact, it is called in the ikcp_update method. This method needs to be called repeatedly by the application layer. Generally, it can be 10ms and 100ms. The time will determine the real-time nature of data transmission. That is to say, it will regularly refresh the data of the queue to judge the sending window or the data that needs to be retransmitted, and send the data through the underlying UDP. There is no logic to this method.

void ikcp_update(ikcpcb *kcp, IUINT32 current)
{
     IINT32 slap;
     kcp->current = current;
     if (kcp->updated == 0) {
         kcp->updated = 1;
         kcp->ts_flush = kcp->current;
     }
     slap = _itimediff(kcp->current, kcp->ts_flush);
     if (slap >= 10000 || slap < -10000) {
         kcp->ts_flush = kcp->current;
         slap = 0;
     }
     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);
     }
}

Then call the ikcp_parse_una and ikcp_shrink_buf methods according to the fields. The former is to parse una to determine which other parties have received the data packets that have been sent. If received directly re-accept the window to remove. The latter is send_una to update kcp. send_una means that the previous packet has been confirmed to be received.

2. If it is an ACK command, it is actually doing some processing.

ikcp_update_ack is mainly to update some parameters of kcp, including rtt and rto. First, the ikcp_parse_ack method is mainly to remove the corresponding segment in the sending queue according to sn. Then update maxack and time, and record the log

if (cmd == IKCP_CMD_ACK) {
   if (_itimediff(kcp->current, ts) >= 0) {
      ikcp_update_ack(kcp, _itimediff(kcp->current, ts));
   }
   ikcp_parse_ack(kcp, sn);
   //根据snd队列去更新una     
   ikcp_shrink_buf(kcp);
   if (flag == 0) {
      flag = 1;
      maxack = sn;
      latest_ts = ts;
   }  else {
      if (_itimediff(sn, maxack) > 0) {
      #ifndef IKCP_FASTACK_CONSERVE
        //记录最大ACK
         maxack = sn;
         latest_ts = ts;
      #else
         if (_itimediff(ts, latest_ts) > 0) {
            maxack = sn;
            latest_ts = ts;
         }
      #endif
      }
   }
//打印日志
}

3. If the data packet is received, the logic is actually very simple. It is to detect the data and put the valid data in the receiving queue. The first is to judge whether the data packet is valid. If so, construct a segment. Put the data in. , and then call the ikcp_parse_data method. The logic of this method is also relatively simple. In fact, it is to judge whether it is valid. If it has been received, it will be discarded. Otherwise, it will be inserted into the receiving queue according to the sn (number).

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);
      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);
      }
   }
}

If it is a package that asks for the window size. This is actually a mark, because the header of each kcp has the size of win. All that's left is to update the congestion and window size based on network conditions.

4. Summary

Looking at the implementation of kcp, it is found that it is similar to the TCP of the transport layer, but it is fine-tuned and controllable. For example, sacrificing flow control to ensure real-time transmission of data packets. Or speed up retransmission and so on. There is also selective retransmission through una and ack. In general, it still has certain advantages in the field of game frame synchronization or real-time data transmission.

Guess you like

Origin blog.csdn.net/m0_60259116/article/details/124363854