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

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

objeto de búfer

Buffer, como su nombre lo indica, es un objeto de búfer que almacena en caché los datos recibidos del socket y los datos que deben enviarse al socket.

Si los datos se reciben desde el conector, la función de devolución de llamada de procesamiento de eventos agrega constantemente datos al objeto de búfer. Al mismo tiempo, la aplicación necesita procesar continuamente los datos en el objeto de búfer, de modo que el objeto de búfer pueda desocupar un nuevo posición Para acomodar más datos.

Si se trata de datos enviados al socket, la aplicación agrega continuamente datos al objeto de búfer. Al mismo tiempo, la función de devolución de llamada de procesamiento de eventos llama continuamente a la función de envío en el socket para enviar los datos, reduciendo los datos de escritura en el búfer. objeto.

Puede verse que el objeto de búfer se puede utilizar en las direcciones de búfer de entrada y de salida al mismo tiempo, pero en los dos casos, los objetos escritos y leídos son diferentes.

A continuación se muestra el diseño del objeto de búfer:

                       

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

El writeIndex en el objeto de búfer identifica la posición actual que se puede escribir; readIndex identifica la posición actual de los datos que se pueden leer. La parte roja en la figura de readIndex a writeIndex es la parte que necesita leer datos, y la parte verde la parte es de writeIndex a la caché. Al final está la parte que se puede escribir.

A medida que pasa el tiempo, cuando readIndex y writeIndex se acercan cada vez más al final del búfer, el área front_space_size en la parte frontal se vuelve muy grande y los datos en esta área ya son datos antiguos. En este momento, debe ajustar La estructura completa del objeto de búfer mueve la parte roja hacia la izquierda y, al mismo tiempo, la parte verde también se moverá hacia la izquierda y aumentará la parte de escritura de todo el búfer.

La función make_room desempeña este papel. Si el espacio verde continuo a la derecha no es suficiente para acomodar nuevos datos, y la parte gris a la izquierda más la parte verde a la derecha pueden acomodar los nuevos datos, se activará una copia móvil de este tipo. , y la parte roja eventualmente estará ocupada. En el extremo izquierdo, la parte verde ocupa la parte derecha y la parte verde de la derecha se convierte en un espacio de escritura continuo, que puede acomodar nuevos datos. La siguiente figura explica este proceso.

                                    

void make_room(struct buffer *buffer, int size) {
    if (buffer_writeable_size(buffer) >= size) {
        return;
    }
    //如果front_spare和writeable的大小加起来可以容纳数据,则把可读数据往前面拷贝
    if (buffer_front_spare_size(buffer) + buffer_writeable_size(buffer) >= size) {
        int readable = buffer_readable_size(buffer);
        int i;
        for (i = 0; i < readable; i++) {
            memcpy(buffer->data + i, buffer->data + buffer->readIndex + i, 1);
        }
        buffer->readIndex = 0;
        buffer->writeIndex = readable;
    } else {
        //扩大缓冲区
        void *tmp = realloc(buffer->data, buffer->total_size + size);
        if (tmp == NULL) {
            return;
        }
        buffer->data = tmp;
        buffer->total_size += size;
    }
}

Por supuesto, si la parte roja ocupa demasiado y la parte de escritura no es suficiente, activará la expansión del búfer. Aquí completo la expansión del búfer llamando a la función realloc.

                                    

Procesamiento de flujo de bytes TCP

  • Recibir datos

La recepción de datos de socket se realiza mediante handle_read en tcp_connection.c. En esta función, el flujo de datos del socket se recibe llamando a la función buffer_socket_read y se almacena en el objeto buffer. Después de eso, puede ver que pasamos el objeto buffer y el objeto tcp_connection a la función de procesamiento real messageCallBack de la aplicación para el análisis del mensaje. La muestra de esta parte se ampliará en el análisis de paquetes HTTP.

int handle_read(void *data) {
    struct tcp_connection *tcpConnection = (struct tcp_connection *) data;
    struct buffer *input_buffer = tcpConnection->input_buffer;
    struct channel *channel = tcpConnection->channel;

    if (buffer_socket_read(input_buffer, channel->fd) > 0) {
        //应用程序真正读取Buffer里的数据
        if (tcpConnection->messageCallBack != NULL) {
            tcpConnection->messageCallBack(input_buffer, tcpConnection);
        }
    } else {
        handle_connection_closed(tcpConnection);
    }
}

