TinywebServer代码详解–http连接处理-上(4)

该blog内容转自:最新版Web服务器项目详解 - 04 http连接处理(上)

该blog对上述内容进行补充(在本人的角度)

结合此前该项目记录的blog一起学习:TinywwebServer知识点(3)(点击跳转)

结合此前该项目记录的blog一起学习:TinywwebServer知识点(1)(点击跳转)

结合此前记录的blog一起学习:牛客WebServer项目实战(点击跳转)

原项目地址(点击跳转)

博主添加注释后项目地址(点击跳转)


一、I/O复用

IO复用是一种允许程序同时监控多个文件描述符的机制,从而使得程序可以在某个或多个文件描述符准备好进行IO操作(读或写)时得到通知并进行处理

该项目实现I/O多路转接复用的方式是epoll

epoll 全称 eventpoll,是linux内核实现IO多路转接/复用(IO multiplexing)的一个实现。**IO多路转接的意思是在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作**。epollselectpoll的升级版,相较于selectpollepoll改进了工作方式,因此它更加高效,但是与poll一样,epoll不可以跨平台,只能在Linux上面使用



1.项目为什么使用epoll

TinywwebServer知识点(1)(点击跳转)



2.相关函数

(1)epoll_create()

epoll_create()函数的作用是创建一个红黑树模型的实例(创建一个指示epoll内核事件表的文件描述符),用于管理待检测的文件描述符的集合,size不起作用

1
2
#include <sys/epoll.h>
int epoll_create(int size);

参数:

  • size:在Linux内核2.6.8版本以后,这个参数是被忽略的,只需要指定一个大于0的数值就可以了

返回值:

  • 失败:返回-1
  • 成功:返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的epoll实例了

(2)epoll_ctl()

函数的作用是管理红黑树实例上的节点(操作内核事件表监控的文件描述符上的事件),可以进行添加、删除、修改操作

1
2
3
4
5
6
7
8
9
10
11
12
13
// 联合体, 多个变量共用同一块内存        
typedef union epoll_data {
void *ptr;
int fd; // 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可
uint32_t u32;
uint64_t u64;
} epoll_data_t;

struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数:

  • epfdepoll_create() 函数的返回值(epoll_create的句柄),通过这个参数找到epoll实例

  • op:这是一个枚举值,控制通过该函数执行什么操作

    • EPOLL_CTL_ADD:往epoll模型中添加新的节点
    • EPOLL_CTL_MOD:修改epoll模型中已经存在的节点
    • EPOLL_CTL_DEL:删除epoll模型中的指定的节点
  • fd:文件描述符,即要添加/修改/删除的文件描述符

  • eventepoll事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件(告诉内核需要监听的事件

    events:委托epoll检测的事件, struct epoll_event 结构体中的字段 events

    • EPOLLIN:读事件, 接收数据, 检测读缓冲区,如果有数据该文件描述符就绪
    • EPOLLOUT:写事件, 发送数据, 检测写缓冲区,如果可写该文件描述符就绪
    • EPOLLERR:异常事件

    data:用户数据变量,这是一个联合体类型,通常情况下使用里边的fd成员,用于存储待检测的文件描述符的值,在调用epoll_wait()函数的时候这个值会被传出

返回值:

  • 失败:返回-1
  • 成功:返回0
1
2
3
4
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

events描述事件类型,其中epoll事件类型有以下几种:

  • EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
  • EPOLLOUT:表示对应的文件描述符可以写
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
  • EPOLLERR:表示对应的文件描述符发生错误
  • EPOLLHUP:表示对应的文件描述符被挂断;
  • EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

(3)epoll_wait()

该函数用于等待所监控文件描述符上有事件的产生,返回就绪的文件描述符个数

1
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

参数:

  • epfdepoll_create() 函数的返回值, 通过这个参数找到epoll实例
  • events:传出参数, 这是一个结构体数组的地址, 里边存储了已就绪的文件描述符的信息
  • maxevents:修饰第二个参数, 结构体数组的容量(元素个数)
  • timeout:如果检测的epoll实例中没有已就绪的文件描述符,该函数阻塞的时长, 单位ms 毫秒
    • 0:函数不阻塞,不管epoll实例中有没有就绪的文件描述符,函数被调用后都直接返回
    • 大于0:如果epoll实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回
    • -1:函数一直阻塞,直到epoll实例中有已就绪的文件描述符之后才解除阻塞

返回值:

  • 成功,等于0:函数是阻塞被强制解除了, 没有检测到满足条件的文件描述符;大于0:检测到的已就绪的文件描述符的总个数
  • 失败,返回-1

(4)select/poll/epoll区别

文件描述符数量

  • select通过线性表描述文件描述符集合,文件描述符有上限,一般是1024,但可以修改源码,重新编译内核,不推荐
  • poll是链表描述,突破了文件描述符上限,最大可以打开文件的数目
  • epoll通过红黑树描述(只能用于Linux系统上),最大可以打开文件的数目,可以通过命令ulimit -n number修改,仅对当前终端有效

将文件描述符从用户传给内核

  • select和poll通过将所有文件描述符拷贝到内核态,每次调用都需要拷贝(每次调用 select/poll 时,都需要把整个文件描述符集从用户空间复制到内核空间)
  • epoll通过epoll_create建立一棵红黑树,通过epoll_ctl将要监听的文件描述符注册到红黑树上。epoll内核中维护一个事件表,这个事件表是一个数据结构,用于存储所有被监控的文件描述符及其对应的事件。当用户空间应用程序调用 epoll_ctl() 添加、修改或删除文件描述符时,内核会更新这个事件表
  • epoll 的设计减少了需要复制的数据量,因为只有发生事件的文件描述符信息会被复制回用户空间,与 selectpoll 相比,这大大减少了不必要的数据传输和处理。此外,因为事件表是在内核中维护的,所以不需要每次调用时都传递大量数据结构,也就减少了系统调用的开销

内核判断就绪的文件描述符

  • select和poll通过遍历文件描述符集合,判断哪个文件描述符上有事件发生
  • epoll_create时,内核除了帮我们在epoll文件系统里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。
  • epoll是根据每个fd上面的回调函数(中断函数)判断,只有发生了事件的socket才会主动的去调用 callback函数,其他空闲状态socket则不会,若是就绪事件,插入list

应用程序索引就绪文件描述符

  • select/poll只返回发生了事件的文件描述符的个数,若知道是哪个发生了事件,同样需要遍历
  • epoll返回的发生了事件的个数和结构体数组,结构体包含socket的信息,因此直接处理返回的数组即可

工作模式

  • select和poll都只能工作在相对低效的LT模式下
  • epoll则可以工作在ET高效模式,并且epoll还支持EPOLLONESHOT事件,该事件能进一步减少可读、可写和异常事件被触发的次数。

应用场景

  • 当所有的fd都是活跃连接,使用epoll,需要建立文件系统,红黑书和链表对于此来说,效率反而不高,不如selece和poll
  • 当监测的fd数目较小,且各个fd都比较活跃,建议使用select或者poll
  • 当监测的fd数目非常大,成千上万,且单位时间只有其中的一部分fd处于就绪状态,这个时候使用epoll能够明显提升性能

(5)epoll的工作模式

TinywwebServer知识点(1)(点击跳转)

IO多路复用-多线程并发通信(点击跳转)

  • LT水平触发模式

    • epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序可以不立即处理该事件。
    • 当下一次调用epoll_wait时,epoll_wait还会再次向应用程序报告此事件,直至被处理
  • ET边缘触发模式

    • epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序必须立即处理该事件
    • 必须要一次性将数据读取完,使用非阻塞I/O,读取到出现eagain
  • EPOLLONESHOT

    • 一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket
    • 我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件
    • 通俗解释:它在文件描述符被触发一次事件之后,自动将该文件描述符从 epoll 的监控列表中移除。要重新监控这个文件描述符,需要再次调用 epoll_ctl 函数来添加这个文件描述符
    • 使用场景EPOLLONESHOT 通常用于需要在处理一次事件后进行额外处理或改变状态的场景。例如,一个多线程服务器可以使用 EPOLLONESHOT 来确保某个连接在一个线程处理时不会被其他线程处理
  • EPOLLRDHUP

    • 用于检测套接字关闭。它表示在某个文件描述符上检测到远程关闭 (Remote Shutdown)。当一个套接字连接被对端关闭时,会触发此事件
    • 用途
      • 检测远程关闭: 主要用于非阻塞套接字编程中,检测远程关闭事件,方便程序及时清理资源或进行其他处理
      • 高效的网络编程: 在高并发网络服务器中,通过 epoll 和 EPOLLRDHUP 事件,可以高效地处理连接的关闭,避免不必要的资源浪费



二、HTTP报文

牛客WebServer项目实战(点击跳转)

浏览器端向服务器发送的为请求报文,服务器处理后返回给浏览器端的为响应报文

1.请求报文

HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。

其中,请求分为两种,GET和POST,具体的:


(1)GET
1
2
3
4
5
6
7
8
9
10
GET /562f25980001b1b106000338.jpg HTTP/1.1
Host:img.mukewang.com
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept:image/webp,image/*,*/*;q=0.8
Referer:http://www.imooc.com/
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8
空行
请求数据为空

(2)POST
1
2
3
4
5
6
7
8
POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive
空行
name=Professional%20Ajax&publisher=Wiley

请求行:用来说明请求类型,要访问的资源以及所使用的HTTP版本。
GET说明请求类型为GET,/562f25980001b1b106000338.jpg(URL)为要访问的资源,该行的最后一部分说明使用的是HTTP1.1版本

请求头部:用来说明服务器要使用的附加信息

  • HOST,给出请求资源所在服务器的域名
  • User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等
  • Accept,说明用户代理可处理的媒体类型
  • Accept-Encoding,说明用户代理支持的内容编码
  • Accept-Language,说明用户代理能够处理的自然语言集
  • Content-Type,说明实现主体的媒体类型
  • Content-Length,说明实现主体的大小
  • Connection,连接管理,可以是Keep-Alive或close

空行:请求头部后面的空行是必须的即使第四部分的请求数据为空,也必须有空行

请求数据:也叫主体,可以添加任意的其他数据



2.响应报文

HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 200 OK
Date: Fri, 22 May 2009 06:07:21 GMT
Content-Type: text/html; charset=UTF-8
空行
<html>
<head></head>
<body>
<!--body goes here-->
</body>
</html>

状态行:由HTTP协议版本号, 状态码, 状态消息 三部分组成。

  • 第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为OK

消息报头:用来说明客户端要使用的一些附加信息

  • 第二行和第三行为消息报头
  • Date:生成响应的日期和时间
  • Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8

空行:消息报头后面的空行是必须的

响应正文:服务器返回给客户端的文本信息。空行后面的html部分为响应正文



3.HTTP状态码

HTTP有5种类型的状态码,具体的:

1xx:指示信息–表示请求已接收,继续处理

2xx:成功–表示请求正常处理完毕

  • 200 OK:客户端请求被正常处理
  • 206 Partial content:客户端进行了范围请求

3xx:重定向–要完成请求必须进行更进一步的操作

  • 301 Moved Permanently:永久重定向,该资源已被永久移动到新位置,将来任何对该资源的访问都要使用本响应返回的若干个URI之一
  • 302 Found:临时重定向,请求的资源现在临时从不同的URI中获得

4xx:客户端错误–请求有语法错误,服务器无法处理请求

  • 400 Bad Request:请求报文存在语法错误
  • 403 Forbidden:请求被服务器拒绝
  • 404 Not Found:请求不存在,服务器上找不到请求的资源

5xx:服务器端错误–服务器处理请求出错

  • 500 Internal Server Error:服务器在执行请求时出现错误


4.有限状态机

有限状态机,是一种抽象的理论模型,它能够把有限个变量描述的状态变化过程,以可构造可验证的方式呈现出来。比如,封闭的有向图

有限状态机可以通过if-else,switch-case函数指针来实现,从软件工程的角度看,主要是为了封装逻辑

带有状态转移的有限状态机示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
STATE_MACHINE(){
State cur_State = type_A;
while(cur_State != type_C){
Package _pack = getNewPackage();
switch(){
case type_A:
process_pkg_state_A(_pack);
cur_State = type_B;
break;
case type_B:
process_pkg_state_B(_pack);
cur_State = type_C;
break;
}
}
}

该状态机包含三种状态:type_Atype_Btype_C。其中,type_A是初始状态,type_C是结束状态。

状态机的当前状态记录在cur_State变量中,逻辑处理时,状态机先通过getNewPackage获取数据包,然后根据当前状态对数据进行处理,处理完后,状态机通过改变cur_State完成状态转移

有限状态机一种逻辑单元内部的一种高效编程方法,在服务器编程中,服务器可以根据不同状态或者消息类型进行相应的处理逻辑,使得程序逻辑清晰易懂




三、http代码解析

1.http报文处理流程

  • 浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应buffer(同步IO模拟Proactor模式下,主线程也负责I/O数据读写),将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。(本篇讲)
  • 工作线程取出任务后,调用process_read函数,通过主、从状态机对请求报文进行解析。(中篇讲)
  • 解析完之后,跳转do_request函数完成请求资源的内存映射,并生成响应报文,通过process_write写入buffer,返回给浏览器端。(下篇讲)


2.http类

(1)http类定义

这一部分代码在TinyWebServer/http/http_conn.h

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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <map>
#include <fcntl.h>


class http_conn{
public:
// 设置读取文件的名称m_real_file大小
static const int FILENAME_LEN = 200;
// 设置读缓冲区m_read_buf大小
static const int READ_BUFFER_SIZE = 2048;
// 设置读缓冲区m_read_buf大小
static const int WRITE_BUFFER_SIZE = 1024;
// HTTP方法名--本项目只使用到GET与POST
enum METHOD
{
GET = 0,
POST,
HEAD,
PUT,
DELETE,
TRACE,
OPTIONS,
CONNECT,
PATH
};
// 主状态机状态--检查请求报文中元素
enum CHECK_STATE
{
// 请求行
CHECK_STATE_REQUESTLINE = 0,
// 请求头
CHECK_STATE_HEADER,
// 请求数据
CHECK_STATE_CONTENT
};
// HTTP状态码--请求报文解析的结果
enum HTTP_CODE
{
// 请求不完整,需要继续读取客户数据
NO_REQUEST,
// 获得了一个完成的客户请求
GET_REQUEST,
// 客户请求语法错误
BAD_REQUEST,
// 服务器没有资源
NO_RESOURCE,
// 客户对资源没有足够的访问权限
FORBIDDEN_REQUEST,
// 文件请求,获取文件成功
FILE_REQUEST,
// 服务器内部错误
INTERNAL_ERROR,
// 客户端已经关闭连接了
CLOSED_CONNECTION
};
// 从状态机的状态--文本解析是否成功(请求报文的每一行的解析情况: 请求行-单独一行/请求头部-多行组成/)
enum LINE_STATUS
{
// 读取到一个完整的行
LINE_OK = 0,
// 行出错
LINE_BAD,
// 行数据尚且不完整
LINE_OPEN
};

public:
http_conn(){}
~http_conn(){}

public:
// 初始化套接字--函数内部会调用私有方法init
void init(int sockfd, const sockaddr_in &addr, char *, int, int, string user, string passwd, string sqlname);
// 关闭http连接
void close_conn(bool real_close = true);
// http处理函数
void process();
// 读取浏览器端发来的全部数据
bool read_once();
// 响应报文写入函数
bool write();
// 获取服务器ip信息
sockaddr_in *get_address()
{
return &m_address;
}

/*******************数据库:函数需要补充*****************/
// 初始化数据库读取线程
void initmysql_result(connection_pool *connPool);
/*******************数据库:函数需要补充*****************/

// 是否关闭连接
int timer_flag;
// 是否正在处理数据中
int improv;

private:
// 对类私有的成员变量进行初始化
void init();
// 从m_read_buf读取,并处理请求报文
HTTP_CODE process_read();
// 向m_write_buf写入响应报文数据
bool process_write(HTTP_CODE ret);
// 主状态机解析报文中的请求行数据
HTTP_CODE parse_request_line(char *text);
// 主状态机解析报文中的请求头数据
HTTP_CODE parse_headers(char *text);
// 主状态机解析报文中的请求内容
HTTP_CODE parse_content(char *text);
// 生成响应报文
HTTP_CODE do_request();
// 从状态机(从每个部分中--请求行/请求头/请求数据--获取一行)--分析是请求报文的哪一部分
LINE_STATUS parse_line();
// 移动到当前处理行的初始位置
// m_start_line是已经解析的字符
// get_line用于将指针向后偏移,指向未处理的字符
char *get_line() { return m_read_buf + m_start_line; };
// 撤销内存映射
void unmap();

// 下面一组函数用于填充HTTP应答
// 根据响应报文格式,生成对应8个部分,以下函数均由do_request调用
bool add_response(const char *format, ...);
bool add_content(const char *content);
bool add_status_line(int status,const char *title);
bool add_headers(int content_length);
bool add_content_type();
bool add_content_length(int content_length);
bool add_linger();
bool add_blank_line();

public:
// epoll_create创建的epoll树实例
static int m_epollfd;
// 当前的连接客户端计数
static int m_user_count;
/*******************数据库对象*****************/
// 数据库对象
MYSQL *mysql;
/*******************数据库对象*****************/
// IO 事件类别: 读事件为0,写事件为1
int m_state;

private:
// 该http对象用于通信的套接字
int m_sockfd;
// 客户端ip信息
sockaddr_in m_address;
// 存储读取的请求报文数据
char m_read_buf[READ_BUFFER_SIZE];
// 读缓冲区m_read_buf中数据最后一个字节的下一个位置
int m_read_idx;
// m_read_buf当前正在读取的位置m_checked_idx(正在分析的字符在读缓冲区的位置)
int m_checked_idx;
// m_read_buf中已经解析的字符个数(当前正在解析的行的起始位置)
int m_start_line;
// 存储发出的响应报文数据
char m_write_buf[WRITE_BUFFER_SIZE];
// 指示m_write_buf中的长度
int m_write_idx;
// 主状态机的状态
CHECK_STATE m_check_state;
// 请求方法
METHOD m_method;

// 以下为解析请求报文中对应的6个变量
// m_real_file存储读取文件的名称
// 存储客户请求文件的完整路径,其内容等于 doc_root + m_url, doc_root是网站根目录
char m_real_file[FILENAME_LEN];
// 请求目标文件的文件名,客户端想要访问的资源
char *m_url;
// HTTP协议版本,仅支持HTTP 1.1
char *m_version;
// 指定了服务器的域名或IP地址
char *m_host;
// 请求报文的请求数据(请求体)的总长度
int m_content_length;
// 判断HTTP请求是否保持连接
bool m_linger;

// 读取服务器上的文件地址(客户请求的资源文件被mmap(内存映射)到内存中的起始位置)
char *m_file_address;
// 客户端请求的资源文件状态(是否存在/是否为目录/是否可读/并获取文件大小等信息)
struct stat m_file_stat;
// io向量机制iovec
struct iovec m_iv[2];
int m_iv_count;
// 是否启用的POST
int agi;
// 存储请求头数据
char *m_string;
// 剩余发送字节数
int bytes_to_send;
// 已发送字节数
int bytes_have_send;
// 服务器根目录
char *doc_root;

/*******************数据库相关变量*****************/
// 数据库用户名密码匹配表
map<string, string> m_users;
// 触发模式
int m_TRIGMode;
// 是否开启日志
int m_close_log;
char sql_user[100];
char sql_passwd[100];
char sql_name[100];
/*******************数据库相关变量*****************/
};

(2)read_once()

循环读取从浏览器(客户端)端,发送的请求报文数据,直到无数据可读或对方关闭连接。从该http连接的通信套接字读缓冲区读取数据到程序的读缓冲区m_read_buf,也是用于更新m_read_idxm_read_idx用于记录当前读取到m_read_buf中的数据字节数量

需要注意的是,在非阻塞的边沿工作模式(ET)下,需要一次性将连接的套接字读缓冲区的数据全部读取完毕

TinyWebServer/http/http_conn.cpp

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
/*
* func:循环读取客户数据,直到无数据可读或对方关闭连接
* 从该http连接的通信套接字读缓冲区读取数据到程序的读缓冲区m_read_buf中
* 也是用于更新m_read_idx
* note:非阻塞ET工作模式下,需要一次性将数据读完
* return:返回true成功
* 返回false失败
*/
bool http_conn::read_once()
{
// m_read_buf满
if (m_read_idx >= READ_BUFFER_SIZE)
{
return false;
}
int bytes_read = 0;

// LT(水平工作模式)读取数据
// LT模式下套接字读缓冲区有数据,就会一直有事件触发
if(0 == m_TRIGMode)
{
// recv函数
// <0 出错 =0 关闭连接 >0 接收到的数据字节数量
bytes_read = recv(m_sockfd,m_read_buf + m_read_idx,READ_BUFFER_SIZE - m_read_idx,0);
// 记录m_read_buf读了多少数据了
m_read_idx += bytes_read;

if(bytes_read <= 0)
{
// 出错
return false;
}
return true;
}

// ET(边沿工作模式)读取数据,非阻塞的读,需要一次性将该http对象的通信套接字读缓冲区数据读取完
else
{
while (true)
{
// 从通信套接字的读缓冲区,读取数据到m_read_buf
bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
if (bytes_read == -1)
{
// 非阻塞ET模式下,需要一次性将数据读完,无错
// 套接字属性为非阻塞情况下,判断errno == EAGAIN,表示套接字读缓冲区为空
// 即套接字读缓冲区的数据全部读取完毕
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
break;
}
// 读取出错
return false;
}
else if (bytes_read == 0)
{
// 连接的客户端断开连接
return false;
}
// 记录读了多少数据到m_read_buf中
m_read_idx += bytes_read;
}
return true;
}
}


