TinywebServer代码详解–http连接处理-中(5)

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

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

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

原项目地址(点击跳转)

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



一、流程图与状态机

1.流程图

使用主从状态机模式对请求报文进行解析,从状态机负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机

image-20240710203008931



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
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
/*
* func:由线程池的工作线程调用,对任务进行处理,这是处理HTTP请求入口函数
* process_read()/process_write()分别处理m_read_buf/m_write_buf中的数据进行报文的解析/响应
*/
void http_conn::process()
{
// 解析HTTP请求报文
HTTP_CODE read_ret = process_read();
if(read_ret == NO_REQUEST)
{
// NO_REQUEST请求报文,不完整,需要继续获取客户端数据
// 修改该通信套接字事件,重新注册EPOLLONESHOT事件,继续监听该通信套接字读事件
modfd(m_epollfd,m_sockfd,EPOLLIN,m_TRIGMode);
// 跳转主线程继续监测读事件
return;
}

// 当read_ret返回为其他情况,则会调用process_write 完成报文响应
// NO_RESOURCE 请求资源不存在
// BAD_REQUEST HTTP请求报文有语法错误或请求资源为目录
// FORBIDDEN_REQUEST 请求资源禁止访问,没有读取权限
// FILE_REQUEST 请求资源可以正常访问
// INTERNAL_ERROR 服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发
// 调用process_write()生成响应报文
bool write_ret = process_write(read_ret);
if(!write_ret)
{
// 生成响应报文失败,关闭套接字连接
close_conn();
}
// 写成功了,为该套接字重新注册EPOLLONESHOT事件,继续监听写事件
modfd( m_epollfd, m_sockfd, EPOLLOUT,m_TRIGMode);
}

