How to implement a high-concurrency http server from 0 to 1

1. Introduction to http server

        We know that the browser is a client of http(s). The purpose is to connect to the remote http server , and then the server returns the browser data. The browser receives the data and displays it after parsing the data.

        The external performance we see is that the browser accesses a URL and then gets the corresponding web page. During this period, the browser and http server transmit data through the http protocol. The transport layer is the tcp protocol because it is a connected and reliable protocol.

2. Tools needed

If you want to do your job well, you must first sharpen your tools! ! !

1. A Linux virtual machine (or cloud server)

Here I use Alibaba Cloud lightweight application server. If possible, you can also buy a domain name.

2. A C language editor that can be used proficiently

2.1 Visual Studio 2022

After downloading and installing, open the Visual Studio Installer to install the plug-in for Linux.

After installation, create the project. 

Select "Tools"---->Select "Options"-------->Select "Cross-Platform"----->Select "Add", and then enter:

  • Hostname: the public IP address of the server
  • Username: root
  • Password: the password you set yourself

 Right-click the project, select "Properties" -----> select "C/C++", and set the c or c++ compiler.

Use gcc for c and g++ for c++. If the compilation process finds that it cannot be found, you can use a directory instead. For example, my c compiler uses directories.

 Click OK, and we can copy the local code to a projects folder under the Linux server (in the root directory)

 2.2 Clion

The configuration of Clion is more complicated (CMake, gdb, etc. need to be installed on the server), but I think it is relatively easy to use.

The one on the right is the internal file of the server, the bottom is the server remote terminal, and the top is the file opened directly from the server.

 3. Implement http server

1. http protocol

        Before typing the code, we should have a general understanding of the working process of the http server .

1.1 Client connects to web server 

        The browser establishes a TCP socket connection with the Web server's HTTP port (default is 80). For example, http://www.raying.top

1.2Send  HTTP request

        Through the TCP socket, the client sends a text request message to the Web server.

1.3 The server accepts the request and returns an HTTP response

        The web server parses the request and locates the requested resource. The server writes a copy of the resource to the TCP socket, which is read by the client.

1.4 Release the TCP connection

        If the connection mode is close, the server actively closes the TCP connection, and the client passively closes the connection and releases the TCP connection; if the connection mode is keepalive, the connection will be maintained for a period of time, during which time it can continue to receive requests.

1.5 Client browser parses HTML content

        The client browser first parses the status line for a status code indicating whether the request was successful. Then each response header is parsed, and the response header tells the following HTML document of several bytes and the character set of the document. The client browser reads the response data HTML, formats it according to the syntax of HTML, and displays it in the browser window.

2. Requests and Responses

        Open the browser and analyze the packet capture to get a general understanding of the request headers and response headers.

2.1 Request header

2.2 Response header 

 3. Implement the code

3.1 Receive http request

3.1.1 Here you can read the request header line by line and read the client data character by character:

  • Parameters: sock socket buf buffer size sizeof(buf)
  • Return value: =-1 Reading error =0 Read a blank line >0 Read a line successfully

3.1.2 The read() function  here  is in the header file <unistd.h>, which is equivalent to the recv() function in <windows.h>.

//返回值: -1 表示读取出错, 等于0表示读到一个空行, 大于0 表示成功读取一行
int get_line(int sock, char *buf, int size) {
    int count = 0; // 已经读到的字符数
    char ch = '\0'; // 读到的字符
    int len = 0; // 已经读到的的长度
    while ((count < size - 1) && ch != '\n') {
        len = read(sock, &ch, 1); // 读取客户端发送的数据,1个字符一个字符读
        if (len == 1) { // 成功读到一个字符
            if (ch == '\r') { // 回车符号
                continue;
            } else if (ch == '\n') { // 换行符
                break;
            }
            buf[count] = ch; // 处理正常的字符,非回车换行符
            count++;
        } else if (len == -1) {    //读取出错
            perror("read failed");
            count = -1;     // 返回-1表示读取出错
            break;
        } else { // read 返回0,客户端关闭sock 连接.
            fprintf(stderr, "client close.\n");
            count = -1;
            break;
        }
    }
    if (count >= 0) buf[count] = '\0';
    return count;
}