3.epoll相关代码

TinyWebServer/http/http_conn.cpp下的相关函数代码

(1)setnonblocking()

使用fcntl()函数将套接字文件描述符设置为非阻塞模式,这是因为在边沿工作模式下,套接字的读事件就绪通知,只会在套接字的读缓冲区从无数据变为有数据时发送,并且只会通知一次,那么必须要保证得到通知后将数据全部从读缓冲区中读出。那我们该如何应对这个情况呢?

方法循环接收数据

1
2
3
4
5
int len = 0;
while((len = recv(curfd, buf, sizeof(buf), 0)) > 0)
{
// 数据处理...
}

但是,这样做也是有弊端的,因为套接字操作默认是阻塞的,当读缓冲区数据被读完之后,读操作就阻塞了也就是调用的read()/recv()函数被阻塞了,当前进程/线程被阻塞之后就无法处理其他操作了

要解决阻塞问题,就需要将套接字默认的阻塞行为修改为非阻塞,需要使用fcntl()函数进行处理:

1
2
3
4
// 设置完成之后, 读写都变成了非阻塞模式
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);

通过上述分析就可以得出一个结论:**epoll在边沿模式下,必须要将套接字设置为非阻塞模式**,但是,这样就会引发另外的一个bug,在非阻塞模式下,循环地将读缓冲区数据读到本地内存中,当缓冲区数据被读完了,调用的read()/recv()函数还会继续从缓冲区中读数据,此时函数调用就失败了,返回-1,对应的全局变量 errno 值为 EAGAIN 或者 EWOULDBLOCK如果打印错误信息会得到如下的信息:Resource temporarily unavailable

