TinyWebServer详解(3):http连接处理

(1)将文件描述符设置为非阻塞模式

这个函数 setnonblocking 用于将文件描述符 fd 设置为非阻塞模式,并返回设置前的文件状态标志。具体实现如下:

函数 setnonblocking 的实现

cpp 复制代码
int setnonblocking(int fd)
{
    int old_option = fcntl(fd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}

代码解析

1、获取当前文件状态标志

cpp 复制代码
int old_option = fcntl(fd, F_GETFL);

2、使用 fcntl 函数获取文件描述符 fd 的当前状态标志。

F_GETFL 命令用于获取文件描述符的状态标志,返回值保存在 old_option 中。

3、设置非阻塞模式

cpp 复制代码
int new_option = old_option | O_NONBLOCK;

使用按位或运算符 | 将 O_NONBLOCK 标志添加到当前文件状态标志中。

O_NONBLOCK 标志用于将文件描述符设置为非阻塞模式,即读写操作不会被阻塞。

4、更新文件状态标志

cpp 复制代码
fcntl(fd, F_SETFL, new_option);

使用 fcntl 函数将文件描述符的状态标志更新为 new_option。

F_SETFL 命令用于设置文件描述符的状态标志。

5、返回旧的文件状态标志

cpp 复制代码
return old_option;

返回设置前的文件状态标志,这样调用者可以在需要时恢复文件描述符的原始状态。
fcntl 函数说明

fcntl 函数用于操作文件描述符,原型如下:

int fcntl(int fd, int cmd, ... /* arg */ );

参数说明:

fd:要操作的文件描述符。

cmd:要执行的命令,如 F_GETFL 和 F_SETFL。

arg:根据 cmd的不同,可能是一个整数值,也可能是一个指针。

使用场景

将文件描述符设置为非阻塞模式在网络编程中非常常见,特别是在使用 epoll 或 select 进行 I/O 多路复用时,可以避免阻塞 I/O

操作导致的性能问题。通过设置非阻塞模式,程序可以在没有数据可读或可写时立即返回,并继续处理其他任务。

(2)内核表注册新事件

这个 addfd 函数用于将文件描述符 fd 添加到 epoll 实例 epollfd 的监听列表中,并根据参数设置触发模式和其他选项。具体实现如下:

函数 addfd 的实现

cpp 复制代码
void addfd(int epollfd, int fd, bool one_shot, int TRIGMode)
{
    epoll_event event;
    event.data.fd = fd;

    if (1 == TRIGMode)
        event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
    else
        event.events = EPOLLIN | EPOLLRDHUP;

    if (one_shot)
        event.events |= EPOLLONESHOT;

    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd);
}

代码解析

1、定义 epoll_event 结构体

cpp 复制代码
epoll_event event;
event.data.fd = fd;

定义一个 epoll_event 结构体,并将其 data.fd 成员设置为传入的文件描述符 fd。

2、设置事件类型

cpp 复制代码
if (1 == TRIGMode)
    event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
else
    event.events = EPOLLIN | EPOLLRDHUP;

根据 TRIGMode 参数设置事件类型。

如果 TRIGMode 为 1,设置为边缘触发模式(EPOLLET)、读事件(EPOLLIN)和挂断事件(EPOLLRDHUP)。

如果 TRIGMode 不为 1,设置为水平触发模式(默认)和挂断事件(EPOLLRDHUP)。

3、设置 EPOLLONESHOT 选项

cpp 复制代码
if (one_shot)
    event.events |= EPOLLONESHOT;

如果 one_shot 参数为真,则在事件类型中添加 EPOLLONESHOT 选项。

EPOLLONESHOT 确保每个文件描述符在任意时刻只会被一个线程处理,防止多线程并发处理同一个文件描述符导致的竞态条件。

4、将文件描述符添加到 epoll 实例

cpp 复制代码
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);

使用 epoll_ctl 函数将文件描述符 fd 添加到 epollfd 引用的 epoll 实例中,并指定监视的事件类型。

5、将文件描述符设置为非阻塞模式

cpp 复制代码
setnonblocking(fd);

调用 setnonblocking 函数将文件描述符 fd 设置为非阻塞模式,确保读写操作不会阻塞。
epoll_ctl 函数说明

epoll_ctl 函数用于控制 epoll 文件描述符的事件表,原型如下:

cpp 复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数说明:

