Programación de red Linux: escriba su propio marco de servidor HTTP de alto rendimiento (1)

Antes de comenzar a escribir un servidor HTTP de alto rendimiento, es más fácil crear un marco de programación de red de alto rendimiento que admita TCP y luego agregar soporte para funciones HTTP.

github: https://github.com/froghui/yolanda

Demanda

En primer lugar, el marco de trabajo de red de alto rendimiento TCP debe cumplir los siguientes tres puntos:

Primero, usando el modelo de reactor, poll / epoll se puede usar de manera flexible como una implementación de distribución de eventos.

En segundo lugar, debe admitir subprocesos múltiples, que pueden admitir el modo de reactor único de subproceso único y el modo de reactor maestro-esclavo de subprocesos múltiples. Los eventos de E / S en el zócalo se pueden separar en varios subprocesos.

En tercer lugar, encapsule las operaciones de lectura y escritura en objetos Buffer.

De acuerdo con estos tres requisitos, la idea de diseño general se puede dividir en tres partes para explicar, incluido el diseño del modelo de reactor, el modelo de E / S y el diseño del modelo de múltiples subprocesos, el paquete de lectura y escritura de datos y el búfer .

 

Ideas de diseño de modelo de reactor

Principalmente para diseñar un marco de reactor basado en distribución de eventos y devolución de llamada. Los principales objetos de este marco incluyen:

  • event_loop

Puedes entender el objeto event_loop como un bucle de eventos infinito vinculado a un hilo. Verás la abstracción de event_loop en varios idiomas. Qué significa eso? En pocas palabras, es un despachador de eventos de bucle infinito . Una vez que ocurre un evento, llamará a una función de devolución de llamada predefinida para completar el procesamiento del evento.

Específicamente, event_loop encuesta usos o métodos epoll para bloquear un hilo y esperan a que varios eventos de E / S a ocurrir .

  • canal

Extraemos todo tipo de objetos registrados en event_loop en canales para representarlos , como eventos de escucha registrados en event_loop, eventos de lectura y escritura de socket, etc. En las API de varios lenguajes, verá el objeto de canal. En general, el significado que expresan es más consistente con nuestras ideas de diseño aquí.

  • aceptador

El objeto aceptador representa al oyente del lado del servidor, y el objeto aceptador eventualmente se utilizará como un objeto de canal y se registrará en event_loop para la distribución de eventos y la detección de la finalización de la conexión .

  • event_dispatcher

event_dispatcher es una abstracción del mecanismo de distribución de eventos , es decir, puede implementar un poll_dispatcher basado en encuestas, o puede implementar un epoll_dispatcher basado en epoll. Aquí, diseñamos una estructura event_dispatcher unificada para abstraer estos comportamientos.

  • channel_map

El channel_map guarda el mapeo de la palabra de descripción al canal , de modo que cuando ocurre un evento, puede encontrar rápidamente la función de procesamiento de eventos en el objeto de canal de acuerdo con el conector correspondiente al tipo de evento.

 

Ideas de diseño de modelos de E / S y modelos de subprocesos múltiples

Principalmente resuelve el problema de ejecución de subprocesos de event_loop y el problema de ejecución de subprocesos de distribución de eventos y devolución de llamada.

  • thread_pool
struct thread_pool {
    //创建thread_pool的主线程
    struct event_loop *mainLoop;
    //是否已经启动
    int started;
    //线程数目
    int thread_number;
    //数组指针,指向创建的event_loop_thread数组
    struct event_loop_thread *eventLoopThreads;
    //表示在数组里的位置,用来决定选择哪个event_loop_thread服务
    int position;

};

struct thread_pool *thread_pool_new(struct event_loop *mainLoop, int threadNumber);
void thread_pool_start(struct thread_pool *);
struct event_loop *thread_pool_get_loop(struct thread_pool *);

thread_pool mantiene una lista de subprocesos del reactor, que se puede proporcionar al subproceso del reactor principal para su uso. Cada vez que se establece una nueva conexión, se puede obtener un subproceso de thread_pool para que se pueda usar para completar el nuevo socket de conexión. / write event registration separa el hilo de E / S del hilo del reactor principal .

  • event_loop_thread
struct event_loop_thread {
    struct event_loop *eventLoop;
    pthread_t thread_tid;        /* thread ID */
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    char * thread_name;
    long thread_count;    /* # connections handled */
};

