Análisis de la biblioteca de red de moudo

Introducción a Muduo


muduo es una biblioteca de red de alto rendimiento desarrollada por el maestro Chen Shuo basada en C/C++ bajo la plataforma Linux.Sobre esta base, se puede expandir fácilmente para desarrollo secundario y escritura, como servidor http. El marco central de la biblioteca de red muduo, un hilo por hilo + modo Reactor. Esta es también la arquitectura principal de la mayoría de las aplicaciones de marco de programación de red de alto rendimiento del lado de Linux.

La "Programación del lado del servidor de subprocesos múltiples de Linux" escrita por él proporciona una introducción y un análisis muy detallados de todo el marco y los detalles de diseño de la biblioteca muduo. Es muy recomendable que todos lo lean, no solo es útil para la programación de redes.

Este artículo usa muduo para escribir un servidor echoserver simple y explica el marco central de muduo, la interacción de varios componentes, el excelente diseño de código y los detalles de implementación desde la perspectiva de los usuarios. Desde la perspectiva del código fuente para ayudar a comprender la realización de todo el muduo, espero tener una comprensión más profunda de la programación de redes.

La lógica central se extrae del código fuente de muduo y se proporciona una biblioteca de muduo con anotaciones detalladas, que se pueden leer con el código fuente.Eventos de temporizador, una gran cantidad de errores de aserción y el cliente solo retiene el marco central del Programación del lado del servidor de la biblioteca muduo. , el número de líneas de código es de aproximadamente 2000+, lo que es fácil de analizar en su núcleo.

Versión abreviada de muduo C++11



Conceptos bloqueantes, no bloqueantes, síncronos, asíncronos


En pocas palabras, bloquear y no bloquear son comportamientos para operar E/S, y E/S no es más que leer (leer) y escribir (escribir).

Tomando un sockfd como ejemplo, hay dos fases tanto para leer como para escribir: 1. Esperar a que el búfer de recepción/envío en el kernel esté listo (legible/escribible), 2. Copiar los datos del búfer de recepción en el kernel al área de usuario o copiarlos en el área de usuario Los datos del área se copian en el búfer de envío.

En Linux, la operación de lectura/escritura predeterminada es para sockfd. Si no está listo, se bloqueará hasta que ocurra el evento listo de manera predeterminada, y el programa continuará ejecutándose. Si se llama al sistema fcntl, fd se establece en sin bloqueo, incluso si los datos no están listos También devuelve directamente.


síncrono y asíncrono


Las palabras originales de Chen Shuo: "Cuando se trata de E/S, tanto el bloqueo como el no bloqueo son E/S síncronas. Solo cuando se usa una API especial es E/S asíncrona".

Según tengo entendido, ya sea E/S de bloqueo, E/S sin bloqueo o multiplexación de E/S, es E/S síncrona, y todos son solo un esquema de notificación de eventos listos, ya sea que sockfd se pueda leer o escribir . Sí , todos los eventos devueltos son eventos listos, y el programa de usuario todavía tiene que soportar el tiempo para copiar datos del área de usuario al búfer de envío del núcleo o copiar datos del búfer de recepción del núcleo al área de usuario, y la notificación de E/S asíncrona es la finalización del evento , el programa de usuario no necesita soportar el tiempo dedicado a la copia de datos, el kernel ha completado la operación de copia por usted, y el siguiente paso puede procesarse tan pronto como regrese.

En comparación con la E/S sincrónica, la E/S asíncrona tiene un mejor rendimiento, pero la lógica de programación se vuelve más complicada y es más difícil solucionar problemas.



Cinco modelos IO


bloqueo de E/S

inserte la descripción de la imagen aquí

E/S sin bloqueo sin bloqueo

inserte la descripción de la imagen aquí

Multiplexación IO (multiplexación IO)

inserte la descripción de la imagen aquí

E/S controlada por señal (controlada por señal)

inserte la descripción de la imagen aquí

E/S asíncrona (asíncrona)