3.2 Parse the request

After getting the http request, we can parse it according to the http protocol. The figure below is the idea diagram when parsing the request. You can use this idea to design the code.

void *do_http_request(void *pclient_sock) {
    int len = 0;
    char buf[256];
    char method[64];
    char url[256];
    char path[256];
    int client_sock = *(int *) pclient_sock;

    struct stat st;

    /*读取客户端发送的http 请求*/
    //1.读取请求行
    len = get_line(client_sock, buf, sizeof(buf));

    if (len > 0) {//读到了请求行
        int i = 0, j = 0;
        while (!isspace(buf[j]) && (i < sizeof(method) - 1)) {
            method[i] = buf[j];
            i++;
            j++;
        }
        method[i] = '\0';
        if (debug) printf("request method: %s\n", method);

        if (strncasecmp(method, "GET", i) == 0) { //只处理get请求
            if (debug) printf("method = GET\n");

            //获取url
            while (isspace(buf[j++]));//跳过白空格
            i = 0;

            while (!isspace(buf[j]) && (i < sizeof(url) - 1)) {
                url[i] = buf[j];
                i++;
                j++;
            }
            url[i] = '\0';
            if (debug) printf("url: %s\n", url);

            //继续读取http 头部
            do {
                len = get_line(client_sock, buf, sizeof(buf));
                if (debug) printf("read: %s\n", buf);

            } while (len > 0);

            //***定位服务器本地的html文件***
            //处理url 中的?
            {
                char *pos = strchr(url, '?');    // 查找字符串中有无?
                if (pos) {
                    *pos = '\0';
                    printf("real url: %s\n", url);
                }
            }
//            sprintf(path, "./html_docs/%s", url);
            sprintf(path, "./resource/%s", url);

            if (debug) printf("path: %s\n", path);

            //执行http 响应
            //判断文件是否存在,如果存在就响应200 OK,同时发送相应的html 文件,如果不存在,就响应 404 NOT FOUND.
            if (stat(path, &st) == -1) {//文件不存在或是出错
                fprintf(stderr, "stat %s failed. reason: %s\n", path, strerror(errno));
                not_found(client_sock);
            } else {//文件存在
                if (S_ISDIR(st.st_mode)) {    // 判断路径是不是目录
                    strcat(path, "/index.html");     // 追加字符串index.html到结尾
                }
                do_http_response(client_sock, path);
            }
        } else {//非get请求, 读取http 头部,并响应客户端 501 	Method Not Implemented
            fprintf(stderr, "warning! other request [%s]\n", method);
            do {
                len = get_line(client_sock, buf, sizeof(buf));
                if (debug) printf("read: %s\n", buf);

            } while (len > 0);
            unimplemented(client_sock);   //请求未实现
        }
    } else {//请求格式有问题,出错处理
        bad_request(client_sock);   //在响应时再实现
    }
    close(client_sock);
    if (pclient_sock) free(pclient_sock);//释放动态分配的内存
    return NULL;
}

It is worth noting that the usage of multi-threading  is added here , which will be explained in detail later!

3.3 Respond to http requests

After we let the server get the parsed http request, it can respond to the browser.

The following figure is an idea for responding to http requests:

void do_http_response(int client_sock, const char *path) {
    int ret = 0;
    FILE *resource = NULL;
    resource = fopen(path, "r");

    if (resource == NULL) {
        not_found(client_sock);
        return;
    }

    //1.发送http 头部
    ret = headers(client_sock, resource);

    //2.发送http body .
    if (!ret) {
        cat(client_sock, resource);
    }

    fclose(resource);
}

3.4 Send http header

3.4.1 When sending the http header, two parameters need to be passed in:

  • client_sock client socket
  • resource resource file (get the error code of the file transfer)

