[Linux] IO avanzada --- Patrón de diseño de IO de red de reactor

De hecho, a las personas les resulta difícil resistir la tentación y sólo pueden mantenerse alejadas de la tentación, así que no sobreestimes tu propia concentración.
Insertar descripción de la imagen aquí


1. Modos LT y ET

1. Comprenda cómo funcionan LT y ET

1.
El trabajo realizado por la interfaz de transferencia multicanal select poll epoll es en realidad una notificación de eventos. Solo notifica a la capa superior de la llegada del evento. El trabajo de procesar el evento listo no lo completan estas API. Cuando estas interfaces realizar notificación de eventos, ¿tienen su propia estrategia?
De hecho, los hay. En la programación de red, la encuesta selecta solo admite el modo de trabajo LT, mientras que epoll admite el modo de trabajo ET además del modo de trabajo LT. Los diferentes modos de trabajo corresponden a diferentes estrategias de notificación de eventos listos. El modo LT son estos IO El trabajo predeterminado modo de la interfaz, el modo ET es el modo de trabajo eficiente de epoll.

2.
Demos un ejemplo para ayudar a todos a comprender la diferencia entre los modos ET y LT (un ejemplo de entrega urgente): El
mensajero recién nombrado Xiao Li quiere realizar entrega urgente a Zhang San en el edificio de dormitorios 24. Zhang San compró un mucha entrega urgente. Supongo que con 6-7 mensajeros, Xiao Li fue a la planta baja de Xue 24 y luego llamó a Zhang San arriba para informarle a Zhang San que bajara y recogiera a los mensajeros, pero Zhang San estaba jugando juegos negros. con sus amigos, así que Zhang San me prometió bajar inmediatamente, pero nunca bajó. Cuando Xiao Li, un hombre honesto, vio que Zhang San todavía no bajaba a recoger el envío urgente, llamó a Zhang San nuevamente y le preguntó. Zhang San vino a recoger el envío urgente, pero Zhang San dijo. También dijo: Bajaré a recoger el envío urgente de inmediato, realmente de inmediato, pero después de un tiempo, Zhang San todavía no bajó, así que Xiao Li solo pudo llamar a Zhang San, Zhang San, su entrega urgente ha llegado, baje y recoja la entrega urgente lo antes posible. Finalmente, Zhang San y sus amigos terminaron de empujar los cristales en el lado opuesto y bajaron las escaleras para recoger los mensajeros. Sin embargo, Zhang San solo se llevó 3 mensajeros y todavía quedaban tres mensajeros. Zhang San no tenía nada que hacer. Zhang San solo puede aceptar una cantidad limitada de entrega urgente a la vez, por lo que Zhang San tomó sus tres entregas urgentes. artículos de arriba y continuó jugando juegos en blanco y negro con su compañero de cuarto. Después de un tiempo, Xiao Li llamó a Zhang San nuevamente y le dijo: "Zhang San, no has terminado de recoger tu entrega urgente. Compraste 6 artículos, pero solo tomaste 3. Quedan 3 paquetes que no has elegido". arriba. Zhang San dijo de nuevo: "Está bien, está bien, bajaré y lo recogeré de inmediato", pero de hecho, repitió la acción anterior. Después de un rato, bajó las escaleras y se llevó los tres paquetes restantes. Se llevaron todos los paquetes, Xiao Li. Ya no llamaré a Zhang San.
Xiao Wang, el antiguo mensajero youtiao, estaba entregando mensajero a Zhang San en el edificio de dormitorios número 24. Casualmente, Zhang San compró 6 mensajeros más esta vez, por lo que Xiao Wang también entregó 6 paquetes a Zhang San. Xiao Wang llegó al fondo del edificio de Zhang San y llamó a Zhang San, diciéndole, Zhang San, solo te llamaré una vez. Si no bajas a recoger el envío urgente ahora, no te llamaré más tarde a menos que Compré un nuevo mensajero. Cuando el número de sus mensajeros en mi mano aumente, tendré la amabilidad de llamarlo nuevamente. De lo contrario, en otras circunstancias, solo lo llamaré una vez. Si no baja a recoger el Mensajero, entonces no me preocuparé por ti, enviaré entrega urgente a otros clientes. Cuando Zhang Sanyi escuchó esto, esto no es posible. Si no bajo a recoger el expreso ahora, el mensajero dejará de llamarme en el futuro. Entonces, ¿qué debo hacer si no puedo encontrar al mensajero cuando llegue? ¿Bajo las escaleras y no puedo recibir mi expreso? Así que Zhang Sanyi San inmediatamente bajó las escaleras para recoger el envío expreso. Zhang San no puede aceptar tantas entregas urgentes a la vez, pero Zhang San no puede perderse algunas entregas urgentes, porque Xiao Wang no volverá a llamar a Zhang San la próxima vez, por lo que Zhang San simplemente subió a dejar los tres expresos. artículos de entrega en su mano, e inmediatamente baja las escaleras y recoge a los tres mensajeros restantes.

3.
En los dos ejemplos anteriores, el modo de trabajo de Xiao Li es en realidad el modo activado por nivel, denominado modo LT, y el modo de trabajo de Xiao Wang es el modo activado por borde, denominado modo ET, que también es una interfaz de conmutación multicanal. Modo eficiente.
La forma en que LT trabaja con epoll es que cuando epoll detecta un evento listo en el calcetín, epoll_wait regresará inmediatamente para notificar al programador que el evento está listo. El programador puede elegir leer solo parte de los datos en el búfer del calcetín, y el Los datos restantes se dejan de leer temporalmente y luego leen los datos restantes en el búfer de calcetín cuando se llama a recv la próxima vez. ¿Cómo llamar a recv la próxima vez? Por supuesto, se notifica a epoll_wait y luego se llama, de modo que siempre que el programador no elimine los datos del calcetín de una vez, cuando se llame a epoll_wait más tarde, epoll_wait seguirá notificando el evento listo y le dirá al programador que lea los datos. en el calcetín Los datos restantes, y este método es el modo LT, es decir, siempre que haya datos en la capa inferior que no se hayan leído, siempre se notificará al usuario que lea los datos cuando regrese epoll_wait.
El método de trabajo correspondiente de ET es que si hay datos en la capa subyacente que no se han leído, epoll_wait posterior no notificará al programador que el evento está listo, solo cuando los datos subyacentes aumenten, epoll_wait notificará al programador nuevamente, de lo contrario epoll_wait solo notificará al programador una vez.