inserte la descripción de la imagen aquí



Diseño del marco de la biblioteca muduo.


Las CPU modernas tienen muchos núcleos, y los programas como los servidores también deben aprovechar al máximo las ventajas de las CPU multinúcleo.¿Cómo diseñar un servidor de subprocesos múltiples?

Citando al autor de la biblioteca de red libevent: un bucle por hilo suele ser un buen modelo

En este punto, el problema se transforma en cómo diseñar un EventLoop eficiente, un subproceso ejecuta un EventLoop.

EventLoop es el núcleo de la biblioteca muduo, que utiliza la multiplexación de E/S + el modo de E/S sin bloqueo + LT.

La multiplexación de IO generalmente se usa junto con IO sin bloqueo. No se pueden usar solos. Nadie sondeará y leerá un fd sin bloqueo todo el tiempo, desperdiciando recursos de CPU. Nadie utilizará el bloqueo de E/S en la multiplexación de E/S. El flujo de ejecución actual puede bloquearse durante el proceso de lectura, lo que hace que las conexiones restantes no respondan.


Modo LT de Muduo

El modo ET de Epoll suele ser sinónimo de alta eficiencia. Al usar el modo ET, el evento solo se notificará una vez, incluso si los datos no se han leído una vez, no se volverá a notificar, lo que reduce la cantidad de activaciones del mecanismo de devolución de llamada subyacente de Epoll. y mejora el rendimiento.


La eficiencia del modo ET es relativa, ¿por qué muduo usa el modo LT? Creo que hay los siguientes beneficios:

  1. Baja latencia: solo se llama a una llamada al sistema para cada lectura/escritura, teniendo en cuenta cada conexión, y no afectará a otras conexiones porque una determinada conexión requiere una gran cantidad de lecturas y escrituras.
  2. Integridad de los datos: el modo LT seguirá informando mientras no se hayan leído los datos.


modelo de reactor


El modelo Reactor encapsula la lectura y escritura de IO, devoluciones de llamadas para eventos correspondientes y operaciones de eventos.

El patrón de diseño del reactor es un patrón de manejo de eventos para manejar solicitudes de servicio
entregadas simultáneamente a un manejador de servicios por una o más entradas. El controlador de servicios
luego desmultiplexa las solicitudes entrantes y las envía de forma síncrona a los
controladores de solicitudes asociados.

Un solo
inserte la descripción de la imagen aquí
componente Reactor Reactor: evento de evento, reactor Reactor, distribuidor de eventos Demultiplex, procesador de eventos EvantHandler



Modelo de reactores múltiples de la biblioteca muduo

inserte la descripción de la imagen aquí

Cada Reactor en la figura corresponde a un EventLoop, y un subproceso ejecuta un EventLoop, y la conexión del cliente en cada bucle solo puede ser leída y escrita por el bucle donde se encuentra.

Tome un MianReactor y dos SubReactors en la imagen como ejemplo:

En la biblioteca muduo, la interacción entre MianReactor y SubReactor no usa una cola de sincronización simple (como un modelo productor-consumidor), sino que usa una llamada al sistema eventfd eficiente para implementar un mecanismo de notificación/activación. Para notificar un bucle, simplemente escriba datos en el eventfd que posee.

Si se usa una cola síncrona, es probable que la cola síncrona se convierta en un cuello de botella de rendimiento en un escenario instantáneo de alta concurrencia. La cola síncrona simplifica la lógica y hace que el código sea más fácil de implementar, pero la pérdida de rendimiento de mantener la sincronización puede convertirse en un problema de rendimiento. asesino en un escenario de alta concurrencia El uso de eventfd no implica ninguna condición de carrera

Asesino de rendimiento: competencia de bloqueo, cambio de contexto, copia de datos, aplicación de memoria



Componentes básicos de la biblioteca Muduo


Canal

