64

Nginx源码阅读笔记-接收HTTP请求流程

 5 years ago
source link: https://www.codedump.info/post/20190131-nginx-read-http-request/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前面已经描述过nginx的事件模块了,接下来具体分析nginx如何接收一个HTTP请求,下一部分接着解析nginx解析HTTP请求的流程。

协议状态机编程模式

TCP协议是一种流协议(stream protocol),这意味着数据是以字节流形式给数据接收者的,一次网络接收不一定能接收完毕,需要上面的应用层根据自己协议的情况来解析处理。它的数据没有边界,需要应用层自己根据协议来判断边界的存在。

如果两次请求,分开为几次接收,但是某次接收的数据中,有跨两次请求的数据,这就是所谓的“粘包(sticky-package)”问题。如下图所示:

I36raez.png!web

结合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请求的流程。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK