如何从0到1实现一个高并发http服务器

一、http服务器介绍

        我们知道浏览器是http(s)的客户端,目的是连接远程的http服务器,然后服务器返回浏览器数据。浏览器接收数据解析数据之后展现出来。

        我们看到的外在表现就是,浏览器访问一个url,然后就得到相应的web页面。在此期间,浏览器与http服务器是通过http协议传输数据。传输层是tcp协议,因为他是有连接,可靠的协议。

二、需要用到的工具

工欲善其事必先利其器!!!

1. 一台Linux虚拟机(或者云服务器)

这里我使用了阿里云轻量化应用服务器。有条件还可以买个域名。

2. 一个可以熟练使用的C语言编辑器

2.1 Visual Studio 2022

下载安装好以后,打开Visual Studio Installer安装关于Linux的插件。

安装好以后,创建项目。 

选择《工具》---->选择《选项》-------->选择《跨平台》----->选择《添加》 ,然后输入:

  • 主机名:服务器的公网ip地址
  • 用户名:root
  • 密码:自己设置的密码

 右键项目,选择《属性》----->选择《C/C++》,设置c或者c++的编译器。

c用gcc,c++用g++,若编译过程发现找不到,可以用目录代替。比如我的c编译器就使用了目录。

 点击确定,我们就可以将本地代码复制到Linux服务器下的一个projects的文件夹下(在根目录)

 2.2 Clion

Clion的配置较为复杂(需要在服务器上安装CMake,gdb等等),但我认为却是相对好用的一个。

右边的是服务器内部文件,下边是服务器远程终端,上面是直接从服务器里打开的文件。

 三、实现http服务器

1. http协议

        在敲代码前,我们应该大致了解一下http服务器的工作过程。

1.1 客户端连接到Web服务器

        浏览器与Web服务器的HTTP端口(默认为80)建立一个TCP套接字连接。例如,http://www.raying.top

1.2 发送HTTP请求

        通过TCP套接字,客户端向Web服务器发送一个文本的请求报文。

1.3服务器接受请求并返回HTTP响应

        Web服务器解析请求,定位请求资源。服务器将资源复本写到TCP套接字,由客户端读取。

1.4 释放连接TCP连接

        若connection 模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接;若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求。

1.5 客户端浏览器解析HTML内容

        客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的HTML文档和文档的字符集。客户端浏览器读取响应数据HTML,根据HTML的语法对其进行格式化,并在浏览器窗口中显示。

2. 请求和响应

        打开浏览器通过抓包分析,即可对请求头和响应头有大致了解。

2.1 请求头

2.2 响应头 

 3. 实现代码

3.1 接收http请求

3.1.1 这里可以按行读取请求头部,一个字符一个字符的读取客户端数据:

  • 参数:sock 套接字   buf 缓冲区    size sizeof(buf)
  • 返回值:=-1 读取出错    =0  读到一个空行    >0  成功读取一行

3.1.2 这里的 read() 函数 是头文件<unistd.h>里面的,相当于<windows.h> 力的recv() 函数。

//返回值: -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 解析请求

获取到http请求后,我们就可以对其按照http协议规定的那样进行解析。下图是解析请求时候的思路图,可以通过这个思路去设计代码。

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

 值得注意的是,这里加入了多线程的用法,后文详细讲解!

3.3 响应http请求

在我们放服务器拿到解析后的http请求后,就可以向浏览器做出响应了。

下图是响应http请求的一个思路:

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 发送http头部

3.4.1  在发送http头部时候,需要传入两个参数:

  • client_sock   客户端套接字
  • resource  资源文件(获得文件传输的错误代码)

3.4.2 这里要用到一个 stat() 函数 用来返回文件的状态信息,需要调用3个头文件   

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

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

        参数:

        path:

                    文件的路径

        buf:

                    传入的保存文件状态的指针,用于保存文件的状态

        返回值:

                    成功返回0,失败返回-1,设置errno

3.4.3 通过 strcat() 函数 将服务器信息追加到buf里面

3.4.4 将buf信息传递给客户端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 发送指定的html文件

 除了传入和发送http头部传入的参数外,还需要用到以下三个函数:

  • fgets():把html中按字符读取到buf中。
  • feof():检测是否读到了文件的末尾。
  • write():用法和read()类似。将读到的文件发送到客户端。
/****************************
 *说明:实现将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 出错处理

为了增强代码的健壮性,我们必须对一些容易产生的错误进行错误处理。

这里为了方便看到效果,没有编写专门的错误页面。直接将html代码发送给客户端。

 3.6.1 500(服务器内部错误) 服务器遇到错误,无法完成请求。

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(错误请求) 服务器不理解请求的语法。

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  服务器内部出错

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(未找到) 服务器找不到请求的网页。

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

 四、测试

1. 编写主函数

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. 启动服务器

3. 输入域名(或公网ip)加 html 文件测试

 可以发现,服务器也打印除了很多客户端的相关信息。

 五、 项目源码

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

猜你喜欢

转载自blog.csdn.net/weixin_51418964/article/details/124282294