Conecte el cliente conectado a connfd y empaquételo en un canal, que encapsula connfd, loop_ (EventLoop donde se encuentra este canal), evento (EPOLLIN/OUT), revent (evento de respuesta) y un evento correspondiente cuando ocurre una serie de devoluciones de llamada.

Solo hay dos canales en la biblioteca muduo, el listenfd-acceptorChannel para escuchar nuevas conexiones y el connChannel del cliente.El acceptorChannel está registrado en el Poller de MainLoop.


noray

Poller corresponde al distribuidor de eventos en el modelo Reactor y devuelve un conjunto de eventos listos para EventLoop. Es un objeto de multiplexación de IO específico. Poller en la biblioteca muduo es una clase abstracta. Muduo proporciona dos implementaciones de multiplexación de IO específicas, EpollPoller y PollPoller heredan la clase Poller, reescribir su función virtual y darse cuenta de su rendimiento multiplataforma. Epoll se utiliza de forma predeterminada.



bucle de eventos

ChannelList activeChannels_;
std::unique_ptr poller_;
int wakeupFd; -> loop
std::unique_ptr wakeupChannel ;

EventLoop es el núcleo de muduo. Un subproceso corresponde a un EventLoop, y EventLoop corresponde al reactor Reactor en el modelo Reactor de la figura anterior. Un EventLoop puede administrar una gran cantidad de canales y un Poller. Channel no puede interactuar directamente con Poller.Si Channel quiere modificar los eventos que le interesan, debe llamar a la interfaz de Poller a través de EventLoop.

inserte la descripción de la imagen aquí


Zócalo y aceptador


Socket : encapsulación de socket, enlace, escucha, aceptación y una serie de funciones setsockopt, como la configuración de multiplexación de puertos.

Aceptador:

Encapsula principalmente la operación de escucha de enlace de socket relacionada con listenfd, ejecutándose en baseLoop



Thread和EventLoopThread

Subproceso: una encapsulación adicional de la clase de subprocesos C ++ 11

EventLoopThread: la realización de un bucle por subproceso, que encapsula un subproceso y un EventLoop, y este subproceso ejecuta este EventLoop



EventLoopThreadPool

En el grupo de subprocesos de bucle de eventos, el usuario llama a SetThreadNum para establecer la cantidad de SubLoops, excluyendo BaseLoop. Si no se establece la cantidad de subprocesos, la biblioteca muduo predeterminada tiene solo un BaseLoop para manejar eventos de lectura y escritura entrantes y existentes de nuevos conexiones

GetNextLoop() //通过Round Robin轮询算法获取下一个SubLoop


Buffer

La biblioteca muduo establece el búfer de la capa de aplicación, que está diseñada para imitar la biblioteca de red Netty

/// A buffer class modeled after org.jboss.netty.buffer.ChannelBuffer
/// @code
/// +-------------------+------------------+------------------+
/// | prependable bytes |  readable bytes  |  writable bytes  |
/// |                   |     (CONTENT)    |                  |
/// +-------------------+------------------+------------------+
/// |                   |                  |                  |
/// 0      <=      readerIndex   <=   writerIndex    <=     size                                                                        
/// @endcode

Datos de lectura de la aplicación: llame al búfer de recepción Tcp de lectura -> InputBuffer

Datos de escritura de la aplicación: llamar enviar OutputBuffer -> Tcp enviar búfer (si el envío no se completa a la vez, debe guardar los datos restantes en el OutputBuffer y dejar que Poller preste atención al evento EPOLLOUT de esta conexión y esperar a que la siguiente notificación antes de continuar con el envío)


La programación de red sin bloqueo debe tener búferes de capa de aplicación.
La idea central de la E/S sin bloqueo es evitar el bloqueo en lectura () o escritura () u otras llamadas del sistema de E/S, de modo que el subproceso de control se pueda reutilizar al máximo, de modo que un subproceso pueda servir múltiples conexiones de enchufe. El subproceso IO solo se puede bloquear en select()/poll()/epoll_wait(). De esta manera, es necesario el almacenamiento en búfer en la capa de aplicación, y cada socket TCP debe tener un búfer de entrada y un búfer de salida.