因此可以通过非阻塞状态下的套接字errno的判断,什么时候缓冲区的数据全部读取完毕

检查recv的返回值

recv 函数的返回值可以提供关于读取状态的直接信息:

  • 如果返回正数,这代表读取到的字节数。如果这个数小于你请求的字节数(第三个参数),这通常意味着缓冲区中没有更多的数据可读(或者数据正好被读完)
  • 如果返回0,表示对端已经关闭了连接。在TCP协议中,这意味着对端的套接字已经执行了正常的关闭过程
  • 如果返回-1,并且 errno 设置为 EAGAINEWOULDBLOCK,表示没有数据可读,且套接字被设置为非阻塞模式。此时,可以认为之前已经读取了所有可用数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 非阻塞模式下recv() / read()函数返回值 len == -1
int len = recv(curfd, buf, sizeof(buf), 0);
if(len == -1)
{
if(errno == EAGAIN)
{
printf("数据读完了...\n");
}
else
{
perror("recv");
exit(0);
}
}

setnonblocking()函数如下:

1
2
3
4
5
6
7
8
9
10
/*
* func:对文件描述符设置为非阻塞
*/
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;
}

(2)addfd()

通过epoll_ctl函数向epoll实例中添加需要监听的文件描述符。将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT

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
/*
* func:向epoll实例中添加需要监听的文件描述符
* 将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT
*/
void addfd(int epollfd, int fd, bool one_shot, int TRIGMode)
{
epoll_event event;
event.data.fd = fd;

// TRIGMode 为 epoll工作模式选择
// TRIGMode == 0 缺省为水平工作模式LT
// TRIGMode == 1 为边沿工作模式 ET
// EPOLLIN 写事件
// EPOLLRDHUP 监测对端(客户端)套接字关闭事件
// EPOLLET 边沿触发工作模式
if (1 == TRIGMode)
event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
else
event.events = EPOLLIN | EPOLLRDHUP;

if (one_shot)
// EPOLLONESHOT 件描述符被触发一次事件之后,自动将该文件描述符的监测事件从内核维护的事件表中进行删除
// 确保某个连接在一个线程处理时不会被其他线程处理(防止同一个通信被不同的线程处理)
event.events |= EPOLLONESHOT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}

