Linuxネットワークプログラミング:独自の高性能HTTPサーバーフレームワークを作成する(3)

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

バッファオブジェクト

バッファは、その名前が示すように、ソケットから受信したデータとソケットに送信する必要のあるデータをキャッシュするバッファオブジェクトです。

ソケットからデータを受信した場合、イベント処理コールバック関数は常にデータをバッファオブジェクトに追加します。同時に、アプリケーションはバッファオブジェクト内のデータを継続的に処理して、バッファオブジェクトが新しいデータを空にすることができるようにする必要があります。位置より多くのデータに対応するため。

ソケットに送信されるデータの場合、アプリケーションは継続的にデータをバッファオブジェクトに追加します。同時に、イベント処理コールバック関数は継続的にソケットの送信関数を呼び出してデータを送信し、バッファへの書き込みデータを削減します。オブジェクト。

バッファオブジェクトは、入力バッファと出力バッファの両方の方向で同時に使用できることがわかりますが、2つのケースでは、書き込まれるオブジェクトと読み取られるオブジェクトが異なります。

以下に、バッファオブジェクトの設計を示します。

                       

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

バッファオブジェクトのwriteIndexは、書き込むことができる現在の位置を識別します。readIndexは、読み取ることができるデータの現在の位置を識別します。readIndexからwriteIndexまでの図の赤い部分は、データを読み取る必要がある部分であり、緑の部分は、データを読み取る必要がある部分です。一部はwriteIndexからキャッシュへです。最後は書き込み可能な部分です。

時間が経つにつれて、readIndexとwriteIndexがバッファの終わりに近づくと、前部のfront_space_size領域が非常に大きくなり、この領域のデータはすでに古いデータになっています。このとき、調整する必要があります。全体バッファオブジェクトの構造が赤い部分を左に移動すると同時に、緑の部分も左に移動し、バッファ全体の書き込み可能な部分が増加します。

make_room関数がこの役割を果たします。右側の緑色の連続スペースでは新しいデータを収容できず、左側の灰色の部分と右側の緑色の部分で新しいデータを収容できる場合、このようなモバイルコピーがトリガーされます。 、そして最終的には赤の部分が占有されます。左端の緑の部分が右を占め、右の緑の部分が連続書き込み可能スペースになり、新しいデータを収容できます。次の図は、このプロセスを説明しています。

                                    

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

もちろん、赤い部分が多すぎて書き込み可能な部分が足りない場合は、バッファの拡張がトリガーされます。ここでは、realloc関数を呼び出してバッファーの拡張を完了します。

                                    

TCPバイトストリーム処理

  • データを受信する

データを受信するソケットは、tcp_connection.cのhandle_readによって実行されます。この関数では、ソケットからのデータストリームは、buffer_socket_read関数を呼び出すことによって受信され、バッファオブジェクトにバッファリングされます。その後、メッセージ分析のために、バッファオブジェクトとtcp_connectionオブジェクトをアプリケーションの実際の処理関数messageCallBackに渡すことがわかります。この部分のサンプルは、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);
    }
}

buffer_socket_read関数で、readvを呼び出して2つのバッファーにデータを書き込みます。1つはバッファーオブジェクトで、もう1つはadditional_bufferです。これは、バッファーオブジェクトがソケットからのデータストリームに対応できないためです。バッファオブジェクトの拡張をトリガーする方法はありません。追加のバッファーを使用することにより、ソケットから読み取られたデータがバッファーオブジェクトの実際の最大書き込み可能サイズを超えていると判断されると、バッファーオブジェクトの拡張操作をトリガーできます。ここで、buffer_append関数は、前に紹介したmake_room関数を呼び出します。バッファオブジェクトの完全な拡張。

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;
}
  • データを送る

アプリケーションがデータをソケットに送信する必要がある場合、つまり、read-decode-compute-encodeプロセスが完了した後、エンコード後のデータがバッファーオブジェクトに書き込まれ、tcp_connection_send_bufferが呼び出されてバッファー内のデータがバッファーに入れられます。送信されたソケットエリア。

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

