内容简介:前面已经描述过nginx的事件模块了,接下来具体分析nginx如何接收一个HTTP请求,下一部分接着解析nginx解析HTTP请求的流程。TCP协议是一种流协议(stream protocol),这意味着数据是以字节流形式给数据接收者的,一次网络接收不一定能接收完毕,需要上面的应用层根据自己协议的情况来解析处理。它的数据没有边界,需要应用层自己根据协议来判断边界的存在。如果两次请求,分开为几次接收,但是某次接收的数据中,有跨两次请求的数据,这就是所谓的“粘包(sticky-package)”问题。如下图所示
前面已经描述过nginx的事件模块了,接下来具体分析nginx如何接收一个HTTP请求,下一部分接着解析nginx解析HTTP请求的流程。
协议状态机编程模式
TCP协议是一种流协议(stream protocol),这意味着数据是以字节流形式给数据接收者的,一次网络接收不一定能接收完毕,需要上面的应用层根据自己协议的情况来解析处理。它的数据没有边界,需要应用层自己根据协议来判断边界的存在。
如果两次请求,分开为几次接收,但是某次接收的数据中,有跨两次请求的数据,这就是所谓的“粘包(sticky-package)”问题。如下图所示:
结合epoll之类的事件派发器来设计一个TCP协议的服务器时,因为并不能确保每一次接收数据,都能完整的接收到协议所需的所有数据。因此一般而言,写一个高性能服务器的协议解析部分,会以状态机的方式来实现,即定义了协议数据的每个部分,如下伪代码所示:
// 定义协议头数据 typedef struct header_t { // 协议版本号 int version; // 定义body部分大小 int size; } header_t; // 定义协议数据 typedef struct protocol_t { header_t header; char body[0]; } protocol; // 定义接收数据的状态机类型 enum state_t { RECV_HEADER, // 接收包头 RECV_BODY, // 接收包体 PROCESS_PROTOCOL, // 处理协议 SEND_RESPONSE // 发送回复 }; // 处理请求的状态机 void statemachine() { switch (state) { case RECV_HEADER: // 接收协议包头数据 // 接收完毕之后,切换state到RECV_BODY case RECV_BODY: // 接收协议包体数据 // 接收完毕之后,切换state到PROCESS_PROTOCOL case PROCESS_PROTOCOL: // 处理协议 // 处理完毕之后,切换state到SEND_RESPONSE case SEND_RESPONSE: // 发送应答 } }
如上面的伪代码所示,接收一个请求之后,会初始化一个变量state用于保存当前协议处理的状态类型,假如第一次接收数据时还不能接收完毕协议的数据,就将接收fd重新放入到事件派发器中,下一次被唤醒之后再根据当前的状态继续接收数据进行处理。
协议的包头部分,一般有两个特点:
- 包头大小固定,这个原因还是因为不能确定每次都能接收完整的数据,而总是需要一个长度或者确定的结束符(如HTTP协议最后的两个\r\n)来告诉你是否接收完了数据。如果后面还需要对包头数据有扩展,会根据不同的版本进行区别,所以一般而言包头部分还会有个版本号,以便以后包头数据发生了变化,可以根据包头来进行区分。
- 包头内有字段定义包体部分的大小,这样在状态切换到接收包体时才有依据何时接收完毕了数据。
这是一般的思路,实际中还有一些不一样的地方:
- 有的服务不是以暴露给使用者事件派发器接口的方式来实现的,内部采用了协程之类的机制,这样应用开发者写起代码了就好像独占了一个“线程”,比如使用golang来写代码,每个连接对应goroutine的情况下,这时候就没有必要连接内部保存一个状态变量了。
- 上面的协议定义中,用长度来定义每部分的边界:即包头固定长度,包头内部的长度成员来表示包体的大小。有一些协议就不是这么实现协议的,比如HTTP协议采用连续两个\r\n来表示包头部分结束或者包体部分结束(在包体部分长度不确定时)。
有了协议状态机之后,处理前面的粘包问题就很简单了,无非就是当事件被回调的时候进入状态机,看当前在哪个协议状态来进行处理。
Nginx接收HTTP请求流程
以上解释了以状态机来驱动的协议接收流程,Nginx也是类似状态机的机制,只不过内部并没有一个state这样的变量保存当前到哪个状态了,而是切换不同状态对应的handler函数来做解析。
nginx既可以做7层代理,也可以做4层代理,因此在实现的时候,需要考虑兼容不同的应用层协议,具体来说设置了一个监听端口的时候,该端口可能处理的是HTTP请求,也可能是配置在stream块中处理的是4层的TCP请求。
而nginx采用了统一的ngx_connection_t结构体来表示一个tcp连接,这里就要根据不同的协议做区分了,来看看这个结构体中相关的字段:
字段 | 说明 |
---|---|
void *data | 连接相关数据 |
ngx_event_t *read | 读事件 |
ngx_event_t *write | 写事件 |
ngx_recv_pt recv | 接收请求的函数指针,每个平台可能有区别 |
ngx_send_pt send | 发送应答的函数指针 |
即:不同的tcp协议,对应的连接相关的data是不一样的,这样读写事件对应的回调函数也就不一样。
类似的,nginx中ngx_listening_t来表示监听socket,其中的成员handler也是区分了不同的协议有不同的注册回调函数。
如果这个监听端口,处理的是HTTP请求,那么注册进去的回调函数就是ngx_http_init_connection,这样在接收到一个HTTP请求时就会回调该函数进行处理。
接下来具体看接收HTTP请求的流程中对应的几个回调函数。
ngx_http_init_connection
这个函数是接收到HTTP请求之后的第一个回调函数,用于初始化请求连接相关的一些数据,主要做的工作是:
- 初始化读事件的回调函数为ngx_http_wait_request_handler,而写事件的回调函数是一个什么也不做的空函数ngx_http_empty_handler。这是因为,该接收完连接还没有应答数据,所以即使可写也什么都不做。
- 将读事件添加到一个定时器中,这样如果一段时间内都接收不完请求则关闭连接,不至于被占用资源。
ngx_http_wait_request_handler
ngx_http_wait_request_handler函数是接收到HTTP请求之后第一次被读事件回调的函数,主要工作:
- 判断连接是否超时,如果超时则关闭连接。
- 分配读缓存空间。
- 调用recv函数指针读客户端请求。此后需要对这个函数调用的结果做判断:
- NGX_AGAIN:说明还没有读完请求,将会再次将读事件加入定时器,读事件加入到epoll监控事件中,等待下一次被读事件唤醒,或者超时。
- NGX_ERROR:请求出错了,关闭连接。
- 没有读到任何数据,说明对端关闭连接。
- 其他情况说明已经读出一部分数据,此时将该读事件的handler切换为ngx_http_process_request_line,开始接收请求行。
ngx_http_process_request_line
ngx_http_process_request_line负责接收处理请求行,这个函数可能被多次调用,就跟前面分析状态机接收请求的情况一样,只要状态没有变化,下一次被读事件唤醒还是会走到响应的状态处理函数中。
- 判断连接是否超时,如果超时则关闭连接。
- 初始化rc为 NGX_AGAIN,接下来进入一个循环处理的流程,分别经历了以下流程:
- 如果rc == NGX_AGAIN:调用ngx_http_read_request_header函数读请求头,返回NGX_AGAIN或者NGX_ERROR则退出循环返回。
- 调用ngx_http_parse_request_line函数分析请求行,下面区分该函数的返回值不同情况处理:
- rc == NGX_OK,说明调用成功:
- 初始化ngx_http_request_t相关的数据,如request_line、method_name等。
- 调用ngx_http_process_request_uri处理请求URI。
- 分析请求HOST。
- 初始化headers_in链表,准备解析header。
- 将读事件的hander切换为ngx_http_process_request_headers,准备处理HTTP header。
- rc != NGX_AGAIN:说明出错了,调用ngx_http_finalize_request终止请求。
- 其他请求就是rc == NGX_AGAIN的情况了,说明此时还没有接受完毕HTTP请求。如果r->header_in->pos == r->header_in->end,说明接受header的缓冲区已经满了,需要分配一块大内存来存储header。
ngx_http_process_request_headers
ngx_http_parse_request_line分析请求行成功之后,就将读事件的handler切换为ngx_http_process_request_headers,该函数处理请求头:
- 判断连接是否超时,如果超时则关闭连接。
- 初始化rc为 NGX_AGAIN,接下来进入一个循环处理的流程,分别经历了以下流程:
- rc == NGX_AGAIN:
- 如果r->header_in->pos == r->header_in->end:说明header_in缓冲区已经被用尽,需要分配大空间来接收header。
- 调用ngx_http_read_request_header函数读请求头,如果返回值为NGX_AGAIN或者NGX_ERROR,则退出循环返回。
- 调用ngx_http_parse_header_line函数解析header_in缓冲区中的字节流,分析header,根据返回值区分以下情况:
- rc == NGX_OK:说明成功解析一个HTTP header,向headers_in数组分配一个新的元素用于存储该header。
- rc == NGX_HTTP_PARSE_HEADER_DONE:说明全部header已经解析完成,将处理HTTP请求的遍历http_state切换为NGX_HTTP_PROCESS_REQUEST_STATE,调用ngx_http_process_request_header处理请求头,如果返回结果不为NGX_OK则退出循环返回,否则进入ngx_http_process_request处理HTTP请求。
- rc == NGX_AGAIN:说明还有未接收完毕的数据,退出循环等待下一次被读事件唤醒再次读取数据进行处理。
- 除了上面以外的其他情况,说明出错了,调用ngx_http_finalize_request终止请求。
以上,基本把接收一个HTTP请求中间过程中涉及到的主要函数分析了一遍。可以看到,nginx对接收HTTP请求的处理,也是状态机驱动的,区别于最开始说明的状态机编程模式,nginx没有在每个连接相关的结构体中用一个状态变量来表示当前的状态,而是通过切换读事件的handler来完成状态处理的切换。
以上的流程和涉及的函数,看起来很多,其实就是分了两部分可能会循环被调用的地方:
- 处理请求URI,对应函数ngx_http_process_request_line。
- 处理请求header,对应函数ngx_http_process_request_headers。
从这里的分析可以看到,高性能TCP协议服务器的一个重点就是读请求的流程,这部分不能阻塞,要点就是通过状态机驱动的模式来读取协议,nginx这里划分的很细致,不会因为一次网络IO阻塞住这个流程。
下一部分解析nginx处理HTTP请求的流程。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 关于在接收POST请求,Tomcat偶发性接收到的参数不全问题排查分析
- 异步接收MSMQ消息
- 如何突破商品期货Tick接收限制
- SpringMVC接收和响应json数据
- 如何使用 jq 接收 blob 数据
- 转的关于公众号接收信息的返回
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。