Linux網絡編程:自己動手寫高性能HTTP服務器框架(三)

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

buffer對象

buffer,顧名思義,就是一個緩衝區對象,緩存了從套接字接收來的數據以及需要發往套接字的數據。

如果是從套接字接收來的數據,事件處理回調函數在不斷地往 buffer 對象增加數據,同時,應用程序需要不斷把 buffer 對象中的數據處理掉,這樣,buffer 對象纔可以空出新的位置容納更多的數據。

如果是發往套接字的數據,應用程序不斷地往 buffer 對象增加數據,同時,事件處理回調函數不斷調用套接字上的發送函數將數據發送出去,減少 buffer 對象中的寫入數據。

可見,buffer 對象是同時可以作爲輸入緩衝(input buffer)和輸出緩衝(output buffer)兩個方向使用的,只不過,在兩種情形下,寫入和讀出的對象是有區別的。

下面展示了 buffer 對象的設計:

                       

//數據緩衝區
struct buffer {
    char *data;          //實際緩衝
    int readIndex;       //緩衝讀取位置
    int writeIndex;      //緩衝寫入位置
    int total_size;      //總大小
};

buffer 對象中的 writeIndex 標識了當前可以寫入的位置;readIndex 標識了當前可以讀出的數據位置,圖中紅色部分從 readIndex 到 writeIndex 的區域是需要讀出數據的部分,而綠色部分從 writeIndex 到緩存的最尾端則是可以寫出的部分。

隨着時間的推移,當 readIndex 和 writeIndex 越來越靠近緩衝的尾端時,前面部分的 front_space_size 區域變得會很大,而這個區域的數據已經是舊數據,在這個時候,就需要調整一下整個 buffer 對象的結構,把紅色部分往左側移動,與此同時,綠色部分也會往左側移動,整個緩衝區的可寫部分就會變多了。

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 函數接收來自套接字的數據流,並將其緩衝到 buffer 對象中。之後你可以看到,我們將 buffer 對象和 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 往兩個緩衝區寫入數據,一個是 buffer 對象,另外一個是這裏的 additional_buffer,之所以這樣做,是擔心 buffer 對象沒辦法容納下來自套接字的數據流,而且也沒有辦法觸發 buffer 對象的擴容操作。通過使用額外的緩衝,一旦判斷出從套接字讀取的數據超過了 buffer 對象裏的實際最大可寫大小,就可以觸發 buffer 對象的擴容操作,這裏 buffer_append 函數會調用前面介紹的 make_room 函數,完成 buffer 對象的擴容。

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 過程後,通過往 buffer 對象裏寫入 encode 以後的數據,調用 tcp_connection_send_buffer,將 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;
}

如果發現當前 channel 沒有註冊 WRITE 事件,並且當前 tcp_connection 對應的發送緩衝無數據需要發送,就直接調用 write 函數將數據發送出去。如果這一次發送不完,就將剩餘需要發送的數據拷貝到當前 tcp_connection 對應的發送緩衝區中,並向 event_loop 註冊 WRITE 事件。這樣數據就由框架接管,應用程序釋放這部分數據。

//應用層調用入口
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 四個階段,每個階段解析的方法各有不同。

在解析狀態行時,先通過定位 CRLF 回車換行符的位置來圈定狀態行,進入狀態行解析時,再次通過查找空格字符來作爲分隔邊界。

在解析頭部設置時,也是先通過定位 CRLF 回車換行符的位置來圈定一組 key-value 對,再通過查找冒號字符來作爲分隔邊界。

最後,如果沒有找到冒號字符,說明解析頭部的工作完成。

parse_http_request 函數完成了 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;
}

處理完了所有的 request 數據,接下來進行編碼和發送的工作。爲此,創建了一個 http_response 對象,並調用了應用程序提供的編碼函數 requestCallback,接下來,創建了一個 buffer 對象,函數 http_response_encode_buffer 用來將 http_response 中的數據,根據 HTTP 協議轉換爲對應的字節流。

可以看到,http_response_encode_buffer 設置瞭如 Content-Length 等 http_response 頭部,以及 http_response 的 body 部分數據。

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 callback 函數,這裏,onRequest 方法已經在 parse_http_request 之後,可以根據不同的 http_request 的信息,進行計算和處理。例子程序裏的邏輯非常簡單,根據 http request 的 URL path,返回了不同的 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>%

                        

這一講我們主要講述了整個編程框架的字節流處理能力,引入了 buffer 對象,並在此基礎上通過增加 HTTP 的特性,包括 http_server、http_request、http_response,完成了 HTTP 高性能服務器的編寫。實例程序利用框架提供的能力,編寫了一個簡單的 HTTP 服務器程序。

 

溫故而知新 !

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章