¿Por qué debe haber un búfer de salida?

Si el programa quiere llamar a send() para enviar 20k de datos en este momento, el búfer de envío de TcpServer solo puede recibir 10k en este momento, y los 10k de datos restantes no se pueden descartar directamente, y el flujo de ejecución no se puede bloquear hasta que se pueda escrito. Los datos restantes deben ser tomados por la biblioteca de red, guardados en el OutputBuffer de TcpConnection, y prestar atención al evento EPOLLOUT. Una vez que el socket se puede escribir, los datos se enviarán inmediatamente hasta que se complete el envío.

¿Por qué debe haber un búfer de entrada?

El cliente envía 20k datos al servidor, el servidor no recibirá directamente 20k datos completos, puede ser 5k ... 10k 15k ... 20k ... Cuando los datos no reciben datos completos, los datos deben almacenarse en caché el InputBuffer, a la espera de recibir un completo Luego notifique al programa de aplicación para llamar al procesamiento comercial correspondiente para su análisis. .



Conexión Tcp

Un cliente conectado con éxito corresponde a una TcpConnection, que encapsula Socket, Channel, InputBuffer, OutputBuffer y las devoluciones de llamada de eventos correspondientes.



Servidor Tcp

TcpServer domina la situación general, y TcpServer es esencial para la programación del lado del servidor utilizando la biblioteca muduo.
TcpServer solo expone una interfaz amigable y fácil de usar, y todos los detalles complejos de la red subyacente han sido encapsulados.

TcpServer encapsula principalmente los siguientes componentes importantes:

Aceptador: se usa para recibir nuevas conexiones, se ejecuta en el baseloop
EventLoopThreadPool: Create SubLoop

También encapsula una serie de devoluciones de llamada configuradas por el usuario

std::unordered_map<std::string, TcpConnectionPtr>  connections_;//保存所有的连接



Analizar muduo desde echoserver


La cadena de llamadas de la biblioteca muduo es muy larga y se utiliza una gran cantidad de funciones/bind para el enlace de devolución de llamada. El siguiente es un proceso en ejecución de ehcoserver, desde la operación de inicialización inicial, el procesamiento de nuevas conexiones, la lectura y escritura de datos. , y desconexión de la conexión Abrir para analizar la lógica operativa central de la biblioteca moduo.

#include <iostream>
#include <functional>

#include <string>

#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>

using namespace std::placeholders;

class CharServer
{
    
    
public:
    CharServer(muduo::net::EventLoop *loop,
               const muduo::net::InetAddress &addr,
               const std::string &name) : server_(loop, addr, name), loop_(loop)
    {
    
    
        //连接建立/连接断开都会触发此回调
        server_.setConnectionCallback(std::bind(&CharServer::OnConnection, this, _1));
        //读事件发生触发此回调
        server_.setMessageCallback(std::bind(&CharServer::OnMessage, this, _1, _2, _3));
    }

	void SetThreadNum(int num)
	{
    
    
	    //设置底层创建的EventLoop个数,不包括base_loop
        server_.setThreadNum(num);
	}

    void Start()
    {
    
    
        server_.start();
    }

    void OnConnection(const muduo::net::TcpConnectionPtr &conn)
    {
    
    
        if (conn->connected())
        {
    
    
            std::cout << conn->peerAddress().toIpPort() << std::endl;
            std::cout << "new conn ... " << std::endl;
        }
        else
        {
    
    
            std::cout << conn->peerAddress().toIpPort() << std::endl;
            std::cout << "conn close ... " << std::endl;
        }
    }

    void OnMessage(const muduo::net::TcpConnectionPtr &conn,
                   muduo::net::Buffer *buf,
                   muduo::Timestamp ts)
    {
    
    
        std::string recv = buf->retrieveAllAsString();
        conn->send(recv);
        conn->shutdown();
    }

private:
    muduo::net::TcpServer server_;
    muduo::net::EventLoop *loop_;
};

