Realización de transmisión confiable basada en UDP

Introducción

UDP y TCP son los protocolos más utilizados en la capa de transporte del modelo de siete capas. Los dos acuerdos principales tienen sus propias características:

  • El protocolo TCP es un protocolo de streaming, con transmisión confiable, sin considerar los problemas de sub-empaque, desorden y pérdida de paquetes; sin embargo, su enorme mecanismo también afecta seriamente su desempeño.
  • El protocolo UDP es un protocolo basado en paquetes, que es simple, en tiempo real y eficiente; sin embargo, se deben considerar problemas como la pérdida de paquetes y el desorden.

En entornos con altos requisitos de red en tiempo real, muchos usan UDP para encapsular SDK livianos y confiables para resolver problemas como la pérdida y el desorden de paquetes. Por ejemplo, el motor UE4 es una transmisión confiable basada en encapsulación UDP. La siguiente es una implementación de transmisión UDP confiable que diseñé hace unos años. El diseño generalmente tiene las siguientes características:

  • Tiene el mismo flujo y transmisión confiable que TCP;
  • Simple, liviano y eficiente;
  • Puede ajustar automáticamente la frecuencia de envío de acuerdo con la calidad de la red.

1. Información general

1.1 Formato de protocolo

Diagrama de formato de protocolo
El formato del protocolo completo es como se muestra en la figura anterior. El encabezado se divide en dos partes. El número de secuencia del paquete (packSeq) es el número de secuencia del paquete actual, utilizando el modo big-endian; el número de secuencia de recepción mínimo esperado (minUnAckSeq ) es el número de secuencia de paquete más pequeño del par que no se ha recibido, utilizando el modo big-endian. Cuando el cuerpo de datos está vacío, el número de secuencia del paquete no tiene sentido y el paquete es solo una respuesta de reconocimiento de recibir los datos del extremo opuesto.

1.2 Análisis de dominio

El modelo de subprocesos de todo el esquema se divide en cuatro, a saber, subproceso IO, subproceso de envío, subproceso de procesamiento de eventos e subproceso comercial.
Inserte la descripción de la imagen aquí
El subproceso de IO es responsable de enviar datos y llamar a las devoluciones de llamada de eventos registrados; el subproceso de envío es responsable de obtener datos de la caché y luego empalmarlos en un paquete de protocolo y entregarlos al subproceso de IO. acelera o reduce la frecuencia de envío de acuerdo con la tasa de pérdida de paquetes de red; cuando el subproceso de E / S Cuando ocurre un evento, la tarea de procesamiento real del evento se entrega al subproceso de procesamiento de eventos para su ejecución; el subproceso de negocios es el subproceso de llamada de la capa superior usando el SDK, que es responsable de enviar datos a la caché y registrar los oyentes de eventos de transmisión.

1.2.1 Número de serie del paquete y número de serie de confirmación

UDP es una transmisión poco confiable y tiene problemas como pérdida de paquetes y desorden.

  • Para resolver el problema del desorden, hemos definido un número de secuencia de paquete (packSeq) en el protocolo. Cuando el extremo opuesto recibe los datos, los almacenará en búfer y volverá a llamar a la persona que llama hasta que todos los paquetes anteriores al número de secuencia hayan sido recibió;
  • Para resolver el problema de pérdida de paquetes, hemos definido el número de secuencia de recepción mínimo esperado (minUnAckSeq). Cuando se recibe un paquete de datos del extremo opuesto, el número de secuencia indica que todos los paquetes anteriores a este número de secuencia han sido recibidos con éxito por el extremo opuesto.
  • Al recibir un paquete de datos del extremo opuesto, si el extremo local está enviando datos, entonces el número de secuencia de recepción mínimo esperado (minUnAckSeq) del extremo local se empaquetará con los datos del extremo local; de lo contrario, enviará un paquete con un cuerpo de datos vacío en el extremo opuesto.

1.2.2 Cola de envío

En el proceso de envío de datos por el hilo de envío, para mejorar la eficiencia, es imposible esperar a que el par devuelva el paquete de confirmación antes de enviar el siguiente paquete, por lo que configuramos una cola para que se envíe con una longitud máxima de maxSize .

  • Cuando hay un búfer de datos en la capa superior, el subproceso de envío buscará los datos del búfer y los dividirá en paquetes de datos hasta que el búfer esté terminado o la cola a enviar alcance el límite superior maxSize;
  • Al recibir el número de secuencia de recepción mínimo esperado (minUnAckSeq) en el paquete de datos del extremo opuesto, registraremos el número de secuencia. En particular, debido a la llegada fuera de secuencia de UDP, si el número de secuencia de recepción esperado mínimo recibido es menor que el número de secuencia de recepción esperado mínimo registrado, no se actualizará el número de secuencia de recepción esperado mínimo;
  • Cuando el subproceso de envío envía periódicamente paquetes de datos en la cola para ser enviados , si el número de secuencia es menor que el número de secuencia de recepción mínimo esperado anterior (minUnAckSeq), descartamos directamente el paquete y luego extraemos otro paquete del búfer de datos superior para complementar la columna perdida en el interior.

1.2.3 Frecuencia de transmisión

Cuando las condiciones de la red son buenas, la pérdida de paquetes es rara; pero cuando las condiciones de la red son malas, habrá mucha pérdida de paquetes. Si los paquetes se envían con frecuencia, habrá más pérdida de paquetes, lo que desperdicia tanto la CPU como el tráfico. Esperamos escalar automáticamente la frecuencia de envío. Esto es lo mismo que el algoritmo de depósito con fugas, pero un paso más cerca: ajustar dinámicamente la tasa de acuerdo con la calidad de la red .

  • Cuente el número de veces de envío, cada vez que se envía totalSendNum (por ejemplo, 100 veces), cuando el número de retransmisiones alcanza up_dupSendNum (por ejemplo, 40), aumentamos el intervalo de envío. Cuando es menor que down_dupSendNum (por ejemplo, 20), reducimos el intervalo de envío;
  • Establezca el valor máximo y mínimo del intervalo de envío, el ajuste anterior del intervalo de envío no puede exceder este intervalo;
  • Supongamos que contamos 100 veces y el número de retransmisiones llega a 80. Si no iniciamos una nueva ronda de estadísticas desde 0, entonces 100 transmisiones posteriores, incluso si estas 100 veces no se retransmiten, harán que se satisfagan 100 transmisiones. El aumento en el intervalo de envío hace que el intervalo crezca demasiado rápido, por lo que ajustamos el intervalo de envío cada vez que se envía totalSendNum (por ejemplo, 100 veces);
  • Siempre que se activa un ajuste del intervalo de envío, debemos cambiar el indicador de retransmisión de cada paquete de datos a falso;
  • Cuando la longitud de la cola a enviar es menor que maxSize, debemos pausar (maxSize-la longitud de la cola a enviar) * la longitud del intervalo de envío antes de comenzar una nueva ronda de retransmisión después de cada ronda de envío, y su valor cambia con la longitud de la cola de envío. De lo contrario, se producirá este tipo de problema: suponga que el tamaño máximo de la cola que se va a enviar es 50, el intervalo de envío actual es 0,1 sy el intervalo de respuesta de confirmación para cada paquete de datos es 4 s. Si llamamos continuamente para enviar datos, entonces no habrá envío repetido de mensajes; pero si enviamos un pequeño paquete de datos cada 10 segundos desde la capa superior. Entonces la cola a enviar no siempre está llena, y siempre se mantiene con una longitud de 1 como máximo, por lo que cada paquete de datos se retransmitirá 40 veces, lo que se traduce en un desperdicio de tráfico; además, incluso si hay un mecanismo para aumentar el intervalo de envío, encontrará que la capa superior posterior acelera la entrega Los datos no se pueden entregar rápidamente y la caché se llena rápidamente y no se puede escribir.

2 Diseño de arquitectura

2.1 Descripción general de la arquitectura

Inserte la descripción de la imagen aquí
Como se muestra en la figura anterior, toda la arquitectura lógica se divide en cinco bloques. El rojo es principalmente para enviar operaciones de subprocesos; el gris claro es principalmente para operaciones de llamadas de SDK; el negro es principalmente para operaciones de subprocesos de E / S subyacentes; el azul es principalmente para operaciones de subprocesos de procesamiento de eventos; el amarillo y el blanco son clases compartidas por múltiples subprocesos. Excepto PolicyContext, que es una clase singleton; las otras clases se mantienen por separado para cada UDPSeqChannel, que es un objeto privado de cada canal UDP.
Nota 1 : Todas las relaciones de agregación en la figura no contienen directamente el puntero de objeto, pero utilizan el contenedor de recuento de referencias atómicas sin bloqueo encapsulado para garantizar que la última referencia bajo subprocesos múltiples se libere y elimine de forma segura.
Nota 2 : Se accede a las propiedades de todas las clases en la figura mediante múltiples subprocesos a través de funciones atómicas para lograr una concurrencia de alto rendimiento sin bloqueos.
Nota 3 : El grupo de subprocesos ejecuta la clase que termina con Trabajador en la figura. Hay muchas formas de diseñar el grupo de subprocesos, por lo que no se dibujará por separado. La siguiente descripción de la función de clase se explicará en el texto.

2.2 Introducción a las funciones de clase

2.2.1 Estrategia de envío de singleton PolicyContext

Introducción a la función
Registre los parámetros de la estrategia de envío actual.

Introducción a la propiedad

  • sendTimesPerTrig: se activa cada vez que se envía sendTimesPerTrig para determinar si se debe ajustar el intervalo de envío.
  • minDupRate: el número mínimo de retransmisiones. Cuando es menor que este valor, el perTick de SendWorker se reduce mediante addPerTick, pero no puede ser menor que minPerTick.
  • minDupRate: el número máximo de retransmisiones. Cuando es mayor que este valor, el perTick de SendWorker aumenta en addPerTick, pero no puede ser mayor que maxPerTick.
  • minPerTick: El perTick mínimo de SendWorker.
  • maxPerTick: El perTick máximo de SendWorker.
  • addPerTick: La cantidad de aumento o disminución de perTick de SendWorker cada vez que se activa.

Requisitos del hilo
Esta clase será programada por todos los SendWorkers de UDPSeqChannel, y sus parámetros deben configurarse y leerse mediante operaciones atómicas.

2.2.2 Enviar caché SendCache

Introducción a la función

Introducción a la propiedad

  • nextSeq: el número de secuencia del siguiente paquete de envío. Cada vez que se construye un SendDataPack, el valor se incrementa atómicamente en 1;
  • búfer: la caché de los datos de entrega de nivel superior.

Requisitos de subproceso
Esta clase será llamada por el subproceso de trabajo de envío de la capa inferior y el negocio de la capa superior, y hay una relación de llamada de subprocesos múltiples. Para la construcción de SendDataPack, los datos de nivel superior, como los datos de entrega en la caché, proporcionan operaciones seguras para subprocesos sin bloqueo. Los objetos ByteArr pueden usar bibliotecas de código abierto o encapsularse por sí mismos, lo cual es relativamente simple.

2.2.3 SendDataPack

Introducción a la función

Introducción a la propiedad

  • sendTag: enviar etiqueta, el valor inicial es 0. Combinado con la frecuencia de envío en el análisis de dominio, cada vez que se envía el paquete de datos, se juzga si el valor de sendTag es igual al sendTag del SendWorker. Si no son iguales, simplemente agregue uno al totalSendNum de SendWorker y luego reasigne el valor al valor sendTag de SendWorker; de lo contrario, agregue uno al totalSendNum y dupSendNum de SendWorker.
  • sendSeq: el número de secuencia del paquete en el protocolo (es decir, packSeq).
  • datos: datos comerciales. Los datos reales entregados para la empresa de nivel superior provienen del búfer en SendCache.

Requisitos de subproceso
Esta clase solo puede ser llamada por SendWorker, y solo un subproceso puede acceder a SendWorker al mismo tiempo. No es necesario considerar la seguridad de los hilos.

2.2.4 SendQueue

Introducción a la función

Introducción a la propiedad

  • maxSize: el número máximo de SendDataPacks que puede contener la cola.
  • currSize: el número de SendDataPacks actualmente en la cola.

Requisitos de subproceso
Esta clase solo puede ser llamada por SendWorker, y solo un subproceso puede acceder a SendWorker al mismo tiempo. Pero aunque no es necesario considerar la seguridad de los subprocesos, es fácil lograr la seguridad de los subprocesos mediante el uso de una lista enlazada sin cadenas.

2.2.5 Enviar trabajador

Introducción a la función
Responsable de sacar el SendDataPack en SendQueue, armar el informe del protocolo y entregarlo a UDPNetChannel.
Cuando la capa superior entrega datos, al ejecutar la función atómica atomic_cmpxchg (& running, false, true), el valor de retorno es falso, y SendWorker se lanzará al árbol rojo-negro de suspensión de envío y se moverá al subproceso de ejecución de envío pool después de que expire el temporizador de suspensión .
Cuando se ejecuta en el subproceso de ejecución de envío, primero elimine el SendDataPack cuyo sendSeq es menor que el minUnSendSeq de AckInfo del encabezado de la cola (en particular, preste atención a la comparación especial después de que los enteros sin firmar están fuera de los límites) y luego obtenga los datos de SendCache hasta que se complete la recuperación o SendQueue esté lleno; luego realice la operación de envío (la lógica de envío se discutirá más adelante); finalmente, si tanto SendQueue como SendCache están vacíos, configure el átomo en ejecución en falso. Para formar un bucle cerrado lógico multiproceso con los datos de entrega de nivel superior, es necesario juzgar si SendCache está vacío nuevamente. Si no está vacío, ejecute la función atómica atomic_cmpxchg (& running, false, true). El valor de retorno es falso y te lanzarás al sueño rojo y negro. Ve al árbol.
El bucle se inicia durante el proceso de envío de SendDataPack, comenzando desde el último SendDataPack enviado. Primero, saque un SendDataPack de SendQueue. Si el sendSeq de SendDataPack es menor que el minUnSendSeq de AckInfo (en particular, debe prestar atención a la comparación especial después de que los enteros sin firmar están fuera de los límites), lo que indica que la otra parte ha recibido el paquete y se descarta directamente para continuar el bucle; de ​​lo contrario, construya los datos Packet packSeq = SendDataPack's sendSeq, minUnAckSeq = AckInfo's minUnRecvSeq y luego se envían (este proceso implica el número total de transmisiones y estadísticas de pérdida de paquetes y si se debe activar el ajuste de la siguiente tiempo de ejecución), luego salga del bucle y la siguiente ejecución enviará SendDataPack nuevamente. En particular, cuando SendQueue está vacío, saldrá del ciclo.
Nota: Cada vez que SendQueue se ejecuta hasta el final de la cola, si la longitud de la cola es menor que maxSize, se detendrá durante los intervalos de tiempo de ejecución de maxSize-currSize.

Introducción a la propiedad

  • en ejecución: si se está ejecutando o durmiendo en el hilo de envío.
  • sendTag: etiqueta de envío, el valor inicial es la marca de tiempo cuando se crea. Combinado con el último punto de la frecuencia de envío en el análisis de dominio, cuando se activa el juicio de si se debe ajustar el intervalo de envío, su valor se establecerá en una nueva marca de tiempo.
  • totalSendNum: el número total de envíos, el valor inicial es 0. Cuando se ajusta sendTag, también se restablecerá a 0.
  • dupSendNum: el número de retransmisiones, el valor inicial es 0.
  • expireTimes: marca de tiempo de ejecución vencida, es decir, la última marca de tiempo de ejecución. El nombre es un poco ambiguo, ¡lo siento!
  • nextick: La diferencia entre la siguiente marca de tiempo de ejecución y expireTimes.
  • perTick: el intervalo de ejecución actual. Se ajustará automáticamente según las estadísticas de cada retransmisión.

Requisitos de subproceso
Esta clase solo puede ser llamada por SendWorker, y solo un subproceso puede acceder a SendWorker al mismo tiempo. No es necesario considerar la seguridad de los hilos.

2.2.6 Interfaz de red UDP UDPNetChannel

Introducción a la función
Responsable de enviar y recibir datos UDP de forma asincrónica del puerto local designado y del puerto remoto. Por lo tanto, cuando el mismo puerto local y varios controles remotos envían y reciben, debe usar el modo de observador más un contador de referencia inteligente para empaquetar la función de socket UDP original de la capa inferior.

Requisitos de subproceso
Esta clase solo puede ser llamada por SendWorker, y solo un subproceso puede acceder a SendWorker al mismo tiempo. En teoría, no se requiere seguridad para subprocesos, pero esta solución requiere seguridad para subprocesos.

2.2.7 Interfaz de recepción de datos UDP IUDPNetListener

Introducción a la función
Volver a llamar a la interfaz después de recibir de forma asincrónica datos UDP de los puertos locales y remotos especificados

2.2.8 La recepción de datos UDP se da cuenta de UDPNetListenerImpl

