webServer

今天开始正式补充完善webServer服务器的内容!想到哪写到哪吧,回头再做整合。

标准C库IO函数

项目难点

1
2
3
4
5
/*
1、如何处理接收到的HTTP请求
2、如何填写HTTP响应
3、如何建立网络连接传输数据
*/

HTTP请求处理

http_conn头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class http_conn{
public:
http_conn() {}
~http_conn() {}
static int m_epollfd;
static int m_user_count;

public:
void init(int sockfd, const so0ckaddr_in &addr); // 初始化连接
void close_conn(bool real_close = true); // 关闭连接,关于需要传入real_close参数的原因,后面会讲到,预留问题

private:
int m_sockfd; // 发起http请求的sockfd
sockaddr_in m_address; // 发起http请求的socket地址
}

我们在http_conn类里主要设置五个对外的接口:

  • 初始化新接受的连接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void init(int sockfd, const sockaddr_in &addr);
    /*
    我们会把所有事件注册到一张内核事件表上,因此定义一个内核事件就好了。static int m_epollfd;
    同时我们会统计当前连接数,同样是使用一个静态变量,所有实例对象共享。static int m_user_count;
    每建立一个新连接,m_user_count就会加1;
    当我们向内核事件表注册一个事件时,我们需要考虑我们所要监听的事件类型,这里我们考虑:
    读事件(EPOLLIN)、边沿触发模式(EPOLLET)、以及EPOLLRDHUP(检测TCP对端连接的关闭或者半关闭状态)
    为了配合ET模式和多线程,我们需要做两个操作,第一是将所监听的文件描述符设置为非阻塞的,第二需要设置为EPOLLONESHOT类型
    注意:我们对m_epollfd和m_user_count的初始化分别为
    */
    int m_epollfd = -1;
    int m_user_count = 0;

    设置非阻塞函数

    1
    2
    3
    4
    5
    6
    7
    // 代码示例:Linux高性能服务器编程 p113
    int setnonblocking(int sockfd){
    int old_option = fcntl(sockfd, F_GETFL); // 获取文件描述符旧的状态标志
    int new_option = old_option | O_NONBLOCK; // 设置非阻塞标志
    fcntl(sockfd, F_SETFL, new_option);
    return old_option; // 返回文件描述符旧的状态标志,以便日后恢复该状态标志
    }

    设置感兴趣事件类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 我们在向内核事件表注册新事件的时候,需要指定自己对这个文件描述符上发生的什么事件感兴趣
    // 可读?可写?
    void addfd(int epollfd, int fd, bool one_shot){
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
    if(one_shot){
    event.events |= EPOLLONESHOT;
    }
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd);
    }

    初始化函数代码实现

    1
    2
    3
    4
    5
    6
    void http_conn::init(int sockfd, const sockaddr_in &addr){
    m_sockfd = sockfd;
    m_address = addr;
    addfd(m_epollfd, sockfd, true);
    m_user_count++;
    }
  • 关闭连接

    1
    2
    3
    4
    5
    6
    7
    void close_conn(bool real_close);
    /*
    我们可以思考一下有关关闭连接需要涉及到哪些操作。
    1、首先,如果一个连接关闭了,我们需要将其从内核事件表上移除
    2、当前连接数目也会减一
    这里我们准备先实现一个从内核事件表上移除文件描述符的函数(void removefd),在实现关闭连接
    */

    移除文件描述符

    1
    2
    3
    4
    void removefd(int epollfd, int fd){
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
    close(fd);
    }

    关闭连接

    1
    2
    3
    4
    5
    6
    7
    void http_conn::close_conn(bool real_close){
    if(real_close && m_sockfd != -1){
    removefd(m_epollfd, m_sockfd);
    m_sockfd = -1;
    m_user_count--;
    }
    }
  • 处理客户请求

    1
    2
    3
    4
    5
    6
    void process();
    /*
    关于如何处理客户连接请求的问题,我们从最原始的地方出发。
    首先,你了解一个http请求的基本格式吗?因为我们只有在了解http请求的通用格式后才知道如何对其进行解析
    下面,我将展示一个最基本的GET请求格式
    */
    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
    GET /path/to/resource?param1=value1&param2=value2 HTTP/1.1
    Host: www.example.com
    User-Agnet: Mozilla/5.0 (platform; rv:geckoversion) Gecko/geckotrail Firefox/firefoxversion
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
    Accept-Language: en-US,en;q=0.5
    Accept-Encoding: gzip, deflate, br
    Connection: keep-alive
    # 关于这个GET请求的详细解释我提一个chatgpt的解释在这,可以阅读一下
    下面是逐行详细解释:
    1. `GET /path/to/resource?param1=value1&param2=value2 HTTP/1.1`
    - `GET`: 这是HTTP请求的方法。`GET` 方法用于请求指定的资源。与POST相比,GET请求是只读的,并且用于获取数据而不是发送数据。
    - `/path/to/resource`: 这是请求的资源路径,通常是文件或者其他资源的位置。
    - `?`: 这个符号表示URL的查询部分的开始。
    - `param1=value1&param2=value2`: 这是查询字符串。在此例中,有两个参数,`param1`和`param2`,它们的值分别是`value1`和`value2`。`&`符号用于分隔查询参数。
    - `HTTP/1.1`: 表示使用的HTTP版本,这里是1.1。

    2. `Host: www.example.com`
    - `Host`: 这是HTTP头的名称。它指定了请求的目标主机和域名。
    - `www.example.com`: 请求的目标域名。

    3. `User-Agent: Mozilla/5.0 (platform; rv:geckoversion) Gecko/geckotrail Firefox/firefoxversion`
    - `User-Agent`: 这是HTTP头的名称。它描述了发出请求的用户代理的类型,通常是浏览器。
    - `Mozilla/5.0`: 这是用户代理的一般标记。虽然名为Mozilla,但它并不仅仅代表Mozilla浏览器,大多数浏览器都会以这种方式标识。
    - `(platform; rv:geckoversion)`: 这部分提供了关于用户代理的详细信息,例如它在哪个平台上运行。
    - `Gecko/geckotrail`: 这是Gecko渲染引擎的标识及其版本。
    - `Firefox/firefoxversion`: 表示用户代理是Firefox浏览器,后面跟着其版本。

    4. `Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8`
    - `Accept`: HTTP头名称,表示客户端可以处理的内容类型。
    - 该头的值列出了浏览器接受的MIME类型,按照优先级排序。例如,`text/html` 表示HTML文档,而`q=0.9`表示相对优先级。

    5. `Accept-Language: en-US,en;q=0.5`
    - `Accept-Language`: HTTP头名称,表示用户代理偏好的自然语言。
    - `en-US,en`: 这指示用户代理首先希望接收美国英语的内容,其次是英语。

    6. `Accept-Encoding: gzip, deflate, br`
    - `Accept-Encoding`: HTTP头名称,表示用户代理可以接受的内容编码。
    - `gzip, deflate, br`: 这些是可以接受的编码方法,用于内容压缩。

    7. `Connection: keep-alive`
    - `Connection`: HTTP头名称,表示是否持续连接。
    - `keep-alive`: 表示浏览器希望服务器保持连接,以便于后续的请求可以复用相同的TCP连接。

    这个请求大体上是一个典型的HTTP GET请求,由HTTP方法、资源路径、HTTP版本、多个头字段组成。每个头字段都有其特定的语义和目的。

    现在我们知道了HTTP请求格式了,那么到底如何解析它呢?这里就要引入一种叫做“有限状态机”的方法了,有关这个方法的具体描述与实现,大家可以看我的另一篇文章。

  • 非阻塞读操作

  • 非阻塞写操作