En la función buffer_socket_read, llame a readv para escribir datos en dos búferes, uno es el objeto búfer y el otro es el búfer adicional aquí. La razón de esto es que el objeto búfer no puede acomodar el flujo de datos del conector, y también hay no hay forma de activar la expansión del objeto de búfer. Mediante el uso de búferes adicionales, una vez que se determina que los datos leídos del socket exceden el tamaño de escritura máximo real del objeto búfer, se puede activar la operación de expansión del objeto búfer, donde la función buffer_append llamará a la función make_room introducida anteriormente en Ampliación completa del objeto de búfer.

int buffer_socket_read(struct buffer *buffer, int fd) {
    char additional_buffer[INIT_BUFFER_SIZE];
    struct iovec vec[2];
    int max_writable = buffer_writeable_size(buffer);
    vec[0].iov_base = buffer->data + buffer->writeIndex;
    vec[0].iov_len = max_writable;
    vec[1].iov_base = additional_buffer;
    vec[1].iov_len = sizeof(additional_buffer);
    int result = readv(fd, vec, 2);
    if (result < 0) {
        return -1;
    } else if (result <= max_writable) {
        buffer->writeIndex += result;
    } else {
        buffer->writeIndex = buffer->total_size;
        buffer_append(buffer, additional_buffer, result - max_writable);
    }
    return result;
}
  • enviar datos

Cuando la aplicación necesita enviar datos al socket, es decir, después de que se completa el proceso de lectura-decodificación-computación-codificación, los datos después de la codificación se escriben en el objeto de búfer y se llama a tcp_connection_send_buffer para almacenar los datos en el búfer a través de el área de enchufe enviado.

int tcp_connection_send_buffer(struct tcp_connection *tcpConnection, struct buffer *buffer) {
    int size = buffer_readable_size(buffer);
    int result = tcp_connection_send_data(tcpConnection, buffer->data + buffer->readIndex, size);
    buffer->readIndex += size;
    return result;
}

Si se encuentra que el canal actual no ha registrado el evento WRITE, y no hay datos para enviar en el búfer de envío correspondiente a la tcp_connection actual, llame directamente a la función de escritura para enviar los datos. Si el envío no se completa esta vez, copie los datos restantes que se enviarán al búfer de envío correspondiente a la tcp_connection actual y registre el evento WRITE en event_loop. De esta manera, el framework se hace cargo de los datos y la aplicación libera esta parte de los datos.

//应用层调用入口
int tcp_connection_send_data(struct tcp_connection *tcpConnection, void *data, int size) {
    size_t nwrited = 0;
    size_t nleft = size;
    int fault = 0;
    struct channel *channel = tcpConnection->channel;
    struct buffer *output_buffer = tcpConnection->output_buffer;

    //先往套接字尝试发送数据
    if (!channel_write_event_registered(channel) && buffer_readable_size(output_buffer) == 0) {
        nwrited = write(channel->fd, data, size);
        if (nwrited >= 0) {
            nleft = nleft - nwrited;
        } else {
            nwrited = 0;
            if (errno != EWOULDBLOCK) {
                if (errno == EPIPE || errno == ECONNRESET) {
                    fault = 1;
                }
            }
        }
    }

    if (!fault && nleft > 0) {
        //拷贝到Buffer中,Buffer的数据由框架接管
        buffer_append(output_buffer, data + nwrited, nleft);
        if (!channel_write_event_registered(channel)) {
            channel_write_event_add(channel);
        }
    }
    return nwrited;
}

Implementación del protocolo HTTP

Con este fin, primero definimos una estructura http_server. Este http_server es esencialmente un TCPServer, pero la función callback expuesta a la aplicación es más simple. Solo necesitas ver las estructuras http_request y http_response.

typedef int (*request_callback)(struct http_request *httpRequest, struct http_response *httpResponse);

struct http_server {
    struct TCPserver *tcpServer;
    request_callback requestCallback;
};

En http_server, el punto clave es completar el análisis del mensaje y convertir el mensaje analizado en un objeto http_request, esto se hace a través de la función de devolución de llamada http_onMessage. En la función http_onMessage, se llama a parse_http_request para completar el análisis del mensaje.

