Programmation réseau Linux: écrivez votre propre framework de serveur HTTP hautes performances (3)

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

objet tampon

Buffer, comme son nom l'indique, est un objet tampon qui met en cache les données reçues du socket et les données qui doivent être envoyées au socket.

Si les données sont reçues du socket, la fonction de rappel du traitement des événements ajoute constamment des données à l'objet tampon. En même temps, l'application doit traiter en permanence les données de l'objet tampon, afin que l'objet tampon puisse en libérer un nouveau position Pour accueillir plus de données.

S'il s'agit de données envoyées au socket, l'application ajoute en permanence des données à l'objet tampon. En même temps, la fonction de rappel de traitement d'événements appelle en permanence la fonction d'envoi sur le socket pour envoyer les données, réduisant ainsi les données d'écriture dans le tampon objet.

On voit que l'objet tampon peut être utilisé à la fois dans le sens tampon d'entrée et dans le sens tampon de sortie en même temps, mais dans les deux cas, les objets écrits et lus sont différents.

Ce qui suit montre la conception de l'objet tampon:

                       

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

Le writeIndex dans l'objet buffer identifie la position actuelle qui peut être écrite; readIndex identifie la position actuelle des données qui peuvent être lues. La partie rouge de la figure de readIndex à writeIndex est la partie qui doit lire les données, et le vert part est de writeIndex au cache A la fin se trouve la partie qui peut être écrite.

Au fil du temps, lorsque readIndex et writeIndex se rapprochent de plus en plus de la fin du tampon, la zone front_space_size dans la partie avant devient très grande et les données de cette zone sont déjà d'anciennes données. À ce stade, vous devez ajuster la totalité La structure de l'objet tampon déplace la partie rouge vers la gauche, et en même temps, la partie verte se déplace également vers la gauche, et la partie inscriptible de l'ensemble du tampon augmente.

La fonction make_room joue ce rôle. Si l'espace vert continu à droite n'est pas suffisant pour accueillir de nouvelles données et que la partie grise à gauche plus la partie verte à droite peuvent accueillir les nouvelles données, une telle copie mobile sera déclenchée , et la partie rouge sera éventuellement occupée. À l'extrême gauche, la partie verte occupe la droite, et la partie verte à droite devient un espace inscriptible continu, qui peut accueillir de nouvelles données. La figure suivante explique ce processus.

                                    

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;
    }
}

Bien sûr, si la partie rouge occupe trop et que la partie inscriptible ne suffit pas, cela déclenchera l'expansion du tampon. Ici, je termine l'expansion du tampon en appelant la fonction realloc.

                                    

Traitement du flux d'octets TCP

  • Recevoir des données

La réception des données par le socket est effectuée par handle_read dans tcp_connection.c. Dans cette fonction, le flux de données du socket est reçu en appelant la fonction buffer_socket_read et mis en mémoire tampon dans l'objet tampon. Après cela, vous pouvez voir que nous transmettons l'objet tampon et l'objet tcp_connection à la fonction de traitement réelle de l'application messageCallBack pour l'analyse des messages. L'exemple de cette partie sera développé dans l'analyse des paquets 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);
    }
}

Dans la fonction buffer_socket_read, appelez readv pour écrire des données dans deux tampons, l'un est l'objet buffer et l'autre est le additional_buffer ici. La raison en est que l'objet buffer ne peut pas accueillir le flux de données du socket, et il y a aussi aucun moyen de déclencher l'expansion de l'objet tampon. En utilisant des tampons supplémentaires, une fois qu'il est déterminé que les données lues à partir du socket dépassent la taille d'écriture maximale réelle de l'objet tampon, l'opération d'expansion de l'objet tampon peut être déclenchée, où la fonction buffer_append appellera la fonction make_room introduite précédemment dans Extension complète de l'objet tampon.

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;
}
  • envoyer des données

Lorsque l'application a besoin d'envoyer des données au socket, c'est-à-dire une fois le processus de lecture-décodage-calcul-encodage terminé, les données après le codage sont écrites dans l'objet tampon et tcp_connection_send_buffer est appelé pour tamponner les données dans le tampon via la zone de prise envoyée.

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;
}