3.4.2 A stat() function is used here to return the status information of the file. Three header files need to be called.   

        #include <sys/types.h>    #include <sys/stat.h>    #include <unistd.h>

        int stat(const char *path, struct stat *buf);

        parameter:

        path:

                    file path

        buf:

                    The pointer passed in to save the file status is used to save the status of the file.

        return value:

                    Returns 0 on success, -1 on failure, and sets errno

 3.4.3 Append server information to buf through the strcat() function

3.4.4 Pass buf information to client socket

/****************************
 *返回关于响应文件信息的http 头部
 *输入:
 *     client_sock - 客服端socket 句柄
 *     resource    - 文件的句柄
 *返回值: 成功返回0 ,失败返回-1
******************************/
int headers(int client_sock, FILE *resource) {
    struct stat st;
    int fileid = 0; //文件传输错误代码
    char tmp[64];
    char buf[1024] = {0};

    strcpy(buf, "HTTP/1.0 200 OK\r\n");  //将src指针指向的字符串复制(替换)到buf指向的数组中
    strcat(buf, "Server: Ray Server\r\n");  //将src指针指向的字符串添加到dst指针指向的字符串后面
    strcat(buf, "Content-Type: text/html\r\n");
    strcat(buf, "Connection: Close\r\n");

    fileid = fileno(resource);

    if (fstat(fileid, &st) == -1) {     // 服务器内部出错了
        inner_error(client_sock);
        return -1;
    }

    snprintf(tmp, 64, "Content-Length: %ld\r\n\r\n", st.st_size);
    strcat(buf, tmp);

    if (debug) fprintf(stdout, "header: %s\n", buf);

    // 将文件内容发送给客户端socket,0是一个flag
    if (send(client_sock, buf, strlen(buf), 0) < 0) {
        fprintf(stderr, "send failed. data: %s, reason: %s\n", buf, strerror(errno));
        return -1;
    }
    return 0;
}

3.5 Send the specified html file

 In addition to passing in and sending the parameters passed in the http header, the following three functions also need to be used:

  • fgets(): Read the html characters into buf.
  • feof(): Check whether the end of the file has been read.
  • write(): Usage is similar to read(). Send the read file to the client.
/****************************
 *说明:实现将html文件的内容按行
        读取并送给客户端
 ****************************/
void cat(int client_sock, FILE *resource) {
    char buf[1024];

    // 先读取一行并保存
    // 从 resource 流中读取 size 个字符存储到字符指针变量 buf 所指向的内存空间
    fgets(buf, sizeof(buf), resource);

    // feof()是检测流上的文件结束符的函数,如果文件结束,则返回非0值,否则返回0
    while (!feof(resource)) {
        int len = write(client_sock, buf, strlen(buf));

        if (len < 0) {//发送body 的过程中出现问题,怎么办?1.重试? 2.break
            fprintf(stderr, "send body error. reason: %s\n", strerror(errno));
            break;
        }
        if (debug) fprintf(stdout, "%s", buf);
        fgets(buf, sizeof(buf), resource);
    }
}

3.6 Error handling

In order to enhance the robustness of the code, we must error handle some easily generated errors.

In order to make it easier to see the effect, no special error page is written. Send the html code directly to the client.

 3.6.1 500 (Server Internal Error) The server encountered an error and could not complete the request.

void unimplemented(int client_sock) {
    const char *reply = "HTTP/1.0 501 Method Not Implemented\r\n\
Content-Type: text/html\r\n\
\r\n\
<HTML>\r\n\
<HEAD>\r\n\
<TITLE>Method Not Implemented</TITLE>\r\n\
</HEAD>\r\n\
<BODY>\r\n\
    <P>HTTP request method not supported.\r\n\
</BODY>\r\n\
</HTML>";

    int len = write(client_sock, reply, strlen(reply));
    if (debug) fprintf(stdout, reply);

    if (len <= 0) {
        fprintf(stderr, "send reply failed. reason: %s\n", strerror(errno));
    }
}