(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_linetext指向的一行数据进行解析,若解析成功,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
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
/*
* func:有限状态机处理请求报文
* 根据当前主状态机的状态对报文进行处理
* note:GET和POST请求报文的区别之一是有无消息体部分
*/
http_conn::HTTP_CODE http_conn::process_read()
{
// 初始化从状态机状态、HTTP请求解析结果
LINE_STATUS line_status = LINE_OK;
HTTP_CODE ret = NO_REQUEST;
char *text = 0;

// 两种情况均可以进入while
// 1. 从状态机成功获取请求报文的一行 请求行/请求头部/空行/ 均以/r/n 结束
// 2. 从状态机之前的请求行、请求头部、空行均成功获取,此时已经开始获取请求体信息,即:
// m_check_state == CHECK_STATE_CONTENT 当前主状态机状态为请求体(请求数据)
// parse_line为从状态机的具体实现
while(((m_check_state == CHECK_STATE_CONTENT) && (line_status == LINE_OK))
|| ((line_status == parse_line()) == LINE_OK))
{
// 解析到了一行完整数据 或者 解析到了请求体,并且之前的解析没问题,即可进入while循环

// 移动到当前处理行的初始位置
// m_start_line是行在buffer中的起始位置,将该位置后面的数据赋给text
// 此时从状态机已提前将一行的末尾字符\r\n变为\0\0,所以text可以直接取出完整的行进行解析
text = get_line();

// m_start_line是每一个数据行在m_read_buf中的起始位置
// 进入循环表示parse_line==LINE_OK
// 在parse_line()函数中,m_checked_idx成功移动到m_read_buf中下一次处理数据行的初始索引位置
// 因此通过m_checked_idx可以对m_read_buf中数据下一次需要处理数据行的行首位置更新
m_start_line = m_checked_idx;

/***************************************日志**************************************/
// printf("Got 1 http line: %s\n",text);
// LOG_INFO("%s", text);
/***************************************日志**************************************/

// 主状态机三种状态转移逻辑,主状态机状态(请求行or请求头or请求体)
switch (m_check_state)
{
// 请求行
case CHECK_STATE_REQUESTLINE:
{
// 解析HTTP请求行
ret = parse_request_line(text);
if(ret == BAD_REQUEST)
{
// 客户端请求报文语法错误,返回
return BAD_REQUEST;
}
break;
}
// 请求头
case CHECK_STATE_HEADER:
{
// 解析HTTP请求头
ret = parse_headers(text);
if(ret == BAD_REQUEST)
{
// 客户端请求报文语法错误,返回
return BAD_REQUEST;
}
// 完整解析GET请求后,跳转到报文响应函数
else if(ret == GET_REQUEST)
{
// 表示获得了一个完整的客户请求
// 因为可能存在请求报文,没有请求体
// 因此当parse_headers返回GET_REQUEST,则表示当前请求报文无请求体
// 即 获得了一个完整的客户请求
// 解析具体的请求信息
// 调用do_request完成请求资源映射
return do_request();
}
break;
}
// 请求体
case CHECK_STATE_CONTENT:
{
// 解析HTTP请求体
ret = parse_content(text);
// 完整解析客户的POST请求,跳转到报文响应函数
if(ret == GET_REQUEST)
{
// 解析具体请求信息
// 获取url 等请求资源
return do_request();
}
// 解析完消息体(请求体)即完成报文解析,避免再次进入循环,更新line_status
line_status = LINE_OPEN;
break;
}
default:
{
// 服务器内部错误
return INTERNAL_ERROR;
}
}
}
// 请求不完整,需要继续获取客户端数据
return NO_REQUEST;
}

为什么该函数中需要使用如下的判断

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
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
/*
* func:从状态机,用于(在请求报文中)分析出一行内容,并且将一行的末尾\r\n变为\0\0
* 解析一行,判断依据,每一行均以\r\n结束 空行则是仅仅是字符\r\n
* return:
* LINE_OK 读取到一个完整的行
* LINE_BAD 行出错
* LINE_OPEN 行数据尚且不完整
*/
http_conn::LINE_STATUS http_conn::parse_line()
{
char temp;
// m_read_idx 为读缓冲区m_read_buf的数据字节数量(指向缓冲区m_read_buf的数据末尾的下一个字节索引)
for(;m_checked_idx < m_read_idx; ++m_checked_idx)
{
// m_checked_idx为当前分析的字符位置
temp = m_read_buf[m_checked_idx];

// 如果当前是\r字符,则有可能会读取到完整行
if(temp == '\r')
{
if((m_checked_idx +1) == m_read_idx)
{
// 下一个字符达到了buffer结尾,则接收不完整,需要继续接收
return LINE_OPEN;
}
else if(m_read_buf[m_checked_idx+1] == '\n')
{
// 下一个字符是\n,将\r\n改为\0\0
// m_checked_idx++ (后增)执行完这句代码之后,m_checked_idx才会自增
m_read_buf[m_checked_idx++] = '\0';
m_read_buf[m_checked_idx++] = '\0';
// 此时的m_check指向的 下一行开始的索引位置
// 完整的接收一行
return LINE_OK;
}
// 否则行出错
return LINE_BAD;
}

// 如果当前读取的字符是\n,也有可能读取到完整行
// 一般是上次读取到\r就到buffer末尾了,没有接收完整,再次接收时会出现这种情况
else if(temp == '\n')
{
// 判断前一个字符是否是\r,若是则接收到完整行
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;
}
}
// 并没有找到\r\n,行数据不完整,需要继续接收
return LINE_OPEN;
}


4.主状态机逻辑

主状态机初始状态是CHECK_STATE_REQUESTLINE(请求报文从请求行开始),通过调用从状态机来驱动主状态机,在主状态机进行解析前,从状态机(parse_line)已经将每一行的末尾\r\n符号改为\0\0,以便于主状态机直接取出对应字符串(每次从请求报文中取出一行)进行处理

(1)parse_request_line()

CHECK_STATE_REQUESTLINE

  • 主状态机的初始状态,调用parse_request_line函数解析请求行
  • 解析函数从m_read_buf中解析HTTP请求行,获得请求方法、目标URLHTTP版本号
  • 解析完成后主状态机的状态变为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
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
/*
* func:解析http请求行(主状态机的初始状态),获得请求方法,目标url及http版本号
* 解析成功,主状态机状态转移至 请求头
* text是指向m_read_buf中每一行数据的起始位置,此时的m_read_buf已经被处理了,\r\n均被处理为\0
* 因此char *text就可以指向从该行起始位置至\0索引处截至的数据
* note:在HTTP报文中,请求行用来说明请求类型
* 请求行组成: 请求方法 空格 URL(请求的资源) 空格 HTTP协议版本
* GET /index HTTP/1.1 ---> 此处的URL 若无 所以为 /
* 要访问的资源以及所使用的HTTP版本,其中各个部分之间通过\t或空格分隔。
* 请求行中最先含有空格和\t任一字符的位置并返回
*/
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{
// strpbrk 判断 空格 与 \t 哪一个在text最先出现,成功找到返回指向该字符的指针
// 此处考虑制表符号 \t 可能是考虑服务器的容错能力
m_url = strpbrk(text," \t");
// 如果没有空格或\t,则报文格式有误
if (!m_url)
{
return BAD_REQUEST;
}

// 将该位置改为\0,用于将前面数据取出
// GET\0/index.html HTTP/1.1
// 置位空字符,字符串结束符---向将m_url索引处字符置\0,然后再后移
*m_url++ = '\0';

// 取出数据,并通过与GET和POST比较,以确定请求方式
char *method = text;
// strcasecmp函数比较两个字符是否相等,会忽略大小写的差异
if (strcasecmp(method, "GET") == 0)
{
// 此次请求为GET方法
m_method = GET;
}
else if (strcasecmp(method, "POST") == 0)
{
// 此次请求为POST方法
m_method = POST;
cgi = 1;
}
else
{
// 请求方法不正确,则请求报文的请求语法有错误
return BAD_REQUEST;
}

// 最好的情况:
// /index.html HTTP/1.1
// 以上面的实例: 此时的m_url指向 /index.html 的 / 位置
// 其他情况: 如 GET\0 /index.html HTTP/1.1 请求方法与URL之间多了很多空格
// m_url此时跳过了第一个空格或\t字符,但不知道之后是否还有
// 将m_url向后偏移,通过查找
// 继续跳过空格和\t字符,指向请求资源的第一个字符
// strspn: 返回 str1 中连续包含 str2 中字符的最长前缀的长度
m_url += strspn(m_url, " \t");
// 找到URL 与 HTTP协议版本之间的 空格或者制表符 \t
m_version = strpbrk(m_url, " \t");
if (!m_version)
{
// 如果没有空格或\t,则报文格式有误
return BAD_REQUEST;
}
// /index.html\0HTTP/1.1
// 置位空字符,字符串结束符
*m_version++ = '\0';

// 此时m_version指向 /index.html\0 后位置的字符,但是这个字符可能是空格或者\t,因此需要继续跳过
m_version += strspn(m_version, " \t");
//仅支持HTTP/1.1
if (strcasecmp(m_version, "HTTP/1.1") != 0)
{
return BAD_REQUEST;
}

// 对请求资源前7个字符进行判断
// 可能的URL会出现如下情况: http://192.168.110.129:10000/index.html
// 这里主要是有些报文的请求资源中会带有http://
// 这里需要对这种情况进行单独处理
if (strncasecmp(m_url, "http://", 7) == 0)
{
m_url += 7;
// 使用 strchr 查找第一个斜杠 /,这通常是URL路径的开始
m_url = strchr(m_url, '/');
}

//同样增加https情况
if (strncasecmp(m_url, "https://", 8) == 0)
{
m_url += 8;
// 使用 strchr 查找第一个斜杠 /,这通常是URL路径的开始
m_url = strchr(m_url, '/');
}

// 一般的不会带有上述两种符号,直接是单独的/或/后面带访问资源
if (!m_url || m_url[0] != '/')
return BAD_REQUEST;

// 当url为/时,显示欢迎界面
if (strlen(m_url) == 1)
strcat(m_url, "judge.html");

// 请求行处理完毕,将主状态机转移处理请求头
m_check_state = CHECK_STATE_HEADER;
return NO_REQUEST;
}

(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
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
/*
* func:解析http请求的一个头部信息
* 在报文中,请求头和空行的处理使用的同一个函数
* 通过判断当前的text首位是不是\0字符,若是,则表示当前处理的是空行,若不是,则表示当前处理的是请求头
* note:请求头可以有多行数据,而text每次只指向请求报文的一行数据
*/
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{
// 判断是空行还是请求头
// 解析空行
if(text[0] == '\0')
{
// 判断是GET还是POST请求
// GET请求,参数通过 URL 传递,无请求体数据
// POST请求,参数放到请求体中
// 判断是否有请求体数据,即m_content_length != 0
if(m_content_length != 0)
{
// 主状态机状态转移到 请求体 CHECK_STATE_CONTENT
m_check_state = CHECK_STATE_CONTENT;
// 因为该请求报文,还有请求体数据,因此请求数据不完整,继续获取
return NO_REQUEST;
}
// 该请求报文,无请求体数据,则得到一个完整的GET请求
return GET_REQUEST;
}

// 解析请求头
// 解析请求头部connection字段
else if(strncasecmp(text, "Connection:", 11) == 0)
{
// 比较text中的前11个字符是否是Connection,忽略大小写
text += 11;

// 跳过空格和\t字符
text += strspn(text, " \t");
if (strcasecmp(text, "keep-alive") == 0)
{
// 如果是长连接,则将linger标志设置为true
m_linger = true;
}
}
// 解析请求头部Content-length字段
else if(strncasecmp(text, "Content-length:", 15) == 0)
{
text += 15;
// 跳过空格和\t字符
text += strspn(text, " \t");
// 获得请求体数据长度
m_content_length = atol(text);
}
// 解析请求头部Host字段
else if(strncasecmp(text, "Host:", 5) == 0)
{
text += 5;
// 跳过空格和\t字符
text += strspn(text, " \t");
m_host = text;
}
else
{
LOG_INFO("oop!unknow header: %s", text);
}
// 请求不完整,继续获取请求数据
return NO_REQUEST;
}

如果仅仅是GET请求,如项目中的欢迎界面,那么主状态机只设置之前的两个状态就满足了,但是GET请求与POST的请求区别就是有无请求体(请求数据)部分。GET请求没有消息体,当解析完空行之后,便完成了报文的解析

但后续的登录和注册功能,为了避免将用户名和密码直接暴露在URL中,我们在项目中改用了POST请求,将用户名和密码添加在报文中作为消息体进行了封装

为此,我们需要在解析报文的部分添加解析消息体的模块


(3)parse_content()

若当前请求为POST请求则还需要对请求报文的请求体部分进行解析

CHECK_STATE_CONTENT

  • 仅用于解析POST请求,调用parse_content函数解析消息体
  • 用于保存POST请求消息体,为后面的登录和注册做准备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* func:仅用于解析POST请求报文中的请求体
* 用于保存post请求消息体,为后面的登录和注册做准备
* note:该项目将用于登陆与注册的用户名封装在POST请求报文的请求体中
*/
http_conn::HTTP_CODE http_conn::parse_content(char *text)
{
if (m_read_idx >= (m_content_length + m_checked_idx))
{
// 将请求体末尾字符的下一个位置标识为\0
// 方便通过text直接获取请求体内容
text[m_content_length] = '\0';
// 获取请求体内容
// POST请求中最后为输入的用户名和密码
m_string = text;
// 获得一个完整的客户请求,此时为POST请求
return GET_REQUEST;
}
// 客户端请求体内容还没有完全得到,需要继续获取请求数据
return NO_REQUEST;
}