S'il s'avère que le canal actuel n'a pas enregistré l'événement WRITE et qu'il n'y a pas de données à envoyer dans le tampon d'envoi correspondant à la tcp_connection actuelle, appelez directement la fonction d'écriture pour envoyer les données. Si l'envoi n'est pas terminé cette fois, copiez les données restantes à envoyer dans le tampon d'envoi correspondant au tcp_connection actuel, et enregistrez l'événement WRITE dans event_loop. De cette manière, les données sont reprises par le framework et l'application libère cette partie des données.

//应用层调用入口
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;
}

Implémentation du protocole HTTP

Pour cela, nous avons d'abord défini une structure http_server. Ce http_server est essentiellement un TCPServer, mais la fonction de rappel exposée à l'application est plus simple. Il suffit de voir les structures http_request et http_response.

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

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

Dans http_server, le point clé est de terminer l'analyse du message et de convertir le message analysé en un objet http_request, via la fonction de rappel http_onMessage. Dans la fonction http_onMessage, parse_http_request est appelée pour terminer l'analyse du message.

// 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 utilise le retour chariot et le saut de ligne comme limite du protocole de message HTTP:

                 

L'idée de parse_http_request est de trouver la limite du message et d'enregistrer l'état actuel du travail d'analyse. Selon la séquence du travail d'analyse, le travail d'analyse des messages est divisé en quatre étapes: REQUEST_STATUS, REQUEST_HEADERS, REQUEST_BODY et REQUEST_DONE, et la méthode d'analyse à chaque étape est différente.

Lors de l'analyse de la ligne d'état, définissez d'abord la ligne d'état en localisant la position du retour chariot CRLF et du saut de ligne. Lors de la saisie de l'analyse de la ligne d'état, recherchez à nouveau le caractère espace comme limite de séparation.

Lors de l'analyse des paramètres d'en-tête, il faut également d'abord définir un ensemble de paires clé-valeur en localisant la position du retour chariot CRLF et du saut de ligne, puis rechercher le caractère deux-points comme limite de séparation.

Enfin, si le caractère deux-points n'est pas trouvé, le travail d'analyse de l'en-tête est terminé.

La fonction parse_http_request complète les quatre étapes de l'analyse des messages 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;
}

Après avoir traité toutes les données de la demande, le travail d'encodage et d'envoi est effectué ensuite. Pour cela, un objet http_response est créé et la fonction de codage requestCallback fournie par l'application est appelée. Ensuite, un objet buffer est créé. La fonction http_response_encode_buffer est utilisée pour convertir les données de http_response dans le flux d'octets correspondant selon le protocole HTTP .

Comme vous pouvez le voir, http_response_encode_buffer définit l'en-tête http_response tel que Content-Length, ainsi que les données de la partie du corps 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);
}

Exemple de serveur HTTP complet

Maintenant, écrire un exemple de serveur HTTP devient très simple. Dans cet exemple, la partie la plus importante est la fonction de rappel onRequest. Ici, la méthode onRequest a été après parse_http_request, et peut être calculée et traitée en fonction de différentes informations http_request. La logique de l'exemple de programme est très simple: selon le chemin de l'URL de la requête http, différents types http_response sont renvoyés. Par exemple, lorsque la requête est le répertoire racine, les formats 200 et HTML sont renvoyés.

#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);
}

Après avoir exécuté ce programme, nous pouvons y accéder via le navigateur et la commande curl. Vous pouvez ouvrir plusieurs navigateurs et commandes curl en même temps, ce qui prouve également que notre programme peut répondre à des exigences élevées de concurrence.

$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>%

                        

Dans cette conférence, nous avons principalement parlé des capacités de traitement de flux d'octets de l'ensemble du cadre de programmation, introduit l'objet tampon, et sur cette base, en ajoutant des fonctionnalités HTTP, notamment http_server, http_request, http_response, terminé la préparation de la haute performance HTTP serveur. L'exemple de programme utilise les capacités fournies par le framework pour écrire un programme serveur HTTP simple.

 

Apprenez le nouveau en revoyant le passé!

 

Je suppose que tu aimes

Origine blog.csdn.net/qq_24436765/article/details/105049360
conseillé
Classement