WebServer的这些那些

0 开始之前

别的不多说,计划从浅到深逐渐实现一个webserver,写个折腾的记录。

由于要使用到IO多路复用,因此这个程序是在Linux下折腾的,也试过在mac下编译,不过会有点报错,也懒得修改了。

0.1 大概的架构

首先是了解网络通信的常见手段都有什么,其中作为基础的网络套接字是什么,怎么使用网络套接字进行信息的跨节点传递。

了解信息传递的方式之后就可以用浏览器作为客户端,服务端向浏览器返回一个完整地HTTP报文,从而让浏览器显示出网页内容。

再进一步,利用Linux中的IO方法读取html文件,将它作为HTTP的数据段和HTTP标头结合返回给浏览器。

然后是分析浏览器传入的HTTP请求内容,拆分出对应的请求资源内容,将其和HTTP标头组合传回,让浏览器能够根据不同地址显示不同的网页。

精细打磨,根据方法传回的不同返回值判断程序的运行状态,并根据这些状态控制任务流,做到一些错误状态控制的功能。

最后就是结合IO多路复用的架构让这个单线程服务器能够同时响应多个HTTP请求

1 套接字——跨节点通信的中转站

能够实现进程间通信的方式有很多,但是在网络编程中最常用的应该还是网络套接字,因此我们就先从它开始:

image-20230919111621112

当然这个图中关于服务端其实并不是很正确,在关闭入站套接字之后服务端依旧在死循环中运行,会继续accept()传入的请求,如此往复,直到触发了某些特定操作跳出这个死循环之后进程结束。

1.1 我们先看服务端

通常来说,服务端中会有多个socket,其中监听套接字用来管理其他accept()到的套接字,就像是家里水管的总阀门。

  1. 通过socket()方法创建一个网络套接字,此方法返回的文件描述符即监听套接字的fd;

  2. 创建一个sockaddr_in结构体,这个结构体描述了套接字的一些属性:

    1
    2
    3
    4
    5
    6
    
    struct sockaddr_in {
        short            sin_family;   // 地址族
        unsigned short   sin_port;     // 端口
        struct in_addr   sin_addr;     // 地址
        char             sin_zero[8];  // zero this if you want to
    };
    
  3. sockaddr_in结构体进行赋值,确定这个套接字的一些属性;

  4. 通过bind()方法将套接字描述符和sockaddr_in结构体进行绑定;

  5. 使用listen()方法开始监听刚刚绑定的地址;

  6. 接下来一般是死循环:反复执行accept()方法接收传入套接字的数据,返回一个新的入站文件描述符;

  7. 当拿到传入的文件描述符之后对它进行操作:

    1. 通过recv()read()方法从入站文件描述符中获取其中的数据;
    2. 通过send()write()的方式向入站文件描述符中写入数据;
  8. 通过close()方式关闭入站的套接字;

  9. 通过close()关闭本机的监听套接字;