epfd:epoll 文件描述符。

op:操作类型,可以是EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(删除)。

fd:要操作的目标文件描述符。

event:指向 epoll_event 结构体的指针,指定要监视的事件类型。在删除操作中,此参数可以为 NULL 或 0。

epoll_event 结构体说明

epoll_event 结构体用于描述 epoll 事件,定义如下:

cpp 复制代码
typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event {
    uint32_t events;    // Epoll events
    epoll_data_t data;  // User data variable
};

events 成员用于指定要监视的事件类型,可以是 EPOLLIN(可读)、EPOLLOUT(可写)、EPOLLET(边缘触发)、EPOLLONESHOT(一次性事件)等。

data 成员用于存储用户数据,可以是文件描述符、指针等。
总结

addfd 函数用于将文件描述符添加到 epoll 实例,并根据需要设置触发模式和其他选项。通过将文件描述符设置为非阻塞模式,确保在 epoll 事件处理过程中不会发生阻塞。

(3)dealwithsignal()

cpp 复制代码
bool WebServer::dealwithsignal(bool &timeout, bool &stop_server)
{
    int ret = 0;
    int sig;
    char signals[1024];
    ret = recv(m_pipefd[0], signals, sizeof(signals), 0);
    if (ret == -1)
    {
        return false;
    }
    else if (ret == 0)
    {
        return false;
    }
    else
    {
        for (int i = 0; i < ret; ++i)
        {
            switch (signals[i])
            {
            case SIGALRM:
            {
                timeout = true;
                break;
            }
            case SIGTERM:
            {
                stop_server = true;
                break;
            }
            }
        }
    }
    return true;
}

这段代码是**处理服务器程序中的信号处理函数,**主要用于处理从管道接收到的信号。下面是对代码功能的解释:

函数目的

函数 WebServer::dealwithsignal 用于处理从管道接收到的信号 ,主要是处理 SIGALRM 和 SIGTERM 两种信号。

参数

timeout:引用类型的布尔变量,用于表示是否超时。

stop_server:引用类型的布尔变量,用于表示是否停止服务器。

实现细节解析

1、接收信号:

cpp 复制代码
ret = recv(m_pipefd[0], signals, sizeof(signals), 0);

从管道的读端 m_pipefd[0] 接收信号数据,存储在 signals 数组中。sizeof(signals) 表示可以接收的最大字节数。

2、处理接收到的信号:

如果 ret 等于 -1,表示接收失败,函数返回 false。

如果 ret 等于 0,表示没有接收到数据,函数返回 false。

否则,遍历接收到的信号数据 signals。

cpp 复制代码
for (int i = 0; i < ret; ++i)

根据每个信号的值,进行不同的处理:

如果是 SIGALRM 信号,设置 timeout 变量为 true。

如果是 SIGTERM 信号,设置 stop_server 变量为 true。

3、返回值:

函数返回 true 表示处理信号成功。

使用场景

该函数通常在主事件循环中调用,用于处理服务器运行过程中的信号,例如处理超时或者请求停止服务器的情况。通过管道将信号传递给主线程或主进程,然后由该函数解析处理不同的信号类型,以执行相应的操作。

从管道的读端 m_pipefd[0] 接收信号数据,存储在 signals 数组中。sizeof(signals) 表示可以接收的最大字节数。

确保管道的正确创建和使用,确保信号的传递和接收是可靠的。

需要在主循环中适时调用该函数,以响应管道中的信号。

对信号的处理应当是非阻塞的,以保证服务器在处理信号时不会阻塞其他重要操作。

这种信号处理机制能够有效地管理和控制服务器的运行状态,增强服务器的稳定性和可靠性。

(4)定时器管理

这段代码定义了一个 util_timer 类,用于管理定时器。这个类主要用于实现链表结构的定时器管理。让我们逐步解释代码的各个部分:

类定义

cpp 复制代码
class util_timer
{
public:
    util_timer() : prev(NULL), next(NULL) {}

public:
    time_t expire;
    void (*cb_func)(client_data *);
    client_data *user_data;
    util_timer *prev;
    util_timer *next;
};

成员变量

cpp 复制代码
time_t expire:

定时器的超时时间。time_t 是一种表示时间的类型,通常是自纪元时间(1970年1月1日00:00:00 UTC)以来的秒数。

void (*cb_func)(client_data *):