int main()
{
    
    
    muduo::net::EventLoop loop;

    CharServer server(&loop, {
    
    "127.0.0.1", 8080}, "EchoServer");
    
    server.setThreadNum(3);	//设置SubLoop个数
    server.Start(); // epoll_ctl 添加listen_fd
    loop.loop();    //  epoll wait 阻塞等待
}

Tomando la implementación de un servidor de eco como ejemplo, el usuario solo necesita vincular varias devoluciones de llamadas de procesamiento de eventos y luego llamar a Start para implementar un servidor de eco simple.


inicialización


Crear un objeto BaseLoop

inserte la descripción de la imagen aquí

Crear un objeto TcpServer

El usuario pasa OnConnection/Onmessage -> Tcpserver -> TcpConnection -> Channel y finalmente se une al canal.

Un objeto TcpServer se encapsula en echoserver y la construcción de TcpServer debe pasarse a BaseLoop. TcpServer encapsula dos componentes importantes Acceptor EventThreadPool

Asumiendo que la operación de inicialización ha sido completada en este momento, el usuario llama a setThreadNum(3) para crear tres SubLoops. En este momento, listenfd ha sido encapsulado en un Canal y registrado en el Poller del BaseLoop. El Poller de cada SubLoop ha también registró el wakeupChannel.Todos los bucles están bloqueados en epoll_wait.
inserte la descripción de la imagen aquí



Llega una nueva conexión llamando al procedimiento

inserte la descripción de la imagen aquí



El proceso de llamada para la recepción de datos.

Se ha establecido una serie de devoluciones de llamada cuando ocurre el evento Channel en el constructor TcpConnection

   //给Channel设置相对应的回调
    channel_->SetReadCallBack(
        std::bind(&TcpConnection::HandleRead, this, std::placeholders::_1));
    channel_->SetWriteCallBack(
        std::bind(&TcpConnection::HandleWrite, this));
    channel_->SetCloseCallBack(
        std::bind(&TcpConnection::HandleClose, this));
    channel_->SetErrorCallBack(
        std::bind(&TcpConnection::HandleError, this));

inserte la descripción de la imagen aquí



Proceso de llamada para el envío de datos

Llame a send() directamente en CharServer::OnMassage para devolver los datos intactos.

inserte la descripción de la imagen aquí

Llamar a TcpConnection::shutdown() no desconectará directamente la conexión, sino que esperará a que se envíen los datos restantes antes de llamar a ShutdownWrite() para cerrar el final de la escritura, y el método de conexión de cierre de la biblioteca muduo debe permitir el final del par. para leer a 0, también llame a close() para cerrar la conexión; de lo contrario, la conexión permanecerá en un estado medio cerrado durante mucho tiempo.



La llamada para cerrar la conexión.


Cerrar pasivamente la conexión
inserte la descripción de la imagen aquí



Cerrar activamente la conexión

inserte la descripción de la imagen aquí

El resto de la lógica es la misma que cerrar pasivamente la conexión.



Resumir

Solo un ejemplo simple para explicar la cadena de llamadas central del lado del servidor de la biblioteca muduo. Hay muchos detalles importantes y un excelente diseño de código que no se puede expresar con palabras. De hecho, es demasiado perezoso. . Si realmente desea comprender la biblioteca de muduo, vaya al código fuente. .

A través del estudio de la biblioteca muduo, tengo una comprensión profunda de la programación basada en objetos, cómo usar punteros inteligentes para administrar correctamente el ciclo de vida de los recursos en subprocesos múltiples, cómo encapsular los detalles de implementación complejos subyacentes y solo exponer fácil -para usar interfaces con el mundo exterior? Cómo diseñar cada componente en el proyecto real sin redundancia ni acoplamiento débil

Supongo que te gusta

Origin blog.csdn.net/juggte/article/details/124875330
Recomendado
Clasificación