在这个生命周期中,出现了两个套接字监听套接字与本地设置的sockaddr_in绑定,对本机而言提供了一个对外的出口用以进行listen操作。另一个套接字则是在不断循环accept方法之后从外部获得的,本机可以使用send和`recv等方式对传入套接字进行读写操作。

1.2 然后是客户端

在客户端内,socket的生命周期与服务端大致相同,都需要socket()创建以及 close()关闭。较显著的区别就是不需要bind()listen()去监听本机的某个端口,以及不使用死循环进行accept(),而是使用connect()方法利用输入的sockaddr_in连接到服务端上已绑定套接字的端口上,返回客户端一个新的套接字。

这时候就可以通过sendrecv这两个方式操作套接字。

1.3 简单的代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>

int main(int argc, char** argv){
    struct sockaddr_in server_address;
    int listenfd, connfd;
		listenfd = socket(AF_INET, SOCK_STREAM, 0);
    memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(8000);
    bind(listenfd, (struct sockaddr*)&server_address, sizeof(server_address));
    listen(listenfd, 10);
    printf("Server running at http://127.0.0.1:8000/\n");
    char buff[4096];
    int buff_len;
    while(1){
        connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);
        
        buff_len = recv(connfd, buff, 4096, 0);
        buff[buff_len] = '\0';
        printf("recv msg from client: %s\n", buff);

        char send_back[4096]="Hello, I have received your message.\nYour message is: \n";
        strcat(send_back, buff);
        send(connfd, send_back, strlen(send_back), 0);
        close(connfd);
    }
    close(listenfd);
    return 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Client.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<netinet/in.h>
#include <arpa/inet.h>

#define MAXLINE 4096 // 定义最大接受长度

int main(int argc, char** argv){
    struct  sockaddr_in         servaddr;
    int     listenfd;
    char    recvline[MAXLINE],  sendline[MAXLINE];
    int     recvline_length, rc;

    // 判断参数是否正确
    if( argc != 2){
    printf("usage: ./client <ipaddress>\n");
    return 1;
    }

    // 创建socket
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    // 定义连接方式 这里使用IPV4 端口用6666
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8000);
    inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0;

    // 客户端向服务端发起连接
    connect(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0;

    // 输入要发送的消息
    printf("send msg to server: ");
    fgets(sendline, MAXLINE, stdin);

    // 发送消息
    send(listenfd, sendline, strlen(sendline), 0) < 0;
    // 接受消息
    recvline_length = recv(listenfd, recvline, MAXLINE, 0);
    recvline[recvline_length]='\0';
    printf("You recive:%s\n", recvline);

    rc=close(listenfd);
    return 1;
}

image-20230911103946244

不过这些代码有一些问题:

  1. 发送的数据段其实是定长的,在数据报文较小的情况下定长数据包影响了传输效率,当数据段过长的时候则可能会出现数据溢出的情况;
  2. 没有错误管理的功能;
  3. 只能简单地收发文字信息,并没有实现一个网页的功能;

但是万里长征第一步,起码我们实现了跨节点收发数据,剩下的事情就接下来再说:)

尾巴——当你在浏览器中输入127.0.0.1:8000之后,服务端会出现这么个内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
recv msg from client: GET / HTTP/1.1
Host: 127.0.0.1:8000
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7

这就是HTTP的GET请求,在之后的章节中我们会继续分析,就先卖一个关子吧。

2 一触即达——让你看到网页

上一节中我们做了一个服务端和对应的客户端,两者通过本地套接字通信。

这一节不妨再进一步,写一个服务端,让电脑上的浏览器作为客户端发送HTTP的GET请求,服务端响应请求并返回html信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Server1.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>

int main(int argc, char** argv){
    struct sockaddr_in server_address;
    int listenfd, connfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(8000);
    bind(listenfd, (struct sockaddr*)&server_address, sizeof(server_address));
    listen(listenfd, 10);
    printf("Server running at http://127.0.0.1:8000/\n");
    while(1){
        connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);

        char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n"
                        "<html><head><title>My Web Server</title></head>"
                        "<body><h1>Hello, world!</h1></body></html>";
        write(connfd, response, strlen(response));
        close(connfd);
    }
    close(listenfd);
    return 0;
}

这个和刚刚的server代码区别不大,在死循环外部完全一样,不同点可能就有以下几点:

  1. 不再读取客户端发送的信息,因此recv()相关内容彻底消失;
  2. 写回内容不一致,写回的方法也不一致;

当程序运行之后通过浏览器访问127.0.0.1:8000确实就能看到一个“HelloWorld”的页面了。

3 一切皆文件——如何加载一个静态网页文件

上一节通过代码实现了一个简单的服务端,能够在浏览器上打印出来"Hello World",但是有点奇怪:这个字符串只是一个包含了HTTP响应头和html内容的字符串,并不是真正加载了html静态页面的服务端。

这一节我们借助read和write方法,实现读取一个html文档,并写入到入站文件描述符中,从而实现一个“符合我的刻板印象的”服务端。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Server2.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <fcntl.h>
#define DATA_LEN 4096
int main(int argc, char** argv){
    struct sockaddr_in server_address;
    int listenfd, connfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(8000);
    bind(listenfd, (struct sockaddr*)&server_address, sizeof(server_address));
    listen(listenfd, 10);
    printf("Server running at http://127.0.0.1:8000/\n\n\n");
    char buff[DATA_LEN] = {0};
    while(1){
        connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);
        char response[DATA_LEN] = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
        printf("=====%s\n%d\n\n", response, strlen(response));
        int html_fd=open("./public/index.html", O_RDONLY);
        int html_len = read(html_fd, buff, DATA_LEN);
        printf("=====%s\n%d\n\n", buff, html_len);
        strcat(response, buff);
        printf("=====%s\n%d\n\n", response, strlen(response)-1);
        write(connfd, response, html_len+strlen(response)-1);
        close(connfd);
    }
    close(listenfd);
    return 0;
}

这里简单使用了linux下的io操作方法open()read(),然后借助strcat()方法将响应头和数据段结合,随后将它写回到入站套接字内。

当然,这个简单的服务端还是有些问题的:

  1. 不管入站请求是什么,返回的永远是HelloWorld,这并不符合我们的刻板印象;
  2. 一开始定义的字符串response是定长的,当html文件过大的时候strcat()方法就会导致数据的截断,导致数据的灭失;
  3. 缺乏错误管理;

在下一节,我们首先处理第一个问题。

4 有因必有果——根据请求进行选择

我们回到第一节的尾巴那里,我们拿到了一个“比较标准的”HTTP请求头:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
recv msg from client: GET / HTTP/1.1
Host: 127.0.0.1:8000
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7

其中GET / 就是说明浏览器想要访问主机下位于根目录的html信息。

当然,在前面几节里我们是不在乎它的请求头是什么的,一律扔了个helloworld。

这一节我们就试着去解析一下这个请求头,并根据请求头去返回不同的html页面。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Server3.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <fcntl.h>
#define DATA_LEN 4096
int main(int argc, char** argv){
    struct sockaddr_in server_address;
    int listenfd, connfd, htmlfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(8000);
    bind(listenfd, (struct sockaddr*)&server_address, sizeof(server_address));
    listen(listenfd, 10);
    printf("Server running at http://127.0.0.1:8000/\n");
    char buff[DATA_LEN] = {0};
    while(1){
        memset(buff, 0, DATA_LEN);
        connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);
        read(connfd, buff, DATA_LEN);
        char* method = strtok(buff, " ");
        char* path = strtok(NULL, " ");
        char lpath[DATA_LEN] = "./public";
        strcat(lpath, path);
        char response[DATA_LEN] = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
        int htmlfd=open(lpath, O_RDONLY);
        memset(buff, 0, DATA_LEN);
        int html_len = read(htmlfd, buff, DATA_LEN);
        strcat(response, buff);
        write(connfd, response, html_len+strlen(response)-1);
        close(connfd);
    }
    close(listenfd);
    return 0;
}

首先我们要创建一个名为pubilc的文件夹,在里面塞进index.html文件,这样子外部准备就做好了;

至于内部,我们使用strtok()方法将传入的请求进行分割,得到方法和地址。随后read对应路径的文件,通过strcat方法和HTTP标头组合后写回到监听套接字上,浏览器就能看到内容了。

5 打磨——流程管理和错误管理

现在其实就已经实现了一个简单的WebServer,但是在工程上,这个程序还是有很多问题需要处理的:

  1. 如果我发送的不是GET请求要怎么做?
  2. 如果发送的HTTP请求头不规范,strtok不到对应的请求类型要怎么做?
  3. buff和response的残留要怎么处理?
  4. 如果我请求了一个不存在的地址要怎么处理?
  5. 如果我请求的地址是正确的,但是打不开要怎么处理?
  6. 在一些特定情况下,上一次写入buff和response的数据长度大于这一次的,因此会污染这次的结果。
  7. 在某些情况下可能会出现的数据溢出问题。

问题有很多,慢慢修

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// Server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <fcntl.h>
#define DATA_LEN 4096

int main(int argc, char** argv){
    int rc;
    struct sockaddr_in server_address;
    int serverfd, inboundfd, htmlfd;
    char buff[DATA_LEN] = {0};
    char response[DATA_LEN] = {0};

    serverfd = socket(AF_INET, SOCK_STREAM, 0);
    if (serverfd < 0){
        perror("SOCKET ERROR");
    }
    memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(8000);

    rc = bind(serverfd, (struct sockaddr*)&server_address, sizeof(server_address));
    if (rc < 0){
        perror("BIND ERROR");
    }

    rc = listen(serverfd, 10);
    if (rc < 0){
        perror("LISTEN ERROR");
    }    
    printf("Server running at http://127.0.0.1:8000/\n");

    while(1){
        memset(buff, 0, DATA_LEN);
        memset(response, 0, DATA_LEN);

        inboundfd = accept(serverfd, (struct sockaddr*)NULL, NULL);
        if (inboundfd < 0){
            perror("ACCEPT ERROR");
        }

        rc = read(inboundfd, buff, DATA_LEN);
        if (rc < 0){
            perror("READ ERROR");
        }
        char* method = strtok(buff, " ");
        if  (!method){
            perror("EMPTY REQUEST");
        }
        else if (strcmp(method, "GET") != 0){
            perror("NOT A GET REQUEST");
            goto end2;
        }
        
        char* path = strtok(NULL, " ");

        if (!path){
            strcpy(response, "HTTP/1.1 403 FORBIDDEN\r\nContent-Type: text/html\r\n\r\n"
                                "<html><head><title>My Web Server</title></head>"
                                "<body><h1>403</h1></body></html>");
            perror("UNKNOWN PATH");
            goto end1;
        }
        else if (strstr(path, "..")){
            strcpy(response, "HTTP/1.1 403 FORBIDDEN\r\nContent-Type: text/html\r\n\r\n"
                                "<html><head><title>My Web Server</title></head>"
                                "<body><h1>403</h1></body></html>");
            perror("INVALID PATH");
            goto end1;
        }
        else{
            char lpath[DATA_LEN] = "./public";
            printf("====lpath:%s\n", lpath);
            printf("====path:%s\n", path);
            strcat(lpath, path);
            printf("====lpath:%s\n", lpath);
            htmlfd=open(lpath, O_RDONLY);
            if (htmlfd < 0){
                strcpy(response, "HTTP/1.1 404 NOT FOUND\r\nContent-Type: text/html\r\n\r\n"
                                "<html><head><title>My Web Server</title></head>"
                                "<body><h1>404</h1></body></html>");
                perror("OPEN ERROR");
                goto end1;
            }
            memset(buff, 0, DATA_LEN);
            rc = read(htmlfd, buff, DATA_LEN);
            if (rc < 0){
                perror("READ HTML ERROR");
            }
            rc = close(htmlfd);
            if(rc < 0){
                perror("CLOSE ERROR");
            }
            strcpy(response, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n");
            strcat(response, buff);
end1:
            rc = write(inboundfd, response, strlen(response));
            if (rc < 0){
                perror("WRITE BACK ERROR");
            }
end2:
            rc = close(inboundfd);
            if (rc < 0){
                perror("CLOSE ERROR");
            }
        }
    }
    close(serverfd);
    return rc;
}

这一节其实没什么技术细节,只是在很多场景下添加了错误检查和状态判断的内容。具体来说就是:

  1. 在返回值不正常的时候通过perror方法打印错误
  2. 在收到HTTP请求的时候会检测请求文件的路径,分为四种情况:
    1. 没有输入请求路径,返回403;
    2. 找不到,查无此人,就返回404 NOT FOUND;
    3. 试图用../方法访问上层文件,返回403 FORBIDDEN;
    4. 正常访问文件,返回200 OK;

大致的流程图如下:

server:client-server4

6 进化——单线程服务器的IO多路复用

虽然折腾了这么久,我们也实现了一个看上去比较正常的不管请求头是什么的永远会返回一个网页的网络服务端,但是这个服务端还有一个最致命的问题:

它只能同时响应一个请求,并发请求的时候直接歇菜。

其实也很合理,毕竟单线程服务器,一个线程绑定了一个文件描述符,在这种情况下服务端是不可能响应多个请求的,那么解决问题的方法也就很清晰了:

  1. 让程序多线程化,这里可以使用pthread处理;
  2. 使用IO多路复用技术让一个线程同时处理多个文件描述符;

在这一节里,我们就尝试使用IO多路复用技术中的EPOLL来实现单线程同时监控多个传入的套接字FD,实现并行访问。

6.1 epoll的一些接口

在介绍代码之前,首先介绍一下epoll相关的一些方法:

  1. epoll_create1():创建一个epoll实例,返回这个实例的文件描述符,通过对这个文件描述符进行操作实现事件的增删改查;

  2. epoll_ctl():它的函数原型是:

    int epoll_ctl(int __epfd, int __op, int __fd,struct epoll_event *__event)

    __epfd即epoll_create1()返回的事件集,我们可以往里面添加事件,删除或修改其中的事件;

    __op就是我们要执行的动作;

    __fd表示我们要对什么文件进行操作,也就是“把什么放进去拿出来”;

    *__event是个结构体,其中event.events表示要监视哪种类型的文件变动,data则作为一个“暂存区”,表示当发生变动的时候,我们需要查看什么数据;

  3. epoll_wait()从epollfd中扒拉出来发生变动的fd,保存在第二个参数中,同时返回发生变动的fd数量,通过遍历第二个参数这个数组就可以看到事件fd是什么了;

6.2 epoll的逻辑

按照上面的解释,epoll的结构其实也没有那么复杂,无非就几步:

  1. 创建epollfd
  2. 往epollfd中添加需要监控的fd,并设置当这个fd出现event.events指定的变动的时候会触发epoll_wait,同时可以返回的events数组中找到之前保存的数据。
  3. 通过epoll_wait阻塞式等待epoll实例中监控的fd变动,发生变动的实例会通过第二参数返回,变动的实例数量就是函数的返回值;

6.3 代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// Server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <sys/epoll.h>
#define DATA_LEN 4096
#define MAX_EVENTS 100

int main(int argc, char** argv){
    // 初始化相关变量
    int rc;
    struct sockaddr_in server_address;
    //serverfd肯定只有一个,入站fd肯定有很多,从入站地址打开的htmlfd只有一个,epollfd也只会有一个
    int serverfd, inboundfd[MAX_EVENTS], htmlfd, epollfd;
    int index;
    struct epoll_event event, events[MAX_EVENTS];
    char buff[DATA_LEN] = {0};
    char response[DATA_LEN] = {0};

    // 创建socket并检查结果
    serverfd = socket(AF_INET, SOCK_STREAM, 0);
    if (serverfd < 0){
        perror("SOCKET ERROR");
        exit (1);
    }
    // 本地地址信息
    memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(8000);

    // 绑定
    rc = bind(serverfd, (struct sockaddr*)&server_address, sizeof(server_address));
    
    // 创建epollfd并检查结果
    epollfd = epoll_create1(0);
    if (epollfd < 0){
        perror("EPOLL CREATE ERROR");
    }
    // 给event赋值
    event.events = EPOLLIN;
    event.data.fd = serverfd;
    // 将event插入到fd中保存
    epoll_ctl(epollfd, EPOLL_CTL_ADD, serverfd, &event);
    if (rc < 0){
        perror("BIND ERROR");
    }
    // 监听
    rc = listen(serverfd, 10);
    if (rc < 0){
        perror("LISTEN ERROR");
    }    
    printf("Server running at http://127.0.0.1:8000/\n");

    while(1){
        // 从epollfd中抓数据,保存在events数组中,返回变动的数据
        // 在程序的一开始,这个epoll_fd中只有死循坏外添加的serverfd
        // 监控发生的EPOLLIN事件,也就是有新的连接
        // 有新的数据就把它塞到events中
        int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }
        // 循环查看events保存的数据
        for (index = 0; index < nfds; index++){
            // 如果发生变动的是serverfd,也就是出现了新的连接
            if (events[index].data.fd == serverfd){
                inboundfd[index] = accept(serverfd, (struct sockaddr*)NULL, NULL);
                if (inboundfd[index] < 0){
                    perror("ACCEPT ERROR");
                }
                // 
                event.events = EPOLLIN;
                event.data.fd = inboundfd[index];
                epoll_ctl(epollfd, EPOLL_CTL_ADD, inboundfd[index], &event);
            // 其他情况,也就是inboundfd发生了变化
            } else {
                // 如果是in事件,内部的业务逻辑就喝Server4.c是一致的
                if (events[index].events & EPOLLIN){
                    memset(buff, 0, DATA_LEN);
                    memset(response, 0, DATA_LEN);

                    rc = read(inboundfd[index], buff, DATA_LEN);
                    if (rc < 0){
                        perror("READ ERROR");
                        exit (1);
                    }
                    char* method = strtok(buff, " ");
                    if  (!method){
                        perror("EMPTY REQUEST");
                    }
                    else if (strcmp(method, "GET") != 0){
                        perror("NOT A GET REQUEST");
                        goto end2;
                    }
                    
                    char* path = strtok(NULL, " ");

                    if (!path){
                        strcpy(response, "HTTP/1.1 403 FORBIDDEN\r\nContent-Type: text/html\r\n\r\n"
                                            "<html><head><title>My Web Server</title></head>"
                                            "<body><h1>403</h1></body></html>");
                        perror("UNKNOWN PATH");
                        goto end1;
                    }        
                    else if (strstr(path, "..")){
                        strcpy(response, "HTTP/1.1 403 FORBIDDEN\r\nContent-Type: text/html\r\n\r\n"
                                            "<html><head><title>My Web Server</title></head>"
                                            "<body><h1>403</h1></body></html>");
                        perror("INVALID PATH");
                        goto end1;
                    }
                    else{
                        char lpath[DATA_LEN] = "./public";
                        printf("====lpath:%s\n", lpath);
                        printf("====path:%s\n", path);
                        strcat(lpath, path);
                        printf("====lpath:%s\n", lpath);
                        htmlfd=open(lpath, O_RDONLY);
                        if (htmlfd < 0){
                            perror("OPEN ERROR");
                            strcpy(response, "HTTP/1.1 404 NOT FOUND\r\nContent-Type: text/html\r\n\r\n"
                                            "<html><head><title>My Web Server</title></head>"
                                            "<body><h1>404</h1></body></html>");
                            goto end1;
                        }
                        memset(buff, 0, DATA_LEN);
                        rc = read(htmlfd, buff, DATA_LEN);
                        if (rc < 0){
                            perror("READ HTML ERROR");
                        }
                        rc = close(htmlfd);
                        if(rc < 0){
                            perror("CLOSE ERROR");
                        }
                        strcpy(response, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n");
                        strcat(response, buff);                    
end1:
                        rc = write(inboundfd[index], response, strlen(response));
                        if (rc < 0){
                            perror("WRITE BACK ERROR");
                        }
end2:
                        rc = close(inboundfd[index]);
                        if (rc < 0){
                            perror("CLOSE ERROR");
                        }
                        epoll_ctl(epollfd, EPOLL_CTL_DEL, inboundfd[index], &event);
                    }
                }
            }
        }
    }
    close(serverfd);
    return rc;
}

大致的流程图如下:

server:client-server5