指向一个函数的指针,当定时器超时时调用这个函数。这个函数接受一个 client_data 类型的指针作为参数。

cpp 复制代码
client_data *user_data:

指向用户数据的指针。这个数据结构通常包含与定时器相关的具体信息,比如客户端连接信息等。

cpp 复制代码
util_timer *prev:

指向链表中前一个定时器的指针,用于实现双向链表。

cpp 复制代码
util_timer *next:

指向链表中下一个定时器的指针,用于实现双向链表。
构造函数

cpp 复制代码
util_timer() : prev(NULL), next(NULL) {}

构造函数初始化定时器的前一个和下一个指针为 NULL。这是为了确保当创建一个新的 util_timer 对象时,它不会意外地引用其他定时器对象。

链表结构

这个类的设计意图是创建一个双向链表,用于管理多个定时器。通过 prev 和 next 指针,可以在链表中遍历定时器,并根据需要插入、删除或修改定时器。

使用示例

通常情况下,这个定时器类将与其他组件一起使用,例如定时器管理器类(如 sort_timer_lst),用于插入、删除和触发定时器。以下是一个简化的使用示例:

cpp 复制代码
// 定义定时器回调函数
void timer_callback(client_data *user_data) {
    // 处理定时器事件
    // 例如,关闭连接或进行一些清理操作
}

// 创建并初始化一个定时器
util_timer *timer = new util_timer();
timer->expire = time(NULL) + 5; // 设置定时器在5秒后超时
timer->cb_func = timer_callback;
timer->user_data = some_client_data; // 指向具体的客户端数据

// 将定时器添加到链表中(假设有一个定时器管理器)
timer_manager.add_timer(timer);

// 在定时器超时后调用回调函数
if (time(NULL) >= timer->expire) {
    timer->cb_func(timer->user_data);
}

这种结构的设计使得管理多个定时器变得更加高效和灵活,特别是在需要定期检查和处理超时事件的场景中。

(5)parse_line()

LINE_OPEN状态:行尚未完全读取

行尚未完全读取的情况通常发生在数据流(例如从网络套接字读取数据)还没有完整的行结束标志(如 \r\n 或 \n)到达时。具体来说,有以下几种情况:

1、缓冲区已满但没有找到行结束标志:

如果缓冲区 m_read_buf 已经填满了数据(即 m_read_idx 达到 READ_BUFFER_SIZE),但在这些数据中没有找到行结束标志,则此时的行是尚未完全读取的。

2、数据流断开,导致部分行数据丢失:

如果在读取数据时,连接突然断开或中止,导致缓冲区中只有部分行数据,没有行结束标志。

3、读取到数据的中途:

如果在读取数据的过程中,只收到了部分行数据,而行结束标志还在后续的数据中,则当前缓冲区内的数据不足以组成完整的一行。

在代码中,这些情况会被 LINE_OPEN 返回值所捕获:

代码中的具体情况

遇到回车符 \r 后,缓冲区已到达末尾:

cpp 复制代码
if (temp == '\r') // \r: 回车符
{
    if ((m_checked_idx + 1) == m_read_idx)
        return LINE_OPEN; // 到达最后一个字节时,返回请求行读取不完整
    // ...
}

这段代码检查当前字符是否是 \r,如果是并且它是缓冲区的最后一个字符,则返回 LINE_OPEN,表示行尚未完全读取。

在读取过程中,没有找到行结束标志 \r\n 或 \n:

cpp 复制代码
for (; m_checked_idx < m_read_idx; ++m_checked_idx)
{
    // 遍历读取的缓冲区
    // 如果遇到 \r 或 \n 会进行相应处理
}
return LINE_OPEN; // 没有找到行结束标志

如果整个缓冲区都被遍历完但没有找到行结束标志,则返回 LINE_OPEN,表示行尚未完全读取。

示例

假设缓冲区大小为 10,已经读取到的数据如下:

vbnet

Copy code

cpp 复制代码
m_read_buf: "GET / HTT"

此时,m_read_idx 等于 10,但由于没有找到行结束标志(即 \r\n 或 \n),parse_line 函数将返回 LINE_OPEN。

当继续读取数据,将剩余的部分数据收到缓冲区中:

swift

Copy code

cpp 复制代码
m_read_buf: "GET / HTTP/1.1\r\n"

此时,m_read_idx 增加到 14,parse_line 再次被调用,找到行结束标志,返回 LINE_OK。