2. Observe la diferencia entre los modos de trabajo LT y ET a través del código

1.
En el artículo anterior, escribimos sobre epoll_server. Por supuesto, el modo de trabajo predeterminado de epoll_server también es el modo LT. En el siguiente código, bloqueé la interfaz HandlerEvent() que maneja eventos listos. Cuando llega la conexión del cliente, el El servidor epoll_wait definitivamente detectará que el evento de lectura en listeningsock está listo, por lo que epoll_wait regresará para informar al programador que los datos deben procesarse. Sin embargo, si el programador no ha procesado los datos, entonces epoll_wait le informará al programador que los datos debe procesarse cada vez, por lo que a juzgar por la salida del monitor, después de que epoll_wait regrese, de acuerdo con el valor de retorno n, debe haber ingresado a la rama predeterminada, y cada vez epoll_wait informará al programador que el evento está listo, por lo que el monitor seguirá imprimiendo eventos listos, porque mientras la capa inferior haya un evento listo. Para listeningsock, siempre que la cola de escucha del núcleo tenga una conexión lista, estará lista. epoll_wait siempre notificará al programador que el evento Está listo, así que date prisa y procésalo. (Al igual que Xiao Li, mientras Zhang San no retire la entrega urgente, Xiao Li seguirá llamando a Zhang San)

Insertar descripción de la imagen aquí

2.
Al agregar listeningsock al árbol rojo-negro en la parte inferior de epoll, no solo nos preocupamos por los eventos de lectura de listeningsock, sino que también configuramos el modo de trabajo de listeningsock en ET, simplemente colocamos EPOLLIN y EPOLLET en OR bit a bit.
Entonces, cuando llega una conexión, puede ver que el servidor solo imprimirá el evento listo una vez. Mientras no llegue una nueva conexión, epoll_wait solo notificará al programador del evento listo una vez. A menos que llegue una nueva conexión, significa que el El kernel está escuchando el evento listo en la cola. Hay más conexiones, en otras palabras, hay más datos subyacentes en listeningsock. En este momento, epoll_wait amablemente le recordará al programador nuevamente que el evento está listo, por lo que debes manejarlo. rápidamente. Por otro lado, mientras los datos subyacentes de listeningsock no aumenten en el futuro, epoll_wait no notificará al programador.
Dado que el tiempo de espera que configuramos bloquea la espera, puede ver que mientras no llegue una nueva conexión, el servidor se bloqueará, la llamada epoll_wait no regresará y no se notificará al programador. Por otro lado, en el modo LT, aunque epoll_wait es una espera de bloqueo cada vez, epoll_wait regresará cada vez y notificará al programador cada vez, esta es la diferencia entre los dos. El disparador de borde solo se disparará una vez, el disparador horizontal se disparará siempre .

Insertar descripción de la imagen aquí

3. La razón por la que el modo ET es eficiente (fd debe ser sin bloqueo)

1. ¿
Por qué es eficiente el modo ET? Esta es una pregunta de entrevista muy importante. Cuando muchos entrevistadores preguntan sobre el aspecto de la red, nos pedirán que hablemos sobre el uso de select poll epoll, los principios subyacentes de epoll, las ventajas y desventajas de las tres interfaces y las dos funciones. del tipo de modo de trabajo de epoll y la razón por la cual el modo ET es eficiente La razón por la cual el modo ET es eficiente también es un problema de alta frecuencia.

2.
En el modo ET, solo cuando los datos subyacentes provienen de nada a más, la capa superior será notificada una vez. El mecanismo de notificación es rbtree+ready_queue+cb, por lo que el mecanismo de notificación ET obligará al programador a leer todo lo subyacente. datos. Si no se lee de inmediato, es posible que se pierdan datos. No puede garantizar que la otra parte continuará enviándole datos. Si no puede garantizar esto, no hay garantía de que epoll_wait le notificará la próxima vez. , si esto No se puede garantizar, es posible que solo haya leído parte de los datos del calcetín, pero es posible que epoll_wait no le notifique nuevamente, lo que resulta en que nunca podrá leer los datos posteriores, por lo que debe leerlos todos de una vez. se leen los datos.
¿Cómo garantizar que todos los datos subyacentes se lean a la vez? Entonces solo puede leer en un bucle. Si solo llama a recv una vez, no hay garantía de que todos los datos subyacentes se lean a la vez. Entonces podemos hacer un bucle while y seguir leyendo los datos en el buffer de recepción del calcetín hasta que no se puedan leer datos, pero en realidad hay otro problema aquí: si el calcetín está bloqueado, definitivamente no habrá datos hasta el final del bucle. leyendo. En este momento, dado que el calcetín está bloqueado, el servidor se bloqueará en la última llamada al sistema de recepción hasta que lleguen los datos, y el servidor se suspenderá en este momento. Una vez que el servidor esté suspendido, finalizará ~ el servidor
Si está suspendido, no podrá ejecutarse y no podrá brindar servicios a los clientes. Esto muy probablemente causará que muchas empresas pierdan ganancias. Por lo tanto, el servidor no debe detenerse, y mucho menos suspenderse. Tiene que estar funcionando todo el tiempo. con el fin de proporcionar servicios a los clientes.Proporcionar servicios. Si utiliza un descriptor de archivo sin bloqueo, cuando recv no puede leer datos, recv devolverá -1 y el código de error se establece en EAGAIN y EWOULDBLOCK. Los valores de estos dos códigos de error son los mismos y puede determinar en este momento, leemos todos los datos subyacentes a la vez.
Por lo tanto, en la práctica de la ingeniería, cuando epoll funciona en modo ET, el descriptor de archivo debe configurarse como sin bloqueo para evitar que el servidor se suspenda debido a la espera de que un determinado recurso esté listo.

3.
Después de explicar por qué fd no debe ser bloqueante en el modo ET, ¿por qué el modo ET es eficiente? Algunas personas pueden decir que debido a que el modo ET solo se notifica una vez, lo que obliga a los programadores a leer todos los datos a la vez, el modo ET es eficiente. Si esta pregunta tiene una puntuación de 100 puntos, su respuesta solo puede obtener 20 puntos, porque su La respuesta es en realidad solo una pista hacia la respuesta. Aún no has dicho la parte más importante.
Si los programadores se ven obligados a leer todos los datos a la vez, ¿no significa eso que la capa superior puede eliminar los datos lo más rápido posible? Después de eliminar los datos lo antes posible, puede enviar un tamaño de ventana más grande de 16 bits a la otra parte, para que la otra parte pueda actualizar un tamaño de ventana deslizante más grande, mejorar la eficiencia del envío de datos subyacentes y utilizar mejor el retardo de TCP. respuesta, ventana corrediza y otras estrategias. ! ! ! ¡Esta es la razón más esencial por la que el modelo ET es eficiente! ! !
Porque el modo ET puede hacer un mejor uso de las diversas estrategias de TCP para mejorar la eficiencia de la transmisión de datos, como respuesta retrasada, ventana deslizante, etc.
Cuando hablamos de TCP antes, hay un campo en el encabezado de TCP llamado PSH. De hecho, si este campo está configurado, epoll_wait convertirá este campo en un mecanismo de notificación y luego notificará a la capa superior para que lea los datos lo antes posible. .

4. Cómo leer en modo LT y ET

2.Reactor

1.tcpServer.hpp

1.1 Estructura de conexión

1.
Sabemos que cuando un socket se comunica, cada calcetín creará un búfer de recepción y un búfer de envío en el kernel. Dichos búferes a menudo se abren en el montón y no seguirán la variable temporal char buffer[1024]. Se destruye cuando la pila El espacio del marco se destruye, esto puede almacenar mejor los datos recibidos en la red y los datos que se enviarán a la red. Si el espacio en la pila se usa para almacenar los datos enviados y recibidos por la red, lo más probable es que los datos se destruyan. Porque siempre que se destruya el marco de pila donde se encuentra la variable, los datos de la variable cambiarán de los datos de red almacenados originalmente a datos aleatorios no inicializados la próxima vez que se vuelva a abrir la variable.
Entonces, para permitir que cada calcetín tenga su propio búfer de envío y recepción, ya no usamos un búfer de caracteres [1024] para almacenar los datos de la red en el calcetín al escribir el servidor, sino que usamos una estructura de Conexión para representar un calcetín de comunicación. esta estructura contiene el descriptor del socket de comunicación _sock y el _inbuffer y _outbuffer correspondientes al calcetín.
Además, la estructura también incluye tres métodos de devolución de llamada _recver, _sender y _excepter, que representan respectivamente el método de lectura, el método de escritura y el método de excepción correspondiente al calcetín. func_t es un tipo de contenedor y el contenido del contenedor es un puntero de función. que devuelve El valor es nulo y el parámetro es el tipo de puntero de Conexión. Estos tres parámetros son en realidad el toque mágico del modo Reactor Reactor. Al resumir Reactor más adelante, sabrá por qué Connection está diseñado de esta manera y por qué Reactor es llamado modo Reactor. Para implementar la biblioteca de red Reactor, esta conexión es la clave.
La estructura también incluye un puntero a un tipo de servidor adicional. En algunos escenarios, por ejemplo, la estructura de conexión y la clase de servidor TcpServer se dividen en archivos. En este momento, si desea llamar a TcpServer en el método de devolución de llamada del método de conexión en la clase, este puntero hacia atrás nos ayudará a obtener el método en TcpServer, no lo necesitamos hoy, porque hoy ambas clases están ubicadas en tcpServer.hpp
Connection también implementa dos funciones, una es la función de registro y la otra es la función para cerrar el calcetín. La función de registro se utiliza para registrar el método de lectura, el método de escritura y el método de excepción correspondiente al calcetín implementado externamente en la estructura Connection. donde se encuentra el calcetín.

Insertar descripción de la imagen aquí

1.2 Inicializar el servidor

1.
La interfaz initServer aún crea listeningsock primero, vincula la dirección IP y el número de puerto del servidor y luego configura el servidor en estado de escucha. Dado que es una biblioteca de red Reactor, la interfaz multipaso utilizada debe ser epoll, por lo que también necesita llamar a epoll_create para crear el modelo epoll. Al igual que sock, también hemos encapsulado la interfaz correspondiente al epoll de hoy y la hemos implementado por separado en epoller.hpp para usarla como un componente.
Cuando el servidor comience a ejecutarse, habrá una gran cantidad de objetos de estructura de conexión que deberán ser nuevos, entonces, ¿es necesario administrar estos objetos de estructura? Por supuesto que es necesario, por lo que en la clase de servidor se define una tabla hash _connections, usando sock como el valor clave de la tabla hash y la conexión de estructura correspondiente a sock como el valor correspondiente al valor clave, que es el hash. El valor almacenado en el depósito no tendrá un conflicto de hash hoy, por lo que el depósito de hash debajo de cada valor clave solo tendrá un valor, es decir, una estructura de conexión. Al inicializar el servidor, el primero debe agregarse al hash
. cubo. El calcetín en la tabla hash debe ser un listeningsock, por lo que en el método initServer, primero agregue el listeningsock a la tabla hash y, al mismo tiempo, pase el método de preocuparse por el evento correspondiente al listeningsock. Para listeningsock, solo necesita prestar atención a Simplemente lea el método y configure los otros dos métodos en nullptr.

2.
En AddConnection, es necesario determinar si los eventos tienen EPOLLET. Si es así, el descriptor de archivo debe ser sin bloqueo, por lo que el calcetín debe configurarse como sin bloqueo. El método de configuración también es simple, solo use fcntl para lograrlo De manera similar, también encapsulamos fcntl en un método SetNonBlock() para su uso. El modo de trabajo de epoll en Reactor es ET, razón por la cual la biblioteca de red de Reactor es eficiente.
El siguiente paso es crear una nueva estructura de conexión y luego completar los campos de la estructura, como establecer el valor del método de devolución de llamada, el valor del descriptor de archivo en la estructura, etc. Después de crear la estructura de conexión, también debemos llamar a la interfaz AddEvent encapsulada y entregar el calcetín y los eventos que le interesan para realizar un epoll para su monitoreo. Finalmente, no olvide entregar la nueva estructura a la tabla hash para su administración. .

2.
En términos de implementación de código, al pasar parámetros a AddConnection, se utiliza un conocimiento de C ++ 11, que es el uso de bind. En términos generales, si pasa el tipo de puntero de función envuelto por el contenedor al tipo de contenedor En este momento, no hay ningún problema, porque el contenedor es esencialmente un funtor, que llama internamente al método del objeto envuelto, por lo que no hay problema al pasar parámetros.
Pero si pasa parámetros dentro de la clase, habrá un problema. Habrá un problema de discrepancia de tipos. Este problema es realmente desagradable, y cuando se informa este problema, se informarán muchos errores, porque la función es una plantilla. Lo más repugnante de los errores de C ++ son los errores de plantilla. La gente explotará cuando se informen de errores. Dicho esto, ¿por qué hay una discrepancia de tipos? Porque cuando se llama a un método dentro de una clase dentro de una clase, en realidad se llama a través del puntero this. Si pasa directamente el método Accepter a AddConnection, los dos tipos no coinciden, porque el primer parámetro de Accepter es el puntero this, correcto El método consiste en utilizar el enlace del adaptador del contenedor para pasar parámetros. bind vincula al Aceptador. Los dos primeros parámetros son el tipo de objeto vinculado y los parámetros pasados ​​al objeto vinculado, porque el primer parámetro del Aceptador es este puntero. , Por lo tanto, el primer parámetro se puede pasar de esta manera de forma fija, y el parámetro posterior no se debe pasar ahora, sino cuando se llama al método Accepter. Solo de esta manera se puede pasar el puntero de función del miembro de la clase al tipo contenedor dentro de la clase.
Sin embargo, existe otro método que no se usa comúnmente, que consiste en usar expresiones lambda para pasar parámetros. Lambda puede capturar este puntero del contexto y luego pasar el tipo lambda al tipo contenedor. Este método no se usa comúnmente. y es incómodo de usar. Sí, function y bind son modos de adaptación. Es más agradable usar los dos juntos. Es bueno entender el método lambda.

Insertar descripción de la imagen aquí

1.3 Despachador de eventos

1.
El despachador de eventos es cuando el servidor real comienza a ejecutarse. El servidor procesará cada conexión lista. Primero, si la conexión no está en la tabla hash, significa que el calcetín en esta conexión no se ha agregado al modelo epoll. El árbol rojo-negro no se puede procesar directamente. Primero debe agregarse al árbol rojo-negro, y luego dejar que epoll_wait tome la conexión lista y luego notificar al programador. Luego procese en este momento, para que no tenga que esperar. , pero procesa directamente los datos.copiar.
El método para manejar eventos listos en Loop es muy, muy simple. Si el fd listo está preocupado por el evento de lectura, entonces llame directamente al método de lectura dentro de la estructura de conexión donde se encuentra el calcetín. Si es un evento de escritura, llame al método de escritura Eso es todo. Algunas personas dicen: ¿Qué pasa si a fd le importan los eventos anormales? De hecho, la mayoría de los eventos anormales son eventos de lectura, pero también hay eventos de escritura, por lo que podemos poner directamente la lógica de manejo de excepciones en el método de lectura y el método de escritura. Cuando llegue un evento anormal, vaya directamente al método de lectura correspondiente. o método de escritura, simplemente ejecute la lógica correspondiente en el interior.
Supongamos que ocurre un evento anormal, entonces el kernel establecerá automáticamente este evento anormal en el conjunto de eventos devuelto por epoll_wait. Este evento anormal definitivamente estará asociado con un calcetín. Por ejemplo, el cliente y el servidor se comunican mediante un calcetín, y De repente, el cliente cierra la conexión. Luego, el calcetín del servidor originalmente se preocupa por el evento de lectura. En este momento, el kernel establecerá automáticamente el evento de excepción en la colección de eventos que le interesa al calcetín. Al procesar el evento de lectura que le interesa al calcetín , el método de lectura manejará incidentalmente el evento de excepción. El método de procesamiento cierra el calcetín de comunicación para el servidor. Debido a que el cliente ha desconectado la conexión, el servidor no necesita mantener la conexión con el cliente. El servidor puede simplemente desconectarse. Esto La lógica se puede implementar en el método de lectura.

Insertar descripción de la imagen aquí

2.
Un despachador de eventos como el que se muestra a continuación es un modo típico de Reactor. Cuando llega una conexión, puede llamar directamente al método de devolución de llamada en la Conexión donde se encuentra el calcetín correspondiente para su procesamiento. Esto es como una reacción química. Cuando se produce una conexión solicitud o Cuando llegan los datos de la red de comunicación, el código es como una reacción química. Puede llamar automáticamente al cable de escucha correspondiente a la conexión o al método del calcetín correspondiente a la comunicación para su procesamiento. Es como un reactor químico. Es por eso que Dicha biblioteca de red se llama Reactor porque cada calcetín tiene sus propios métodos de excepción de lectura y escritura correspondientes.
El método _recver correspondiente a listeningsock es la función Aceptador, el método _recver correspondiente al calcetín de comunicación es la función Recver y el método _sender correspondiente al calcetín de comunicación es la función Remitente.

Insertar descripción de la imagen aquí

1.4 Función de devolución de llamada

1.
Cuando llega una conexión a la parte inferior de listeningsock y epoll_wait notifica al programador que ha llegado un evento, se debe llamar al método de devolución de llamada _recver correspondiente a listeningsock. Para este método de devolución de llamada, al agregar listeningsock a la estructura de conexión, ya hemos vinculado al aceptador.Asignado al método de devolución de llamada _recver de listeningsock.
Después de ingresar a Accepter, comienza a leer la conexión subyacente de listeningsock, pero ¿puede garantizar que puede leer todos los datos subyacentes de listeningsock de una sola vez? Cuando aceptas la llamada del sistema, solo puedes tomar como máximo una conexión a la vez. ¿Qué pasa si hay muchas conexiones en la parte inferior del listeningsock? Hoy epoll está en modo ET, ¿qué pasa si solo lo lees una vez y luego no llega ninguna nueva conexión? El cliente correspondiente a la conexión que no ha sido recuperada no puede comunicarse con el servidor. Este problema es causado por su servidor. Mi cliente se está comunicando bien con usted, pero su servidor no acepta mi solicitud de conexión. Entonces significa que su código de servidor tiene un error.
Por lo tanto, en el Aceptador, los datos subyacentes del calcetines de escucha deben leerse en un bucle para garantizar que todos los datos subyacentes del calcetines de escucha se lean a la vez, por lo que el aceptador debe romper el bucle para leer. el servidor se suspende cuando se lee en el modo ET. Todos los descriptores de archivos han sido configurados como no bloqueantes por nosotros. Cuando aceptar muestra la conexión de comunicación, el siguiente paso es agregar la conexión a la tabla hash _connections. En AddConnection, el calcetín estructura de conexión correspondiente, luego complete los campos en la estructura y establezca el método de devolución de llamada para las variables miembro de la estructura. Además, AddConnection también configurará el calcetín y los eventos que le interesan en el árbol rojo-negro del Modelo epoll, para que epoll ayude a monitorear la preparación de fd que preocupa a los programadores.
Para listeningsock, solo se preocupa por los eventos de lectura, por lo que al pasar parámetros a AddConnection, los dos últimos métodos no se pasan, pero para la comunicación sock, los dos últimos métodos también se llamarán en el futuro, por lo que también deben pasarse. Al pasar parámetros aquí, dado que los parámetros son funciones miembro, el método de vinculación de parámetros fijos también debe usarse para pasar los parámetros.
Cuando el valor de retorno de la llamada al sistema de aceptación es menor que 0 y el código de error se establece en EAGAIN o EWOULDBLOCK, significa que aceptar ha leído todos los datos listos en listeningsock en esta ronda y puede salir del bucle infinito. Si el código de error está configurado en EINTR, significa que el proceso puede estar ejecutando el método del controlador correspondiente a una determinada señal entrante, lo que provoca que se interrumpa la llamada al sistema de aceptación aquí. En este momento, debe continuar leyendo los datos subyacentes de listeningsock en un bucle, así que simplemente continúa, existe otra posibilidad, es decir, la llamada al sistema de aceptación realmente sale mal, y el enfoque en este momento es simplemente salir del bucle.

Insertar descripción de la imagen aquí

2.
Recver todavía tiene el mismo problema que antes. También es un problema que no se ha resuelto al escribir tres servidores de interfaz multicanal. ¿Cómo puede asegurarse de poder leer todos los datos a la vez? Si no se puede garantizar, entonces, al igual que Accepter, el bucle debe cerrarse para leer. Cuando el valor de retorno de recv es mayor que 0, primero colocamos los datos leídos en el búfer. ¿Dónde está el búfer? De hecho, en la estructura a la que apunta el parámetro conn, habrá un búfer de envío y recepción correspondiente al calcetín en la estructura. Luego, se llama a la función de devolución de llamada _service pasada desde el exterior para realizar el procesamiento de lógica empresarial de la capa de aplicación en los datos recibidos por el servidor.
Cuando recv lee 0, significa que el cliente ha cerrado la conexión, entonces esto se considera un evento anormal y se puede volver a llamar directamente al método de manejo de excepciones correspondiente a sock.
Cuando el valor de retorno de recv es menor que 0 y el código de error se establece en EAGAIN o EWOULDBLOCK, significa que recv ha leído todos los datos subyacentes del calcetín. En este momento, puede simplemente salir del bucle o puede ser interrumpido por una señal., Entonces el bucle debe continuar ejecutándose en este momento. Otra situación es que la llamada al sistema recv realmente sale mal, entonces también se puede llamar al método de excepción de calcetín en este momento para manejarlo.
El método de procesamiento de lógica de negocios debe procesar todos los datos después de leer todos los datos en este bucle.

Insertar descripción de la imagen aquí

3.
Al escribir el servidor antes, nunca hemos tratado con eventos de escritura. Los eventos de escritura son diferentes de los eventos de lectura. Necesitamos configurar los eventos de lectura con frecuencia, pero los eventos de escritura generalmente están listos porque el kernel envía el búfer con una alta probabilidad. Hay espacio. Si tenemos que pedirle a epoll que nos ayude a cuidar los eventos de lectura cada vez, esto en realidad es una pérdida de recursos, porque en la mayoría de los casos, cuando envía datos, copiará directamente los datos de la capa de aplicación al kernel. buffer. Sí, no habrá espera, pero recv es diferente. Cuando recv está leyendo, los datos aún pueden estar en la red, por lo que la probabilidad de que recv espere es relativamente alta, por lo que para eventos de lectura, a menudo debe configurarse en la colección de eventos que le importa a Sock.
Pero este no es el caso de los eventos de escritura. Los eventos de escritura deben configurarse ocasionalmente en la colección de atención. Por ejemplo, si no envió todos los datos a la vez esta vez, pero no configuró el calcetín para que se preocupe por el evento de escritura. Incluso si hay espacio en el búfer de envío del kernel, epoll_wait no le notificará, entonces, ¿cómo puede enviar los datos restantes? Entonces, en este momento debe configurar el cuidado del evento de escritura. y deje que epoll_wait lo ayude a monitorear el evento de escritura en el calcetín, para que pueda continuar enviando los datos que no se enviaron la última vez la próxima vez que epoll_wait le notifique.
En este momento, alguien puede preguntar, ¿el modo ET no notifica solo una vez? Si configuro el cuidado de escritura esta vez, pero la próxima vez que envío datos, el envío aún no se completa (porque es posible que no quede espacio en el búfer de envío del kernel), entonces el modo ET no me notificará más tarde, entonces cómo continuar enviando los datos restantes? El modo ET notificará a la capa superior nuevamente cuando cambie el estado del evento listo subyacente. Para eventos de lectura, cuando los datos cambien de cero a multiestado, ET notificará a la capa superior nuevamente. Para eventos de escritura, cuando el espacio restante en el El búfer de envío del kernel cambia de nada a varios estados, ET también notificará a la capa superior una vez, por lo que no hay necesidad de preocuparse por el problema del envío de datos incompletos, porque ET nos notificará.
Fuera del ciclo, solo necesitamos determinar si establecer el evento de escritura considerando si el búfer externo está vacío. Cuando se envíen los datos, cancelaremos el evento de escritura y no ocuparemos recursos de epoll. Si los datos no han sido enviado, luego establezca la preocupación por el evento de escritura, porque queremos asegurarnos de que la próxima vez que el evento de escritura esté listo, epoll_wait pueda notificarnos que procesemos el evento de escritura.

Insertar descripción de la imagen aquí

4.
El siguiente es el método para manejar eventos anormales: primero eliminamos uniformemente todos los eventos anormales del modelo epoll, luego cerramos el descriptor de archivo y finalmente eliminamos conn de la tabla hash _connecions.
Vale la pena señalar que el espacio de la estructura de conexión al que apunta el puntero de conexión debemos liberarlo nosotros mismos, algunas personas dicen, ¿por qué? ¿No se ha borrado tu tabla hash? ¿Por qué los programadores necesitan eliminar ellos mismos el espacio de la estructura de conexión?
Lo que quiero explicarles aquí es que cuando se borran todos los contenedores, solo liberan el espacio creado por el propio contenedor. Un contenedor como una tabla hash creará un nuevo nodo, que almacena el puntero de conexión y el puntero. al siguiente nodo. Cuando se llama al borrado de la tabla hash, la tabla hash solo liberará su propio nuevo espacio de nodo. En cuanto a este espacio de nodo, se almacena un puntero de tipo Conexión y esta variable de puntero apunta a un Estructura. Espacio, a la tabla hash no le importarán estas cosas. El contenedor solo liberará el espacio que ha abierto. Este espacio contiene una variable de puntero y puede haber otras variables. El contenedor solo liberará la estructura donde están estas variables. ubicado. El espacio del cuerpo. Esta estructura debe ser abierta por el contenedor para almacenar algunas cosas que el usuario desea almacenar, como el puntero de conexión de hoy. Por supuesto, también es posible almacenar otras variables. Para tablas hash, También es posible almacenar tipos de nodos de listas vinculadas Puntero, porque la tabla hash se implementa mediante el uso de un vector vinculado a una lista vinculada individualmente.
Por lo tanto, tenemos que liberar manualmente el espacio señalado por conn. Si no desea liberar manualmente el recurso de espacio de montón señalado por conn, puede almacenar objetos de puntero inteligente. De esta manera, cuando se borra la tabla hash, en realidad elimina la estructura que almacena el puntero del tipo de conexión. Esto llamará al destructor de la estructura. No escribiremos el destructor dentro de una estructura como esta nosotros mismos. Podemos usar el generado por el compilador de forma predeterminada. El compilador no procesa los tipos incorporados. El tipo personalizado llamará al destructor de la clase, es decir, se llamará al destructor del objeto de puntero inteligente. Dentro del destructor, se liberará la memoria dinámica de la estructura de conexión apuntada por conn.
En realidad, es muy problemático hacer esto, por lo que solo necesitamos liberarlo manualmente. Si no lo liberamos manualmente, provocará una pérdida de memoria.

Insertar descripción de la imagen aquí
5.
El segundo título de la quinta parte del siguiente artículo describe la estrategia de procesamiento del destructor generado por el compilador para las variables miembro del objeto: no procesa los tipos integrados y llama a clases para tipos personalizados. Incinerador de basuras.
Vale la pena señalar que incluso si es un puntero de un tipo personalizado, el compilador lo considera como un tipo incorporado y no se llamará al destructor del tipo de puntero.
Cuando se llama al destructor, el espacio del montón de destino de eliminación se liberará y se devolverá al sistema operativo.

[C++] Resumen del núcleo de clases y objetos

El siguiente es el proceso de prueba de que cuando el puntero es un tipo personalizado, el destructor generado por el compilador de forma predeterminada no llamará al destructor correspondiente, que es lo mismo que la estrategia de procesamiento de tipos incorporada.
Insertar descripción de la imagen aquí

Insertar descripción de la imagen aquí

1.5 epoller.hpp

Las siguientes son las diversas interfaces del epoll encapsulado. No es difícil. No entraré en detalles aquí porque ya hemos escrito una versión simple del servidor epoll en modo LT antes. Definitivamente no es difícil usar la interfaz epoll. entonces el anciano frente a la pantalla Podemos echar un breve vistazo a la implementación del código. Hoy la atención se centra en la implementación del Reactor anterior, no en cómo se implementan estos pequeños componentes.

Insertar descripción de la imagen aquí

2.protocolo.hpp

2.1 Analizar un mensaje completo

1.
De hecho, después de la explicación de tcpServer.hpp, se ha realizado el enfoque de la biblioteca de red Reactor, es decir, se ha completado el trabajo de procesar conexiones a nivel de E/S de red y procesar la transmisión de datos de red.
El siguiente protocolo.hpp solo está conectado a la capa de aplicación del servidor sobre la base de la biblioteca de red de Reactor, por ejemplo, cómo lidiar con el problema del paquete adhesivo, cómo personalizar el protocolo en la capa de aplicación, agregar o eliminar la aplicación. encabezado del protocolo de capa, serializar e invertir el mensaje. La serialización y otras tareas pertenecen a la capa de aplicación.
Pero, de hecho, ya cuando hablamos de personalización de protocolos, serialización y deserialización, es decir, cuando implementamos la calculadora de versión de red, ya habíamos realizado estas tareas, por lo que protocolo.hpp se copió directamente del código en ese momento. sólo se ha modificado el código para analizar el mensaje.
Entonces, si alguien olvida cómo personalizar el protocolo en ese momento, puede regresar y leer el artículo nuevamente.

Personalización de protocolo + serialización y deserialización.

2.
La siguiente interfaz se utiliza para analizar los datos _inbuffer de sock en la capa de aplicación. Dado que TCP está orientado al flujo de bytes, el problema de cómo analizar un mensaje completo debe resolverlo la capa de aplicación.
Decidimos un protocolo en ese momento. Hay LINE_SEP entre el encabezado del protocolo y la carga útil, que es \r\n, y la cola de la carga útil también es \r\n. El encabezado del protocolo indica el tamaño en bytes de la carga útil. Entonces, en el _inbuffer del flujo de bytes, la lógica de analizar un mensaje completo puede ser la siguiente:
por razones de seguridad, primero establezca el texto del parámetro de salida en una cadena vacía, luego busque la posición del iterador de LINE_SEP en el inbuffer y luego al encontrarlo, intercepte la parte del encabezado substr out y luego convierta su stoi en un número entero, para obtener el tamaño de la carga útil, y luego llame al encabezado interceptado, su función en clase size(), para obtener el tamaño del byte. del encabezado y finalmente agregue dos LINE_SEP. Después de sumar estos tamaños de bytes, puede obtener el tamaño de bytes de un mensaje completo.
El último paso es interceptar total_len bytes directamente comenzando desde 0 y colocar la cadena interceptada en el texto del parámetro de salida. Luego simplemente elimine los datos de 0 a total_len bytes del búfer de entrada, lo que en realidad sobrescribe los datos. De esta manera, interceptamos un mensaje completo a partir de una gran cantidad de datos de flujo de bytes.

Insertar descripción de la imagen aquí

2.2 Personalización del protocolo de capa de aplicación

De hecho, estos códigos sobre el protocolo de la capa de aplicación se discutieron en la versión de red anterior de la calculadora. Aquí lo explicaré brevemente. Si hay algún veterano confundido, consulte el artículo que escribí originalmente.

Ir a: Personalización de protocolo + serialización y deserialización

1.
Lo siguiente es agregar y eliminar el encabezado de la capa de aplicación: al agregar el encabezado, de hecho, siempre que se agregue LINE_SEP entre el encabezado y la carga útil, y al final de la carga útil, el contenido del encabezado es la longitud de la carga útil.
Al eliminar el encabezado, primero use la primera LINE_SEP como delimitador para interceptar la cadena del encabezado, a fin de obtener la longitud de la carga útil, y luego intercepte la subcadena desde la primera posición LINE_SEP, y la longitud de interceptación es la longitud de la carga útil. Sí, de esta manera obtienes la carga útil completa.

Insertar descripción de la imagen aquí

2.3 Serialización y deserialización

1.
El siguiente es el trabajo de serialización y deserialización. Utilizamos principalmente nuestras propias soluciones y soluciones json. Las empresas generalmente usan protobuf internamente y json externamente. No sé cómo usar json, solo puedo usarlo brevemente y no lo he aprendido sistemáticamente, por lo que a continuación solo puedo hablar sobre nuestras propias soluciones de serialización y deserialización, pero vale la pena señalar que en el uso real en la empresa. , porque Existen soluciones listas para usar para serialización y deserialización, ¡y los programadores nunca las escribirán ellos mismos! Pero hoy, como estudiantes, debemos poder comprender mejor qué tipo de trabajo realizan la serialización y deserialización cuando lo escribimos nosotros mismos, lo que definitivamente es de gran beneficio para los estudiantes.

2.
Para la serialización del mensaje de solicitud, en realidad es unir _x _op _y y otros campos en la estructura Solicitud en una cadena. Esto completa el trabajo de serialización, pero no es solo serialización. Se puede serializar. Por lo tanto, al empalmar cadenas, debe haber un SEP como separador entre _x y _op, y _op y _y para facilitar la deserialización de los mensajes de solicitud entrantes. (Datos estructurados → datos de flujo de bytes)
La deserialización es en realidad una operación de cadena, que intercepta _x _y _op en la cadena, los convierte en tipos int int char y los asigna a las tres variables miembro de la estructura Solicitud Dentro, esto completa la deserialización trabajar. (datos de flujo de bytes → datos estructurados)

Insertar descripción de la imagen aquí

3.
Para la serialización del mensaje de respuesta, simplemente convierta el código de salida de tipo int y el resultado del cálculo al tipo de cadena, y empalme un campo SEP en el medio, de modo que los datos estructurados se conviertan en datos serializados.
El trabajo de deserialización también es muy simple: siempre que el código de salida y la parte resultante de la cadena se intercepten y subcadenen, y luego se conviertan al tipo int, esto se convierte de serialización a datos estructurados.

Insertar descripción de la imagen aquí

3.principal.cc

3.1 Procesamiento de lógica empresarial

1.
La siguiente es la lógica de llamada de todo el servidor Reactor: primero inicialice el servidor y luego ejecute la interfaz de despacho de eventos Dispatcher.
El servicio proporcionado por la capa de aplicación del servidor es un servicio informático, por lo que al construir un objeto de servidor, el cálculo de la función lógica de procesamiento de la capa superior también debe pasarse al objeto de servidor.
Cuando el servidor ejecuta el método Recver, después de recibir los datos, se llamará a la función de devolución de llamada y el flujo de ejecución ejecutará el método de cálculo para realizar el procesamiento comercial de los datos leídos.

Insertar descripción de la imagen aquí

2.
Calcular es un método de procesamiento de lógica de negocios. Cree un bucle while dentro del método. Siempre que se pueda analizar un mensaje completo, puede ingresar al bucle y realizar el procesamiento lógico de la capa de aplicación en el mensaje recibido. Cuando _inbuffer Cuando los datos son tomado y los datos restantes no pueden formar un mensaje completo, es decir, cuando ParseOnePackage comete un error, elegimos salir del bucle en este momento y enviar todos los mensajes de solicitud procesados, es decir, los mensajes de respuesta construidos, al terminal del cliente. , ¿alguien dijo cómo enviar todos los mensajes de respuesta al cliente? De hecho, es muy simple: después de que cada mensaje de solicitud se procesa dentro de ParseOnePackage, el mensaje de respuesta correspondiente se colocará en el búfer de envío _outbuffer dentro de conn, de modo que cuando salte el bucle, muchos mensajes listos se habrán almacenado en _outbuffer .Se genera el mensaje de respuesta. En este momento, simplemente llame al método del remitente dentro de conn para enviarlo. Entonces, mirándolo de esta manera, ¿es muy útil esta conexión? Se ejecuta a través de todos los módulos implementados por el código de Reactor.
También es muy simple dentro de ParseOnePackage, porque ya serializamos y deserializamos el mensaje de solicitud/respuesta, separamos el encabezado y la carga útil del mensaje de la capa de aplicación, agregamos el encabezado, etc. dentro de protocol.hpp, por lo que en ParseOnePackage solo es necesario llamar el método implementado internamente en el protocolo.hpp correspondiente. Por ejemplo, primero elimine el encabezado, luego llame a la interfaz de deserialización para obtener una solicitud estructurada y realice el procesamiento de llamadas en la solicitud estructurada y una respuesta estructurada no inicializada. Dentro del procesamiento de llamadas, el trabajo de cálculo correspondiente se realiza realmente y el trabajo de cálculo se completa. Finalmente, simplemente complete el resultado en el mensaje de respuesta estructurado, luego serialice el mensaje de respuesta, agregue encabezados, etc., y finalmente simplemente coloque el mensaje de respuesta completo en el búfer externo. Cuando finalice el ciclo, envíe todos los mensajes de respuesta al otra parte de manera uniforme.

Insertar descripción de la imagen aquí

3.2 Resultados de ejecución del servidor Reactor

1.
No escribiremos el cliente nosotros mismos. Cuando hablamos antes sobre la personalización del protocolo, ya hemos implementado calclient y calserver nosotros mismos, por lo que aquí usamos calclient directamente como cliente.
Se puede ver en los resultados de la ejecución que para solicitudes de cálculo de datos normales, el servidor puede devolvernos los resultados de cálculo correspondientes, y cuando ocurre una excepción en el cliente, como Ctrl + C para desconectar la conexión TCP, el servidor también puede responder. al evento anormal Procesamiento correspondiente, como que el servidor también cierre la conexión TCP correspondiente y libere todos los recursos correspondientes al calcetín, como la estructura de conexión correspondiente al calcetín, elimine el calcetín del modelo epoll y elimine la tabla hash. , cerrando el descriptor del archivo sock, etc.

Insertar descripción de la imagen aquí

4. Resuma el patrón del Reactor

1.
Mi comprensión personal de Reactor es que Reactor gira principalmente en torno al envío de eventos y la respuesta automática. Por ejemplo, cuando llega una solicitud de conexión, epoll_wait recuerda al programador que ha llegado un evento listo. Debe procesarse lo antes posible y el El calcetín asociado con el evento listo corresponderá a la estructura de conexión A. Creo que esta estructura es la esencia del modelo de reactor. No importa qué tipo de evento listo sea, cada calcetín tendrá un método de devolución de llamada correspondiente, por lo que es fácil de manejar. el evento listo. Puede volver a llamar directamente al método correspondiente en la conexión. Es decir, si es un evento de lectura, llame al método de lectura, si es un evento de escritura, llame al método de escritura, si es un evento de excepción, el evento de excepción se manejará mientras se procesa IO en el método de lectura o el método de escritura.
Entonces siento que Reactor es como un reactor químico. Si lanzas algunas solicitudes de conexión o datos de red en este reactor, el reactor automáticamente coincidirá con el mecanismo de procesamiento correspondiente para manejar los eventos entrantes. Es muy conveniente. Al mismo tiempo, debido al modo ET y EPOLL, lo que permite a Reactor mostrar una gran fortaleza cuando se trata de conexiones de alta concurrencia.

2.
El servidor que implementamos hoy es semisincrónico y semiasincrónico. Semisincrónico significa que Reactor no solo garantiza la notificación de eventos de preparación, sino que también es responsable de IO. Semiasincrónico significa que el servidor actual también implementa el procesamiento comercial.

Insertar descripción de la imagen aquí

Supongo que te gusta

Origin blog.csdn.net/erridjsis/article/details/132548615
Recomendado
Clasificación