(3)removefd()

使用epoll_ctl函数从epoll实例中删除监听的文件描述符,并且从内核维护的事件表中删除需要监控的事件

1
2
3
4
5
6
7
8
/*
* func:从内核事件表中删除文件描述符--从epoll树中移除文件描述符
*/
void removefd(int epollfd, int fd)
{
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
close(fd);
}
(4)modfd()

修改文件描述符事件,将事件重置为EPOLLONESHOT,以确保下一次可读时,EPOLLIN事件能被触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* func:修改文件描述符事件,将事件重置为EPOLLONESHOT,以确保下一次可读时,EPOLLIN事件能被触发
*/
void modfd(int epollfd, int fd, int ev, int TRIGMode)
{
epoll_event event;
event.data.fd = fd;

// ET模式
if (1 == TRIGMode)
event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
else
// LT模式
event.events = ev | EPOLLONESHOT | EPOLLRDHUP;

epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}


4.服务器接收http请求

浏览器(客户)端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应m_read_buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理

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
void WebServer::eventLoop()
{
bool timeout = false;
bool stop_server = false;

while (!stop_server)
{
// 等待所监控文件描述符上有事件的产生
int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
// EINTR错误的产生:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。
// 例如:在socket服务器端,设置了信号捕获机制,有子进程,
// 当在父进程阻塞于慢系统调用时由父进程捕获到了一个有效信号时,
// 在epoll_wait时,因为设置了alarm定时触发警告,导致每次返回-1,errno为EINTR,对于这种错误返回
// 忽略这种错误,让epoll报错误号为4时,再次做一次epoll_wait
// EINTR错误的产生(系统调用被打断,产生的假错误)---当发现是假错误,就需要重新epoll_wait再次进行系统调用
if (number < 0 && errno != EINTR)
{
LOG_ERROR("%s", "epoll failure");
break;
}

// 对所有就绪事件进行处理
for(int i =0; i<number; i++)
{
int sockfd = events[i].data.fd;
// 若就绪的负责监听的套接字,处理新到的客户连接
if (sockfd == m_listenfd)
{
// 根据监听套接字事件触发方式,接收客户端的连接
bool flag = dealclientdata();
if (false == flag)
continue;
}
// 处理异常事件
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
// 服务器端关闭该http连接,移除对应的定时器
util_timer *timer = users_timer[sockfd].timer;
deal_timer(timer, sockfd);
}
// 处理定时器信号(就绪的套接字是发送定时器信号的管道套接字读端)
else if ((sockfd == m_pipefd[0]) && (events[i].events & EPOLLIN))
{
// 接收到SIGALRM信号,timeout设置为True
bool flag = dealwithsignal(timeout, stop_server);
if (false == flag)
LOG_ERROR("%s", "dealclientdata failure");
}
// 处理客户连接上接收到的数据(通信套接字接收到的数据)
else if (events[i].events & EPOLLIN)
{
dealwithread(sockfd);
}
else if (events[i].events & EPOLLOUT)
{
std::cout << "write event..." << std::endl;
dealwithwrite(sockfd);
}
}

// 处理定时器为非必须事件,收到信号并不是立马处理
// 完成读写事件后,再进行处理
if (timeout)
{
// 定时处理任务,重新定时以不断触发SIGALRM信号
utils.timer_handler();
LOG_INFO("%s", "timer tick");
timeout = false;
}
}
}