Análisis del principio del protocolo de transmisión de red kcp.

1. Información general

Para el desarrollo de juegos, especialmente los juegos MOBA (Multiplayer Online Arena), es necesario controlar la latencia. Pero para TCP tradicional (amigable con la red, excelente), no es propicio para la transmisión de paquetes en tiempo real, porque su retransmisión de tiempo de espera y control de congestión son amigables con la red, y no hay ninguna ventaja para el rendimiento en tiempo real de nuestros paquetes. . Por lo tanto, generalmente es necesario implementar un conjunto de protocolos de red propios basados ​​en UDP para garantizar paquetes confiables y en tiempo real. De hecho, sacrifica la facilidad de uso de TCP, sacrifica ancho de banda y cambia espacio por tiempo. Basado en UDP, existen algunos protocolos excelentes en Internet, como KCP.

2. Principio de implementación

KCP es solo una implementación de algoritmo simple y no implica ninguna llamada subyacente. Solo necesitamos registrar la función de devolución de llamada KCP cuando se llama al sistema UDP, y luego se puede usar. Por lo que puede entenderse como un protocolo de capa de aplicación. Compara TCP:

  • Duplicar el RTO de TCP. El concepto es aterrador. KCP es 1,5 veces.

  • Retransmisión selectiva, donde solo se transmiten los paquetes perdidos.

  • Retransmisión rápida sin esperar un tiempo de espera. Varias retransmisiones predeterminadas

  • TCP retrasará el envío de ACK. Se puede configurar KCP

  • Control de flujo no concesional. La ventana de envío solo puede depender del tamaño del búfer de envío y del tamaño restante del búfer de recepción en el receptor.

Para implementar la retransmisión selectiva (ARQ), KCP mantiene una ventana de recepción (ventana deslizante). Si se reciben datos solicitados, se colocarán en la cola de recepción para que la capa de aplicación los consuma. Si hay pérdida de paquetes, se evaluará. Si supera el número de veces establecido, optará por retransmitir el paquete correspondiente. De hecho, es a través de un rcv_nxt (el desplazamiento actual de la ventana de recepción) para determinar los paquetes de datos actuales que deben aceptarse. Si el paquete recibido está en el ámbito de la ventana, pero no en rcv_nxt. Guarde primero y espere a que los paquetes sean continuos antes de colocar paquetes de datos continuos en la cola de aceptación para que la capa de aplicación los consuma. En el mismo caso de una red deficiente, KCP también implementará un control de congestión para limitar los paquetes del remitente.

Envíame un mensaje privado para recibir los últimos y más completos materiales de aprendizaje y mejora de audio y video de C++ , incluidos ( C/C++ , Linux , FFmpeg , webRTC , rtmp , hls , rtsp , ffplay , srs )

 

3. Análisis del código fuente

Antes que nada, debemos ir a github para ver cómo usarlo antes de analizar. De hecho, es muy simple: inicializar el objeto kcp y luego implementar la función de devolución de llamada es en realidad implementar su propia llamada al sistema UDP subyacente. Cada vez que enviamos un paquete a través de KCP, llamará a esta devolución de llamada. Después de que UDP reciba el paquete, llame a la función ikcp_input. Finalmente solo necesitamos enviar y recibir datos a través de ikcp_send y ikcp_recv.

Antes de mirar el código, echemos un vistazo a la estructura del paquete kcp, Segmento


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 realiza una comunicación estable a través de estos campos del paquete de datos y puede optimizarse para diferentes puntos. De los campos anteriores, también se puede ver que kcp implementa la retransmisión selectiva con la ayuda de UNA y ACK.

Primero mire la lógica del envío de paquetes, llamaremos al método ikcp_send.

Este método juzgará primero el flujo de kcp. E intente agregar el paquete al párrafo anterior, si es posible. De lo contrario, se realiza la transmisión de fragmentos.

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;

En la lógica del código anterior, el conteo es en realidad el número de fragmentos del paquete. Luego haga un bucle para crear un segmento.La estructura de datos del segmento es principalmente para guardar la información del paquete del fragmento. Por ejemplo, eg->frg guarda el número del fragmento actual. Después de terminar, llame al método iqueue_add_tail para pasar el segmento a la cola de envío. Estos métodos se implementan a través de definiciones de macros. De hecho, es una operación de lista enlazada. Una cola es una lista doblemente enlazada. La lógica es simple. Luego, en este paso, los fragmentos de datos se colocan en la cola. ¿Dónde se implementa la lógica de envío específica? Continúe mirando hacia abajo.

Echemos un vistazo a la lógica de la devolución de llamada, que en realidad es el método ikcp_output, que se llamará en ikcp_flush. Es decir, lo que hace ikcp_output es la transmisión final de datos. ¿Cómo se impulsa eso? Voy a echar un vistazo a este método primero.