//初始化已经分配内存的event_loop_thread
int event_loop_thread_init(struct event_loop_thread *, int);
//由主线程调用,初始化一个子线程,并且让子线程开始运行event_loop
struct event_loop *event_loop_thread_start(struct event_loop_thread *);

event_loop_thread es la implementación del hilo de reactor, y la detección de eventos de lectura / escritura de los sockets conectados se realiza en este hilo.

Ideas de diseño de lectura y escritura de búfer y datos

  • buffer
#define INIT_BUFFER_SIZE 65536
//数据缓冲区
struct buffer {
    char *data;          //实际缓冲
    int readIndex;       //缓冲读取位置
    int writeIndex;      //缓冲写入位置
    int total_size;      //总大小
};

struct buffer *buffer_new();
void buffer_free(struct buffer *buffer);
int buffer_writeable_size(struct buffer *buffer);
int buffer_readable_size(struct buffer *buffer);
int buffer_front_spare_size(struct buffer *buffer);

//往buffer里写数据
int buffer_append(struct buffer *buffer, void *data, int size);
//往buffer里写数据
int buffer_append_char(struct buffer *buffer, char data);
//往buffer里写数据
int buffer_append_string(struct buffer*buffer, char * data);
//读socket数据,往buffer里写
int buffer_socket_read(struct buffer *buffer, int fd);
//读buffer数据
char buffer_read_char(struct buffer *buffer);
//查询buffer数据
char * buffer_find_CRLF(struct buffer * buffer);

El objeto de búfer protege las operaciones de escritura y lectura del conector. Si no hay ningún objeto de búfer, los eventos de lectura / escritura del conector conectado deben lidiar con el flujo de bytes directamente, lo que obviamente no es amigable. Por lo tanto, también proporcionamos un objeto de búfer básico para representar los datos recibidos del socket de conexión y los datos que la aplicación enviará pronto.

  • tcp_connection
struct tcp_connection {
    struct event_loop *eventLoop;
    struct channel *channel;
    char *name;
    struct buffer *input_buffer;   //接收缓冲区
    struct buffer *output_buffer;  //发送缓冲区

    connection_completed_call_back connectionCompletedCallBack;
    message_call_back messageCallBack;
    write_completed_call_back writeCompletedCallBack;
    connection_closed_call_back connectionClosedCallBack;

    void * data; //for callback use: http_server
    void * request; // for callback use
    void * response; // for callback use
};

struct tcp_connection *
tcp_connection_new(int fd, struct event_loop *eventLoop, connection_completed_call_back 
    connectionCompletedCallBack, connection_closed_call_back connectionClosedCallBack,
    message_call_back messageCallBack, write_completed_call_back writeCompletedCallBack);

//应用层调用入口
int tcp_connection_send_data(struct tcp_connection *tcpConnection, void *data, int size);
//应用层调用入口
int tcp_connection_send_buffer(struct tcp_connection *tcpConnection, struct buffer * buffer);
void tcp_connection_shutdown(struct tcp_connection * tcpConnection);

tcp_connection Este objeto describe la conexión TCP establecida. Sus atributos incluyen búfer de recepción, búfer de envío, objeto de canal, etc. Estas son las propiedades naturales de una conexión TCP. tcp_connection es una estructura de datos con la que la mayoría de las aplicaciones interactúan directamente con nuestro marco de alto rendimiento. No queremos exponer el objeto de canal de nivel más bajo a la aplicación, porque el objeto de canal abstracto no solo puede representar tcp_connection, el conector de escucha mencionado anteriormente también es un objeto de canal, y el par de conectores de activación mencionado más adelante también es un objeto de canal. Por lo tanto, diseñamos el objeto tcp_connection con la esperanza de proporcionar a los usuarios una entrada de programación más clara.

 

Diseño específico de modo reactor

Detalles de ejecución de Event_loop:

                                         

Cuando se completa event_loop_run, el subproceso ingresa al bucle y primero ejecuta la distribución del evento de despacho. Una vez que ocurre un evento, se llama a la función channel_event_activate. Las funciones de devolución de llamada de eventos eventReadcallback y eventWritecallback se llaman en esta función, y finalmente se ejecuta el canal event_loop_handle_pending_channel para modificar el monitor actual Después de completar esta parte, ingresa al ciclo de distribución de eventos.

  • análisis event_loop