// buffer是框架构建好的,并且已经收到部分数据的情况下
// 注意这里可能没有收到全部数据,所以要处理数据不够的情形
int http_onMessage(struct buffer *input, struct tcp_connection *tcpConnection) {
    yolanda_msgx("get message from tcp connection %s", tcpConnection->name);

    struct http_request *httpRequest = (struct http_request *) tcpConnection->request;
    struct http_server *httpServer = (struct http_server *) tcpConnection->data;

    if (parse_http_request(input, httpRequest) == 0) {
        char *error_response = "HTTP/1.1 400 Bad Request\r\n\r\n";
        tcp_connection_send_data(tcpConnection, error_response, sizeof(error_response));
        tcp_connection_shutdown(tcpConnection);
    }

    //处理完了所有的request数据,接下来进行编码和发送
    if (http_request_current_state(httpRequest) == REQUEST_DONE) {
        struct http_response *httpResponse = http_response_new();

        //httpServer暴露的requestCallback回调
        if (httpServer->requestCallback != NULL) {
            httpServer->requestCallback(httpRequest, httpResponse);
        }

        //将httpResponse发送到套接字发送缓冲区中
        struct buffer *buffer = buffer_new();
        http_response_encode_buffer(httpResponse, buffer);
        tcp_connection_send_buffer(tcpConnection, buffer);

        if (http_request_close_connection(httpRequest)) {
            tcp_connection_shutdown(tcpConnection);
            http_request_reset(httpRequest);
        }
    }
}

HTTP utiliza el retorno de carro y el avance de línea como límite del protocolo de mensajes HTTP:

                 

La idea de parse_http_request es encontrar el límite del mensaje y registrar el estado actual del trabajo de análisis. Según la secuencia del trabajo de análisis, el trabajo de análisis de mensajes se divide en cuatro etapas: REQUEST_STATUS, REQUEST_HEADERS, REQUEST_BODY y REQUEST_DONE, y el método de análisis en cada etapa es diferente.

Al analizar la línea de estado, primero defina la línea de estado localizando la posición del retorno de carro CRLF y el salto de línea. Al ingresar al análisis de la línea de estado, busque el carácter de espacio nuevamente como el límite de separación.

Al analizar la configuración del encabezado, también es primero definir un conjunto de pares clave-valor localizando la posición del retorno de carro CRLF y el avance de línea, y luego encontrar el carácter de dos puntos como límite de separación.

Finalmente, si no se encuentra el carácter de dos puntos, el trabajo de analizar el encabezado está completo.

La función parse_http_request completa las cuatro etapas del análisis de mensajes HTTP:

int parse_http_request(struct buffer *input, struct http_request *httpRequest) {
    int ok = 1;
    while (httpRequest->current_state != REQUEST_DONE) {
        if (httpRequest->current_state == REQUEST_STATUS) {
            char *crlf = buffer_find_CRLF(input);
            if (crlf) {
                int request_line_size = process_status_line(input->data + input->readIndex, crlf, httpRequest);
                if (request_line_size) {
                    input->readIndex += request_line_size;  // request line size
                    input->readIndex += 2;  //CRLF size
                    httpRequest->current_state = REQUEST_HEADERS;
                }
            }
        } else if (httpRequest->current_state == REQUEST_HEADERS) {
            char *crlf = buffer_find_CRLF(input);
            if (crlf) {
                /**
                 *    <start>-------<colon>:-------<crlf>
                 */
                char *start = input->data + input->readIndex;
                int request_line_size = crlf - start;
                char *colon = memmem(start, request_line_size, ": ", 2);
                if (colon != NULL) {
                    char *key = malloc(colon - start + 1);
                    strncpy(key, start, colon - start);
                    key[colon - start] = '\0';
                    char *value = malloc(crlf - colon - 2 + 1);
                    strncpy(value, colon + 1, crlf - colon - 2);
                    value[crlf - colon - 2] = '\0';

                    http_request_add_header(httpRequest, key, value);

                    input->readIndex += request_line_size;  //request line size
                    input->readIndex += 2;  //CRLF size
                } else {
                    //读到这里说明:没找到,就说明这个是最后一行
                    input->readIndex += 2;  //CRLF size
                    httpRequest->current_state = REQUEST_DONE;
                }
            }
        }
    }
    return ok;
}

Después de procesar todos los datos de la solicitud, se realiza a continuación el trabajo de codificación y envío. Para ello, se crea un objeto http_response y se llama a la función de codificación requestCallback proporcionada por la aplicación. A continuación, se crea un objeto buffer. La función http_response_encode_buffer se utiliza para convertir los datos en http_response en el flujo de bytes correspondiente según el protocolo HTTP .

Como puede ver, http_response_encode_buffer establece el encabezado http_response como Content-Length, así como los datos de la parte del cuerpo de http_response.