(6)parse_request_line()

cpp

Copy code

cpp 复制代码
m_url = strchr(m_url, '/');

这一行代码的作用是查找 m_url 中第一个出现的 / 字符,并返回一个指向该字符的指针。

具体解释

strchr 函数

原型:

c

Copy code

cpp 复制代码
char *strchr(const char *s, int c);

功能:

在字符串 s 中查找字符 c 第一次出现的位置。

返回一个指向该字符的指针。

返回值:

返回指向字符 c 的指针,如果没有找到,返回 NULL。

示例

假设 m_url 的初始值为 "http://www.example.com/path/to/resource":

cpp

Copy code

cpp 复制代码
m_url = strchr(m_url, '/');

调用 strchr:

cpp

Copy code

cpp 复制代码
strchr(m_url, '/');

m_url 是 "http://www.example.com/path/to/resource"。

c 是 '/'。

查找 /:

strchr 会返回指向 "/path/to/resource" 的指针,因为这是字符串中第一个 /。

更新 m_url:

cpp

Copy code

cpp 复制代码
m_url = "/path/to/resource";

在上下文中的作用

在解析 HTTP 请求行时,这段代码用于提取 URL 中的路径部分。例如,如果 URL 以 http:// 或 https:// 开头,函数会跳过这些前缀,并使 m_url 指向路径部分:

cpp

Copy code

cpp 复制代码
if (strncasecmp(m_url, "http://", 7) == 0)
{
    m_url += 7;  // 跳过 "http://"
    m_url = strchr(m_url, '/');  // 查找路径部分的起始位置
}

if (strncasecmp(m_url, "https://", 8) == 0)
{
    m_url += 8;  // 跳过 "https://"
    m_url = strchr(m_url, '/');  // 查找路径部分的起始位置
}

strncasecmp 比较 m_url 的前 7 个或 8 个字符是否等于 "http://" 或 "https://"。

m_url += 7 或 m_url += 8 跳过这些前缀。

m_url = strchr(m_url, '/') 查找路径部分的起始位置,并更新 m_url 指向路径部分。

总结

这行代码用于查找 m_url 中第一个出现的 / 字符,并将 m_url 更新为指向该字符的指针。这在解析 HTTP 请求行时非常有用,确保能够正确提取 URL 的路径部分。

在HTTP服务器中,请求处理完成后将主状态机转移到处理请求头的原因通常是因为HTTP协议的请求处理是分阶段的,需要逐步解析请求内容。具体原因包括:

协议规范要求:HTTP协议规定了请求消息的格式和解析流程,包括请求行、请求头部、消息体等部分。处理完请求行后,必须转移到处理请求头部,以解析和处理进一步的请求信息。

请求头部包含重要信息:HTTP请求头部包含了诸如Host、User-Agent、Content-Type等重要的请求信息,服务器需要这些信息来正确处理请求和生成响应。

状态机设计:HTTP服务器通常使用状态机来处理请求。主状态机在处理请求的不同阶段(如请求行、请求头、消息体)之间切换,确保按照协议要求逐步解析和处理请求。

业务逻辑需求:处理请求头部后,服务器可能需要根据请求头部的信息执行不同的业务逻辑或者权限控制。例如,根据请求的Content-Type判断请求的数据类型,选择相应的处理方式。

综上所述,将主状态机转移到处理请求头部是HTTP服务器按照协议规范和设计原则执行的必要步骤,确保请求能够正确解析和处理。

相关推荐
JaguarJack9 小时前
FrankenPHP 原生支持 Windows 了
后端·php·服务端
BingoGo9 小时前
FrankenPHP 原生支持 Windows 了
后端·php
JaguarJack1 天前
PHP 的异步编程 该怎么选择
后端·php·服务端
BingoGo1 天前
PHP 的异步编程 该怎么选择
后端·php
JaguarJack2 天前
为什么 PHP 闭包要加 static?
后端·php·服务端
ServBay3 天前
垃圾堆里编码?真的不要怪 PHP 不行
后端·php
用户962377954483 天前
CTF 伪协议
php
YuMiao4 天前
gstatic连接问题导致Google Gemini / Studio页面乱码或图标缺失问题
服务器·网络协议
不可能的是5 天前
前端 SSE 流式请求三种实现方案全解析
前端·http
BingoGo5 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php