No es exagerado decir que event_loop es el núcleo de todo el diseño del modelo de reactor. Primero observe la estructura de datos de event_loop. En esta estructura de datos, lo más importante es el objeto event_dispatcher . Simplemente puede pensar en event_dispatcher como poll o epoll, lo que permite que nuestro hilo se suspenda y espere a que ocurra un evento. Aquí hay un pequeño truco, event_dispatcher_data, que se define como un tipo void *, y puede colocar un puntero de objeto que necesitemos arbitrariamente de acuerdo con nuestras necesidades. De esta manera, para diferentes implementaciones, como poll o epoll, se pueden colocar diferentes objetos de datos según los requisitos. Event_loop también retiene varios objetos relacionados con subprocesos múltiples. Por ejemplo, owner_thread_id retiene el ID de hilo de cada bucle de evento, y mutex y con se utilizan para la sincronización de hilos. socketPair es usado por el hilo principal para notificar al hilo secundario que hay un nuevo evento para ser procesado . Pending_head y pendiente_tail son eventos nuevos que deben procesarse en el hilo secundario.

struct event_loop {
    int quit;
    const struct event_dispatcher *eventDispatcher;

    /** 对应的event_dispatcher的数据. */
    void *event_dispatcher_data;
    struct channel_map *channelMap;

    int is_handle_pending;
    struct channel_element *pending_head;
    struct channel_element *pending_tail;

    pthread_t owner_thread_id;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    int socketPair[2];
    char *thread_name;
};

Echemos un vistazo al método principal de event_loop, el método event_loop_run Como se mencionó anteriormente, event_loop es un bucle while infinito que distribuye eventos continuamente.

/**
 * 1.参数验证
 * 2.调用dispatcher来进行事件分发,分发完回调事件处理函数
 */
int event_loop_run(struct event_loop *eventLoop) {
    assert(eventLoop != NULL);

    struct event_dispatcher *dispatcher = eventLoop->eventDispatcher;
    if (eventLoop->owner_thread_id != pthread_self()) {
        exit(1);
    }

    yolanda_msgx("event loop run, %s", eventLoop->thread_name);
    struct timeval timeval;
    timeval.tv_sec = 1;

    while (!eventLoop->quit) {
        //block here to wait I/O event, and get active channels
        dispatcher->dispatch(eventLoop, &timeval);
        //handle the pending channel
        event_loop_handle_pending_channel(eventLoop);
    }

    yolanda_msgx("event loop end, %s", eventLoop->thread_name);
    return 0;
}

El código refleja claramente esto. Aquí estamos haciendo un bucle sin que salga event_loop. El método de despacho del objeto dispatcher se llama en el cuerpo del bucle para esperar a que ocurra el evento.

  • análisis event_dispacher

Para implementar diferentes mecanismos de distribución de eventos, poll, epoll, etc. se abstraen en una estructura event_dispatcher. La implementación específica de event_dispatcher incluye poll_dispatcher y epoll_dispatcher.

/** 抽象的event_dispatcher结构体,对应的实现如select,poll,epoll等I/O复用. */
struct event_dispatcher {
    /**  对应实现 */
    const char *name;

    /**  初始化函数 */
    void *(*init)(struct event_loop * eventLoop);

    /** 通知dispatcher新增一个channel事件*/
    int (*add)(struct event_loop * eventLoop, struct channel * channel);

    /** 通知dispatcher删除一个channel事件*/
    int (*del)(struct event_loop * eventLoop, struct channel * channel);

    /** 通知dispatcher更新channel对应的事件*/
    int (*update)(struct event_loop * eventLoop, struct channel * channel);

    /** 实现事件分发,然后调用event_loop的event_activate方法执行callback*/
    int (*dispatch)(struct event_loop * eventLoop, struct timeval *);

    /** 清除数据 */
    void (*clear)(struct event_loop * eventLoop);
};
  • análisis de objetos de canal

El objeto de canal es la estructura principal utilizada para interactuar con event_dispather, y abstrae la distribución de eventos. Un canal corresponde a una palabra de descripción. La palabra de descripción puede tener eventos READ o eventos WRITE. El objeto de canal está vinculado a las funciones de procesamiento de eventos event_read_callback y event_write_callback.

typedef int (*event_read_callback)(void *data);
typedef int (*event_write_callback)(void *data);

struct channel {
    int fd;
    int events;   //表示event类型

    event_read_callback eventReadCallback;
    event_write_callback eventWriteCallback;
    void *data; //callback data, 可能是event_loop,也可能是tcp_server或者tcp_connection
};
  • análisis de objetos channel_map

