TODO
本文内容
上篇,我们对http连接的基础知识、服务器接收请求的处理流程进行了介绍,本篇将结合流程图和代码分别对状态机和服务器解析请求报文进行详解。
流程图部分,描述主、从状态机调用关系与状态转移过程。
代码部分,结合代码对http请求报文的解析进行详解。
流程图与状态机
从状态机负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。
主状态机
三种状态,标识解析位置。
- CHECK_STATE_REQUESTLINE,解析请求行
- CHECK_STATE_HEADER,解析请求头
- CHECK_STATE_CONTENT,解析消息体,仅用于解析POST请求
从状态机
三种状态,标识解析一行的读取状态。
- LINE_OK,完整读取一行
- LINE_BAD,报文语法有误
- LINE_OPEN,读取的行不完整
代码分析-http报文解析
上篇中介绍了服务器接收http请求的流程与细节,简单来讲,浏览器端发出http连接请求,服务器端主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列后,工作线程从任务队列中取出一个任务进行处理。
各子线程通过process函数对任务进行处理,调用process_read函数和process_write函数分别完成报文解析与报文响应两个任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| void http_conn::process() { HTTP_CODE read_ret=process_read();
if(read_ret==NO_REQUEST) { modfd(m_epollfd,m_sockfd,EPOLLIN); return; }
bool write_ret=process_write(read_ret); if(!write_ret) { close_conn(); } modfd(m_epollfd,m_sockfd,EPOLLOUT); }
|
本篇将对报文解析的流程和process_read函数细节进行详细介绍。
HTTP_CODE含义
表示HTTP请求的处理结果,在头文件中初始化了八种情形,在报文解析时只涉及到四种。
- NO_REQUEST
- GET_REQUEST
- BAD_REQUEST
- INTERNAL_ERROR
- 服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发
解析报文整体流程
process_read通过while循环,将主从状态机进行封装,对报文的每一行进行循环处理。
- 判断条件
- 主状态机转移到CHECK_STATE_CONTENT,该条件涉及解析消息体
- 从状态机转移到LINE_OK,该条件涉及解析请求行和请求头部
- 两者为或关系,当条件为真则继续循环,否则退出
- 循环体
- 从状态机读取数据
- 调用get_line函数,通过m_start_line将从状态机读取数据间接赋给text
- 主状态机解析text
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
|
char* get_line(){ return m_read_buf+m_start_line; }
http_conn::HTTP_CODE http_conn::process_read() { LINE_STATUS line_status=LINE_OK; HTTP_CODE ret=NO_REQUEST; char* text=0;
while((m_check_state==CHECK_STATE_CONTENT && line_status==LINE_OK)||((line_status=parse_line())==LINE_OK)) { text=get_line();
m_start_line=m_checked_idx;
switch(m_check_state) { case CHECK_STATE_REQUESTLINE: { ret=parse_request_line(text); if(ret==BAD_REQUEST) return BAD_REQUEST; break; } case CHECK_STATE_HEADER: { ret=parse_headers(text); if(ret==BAD_REQUEST) return BAD_REQUEST; else if(ret==GET_REQUEST) { return do_request(); } break; } case CHECK_STATE_CONTENT: { ret=parse_content(text);
if(ret==GET_REQUEST) return do_request();
line_status=LINE_OPEN; break; } default: return INTERNAL_ERROR; } } return NO_REQUEST; }
|
从状态机逻辑
上一篇的基础知识讲解中,对于HTTP报文的讲解遗漏了一点细节,在这里作为补充。
在HTTP报文中,每一行的数据由\r\n作为结束字符,空行则是仅仅是字符\r\n。因此,可以通过查找\r\n将报文拆解成单独的行进行解析,项目中便是利用了这一点。
从状态机负责读取buffer中的数据,将每行数据末尾的\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 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
|
http_conn::LINE_STATUS http_conn::parse_line() { char temp; for(;m_checked_idx<m_read_idx;++m_checked_idx) { temp=m_read_buf[m_checked_idx]; if(temp=='\r'){ if((m_checked_idx+1)==m_read_idx) return LINE_OPEN; else if(m_read_buf[m_checked_idx+1]=='\n'){ m_read_buf[m_checked_idx++]='\0'; m_read_buf[m_checked_idx++]='\0'; return LINE_OK; } return LINE_BAD; }
else if(temp=='\n') { if(m_checked_idx>1&&m_read_buf[m_checked_idx-1]=='\r') { m_read_buf[m_checked_idx-1]='\0'; m_read_buf[m_checked_idx++]='\0'; return LINE_OK; } return LINE_BAD; } } return LINE_OPEN; }
|
主状态机逻辑
主状态机初始状态是CHECK_STATE_REQUESTLINE,通过调用从状态机来驱动主状态机,在主状态机进行解析前,从状态机已经将每一行的末尾\r\n符号改为\0\0,以便于主状态机直接取出对应字符串进行处理。
- CHECK_STATE_REQUESTLINE
- 主状态机的初始状态,调用parse_request_line函数解析请求行
- 解析函数从m_read_buf中解析HTTP请求行,获得请求方法、目标URL及HTTP版本号
- 解析完成后主状态机的状态变为CHECK_STATE_HEADER
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
|
http_conn::HTTP_CODE http_conn::parse_request_line(char *text) { m_url=strpbrk(text," \t");
if(!m_url) { return BAD_REQUEST; }
*m_url++='\0';
char *method=text; if(strcasecmp(method,"GET")==0) m_method=GET; else if(strcasecmp(method,"POST")==0) { m_method=POST; cgi=1; } else return BAD_REQUEST;
m_url+=strspn(m_url," \t");
m_version=strpbrk(m_url," \t"); if(!m_version) return BAD_REQUEST; *m_version++='\0'; m_version+=strspn(m_version," \t");
if(strcasecmp(m_version,"HTTP/1.1")!=0) return BAD_REQUEST;
if(strncasecmp(m_url,"http://",7)==0) { m_url+=7; m_url=strchr(m_url,'/'); } if(strncasecmp(m_url,"https://",8)==0) { m_url+=8; m_url=strchr(m_url,'/'); }
if(!m_url||m_url[0]!='/') return BAD_REQUEST;
if(strlen(m_url)==1) strcat(m_url,"judge.html");
m_check_state=CHECK_STATE_HEADER; return NO_REQUEST; }
|
解析完请求行后,主状态机继续分析请求头。在报文中,请求头和空行的处理使用的同一个函数,这里通过判断当前的text首位是不是\0字符,若是,则表示当前处理的是空行,若不是,则表示当前处理的是请求头。
- CHECK_STATE_HEADER
- 调用parse_headers函数解析请求头部信息
- 判断是空行还是请求头,若是空行,进而判断content-length是否为0,如果不是0,表明是POST请求,则状态转移到CHECK_STATE_CONTENT,否则说明是GET请求,则报文解析结束。
- 若解析的是请求头部字段,则主要分析connection字段,content-length字段,其他字段可以直接跳过,各位也可以根据需求继续分析。
- connection字段判断是keep-alive还是close,决定是长连接还是短连接
- content-length字段,这里用于读取post请求的消息体长度
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
|
http_conn::HTTP_CODE http_conn::parse_headers(char *text) { if(text[0]=='\0') { if(m_content_length!=0) { m_check_state=CHECK_STATE_CONTENT; return NO_REQUEST; } return GET_REQUEST; } else if(strncasecmp(text,"Connection:",11)==0) { text+=11;
text+=strspn(text," \t"); if(strcasecmp(text,"keep-alive")==0) { m_linger=true; } } else if(strncasecmp(text,"Content-length:",15)==0) { text+=15; text+=strspn(text," \t"); m_content_length=atol(text); } else if(strncasecmp(text,"Host:",5)==0) { text+=5; text+=strspn(text," \t"); m_host=text; } else{ printf("oop!unknow header: %s\n",text); } return NO_REQUEST; }
|
如果仅仅是GET请求,如项目中的欢迎界面,那么主状态机只设置之前的两个状态足矣。
因为在上篇推文中我们曾说道,GET和POST请求报文的区别之一是有无消息体部分,GET请求没有消息体,当解析完空行之后,便完成了报文的解析。
但后续的登录和注册功能,为了避免将用户名和密码直接暴露在URL中,我们在项目中改用了POST请求,将用户名和密码添加在报文中作为消息体进行了封装。
为此,我们需要在解析报文的部分添加解析消息体的模块。
1 2
| 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,此时可以跳出循环,完成报文解析任务。
- CHECK_STATE_CONTENT
- 仅用于解析POST请求,调用parse_content函数解析消息体
- 用于保存post请求消息体,为后面的登录和注册做准备
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
http_conn::HTTP_CODE http_conn::parse_content(char *text) { if(m_read_idx>=(m_content_length+m_checked_idx)){
text[m_content_length]='\0';
m_string = text;
return GET_REQUEST; } return NO_REQUEST; }
|
状态机和HTTP报文解析是项目中最繁琐的部分,这次我们一举解决掉它,希望对各位小伙伴在理解项目的过程中有所帮助。
下篇,我们将对HTTP报文响应进行详解。