void http_response_encode_buffer(struct http_response *httpResponse, struct buffer *output) {
    char buf[32];
    snprintf(buf, sizeof buf, "HTTP/1.1 %d ", httpResponse->statusCode);
    buffer_append_string(output, buf);
    buffer_append_string(output, httpResponse->statusMessage);
    buffer_append_string(output, "\r\n");

    if (httpResponse->keep_connected) {
        buffer_append_string(output, "Connection: close\r\n");
    } else {
        snprintf(buf, sizeof buf, "Content-Length: %zd\r\n", strlen(httpResponse->body));
        buffer_append_string(output, buf);
        buffer_append_string(output, "Connection: Keep-Alive\r\n");
    }

    if (httpResponse->response_headers != NULL && httpResponse->response_headers_number > 0) {
        for (int i = 0; i < httpResponse->response_headers_number; i++) {
            buffer_append_string(output, httpResponse->response_headers[i].key);
            buffer_append_string(output, ": ");
            buffer_append_string(output, httpResponse->response_headers[i].value);
            buffer_append_string(output, "\r\n");
        }
    }

    buffer_append_string(output, "\r\n");
    buffer_append_string(output, httpResponse->body);
}

Ejemplo completo de servidor HTTP

Ahora, escribir un ejemplo de servidor HTTP se vuelve muy simple. En este ejemplo, la parte más importante es la función de devolución de llamada onRequest. Aquí, el método onRequest ha sido posterior a parse_http_request, y se puede calcular y procesar de acuerdo con diferente información http_request. La lógica del programa de ejemplo es muy simple: según la ruta URL de la solicitud http, se devuelven diferentes tipos de respuesta http. Por ejemplo, cuando la solicitud es el directorio raíz, se devuelven el formato 200 y HTML.

#include <lib/acceptor.h>
#include <lib/http_server.h>
#include "lib/common.h"
#include "lib/event_loop.h"

//数据读到buffer之后的callback
int onRequest(struct http_request *httpRequest, struct http_response *httpResponse) {
    char *url = httpRequest->url;
    char *question = memmem(url, strlen(url), "?", 1);
    char *path = NULL;
    if (question != NULL) {
        path = malloc(question - url);
        strncpy(path, url, question - url);
    } else {
        path = malloc(strlen(url));
        strncpy(path, url, strlen(url));
    }

    if (strcmp(path, "/") == 0) {
        httpResponse->statusCode = OK;
        httpResponse->statusMessage = "OK";
        httpResponse->contentType = "text/html";
        httpResponse->body = "<html><head><title>This is network programming</title></head><body><h1>Hello, network programming</h1></body></html>";
    } else if (strcmp(path, "/network") == 0) {
        httpResponse->statusCode = OK;
        httpResponse->statusMessage = "OK";
        httpResponse->contentType = "text/plain";
        httpResponse->body = "hello, network programming";
    } else {
        httpResponse->statusCode = NotFound;
        httpResponse->statusMessage = "Not Found";
        httpResponse->keep_connected = 1;
    }

    return 0;
}


int main(int c, char **v) {
    //主线程event_loop
    struct event_loop *eventLoop = event_loop_init();

    //初始tcp_server,可以指定线程数目,如果线程是0,就是在这个线程里acceptor+i/o;如果是1,有一个I/O线程
    //tcp_server自己带一个event_loop
    struct http_server *httpServer = http_server_new(eventLoop, SERV_PORT, onRequest, 2);
    http_server_start(httpServer);

    // main thread for acceptor
    event_loop_run(eventLoop);
}

Después de ejecutar este programa, podemos acceder a él a través del navegador y el comando curl. Puede abrir varios navegadores y comandos curl al mismo tiempo, lo que también demuestra que nuestro programa puede cumplir con altos requisitos de concurrencia.

$curl -v http://127.0.0.1:43211/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 43211 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:43211
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 116
< Connection: Keep-Alive
<
* Connection #0 to host 127.0.0.1 left intact
<html><head><title>This is network programming</title></head><body><h1>Hello, network programming</h1></body></html>%

                        

En esta conferencia, hablamos principalmente sobre las capacidades de procesamiento de flujo de bytes de todo el marco de programación, presentamos el objeto búfer y, sobre esta base, agregando características HTTP, incluyendo http_server, http_request, http_response, completamos la preparación de HTTP de alto rendimiento. servidor. El programa de ejemplo utiliza las capacidades proporcionadas por el marco para escribir un programa de servidor HTTP simple.

 

¡Aprenda lo nuevo revisando el pasado!

 

Supongo que te gusta

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