1. Este método primero envía un acuse de recibo. Iterar sobre todos los ACK. Llame al método ikcp_output para enviar.

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 si actualmente se requiere detección de ventana, porque si la ventana es 0, no se pueden enviar datos, por lo que se debe realizar la detección de ventana. Después de la detección, si es necesario, configure el tiempo de la ventana de detección. Envíe una solicitud para sondear la ventana o una solicitud para restaurar la ventana. Lo principal es solicitar el tamaño de la ventana del par e informar el tamaño de la ventana remota.

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

Pon el resultado en un segmento cuando hayas terminado.

3. Calcule el tamaño de ventana disponible para esta transmisión, que está determinado por múltiples factores, y KCP se puede configurar de forma selectiva. Puede optar por no incorporar una ventana de control de flujo.

4. Coloque el mensaje en la cola de envío en el búfer de envío, que en realidad es la ventana de envío. Es decir, todos los datos enviados estarán en este búfer. Antes de enviar datos, también debe configurar los tiempos e intervalos de retransmisión correspondientes.

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

Esta lógica es relativamente simple, de hecho, se toma un segmento de la cola de la ventana de envío. Luego configure los parámetros correspondientes. Y actualice la cola del búfer. y el tamaño de la cola del búfer. Si se establece nodelay, el tiempo de retransmisión se ha *2 convertido en 1,5

5. Atraviese la cola de la ventana de envío. Determine si hay datos que deben enviarse (incluida la retransmisión). De hecho, es para obtener el segmento correspondiente y luego determinar lógicamente si necesita ser retransmitido de acuerdo con la información. O necesita enviar. Una vez que se completa el juicio, se realiza la retransmisión.

La lógica también es muy simple.

  • Si el paquete se transmite por primera vez, envíelo directamente.

  • Si se alcanza el tiempo de retransmisión del paquete, transmítalo nuevamente y registre la bandera de pérdida

  • Si el número de saltos supera el fastack, retransmitir.

De hecho, perdido y cambio son campos que se utilizan para actualizar el tamaño de la ventana. Y los dos algoritmos de actualización son diferentes.

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

Básicamente, toda la lógica de la retransmisión rápida y la retransmisión de tiempo de espera está en este método. Si hay una retransmisión de tiempo de espera (pérdida de paquetes), ingresará al inicio lento, la ventana de congestión se reducirá a la mitad y la ventana deslizante se convertirá en 1. La ventana de congestión también se actualiza si se producen retransmisiones rápidas. Consulte el código del algoritmo específico.

Después de leer este método de descarga, básicamente entendemos la lógica del envío de datos. Luego mira dónde lo llama.

De hecho, se llama en el método ikcp_update. La capa de aplicación debe llamar repetidamente a este método. Generalmente, puede ser de 10 ms y 100 ms. El tiempo determinará la naturaleza en tiempo real de la transmisión de datos. Es decir, actualizará regularmente los datos de la cola para juzgar la ventana de envío o los datos que deben retransmitirse y enviar los datos a través del UDP subyacente. No hay lógica en este método.

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

Luego llame a los métodos ikcp_parse_una y ikcp_shrink_buf según los campos. El primero es analizar una para determinar qué otras partes han recibido los paquetes de datos que se han enviado. Si se recibe directamente, vuelva a aceptar la ventana para eliminar. El último es send_una para actualizar kcp. send_una significa que se ha confirmado la recepción del paquete anterior.

2. Si es un comando ACK, en realidad está procesando.

ikcp_update_ack es principalmente para actualizar algunos parámetros de kcp, incluidos rtt y rto Primero, el método ikcp_parse_ack es principalmente para eliminar el segmento correspondiente en la cola de envío de acuerdo con sn. Luego actualice maxack y time, y registre el registro

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. Si se recibe el paquete de datos, la lógica es realmente muy simple. Es detectar los datos y poner los datos válidos en la cola de recepción. El primero es juzgar si el paquete de datos es válido. Si es así, construir un segmento . Introduce los datos. y luego llame al método ikcp_parse_data. La lógica de este método también es relativamente simple, de hecho, es para juzgar si es válido, si se ha recibido, se descartará, de lo contrario, se insertará en la cola de recepción de acuerdo con el sn (número) .

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

Si es un paquete que pide el tamaño de la ventana. Esto es en realidad una marca, porque el encabezado de cada kcp tiene el tamaño de ganar. Todo lo que queda es actualizar la congestión y el tamaño de la ventana según las condiciones de la red.

4. Resumen

Al observar la implementación de kcp, se encuentra que es similar al TCP de la capa de transporte, pero está ajustado y es controlable. Por ejemplo, sacrificar el control de flujo para garantizar la transmisión de paquetes de datos en tiempo real. O acelerar la retransmisión y así sucesivamente. También hay retransmisión selectiva a través de una y ack. En general, todavía tiene ciertas ventajas en el campo de la sincronización de cuadros de juego o la transmisión de datos en tiempo real.

Supongo que te gusta

Origin blog.csdn.net/m0_60259116/article/details/124363854
Recomendado
Clasificación