TinywebServer代码详解-http连接处理-上(4)
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
多路转接的意思是在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作**。epoll
是select
和poll
的升级版,相较于select
与poll
,epoll
改进了工作方式,因此它更加高效,但是与poll
一样,epoll
不可以跨平台,只能在Linux
上面使用
1.项目为什么使用epoll
2.相关函数
(1)epoll_create()
epoll_create()
函数的作用是创建一个红黑树模型的实例(创建一个指示epoll
内核事件表的文件描述符),用于管理待检测的文件描述符的集合,size
不起作用
1 |
|
参数:
- size:在Linux内核2.6.8版本以后,这个参数是被忽略的,只需要指定一个大于0的数值就可以了
返回值:
- 失败:返回-1
- 成功:返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的
epoll
实例了
(2)epoll_ctl()
函数的作用是管理红黑树实例上的节点(操作内核事件表监控的文件描述符上的事件),可以进行添加、删除、修改操作
1 | // 联合体, 多个变量共用同一块内存 |
参数:
epfd
:epoll_create()
函数的返回值(epoll_create的句柄),通过这个参数找到epoll
实例op
:这是一个枚举值,控制通过该函数执行什么操作EPOLL_CTL_ADD
:往epoll
模型中添加新的节点EPOLL_CTL_MOD
:修改epoll
模型中已经存在的节点EPOLL_CTL_DEL
:删除epoll
模型中的指定的节点
fd
:文件描述符,即要添加/修改/删除的文件描述符event
:epoll
事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件(告诉内核需要监听的事件)events
:委托epoll
检测的事件,struct epoll_event
结构体中的字段events
EPOLLIN
:读事件, 接收数据, 检测读缓冲区,如果有数据该文件描述符就绪EPOLLOUT
:写事件, 发送数据, 检测写缓冲区,如果可写该文件描述符就绪EPOLLERR
:异常事件
data
:用户数据变量,这是一个联合体类型,通常情况下使用里边的fd
成员,用于存储待检测的文件描述符的值,在调用epoll_wait()
函数的时候这个值会被传出
返回值:
- 失败:返回-1
- 成功:返回0
1 | struct epoll_event { |
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); |
参数:
epfd
:epoll_create()
函数的返回值, 通过这个参数找到epoll
实例events
:传出参数, 这是一个结构体数组的地址, 里边存储了已就绪的文件描述符的信息maxevents
:修饰第二个参数, 结构体数组的容量(元素个数)timeout
:如果检测的epoll
实例中没有已就绪的文件描述符,该函数阻塞的时长, 单位ms
毫秒- 0:函数不阻塞,不管
epoll
实例中有没有就绪的文件描述符,函数被调用后都直接返回 - 大于0:如果
epoll
实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回 - -1:函数一直阻塞,直到
epoll
实例中有已就绪的文件描述符之后才解除阻塞
- 0:函数不阻塞,不管
返回值:
- 成功,等于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
的设计减少了需要复制的数据量,因为只有发生事件的文件描述符信息会被复制回用户空间,与select
或poll
相比,这大大减少了不必要的数据传输和处理。此外,因为事件表是在内核中维护的,所以不需要每次调用时都传递大量数据结构,也就减少了系统调用的开销
内核判断就绪的文件描述符
- 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报文
浏览器端向服务器发送的为请求报文,服务器处理后返回给浏览器端的为响应报文
1.请求报文
HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。
其中,请求分为两种,GET和POST,具体的:
(1)GET
1 | GET /562f25980001b1b106000338.jpg HTTP/1.1 |
(2)POST
1 | POST / HTTP1.1 |
请求行:用来说明请求类型,要访问的资源以及所使用的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 | HTTP/1.1 200 OK |
状态行:由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 | STATE_MACHINE(){ |
该状态机包含三种状态:type_A
,type_B
和type_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)read_once()
循环读取从浏览器(客户端)端,发送的请求报文数据,直到无数据可读或对方关闭连接。从该http
连接的通信套接字读缓冲区读取数据到程序的读缓冲区m_read_buf
中,也是用于更新m_read_idx
,m_read_idx
用于记录当前读取到m_read_buf
中的数据字节数量
需要注意的是,在非阻塞的边沿工作模式(ET)下,需要一次性将连接的套接字读缓冲区的数据全部读取完毕
TinyWebServer/http/http_conn.cpp
1 | /* |
3.epoll相关代码
TinyWebServer/http/http_conn.cpp
下的相关函数代码
(1)setnonblocking()
使用fcntl()
函数将套接字文件描述符设置为非阻塞模式,这是因为在边沿工作模式下,套接字的读事件就绪通知,只会在套接字的读缓冲区从无数据变为有数据时发送,并且只会通知一次,那么必须要保证得到通知后将数据全部从读缓冲区中读出。那我们该如何应对这个情况呢?
方法:循环接收数据
1 | int len = 0; |
但是,这样做也是有弊端的,因为套接字操作默认是阻塞的,当读缓冲区数据被读完之后,读操作就阻塞了也就是调用的read()/recv()
函数被阻塞了,当前进程/线程被阻塞之后就无法处理其他操作了
要解决阻塞问题,就需要将套接字默认的阻塞行为修改为非阻塞,需要使用fcntl()
函数进行处理:
1 | // 设置完成之后, 读写都变成了非阻塞模式 |
通过上述分析就可以得出一个结论:**epoll
在边沿模式下,必须要将套接字设置为非阻塞模式**,但是,这样就会引发另外的一个bug
,在非阻塞模式下,循环地将读缓冲区数据读到本地内存中,当缓冲区数据被读完了,调用的read()/recv()
函数还会继续从缓冲区中读数据,此时函数调用就失败了,返回-1,对应的全局变量 errno
值为 EAGAIN
或者 EWOULDBLOCK
如果打印错误信息会得到如下的信息:Resource temporarily unavailable
因此可以通过非阻塞状态下的套接字errno
的判断,什么时候缓冲区的数据全部读取完毕
检查recv
的返回值:
recv
函数的返回值可以提供关于读取状态的直接信息:
- 如果返回正数,这代表读取到的字节数。如果这个数小于你请求的字节数(第三个参数),这通常意味着缓冲区中没有更多的数据可读(或者数据正好被读完)
- 如果返回0,表示对端已经关闭了连接。在TCP协议中,这意味着对端的套接字已经执行了正常的关闭过程
- 如果返回-1,并且
errno
设置为EAGAIN
或EWOULDBLOCK
,表示没有数据可读,且套接字被设置为非阻塞模式。此时,可以认为之前已经读取了所有可用数据
1 | // 非阻塞模式下recv() / read()函数返回值 len == -1 |
setnonblocking()
函数如下:
1 | /* |
(2)addfd()
通过epoll_ctl
函数向epoll实例中添加需要监听的文件描述符。将内核事件表注册读事件,ET
模式,选择开启EPOLLONESHOT
。
1 | /* |
(3)removefd()
使用epoll_ctl
函数从epoll实例中删除监听的文件描述符,并且从内核维护的事件表中删除需要监控的事件
1 | /* |
(4)modfd()
修改文件描述符事件,将事件重置为EPOLLONESHOT
,以确保下一次可读时,EPOLLIN
事件能被触发
1 | /* |
4.服务器接收http请求
浏览器(客户)端发出http连接请求,主线程创建http
对象接收请求并将所有数据读入对应m_read_buffer
,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理
1 | void WebServer::eventLoop() |