Linux 네트워크 프로그래밍 : 고유 한 고성능 HTTP 서버 프레임 워크 작성 (3)

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

버퍼 객체

이름에서 알 수 있듯이 Buffer는 소켓에서받은 데이터와 소켓으로 보내야하는 데이터를 캐시하는 버퍼 개체입니다.

소켓에서 데이터가 수신되면 이벤트 처리 콜백 함수는 지속적으로 버퍼 객체에 데이터를 추가하고 동시에 응용 프로그램은 버퍼 객체의 데이터를 지속적으로 처리해야 버퍼 객체가 새 데이터를 비울 수 있습니다. 위치 더 많은 데이터를 수용하기 위해.

소켓으로 데이터를 보내면 응용 프로그램은 계속해서 버퍼 객체에 데이터를 추가합니다. 동시에 이벤트 처리 콜백 함수는 소켓의 send 함수를 계속 호출하여 데이터를 전송하여 버퍼에있는 쓰기 데이터를 줄입니다. 목적.

버퍼 객체는 입력 버퍼와 출력 버퍼 방향에서 동시에 사용할 수 있지만 두 경우에 쓰거나 읽는 객체가 다르다는 것을 알 수 있습니다.

다음은 버퍼 개체의 디자인을 보여줍니다.

                       

//数据缓冲区
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를 호출하여 두 개의 버퍼에 데이터를 기록합니다. 하나는 버퍼 객체이고 다른 하나는 여기서 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 메시지 구문 분석의 네 단계를 완료합니다.

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