Introducción a la función
Clase de implementación IUDPNetListener.
Cuando se reciben datos y la longitud de los datos es mayor que 0, se construirá un RecvWorker y se entregará al grupo de subprocesos de procesamiento de eventos de recepción para su ejecución, pero antes de la entrega, si minUnAckSeq es mayor que minUnSendSeq de AckInfo, establezca minUnSendSeq = minUnAckSeq.
Cuando se recibe un error, se construirá un ErrorWorker y se entregará al grupo de subprocesos de procesamiento de eventos de error para su ejecución.

Requisitos de subprocesos
Debido a que RecvWorker logra la seguridad de subprocesos, esta clase no necesita considerar la seguridad de subprocesos.

2.2.9 AckInfo

Introducción a la función
Registre el paquete no reconocido más pequeño actual enviado por el extremo opuesto y el número de secuencia no recibido más pequeño del paquete del extremo opuesto.

Introducción a la propiedad

  • minUnSendSeq: el valor mínimo del número de secuencia del paquete de envío no reconocido del par actual.
  • minUnRecvSeq: el valor mínimo del número de secuencia del paquete de pares que aún no se ha recibido.

Varios subprocesos
accederán a los requisitos de subprocesos mediante el subproceso de envío y el subproceso de procesamiento de eventos, y las operaciones seguras para subprocesos se pueden lograr mediante operaciones atómicas.

2.2.10 Recibir búfer de datos RecvIndex

Introducción a la función Guarde en
caché los paquetes de datos recibidos, paquetes y comience a formar un búfer de anillo. minPackSeq determina que el búfer solo puede almacenar el número de secuencia del paquete desde minUnRecvSeq de AckInfo hasta minUnRecvSeq + packs length-1, y otros paquetes de datos serán descartados. De hecho, cuando la longitud de la matriz de paquetes sea mayor o igual al maxSize de SendQueue, esto no sucederá.

Introducción a la propiedad

  • packs: matriz de búfer de puntero RecvDataPack para recibir paquetes de datos. Forme un caché circular con el campo de inicio;
  • begin: El índice de paquetes de inicio del búfer circular. Forma un búfer circular con el campo de paquetes;

Requisitos de subproceso
Esta clase solo puede ser llamada por RecvWorker, y RecvWorker solo puede ser ejecutado por un subproceso al mismo tiempo.

2.2.11 RecvDataPack


Envoltorio de introducción de funciones para recibir datos

Introducción a la propiedad

  • packSeq: el número de secuencia del paquete enviado por el par.
  • búfer: búfer para almacenar los datos recibidos. No incluye el encabezado del protocolo anterior.

Requisitos de subproceso
Este tipo solo puede ser ejecutado por un subproceso al mismo tiempo, sin bloqueo.

2.2.12 Manejo de eventos de error ErrorWorker

Introducción a la función Para
manejar todos los eventos de error de UDPNetChannel, la diferencia de RecvWorker es garantizar que cada evento recibido de UDPNetChannel se procese en serie.

Requisitos de subprocesos
Esta clase simplemente llama al método onError de la interfaz IUDPSeqListener, y su seguridad de subprocesos está garantizada por la implementación de la interfaz IUDPSeqListener.

2.2.13 Recibir procesamiento de eventos RecvWorker

Introducción de la función
Procesar toda la recepción de datos de UDPNetChannel, cada UDPNetChannel tendrá un objeto RecvWorkerQueue, RecvWorker se inserta en RecvWorkerQueue como una lista vinculada no bloqueada; cuando RecvWorkerQueue está vacío, RecvWorkerQueue se insertará en el grupo de subprocesos de procesamiento de eventos de recepción para su ejecución; RecvWorker se eliminará mediante un hilo y se ejecutará.
La ejecución de RecvWorker es: primero construya RecvDataPack y luego insértelo en el búfer de anillo de RecvIndex, especialmente si PackSeq no está dentro del rango del número de secuencia de búfer del búfer de anillo, se descartará (la longitud del búfer de anillo no es menor que el tamaño máximo de SendQueue, este tipo de cosas teóricamente no sucederá. También es necesario prestar atención a la comparación especial después de que el entero sin signo cruza los límites); luego comience a atravesar RecvDataPack desde el búfer de anillo de RecvIndex y salga del bucle hasta que encuentre el final de NULL (es decir, los datos en esta posición no se han recibido) y se eliminarán. Coloque los datos en el vector parcial y establezca el elemento de matriz correspondiente en NULL. En este proceso, se incrementa begin; finalmente, minUnRecvSeq de AckInfo se incrementa en esa cantidad de begin, y luego el RecvDataPack recuperado se usa para llamar secuencialmente a la interfaz IUDPSeqListener de nivel superior para notificar el evento de recepción de datos de nivel superior.
Requisitos de subprocesos
Esta clase simplemente llama al método onError de la interfaz IUDPSeqListener, y su seguridad de subprocesos está garantizada por la implementación de la interfaz IUDPSeqListener.