現在のチャネルがWRITEイベントを登録しておらず、現在のtcp_connectionに対応する送信バッファに送信するデータがない場合は、write関数を直接呼び出してデータを送信します。今回送信が完了しない場合は、送信する残りのデータを現在のtcp_connectionに対応する送信バッファにコピーし、WRITEイベントをevent_loopに登録します。このようにして、データはフレームワークによって引き継がれ、アプリケーションはデータのこの部分を解放します。

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

HTTPプロトコルの実装

この目的のために、最初にhttp_server構造を定義しました。このhttp_serverは本質的にTCPServerですが、アプリケーションに公開されるコールバック関数はより単純です。必要なのはhttp_request構造とhttp_response構造だけです。

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

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

http_serverでは、重要なポイントは、メッセージの分析を完了し、解析されたメッセージをhttp_requestオブジェクトに変換することです。これは、http_onMessageコールバック関数を介して行われます。http_onMessage関数では、メッセージの解析を完了するためにparse_http_requestが呼び出されます。

// 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は、HTTPメッセージプロトコルの境界としてキャリッジリターンとラインフィードを使用します。

                 

parse_http_requestのアイデアは、メッセージの境界を見つけて、解析作業の現在の状態を記録することです。メッセージ分析作業は、分析作業の順序に応じて、REQUEST_STATUS、REQUEST_HEADERS、REQUEST_BODY、REQUEST_DONEの4つの段階に分けられ、各段階での解析方法が異なります。

ステータスラインを解析するときは、最初にCRLFキャリッジリターンとラインフィードの位置を特定してステータスラインを定義します。ステータスライン解析に入るときは、分離境界としてスペース文字を再度見つけます。

ヘッダー設定を解析するときは、最初にCRLFキャリッジリターンとラインフィードの位置を特定してキーと値のペアのセットを定義し、次にコロン文字を分離境界として見つけます。

最後に、コロン文字が見つからない場合、ヘッダーの解析作業は完了です。

parse_http_request関数は、HTTPメッセージ解析の4つの段階を完了します。

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

すべてのリクエストデータを処理した後、次にエンコードと送信の作業が実行されます。このために、http_responseオブジェクトが作成され、アプリケーションによって提供されるエンコーディング関数requestCallbackが呼び出されます。次に、バッファオブジェクトが作成されます。関数http_response_encode_bufferを使用して、HTTPプロトコルに従ってhttp_responseのデータを対応するバイトストリームに変換します。 。

ご覧のとおり、http_response_encode_bufferは、Content-Lengthなどのhttp_responseヘッダーと、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);
}

完全なHTTPサーバーの例

これで、HTTPサーバーの例を書くのが非常に簡単になります。この例では、最も重要な部分はonRequestコールバック関数です。ここで、onRequestメソッドはparse_http_requestの後にあり、さまざまなhttp_request情報に従って計算および処理できます。サンプルプログラムのロジックは非常に単純です。httpリクエストのURLパスに応じて、さまざまなhttp_responseタイプが返されます。たとえば、リクエストがルートディレクトリの場合、200および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);
}

このプログラムを実行した後、ブラウザとcurlコマンドを使用してプログラムにアクセスできます。複数のブラウザとcurlコマンドを同時に開くことができます。これは、プログラムが高い同時実行性の要件を満たすことができることも証明しています。

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

                        

この講義では、主にプログラミングフレームワーク全体のバイトストリーム処理機能について説明し、バッファオブジェクトを紹介しました。これに基づいて、http_server、http_request、http_responseなどのHTTP機能を追加することで、HTTP高性能の準備を完了しました。サーバ。サンプルプログラムは、フレームワークによって提供される機能を使用して、単純なHTTPサーバープログラムを記述します。

 

過去を振り返って新しいことを学びましょう!

 

おすすめ

転載: blog.csdn.net/qq_24436765/article/details/105049360