第一个知识点:iovec

这里先介绍一个iovec结构体,因为我们在写HTTP响应的时候需要用到。

1
2
3
4
5
6
7
8
#include <sys/uio.h>

struct iovec{
ptr_t iov_base;
size_t iov_len;
};

// struct iovec结构体,指针成员iov_base指向一个缓冲区,这个缓冲区是存放read_v所接收的数据或者write_v将要发送的数据。成员iov_len在各种情况下分别确定了接收的最大长度和实际写入的长度。
1
2
int readv(int fd, const struct iovec *vector, int count);
int writev(int fd, const struct iovec *vector, int count);

下面给出一个应用实例

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
#include <stdio.h>
#include <sys/uio.h>

int main(){
static char part1[] = "This is from writev";
static int part2 = 65;
static char part3[] = "[";

struct iovec iov[3];

iov[0].iov_base = part3;
iov[0].iov_len = strlen(part3);

iov[1].iov_base = part1;
iov[1].iov_len = strlen(part1);

iov[2].iov_base = &part2;
iov[2].iov_len = sizeof(int);

writev(1, iov, 3);

printf("\n");

return 0;
}

第二个知识点:va_list, vsnprintf

参考链接:https://blog.csdn.net/dengzhilong_cpp/article/details/54944676

参考链接:https://blog.csdn.net/luliplus/article/details/124123219

以上是今天要写代码的基础知识,下面开始正式代码

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
/*写HTTP响应*/
bool http_conn::write(){
int temp = 0;
int bytes_have_send = 0;
int bytes_to_send = m_write_idx;
if(bytes_to_send == 0){
modfd(m_epollfd, m_sockfd, EPOLLIN);
init();
return true;
}

while(1){
temp = writev(m_sockfd, m_iv, m_iv_count);
if(temp <= -1){
if(errno == EAGAIN){
modfd(m_epollfd, m_sockfd, EPOLLIN);
return true;
}
unmap();
return false;
}

bytes_to_send -= temp;
bytes_have_send += temp;
if(bytes_to_send <= bytes_have_send){
unmap();
if(m_linger){
init();
modfd(m_epollfd, m_sockfd, EPOLLIN);
return true;
}
else{
modfd(m_epollfd, m_sockfd, EPOLLIN);
return false;
}
}
}
}