2.2.14 Interfaz de eventos UDP confiable IUDPSeqListener

Introducción de la función
El mecanismo de notificación de eventos para la recepción de datos en la parte inferior o se produce un error es implementado por la aplicación superior. En particular, el método onRecv de esta interfaz es equivalente a la transmisión de datos por streaming TCP.

2.2.15 Canal UDP confiable UDPSeqChannel

Introducción de funciones
Proporciona clases de entrada de operaciones para aplicaciones de nivel superior.

3 Sublimación resumida

Esta solución logra una transmisión de UDP confiable, liviana y eficiente. Sin embargo, los siguientes problemas no se han resuelto:

  • establecer conexión
  • Desconectar

Para resolver este problema, podemos agregar un campo ConnectionID en el encabezado del protocolo , el tipo es un entero con signo.

3.1 Establecimiento

Cuando necesite conectarse al extremo opuesto, construya un paquete de conexión con un cuerpo de datos vacío con ConnectionID = current timestamp, packSeq = 0, minUnAckSeq = 0 y envíelo al extremo opuesto, y luego construya un objeto UDPSeqChannel local; cuando el El extremo opuesto recibe los datos, también Construye un objeto UDPSeqChannel local; luego, cuando no hay datos para enviar, ambas partes envían regularmente un paquete de acuse de recibo con un cuerpo de datos vacío al extremo opuesto.
Nota 1 : Una vez establecida la conexión, el ConnectionID de las partes de comunicación subsiguientes sigue siendo el mismo. Entonces, el proceso de conexión es esencialmente un proceso de negociación del valor de ConnectionID.
Nota 2 : En comparación con el protocolo de enlace de tres vías de la conexión tcp, la conexión aquí es solo una vez. No existe una relación estricta entre el servidor y el cliente entre las dos partes. Por supuesto, si desea enviar datos solo cuando sepa que se ha establecido la conexión entre pares. Luego, debe esperar a que el extremo opuesto devuelva el primer paquete de confirmación; al mismo tiempo, después de que el extremo opuesto recibe el paquete de conexión, inmediatamente devuelve un paquete de confirmación.
Nota 3 : En casos extremos, si ambos lados inician una conexión al mismo tiempo. Después de recibir el paquete de conexión del extremo opuesto, si el extremo local ha iniciado el paquete de conexión y ha construido un objeto UDPSeqChannel, el ConnectionID se negociará según el principio de prioridad mínima.

3.2 Desconectar

Para la desconexión de la conexión, se divide principalmente en los siguientes métodos:
Desconexión por tiempo de espera : cuando no hay datos para enviar, ambas partes envían regularmente un paquete de acuse de recibo con un cuerpo de datos vacío al extremo opuesto. Cuando los datos de los pares no se reciben durante más de un cierto período de tiempo, el evento notificará la pérdida de conexión.
Desconexión por fuerza bruta : El extremo local libera directamente el objeto UDPSeqChannel, en este momento se perderán los datos que no hayan sido enviados en el búfer y también se perderán los datos que no hayan sido enviados por el extremo opuesto.
Desconexión normal : Construya un ConnectionID = lo opuesto al ConnectionID cuando se establece la conexión, packSeq = 0, minUnAckSeq = 0, y envíe el paquete final del cuerpo de datos vacío al extremo opuesto; después de que el extremo opuesto lo reciba, lo hará también construye un ConnectionID = establece la conexión El número inverso de ConnectionID, packSeq = 0, minUnAckSeq = 0, el paquete final del cuerpo de datos vacío se envía al extremo local. Ambas partes tienen que esperar a que se envíen los datos del búfer local antes de enviar los datos del paquete final antes mencionado. Esto asegura que los datos en la caché no se perderán.

Supongo que te gusta

Origin blog.csdn.net/fs3296/article/details/103960143
Recomendado
Clasificación