TinywebServer代码详解-http连接处理-中(5)
TinywebServer代码详解–http连接处理-中(5)
该blog内容转自:最新版Web服务器项目详解 - 05 http连接处理(中)
该blog对上述内容进行补充(在本人的角度)
结合此前记录的blog一起学习:牛客WebServer项目实战(点击跳转)
原项目地址(点击跳转)
博主添加注释后项目地址(点击跳转)
一、流程图与状态机
1.流程图
使用主从状态机模式对请求报文进行解析,从状态机负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机
2.主从状态机
(1)主状态机
主状态机通过,3种状态,标识当前请求报文解析的位置
CHECK_STATE_REQUESTLINE
,解析请求行CHECK_STATE_HEADER
,解析请求头CHECK_STATE_CONTENT
,解析消息体,仅用于解析POST请求
(2)从状态机
从状态机,从请求报文中一行一行的读取解析,使用3种状态标识解析一行的读取状态
LINE_OK
,完整读取一行LINE_BAD
,报文语法有误LINE_OPEN
,读取的行不完整
二、http报文解析
浏览器端发出http连接请求,服务器端主线程创建http对象接收请求并将所有数据读入对应buffer(同步IO模拟Proactor模式,主线程负责文件描述符的监听,以及IO操作的完成,工作线程只要负责数据的逻辑处理工作),将该对象插入任务队列后,工作线程从任务队列中取出一个任务进行处理
1.请求/响应任务处理
(1)process()
线程池的工作子线程,通过调用process
函数对任务进行处理,调用process_read函数和process_write函数分别完成报文解析与报文响应两个任务,报文响应写入m_write_buf
中还需要为套接字注册写事件,并且进行监听
1 | /* |
(2)HTTP_CODE含义
表示HTTP请求的处理结果,在头文件中初始化了八种情形,在报文解析时只涉及到四种
NO_REQUEST
- 请求不完整,需要继续读取请求报文数据
GET_REQUEST
- 获得了完整的HTTP请求
BAD_REQUEST
- HTTP请求报文有语法错误
INTERNAL_ERROR
- 服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发
2.解析报文整体流程
主从状态机模式
process_read
函数通过while
循环,将主从状态机进行封装,对报文的每一行进行循环处理
判断条件:
- 主状态机转移到
CHECK_STATE_CONTENT
(请求体/请求数据),该条件涉及解析消息体- 从状态机转移到
LINE_OK
(从状态机成功解析请求报文的一行数据),该条件涉及解析请求行和请求头部- 两者为或关系,当条件为真则继续循环,否则退出
循环体:
- 从状态机读取数据
- 调用
get_line
函数,通过m_start_line
将从状态机读取数据间接赋给text
- 主状态机解析
text
(1)process_read()
有限状态机(通过主从状态机模式对报文进行解析)处理请求报文,在switch-case语句中根据当前主状态机的状态对报文进行处理
- 若函数体中的
char *text
指向的是请求报文的一行内容 - 主状态机若当前处于的状态为请求行
CHECK_STATE_REQUESTLINE
,主状态机可以通过函数parse_request_line
对text
指向的一行数据进行解析,若解析成功,parse_request_line
函数会将主状态机的状态转移至请求头CHECK_STATE_HEADER
并且返回NO_REQUEST
表示当前请求报文数据不完整,还需要继续获取客户端数据,是因为一个请求报文还有,请求头、空行、请求体需要进行解析 - 若主状态机的状态已经转移到请求头
CHECK_STATE_HEADER
,主状态机可以通过函数parse_headers
对多行text
内容(因为请求头的内容可以是多行组成)进行解析。parse_headers
函数不仅需要解析请求头还需要解析空行。对text中内容进行判断,因为若是空行则text指向的内容为\0\0
(此前从状态机以及将请求报文空行中的\r\n
变为了\0\0
)- 若
parse_headers
函数当前解析的为空行,需要进一步判断字段m_content_length
是为有值,若不为0,表示还当前的请求报文中还存在请求体(请求数据)需要解析,则继续将主状态机的状态转移到请求体CHECK_STATE_CONTENT
,并且返回NO_REQUEST
表示当前请求报文数据不完整,还需要继续获取客户端数据,是因为还需要进一步获取请求报文的请求体进行解析。若字段m_content_length
为0,则表示当前请求报文不存在请求体,表示当前请求为GET
请求,不需要进一步获取请求体数据进行解析,因此直接返回GET_REQUEST
表示获得了一个完整的GET
请求 - 若
parse_headers
函数当前解析的行均为请求头数据内容,则会对text内容对应的请求头每行中对应字段的内容进行逐个解析,并且返回NO_REQUEST
表示当前请求报文数据不完整,还需要继续获取客户端数据,是因为还需要继续获取请求报文的空行、请求体进行解析
- 若
注意:GET请求与POST请求的区别之一是请求报文有无消息体
1 | /* |
为什么该函数中需要使用如下的判断:
1 | while((m_check_state==CHECK_STATE_CONTENT && line_status==LINE_OK)||((line_status=parse_line())==LINE_OK)) |
- 在
GET
请求报文中,每一行都是\r\n
作为结束,所以对报文进行拆解时,仅用从状态机的状态line_status=parse_line())==LINE_OK
语句即可 - 但,在
POST
请求报文中,消息体的末尾没有任何字符,所以不能使用从状态机的状态,这里转而使用主状态机的状态作为循环入口条件 - 而,后面的
&& line_status==LINE_OK
作用是:解析完消息体后,报文的完整解析就完成了,但此时主状态机的状态还是CHECK_STATE_CONTENT
,也就是说,符合循环入口条件,还会再次进入循环,这并不是我们所希望的。为此,增加了该语句,并在完成消息体解析后,将line_status
变量更改为LINE_OPEN
,此时可以跳出循环,完成报文解析任务
3.从状态机的逻辑
在HTTP报文中,每一行的数据由\r\n
作为结束字符,空行则是仅仅是字符\r\n
。因此,可以通过查找\r\n
将报文拆解成单独的行进行解析,项目中便是利用了这一点
从状态机负责读取m_read_buffe
中的数据,将每行数据末尾的\r\n
置为\0\0
,并更新从状态机在buffer
中读取的位置m_checked_idx
,以此来驱动主状态机解析
从状态机从
m_read_buf
中逐字节读取,判断当前字节是否为\r
- 接下来的字符是
\n
,将\r\n
修改成\0\0
,将m_checked_idx
指向下一行的开头,则返回LINE_OK
,表示读取到一个完整的行- 接下来达到了
buffer
末尾,表示buffer
还需要继续接收,返回LINE_OPEN
,表示行数据尚且不完整- 否则,表示语法错误,返回
LINE_BAD
当前字节不是
\r
,判断是否是\n
(一般是上次读取到\r
就到了buffer
末尾,没有接收完整,再次接收时会出现这种情况)
- 如果前一个字符是
\r
,则将\r\n
修改成\0\0
,将m_checked_idx
指向下一行的开头,则返回LINE_OK
- 当前字节既不是
\r
,也不是\n
,表示接收不完整,需要继续接收,返回LINE_OPEN
(1)parse_line()
1 | /* |
4.主状态机逻辑
主状态机初始状态是CHECK_STATE_REQUESTLINE
(请求报文从请求行开始),通过调用从状态机来驱动主状态机,在主状态机进行解析前,从状态机(parse_line
)已经将每一行的末尾\r\n
符号改为\0\0
,以便于主状态机直接取出对应字符串(每次从请求报文中取出一行)进行处理
(1)parse_request_line()
CHECK_STATE_REQUESTLINE
- 主状态机的初始状态,调用
parse_request_line
函数解析请求行- 解析函数从
m_read_buf
中解析HTTP
请求行,获得请求方法、目标URL
及HTTP
版本号- 解析完成后主状态机的状态变为
CHECK_STATE_HEADER
请求头
1 | /* |
(2)parse_headers()
解析完请求行后,主状态机继续分析请求头。在报文中,请求头和空行的处理使用的同一个函数parse_headers
,这里通过判断当前的text
首位是不是\0
字符,若是,则表示当前处理的是空行,若不是,则表示当前处理的是请求头
CHECK_STATE_HEADER
- 调用
parse_headers
函数解析请求头部信息- 判断是空行还是请求头
- 若是空行,进而判断
content-length
是否为0,如果不是0
,表明是POST
请求,则状态转移到CHECK_STATE_CONTENT
,否则说明是GET
请求,则此时已经获得了一个完整的GET
请求则报文解析结束- 若解析的是请求头部字段,则主要分析
connection
字段,content-length
字段,其他字段可以直接跳过,也可以根据需求继续分析connection
字段判断是keep-alive
还是close
,决定是长连接还是短连接content-length
字段,这里用于读取post
请求的消息体长度
1 | /* |
如果仅仅是GET
请求,如项目中的欢迎界面,那么主状态机只设置之前的两个状态就满足了,但是GET
请求与POST
的请求区别就是有无请求体(请求数据)部分。GET
请求没有消息体,当解析完空行之后,便完成了报文的解析
但后续的登录和注册功能,为了避免将用户名和密码直接暴露在URL
中,我们在项目中改用了POST
请求,将用户名和密码添加在报文中作为消息体进行了封装
为此,我们需要在解析报文的部分添加解析消息体的模块
(3)parse_content()
若当前请求为POST
请求则还需要对请求报文的请求体部分进行解析
CHECK_STATE_CONTENT
- 仅用于解析
POST
请求,调用parse_content
函数解析消息体- 用于保存
POST
请求消息体,为后面的登录和注册做准备
1 | /* |