Una vez que event_dispatcher obtiene la lista de eventos de eventos, necesita encontrar el canal correspondiente a través de la descripción del archivo, para poder llamar de nuevo las funciones de procesamiento de eventos event_read_callback y event_write_callback en el canal. Para esto, se diseña el objeto channel_map.

/**
 * channel映射表, key为对应的socket描述字
 */
struct channel_map {
    void **entries;

    /* The number of entries available in entries */
    int nentries;
};

El objeto channel_map es una matriz, el subíndice de la matriz es la palabra de descripción y el elemento de la matriz es la dirección del objeto de canal. Por ejemplo, el canal correspondiente a la palabra descriptiva 3 se puede obtener directamente de esta manera.

struct chanenl * channel = map->entries[3];

De esta manera, cuando event_dispatcher necesite volver a llamar las funciones de lectura y escritura en el canal, llame a channel_event_activate. La siguiente es la implementación de channel_event_activate. Después de encontrar el objeto de canal correspondiente, vuelva a llamar a la función de lectura o escritura según el tipo de evento . Tenga en cuenta que EVENT_READ y EVENT_WRITE se utilizan aquí para abstraer todos los tipos de eventos de lectura y escritura de poll y epoll.

int channel_event_activate(struct event_loop *eventLoop, int fd, int revents) {
    struct channel_map *map = eventLoop->channelMap;
    yolanda_msgx("activate channel fd == %d, revents=%d, %s", fd, revents, eventLoop->thread_name);

    if (fd < 0)
        return 0;
    if (fd >= map->nentries)
        return (-1);

    struct channel *channel = map->entries[fd];
    assert(fd == channel->fd);
    if (revents & (EVENT_READ)) {
        if (channel->eventReadCallback) 
            channel->eventReadCallback(channel->data);
    }
    if (revents & (EVENT_WRITE)) {
        if (channel->eventWriteCallback) 
            channel->eventWriteCallback(channel->data);
    }
    return 0;
}
  • Agregar, eliminar, modificar el evento del canal

Entonces, ¿cómo agregar un evento de evento de canal nuevo? Estas funciones se utilizan para agregar, eliminar y modificar eventos de eventos de canal.

int event_loop_add_channel_event(struct event_loop *eventLoop, int fd, struct channel *channel1);
int event_loop_remove_channel_event(struct event_loop *eventLoop, int fd, struct channel *channel1);
int event_loop_update_channel_event(struct event_loop *eventLoop, int fd, struct channel *channel1);

Las primeras tres funciones proporcionan capacidades de entrada y la implementación real recae en estas tres funciones:

int event_loop_handle_pending_add(struct event_loop *eventLoop, int fd, struct channel *channel);
int event_loop_handle_pending_remove(struct event_loop *eventLoop, int fd, struct channel *channel);
int event_loop_handle_pending_update(struct event_loop *eventLoop, int fd, struct channel *channel);

Echemos un vistazo a una de las implementaciones, event_loop_handle_pendign_add agrega un nuevo par clave-valor al channel_map del event_loop actual. La clave es la palabra de descripción del archivo y el valor es la dirección del objeto de canal. Luego, llame al método add del objeto event_dispatcher para aumentar el evento del canal. Tenga en cuenta que este método siempre se ejecuta en el hilo de E / S actual.

// in the i/o thread
int event_loop_handle_pending_add(struct event_loop *eventLoop, int fd, struct channel *channel) {
    yolanda_msgx("add channel fd == %d, %s", fd, eventLoop->thread_name);
    struct channel_map *map = eventLoop->channelMap;
    if (fd < 0)
        return 0;
    if (fd >= map->nentries) {
        if (map_make_space(map, fd, sizeof(struct channel *)) == -1)
            return (-1);
    }

    //第一次创建,增加
    if ((map)->entries[fd] == NULL) {
        map->entries[fd] = channel;
        //add channel
        struct event_dispatcher *eventDispatcher = eventLoop->eventDispatcher;
        eventDispatcher->add(eventLoop, channel);
        return 1;
    }
    return 0;
}

para resumir

En esta conferencia, presentamos las principales ideas de diseño y estructuras de datos básicas del marco de programación de redes de alto rendimiento, así como prácticas específicas relacionadas con el diseño de reactores.

 

¡Aprenda lo nuevo revisando el pasado!

 

Supongo que te gusta

Origin blog.csdn.net/qq_24436765/article/details/104952972
Recomendado
Clasificación