3.6.2 400 (Bad Request) The server does not understand the syntax of the request.

void bad_request(client_sock) {
    const char *reply = "HTTP/1.0 400 BAD REQUEST\r\n\
Content-Type: text/html\r\n\
\r\n\
<HTML>\r\n\
<HEAD>\r\n\
<TITLE>BAD REQUEST</TITLE>\r\n\
</HEAD>\r\n\
<BODY>\r\n\
    <P>Your browser sent a bad request!\r\n\
</BODY>\r\n\
</HTML>";

    int len = write(client_sock, reply, strlen(reply));
    if (len <= 0) {
        fprintf(stderr, "send reply failed. reason: %s\n", strerror(errno));
    }
}

3.6.3 Server internal error

void inner_error(int client_sock) {
    const char *reply = "HTTP/1.0 500 Internal Sever Error\r\n\
Content-Type: text/html\r\n\
\r\n\
<HTML lang=\"zh-CN\">\r\n\
<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\r\n\
<HEAD>\r\n\
<TITLE>Inner Error</TITLE>\r\n\
</HEAD>\r\n\
<BODY>\r\n\
    <P>服务器内部出错.\r\n\
</BODY>\r\n\
</HTML>";

    int len = write(client_sock, reply, strlen(reply));
    if (debug) fprintf(stdout, reply);

    if (len <= 0) {
        fprintf(stderr, "send reply failed. reason: %s\n", strerror(errno));
    }
}

3.6.4 404 (Not Found) The server cannot find the requested web page.

void not_found(int client_sock) {
    const char *reply = "HTTP/1.0 404 NOT FOUND\r\n\
Content-Type: text/html\r\n\
\r\n\
<HTML lang=\"zh-CN\">\r\n\
<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\r\n\
<HEAD>\r\n\
<TITLE>NOT FOUND</TITLE>\r\n\
</HEAD>\r\n\
<BODY>\r\n\
	<P>文件不存在!\r\n\
    <P>The server could not fulfill your request because the resource specified is unavailable or nonexistent.\r\n\
</BODY>\r\n\
</HTML>";

    int len = write(client_sock, reply, strlen(reply));
    if (debug) fprintf(stdout, reply);

    if (len <= 0) {
        fprintf(stderr, "send reply failed. reason: %s\n", strerror(errno));
    }
}

 4. Test

1. Write the main function

int main(void) {
    int sock;
    struct sockaddr_in server_addr;
    sock = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;    //选择协议IPV4
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//监听本地所有IP地址
    server_addr.sin_port = htons(SERVER_PORT);//绑定端口号
    bind(sock, (struct sockaddr *) &server_addr, sizeof(server_addr));
    listen(sock, 128);
    printf("等待客户端的连接\n");

    int done = 1;
    while (done) {
        struct sockaddr_in client;
        int client_sock, len, i;
        char client_ip[64];
        char buf[256];
        pthread_t id;
        int *pclient_sock = NULL;
        socklen_t client_addr_len;
        client_addr_len = sizeof(client);
        client_sock = accept(sock, (struct sockaddr *) &client, &client_addr_len);
        //打印客户端IP地址和端口号
        printf("client ip: %s\t port : %d\n",
               inet_ntop(AF_INET, &client.sin_addr.s_addr, client_ip, sizeof(client_ip)),
               ntohs(client.sin_port));

        /*处理http 请求,读取客户端发送的数据*/

        //启动线程处理http 请求
        pclient_sock = (int *) malloc(sizeof(int));
        *pclient_sock = client_sock;

        // 多线程
        pthread_create(&id, NULL, do_http_request, (void *) pclient_sock);
    }
    close(sock);
    return 0;
}

2. Start the server

3. Enter the domain name (or public IP) and add the html file to test

 It can be found that the server also prints a lot of client-related information.

 5. Project source code

OracleRay/MiniHttpServer (github.com)https://github.com/OracleRay/MiniHttpServer

Guess you like

Origin blog.csdn.net/weixin_51418964/article/details/124282294