nginx的请求接收流程(一)

今年我们组计划写一本nginx模块开发以及原理解析方面的书,整本书是以open book的形式在网上会定时的更新,网址为http://tengine.taobao.org/book/index.html。本书分析的nginx源码版本为1.2.0,环境为linux,事件处理模型为epoll,大部分分析流程都基于以上假设。我会负责其中一些章节的编写,所以打算在这里写一系列我负责章节内容相关的文章(主要包括nginx各phase模块的开发,nginx请求的处理流程等)。本篇文章主要会介绍nginx中请求的接收流程,包括请求头的解析和请求体的读取流程。

首先介绍一下rfc2616中定义的http请求基本格式:

[cpp] view plaincopy

  1. <span style="font-size:18px;">Request = Request-Line
  2. *(( general-header
  3. | request-header
  4. | entity-header ) CRLF)
  5. CRLF
  6. [ message-body ]   </span>

第一行是请求行(request line),用来说明请求方法,要访问的资源以及所使用的HTTP版本:

[cpp] view plaincopy

  1. <span style="font-size:18px;">Request-Line   = Method SP Request-URI SP HTTP-Version CRLF</span>

请求方法(Method)的定义如下,其中最常用的是GET,POST方法:

[cpp] view plaincopy

  1. <span style="font-size:18px;">Method = "OPTIONS"
  2. | "GET"
  3. | "HEAD"
  4. | "POST"
  5. | "PUT"
  6. | "DELETE"
  7. | "TRACE"
  8. | "CONNECT"
  9. | extension-method
  10. extension-method = token</span>

要访问的资源由统一资源地位符URI(Uniform Resource Identifier)确定,它的一个比较通用的组成格式(rfc2396)如下:

[cpp] view plaincopy

  1. <span style="font-size:18px;"><scheme>://<authority><path>?<query> </span>

一般来说根据请求方法(Method)的不同,请求URI的格式会有所不同,通常只需写出path和query部分。

http版本(version)定义如下,现在用的一般为1.0和1.1版本:

[cpp] view plaincopy

  1. HTTP/<major>.<minor>

请求行的下一行则是请求头,rfc2616中定义了3种不同类型的请求头,分别为general-header,request-header和entity-header,每种类型rfc中都定义了一些通用的头,其中entity-header类型可以包含自定义的头。

现在开始介绍nginx中请求头的解析,nginx的请求处理流程中,会涉及到2个非常重要的数据结构,ngx_connection_t和ngx_http_request_t,分别用来表示连接和请求,这2个数据结构在本书的前篇中已经做了比较详细的介绍,没有印象的读者可以翻回去复习一下,整个请求处理流程从头到尾,对应着这2个数据结构的分配,初始化,使用,重用和销毁。

nginx在初始化阶段,具体是在init process阶段的ngx_event_process_init函数中会为每一个监听套接字分配一个连接结构(ngx_connection_t),并将该连接结构的读事件成员(read)的事件处理函数设置为ngx_event_accept,并且如果没有使用accept互斥锁的话,在这个函数中会将该读事件挂载到nginx的事件处理模型上(poll或者epoll等),反之则会等到init process阶段结束,在工作进程的事件处理循环中,某个进程抢到了accept锁才能挂载该读事件。

[cpp] view plaincopy

  1. <span style="font-size:18px;">static ngx_int_t
  2. ngx_event_process_init(ngx_cycle_t *cycle)
  3. {
  4. ...
  5. /* 初始化用来管理所有定时器的红黑树 */
  6. if (ngx_event_timer_init(cycle->log) == NGX_ERROR) {
  7. return NGX_ERROR;
  8. }
  9. /* 初始化事件模型 */
  10. for (m = 0; ngx_modules[m]; m++) {
  11. if (ngx_modules[m]->type != NGX_EVENT_MODULE) {
  12. continue;
  13. }
  14. if (ngx_modules[m]->ctx_index != ecf->use) {
  15. continue;
  16. }
  17. module = ngx_modules[m]->ctx;
  18. if (module->actions.init(cycle, ngx_timer_resolution) != NGX_OK) {
  19. /* fatal */
  20. exit(2);
  21. }
  22. break;
  23. }
  24. ...
  25. /* for each listening socket */
  26. /* 为每个监听套接字分配一个连接结构 */
  27. ls = cycle->listening.elts;
  28. for (i = 0; i < cycle->listening.nelts; i++) {
  29. c = ngx_get_connection(ls[i].fd, cycle->log);
  30. if (c == NULL) {
  31. return NGX_ERROR;
  32. }
  33. c->log = &ls[i].log;
  34. c->listening = &ls[i];
  35. ls[i].connection = c;
  36. rev = c->read;
  37. rev->log = c->log;
  38. /* 标识此读事件为新请求连接事件 */
  39. rev->accept = 1;
  40. ...
  41. #if (NGX_WIN32)
  42. /* windows环境下不做分析,但原理类似 */
  43. #else
  44. /* 将读事件结构的处理函数设置为ngx_event_accept */
  45. rev->handler = ngx_event_accept;
  46. /* 如果使用accept锁的话,要在后面抢到锁才能将监听句柄挂载上事件处理模型上 */
  47. if (ngx_use_accept_mutex) {
  48. continue;
  49. }
  50. /* 否则,将该监听句柄直接挂载上事件处理模型 */
  51. if (ngx_event_flags & NGX_USE_RTSIG_EVENT) {
  52. if (ngx_add_conn(c) == NGX_ERROR) {
  53. return NGX_ERROR;
  54. }
  55. } else {
  56. if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
  57. return NGX_ERROR;
  58. }
  59. }
  60. #endif
  61. }
  62. return NGX_OK;
  63. }</span>

当一个工作进程在某个时刻将监听事件挂载上事件处理模型之后,nginx就可以正式的接收并处理客户端过来的请求了。这时如果有一个用户在浏览器的地址栏内输入一个域名,并且域名解析服务器将该域名解析到一台由nginx监听的服务器上,nginx的事件处理模型接收到这个读事件之后,会速度交由之前注册好的事件处理函数ngx_event_accept来处理。

在ngx_event_accept函数中,nginx调用accept函数,从已连接队列得到一个连接以及对应的套接字,接着分配一个连接结构(ngx_connection_t),并将新得到的套接字保存在该连接结构中,这里还会做一些基本的连接初始化工作:
首先给该连接分配一个内存池,初始大小默认为256字节,可通过connection_pool_size指令设置;
分配日志结构,并保存在其中,以便后续的日志系统使用;
初始化连接相应的io收发函数,具体的io收发函数和使用的事件模型及操作系统相关;
分配一个套接口地址(sockaddr),并将accept得到的对端地址拷贝在其中,保存在sockaddr字段;
将本地套接口地址保存在local_sockaddr字段,因为这个值是从监听结构ngx_listening_t中可得,而监听结构中保存的只是配置文件中设置的监听地址,但是配置的监听地址可能是通配符*,即监听在所有的地址上,所以连接中保存的这个值最终可能还会变动,会被确定为真正的接收地址;
将连接的写事件设置为已就绪,即设置ready为1,nginx默认连接第一次为可写;
如果监听套接字设置了TCP_DEFER_ACCEPT属性,则表示该连接上已经有数据包过来,于是设置读事件为就绪;
将sockaddr字段保存的对端地址格式化为可读字符串,并保存在addr_text字段;
最后调用ngx_http_init_connection函数初始化该连接结构的其他部分。

ngx_http_init_connection函数最重要的工作是初始化读写事件的处理函数:将该连接结构的写事件的处理函数设置为ngx_http_empty_handler,这个事件处理函数不会做任何操作,实际上nginx默认连接第一次可写,不会挂载写事件,如果有数据需要发送,nginx会直接写到这个连接,只有在发生一次写不完的情况下,才会挂载写事件到事件模型上,并设置真正的写事件处理函数,这里后面的章节还会做详细介绍;读事件的处理函数设置为ngx_http_init_request,此时如果该连接上已经有数据过来(设置了deferred accept),则会直接调用ngx_http_init_request函数来处理该请求,反之则设置一个定时器并在事件处理模型上挂载一个读事件,等待数据到来或者超时。当然这里不管是已经有数据到来,或者需要等待数据到来,又或者等待超时,最终都会进入读事件的处理函数-ngx_http_init_request。

ngx_http_init_request函数主要工作即是初始化请求,由于它是一个事件处理函数,它只有唯一一个ngx_event_t *类型的参数,ngx_event_t 结构在nginx中表示一个事件,事件处理的上下文类似于一个中断处理的上下文,为了在这个上下文得到相关的信息,nginx中一般会将连接结构的引用保存在事件结构的data字段,请求结构的引用则保存在连接结构的data字段,这样在事件处理函数中可以方便的得到对应的连接结构和请求结构。进入函数内部看一下,首先判断该事件是否是超时事件,如果是的话直接关闭连接并返回;反之则是指之前accept的连接上有请求过来需要处理,ngx_http_init_request函数首先在连接的内存池中为该请求分配一个ngx_http_request_t结构,这个结构将用来保存该请求所有的信息。分配完之后,这个结构的引用会被包存在连接的hc成员的request字段,以便于在长连接或pipelined请求中复用该请求结构。在这个函数中,nginx根据该请求的接收端口和地址找到一个默认虚拟服务器配置(listen指令的default_server属性用来标识一个默认虚拟服务器,否则监听在相同端口和地址的多个虚拟服务器,其中第一个定义的则为默认),因为在nginx配置文件中可以设置多个监听在不同端口和地址的虚拟服务器(每个server块对应一个虚拟服务器),另外还根据域名(server_name指令可以配置该虚拟服务器对应的域名)来区分监听在相同端口和地址的虚拟服务器,每个虚拟服务器可以拥有不同的配置内容,而这些配置内容决定了nginx在接收到一个请求之后如何处理该请求。找到之后,相应的配置被保存在该请求对应的ngx_http_request_t结构中。注意这里根据端口和地址找到的默认配置只是临时使用一下,最终nginx会根据域名找到真正的虚拟服务器配置,随后的初始化工作还包括:

将连接的读事件的处理函数设置为ngx_http_process_request_line函数,这个函数用来解析请求行,将请求的read_event_handler设置为ngx_http_block_reading函数,这个函数实际上什么都不做(当然在事件模型设置为水平触发时,唯一做的事情就是将事件从事件模型监听列表中删除,防止该事件一直被触发),后面会说到这里为什么会将read_event_handler设置为此函数;
为这个请求分配一个缓冲区用来保存它的请求头,地址保存在header_in字段,默认大小为1024个字节,可以使用client_header_buffer_size指令修改,这里需要注意一下,nginx用来保存请求头的缓冲区是在该请求所在连接的内存池中分配,而且会将地址保存一份在连接的buffer字段中,这样做的目的也是为了给该连接的下一次请求重用这个缓冲区,另外如果客户端发过来的请求头大于1024个字节,nginx会重新分配更大的缓存区,默认用于大请求的头的缓冲区最大为8K,最多4个,这2个值可以用large_client_header_buffers指令设置,后面还会说到请求行和一个请求头都不能超过一个最大缓冲区的大小;
同样的nginx会为这个请求分配一个内存池,后续所有与该请求相关的内存分配一般都会使用该内存池,默认大小为4096个字节,可以使用request_pool_size指令修改;
为这个请求分配响应头链表,初始大小为20;
创建所有模块的上下文ctx指针数组,变量数据;
将该请求的main字段设置为它本身,表示这是一个主请求,nginx中对应的还有子请求概念,后面的章节会做详细的介绍;
将该请求的count字段设置为1,count字段表示请求的引用计数;
将当前时间保持在start_sec和start_msec字段,这个时间是该请求的起始时刻,将被用来计算一个请求的处理时间(request time),nginx使用的这个起始点和apache略有差别,nginx中请求的起始点是接收到客户端的第一个数据包开始,而apache则是接收到客户端的整个request line后开始算起;
初始化请求的其他字段,比如将uri_changes设置为11,表示最多可以将该请求的uri改写10次,subrequests被设置为201,表示一个请求最多可以发起200个子请求;
做完所有这些初始化工作之后,ngx_http_init_request函数会调用读事件的处理函数来真正的解析客户端发过来的数据,也就是会进入ngx_http_process_request_line函数中处理。

ngx_http_process_request_line函数的主要作用即是解析请求行,同样由于涉及到网络IO操作,即使是很短的一行请求行可能也不能被一次读完,所以在之前的ngx_http_init_request函数中,ngx_http_process_request_line函数被设置为读事件的处理函数,它也只拥有一个唯一的ngx_event_t *类型参数,并且在函数的开头,同样需要判断是否是超时事件,如果是的话,则关闭这个请求和连接;否则开始正常的解析流程。先调用ngx_http_read_request_header函数读取数据。

由于可能多次进入ngx_http_process_request_line函数,ngx_http_read_request_header函数首先检查请求的header_in指向的缓冲区内是否有数据,有的话直接返回;否则从连接读取数据并保存在请求的header_in指向的缓存区,而且只要缓冲区有空间的话,会一次尽可能多的读数据,读到多少返回多少;如果客户端暂时没有发任何数据过来,并返回NGX_AGAIN,返回之前会做2件事情:1,设置一个定时器,时长默认为60s,可以通过指令client_header_timeout设置,如果定时事件到达之前没有任何可读事件,nginx将会关闭此请求;2,调用ngx_handle_read_event函数处理一下读事件-如果该连接尚未在事件处理模型上挂载读事件,则将其挂载上;如果客户端提前关闭了连接或者读取数据发生了其他错误,则给客户端返回一个400错误(当然这里并不保证客户端能够接收到响应数据,因为客户端可能都已经关闭了连接),最后函数返回NGX_ERROR;

如果ngx_http_read_request_header函数正常的读取到了数据,ngx_http_process_request_line函数将调用ngx_http_parse_request_line函数来解析,这个函数根据http协议规范中对请求行的定义实现了一个有限状态机,经过这个状态机,nginx会记录请求行中的请求方法(Method),请求uri以及http协议版本在缓冲区中的起始位置,在解析过程中还会记录一些其他有用的信息,以便后面的处理过程中使用。如果解析请求行的过程中没有产生任何问题,该函数会返回NGX_OK;如果请求行不满足协议规范,该函数会立即终止解析过程,并返回相应错误号;如果缓冲区数据不够,该函数返回NGX_AGAIN。在整个解析http请求的状态机中始终遵循着两条重要的原则:减少内存拷贝和回溯。内存拷贝是一个相对比较昂贵的操作,大量的内存拷贝会带来较低的运行时效率。nginx在需要做内存拷贝的地方尽量只拷贝内存的起始和结束地址而不是内存本身,这样做的话仅仅只需要两个赋值操作而已,大大降低了开销,当然这样带来的影响是后续的操作不能修改内存本身,如果修改的话,会影响到所有引用到该内存区间的地方,所以必须很小心的管理,必要的时候需要拷贝一份。这里不得不提到nginx中最能体现这一思想的数据结构,ngx_buf_t,它用来表示nginx中的缓存,在很多情况下,只需要将一块内存的起始地址和结束地址分别保存在它的pos和last成员中,再将它的memory标志置1,即可表示一块不能修改的内存区间,在另外的需要一块能够修改的缓存的情形中,则必须分配一块所需大小的内存并保存其起始地址,再将ngx_bug_t的temprary标志置1,表示这是一块能够被修改的内存区域。

再回到ngx_http_process_request_line函数中,如果ngx_http_parse_request_line函数返回了错误,则直接给客户端返回400错误;
如果返回NGX_AGAIN,则需要判断一下是否是由于缓冲区空间不够,还是已读数据不够。如果是缓冲区大小不够了,nginx会调用ngx_http_alloc_large_header_buffer函数来分配另一块大缓冲区,如果大缓冲区还不够装下整个请求行,nginx则会返回414错误给客户端,否则分配了更大的缓冲区并拷贝之前的数据之后,继续调用ngx_http_read_request_header函数读取数据来进入请求行自动机处理,直到请求行解析结束;
如果返回了NGX_OK,则表示请求行被正确的解析出来了,这时先记录好请求行的起始地址以及长度,并将请求uri的path和参数部分保存在请求结构的uri字段,请求方法起始位置和长度保存在method_name字段,http版本起始位置和长度记录在http_protocol字段。还要从uri中解析出参数以及请求资源的拓展名,分别保存在args和exten字段。接下来将要解析请求头,我将在下一篇文章中接着介绍。

时间: 2024-08-05 19:37:41

nginx的请求接收流程(一)的相关文章

nginx的请求接收流程(二)

在ngx_http_process_request_line函数中,解析完请求行之后,如果请求行的uri里面包含了域名部分,则将其保持在请求结构的headers_in成员的server字段,headers_in用来保存所有请求头,它的类型为ngx_http_headers_in_t: [cpp] view plaincopy <span style="font-size: 18px; ">typedef struct { ngx_list_t                

nginx用户请求反向代理流程

保障后端业务正常运行,通过nginx实现多级代理,后端业务官网 小程序 APP H5等 场景: 192.168.0.55 SLB01 192.168.0.42 SLB02 192.168.0.4 WEB01 SLB01配置代理SLB02:prot80 SLB02配置代理后端真实web节点 后端web节点配置如下  ps:为了精简配置文件把所有的配置参数放在了nginx 同一级目录proxy_params里面 流程梳理 以上我们配置好了nginx多级代理 来看下代理请求的流程 三台服务器执行 ta

nginx+uwsgi+django部署流程

当我们在用django开发的web项目时,开发测试过程中用到的是django自带的测试服务器,由于其安全及稳定等性能方面的局限性,django官方并不建议将测试服务器用在实际生产. nginx+uwsgi+django是我们常用的django部署方式.nginx作为最前端的服务器,他负责接收所有的客户端请求,对于请求的静态文件,由nginx服务器自己完成,因为它具有很好处理静态文件的能力,性能进行过优化,支持高并发量:uWSGI服务器作为支持服务器,是用来服务nginx的,nginx将请求的动态

Nginx处理请求的11个阶段

Nginx 处理请求的全过程一共划分为 11 个阶段(如图),按阶段由上到下依次执行 (上一阶段的所有指令执行完毕,才进入下一阶段) 各阶段的含义如下: ² post-read: 接收到完整的http头部后处理的阶段,在uri重写之前.一般跳过 ² server-rewrite: location匹配前,修改uri的阶段,用于重定向,location块外的重写指令(多次执行) ² find-config: uri寻找匹配的location块配置项(多次执行) ² rewrite: 找到locat

nginx实现请求的负载均衡 + keepalived实现nginx的高可用

前言 使用集群是网站解决高并发.海量数据问题的常用手段.当一台服务器的处理能力.存储空间不足时,不要企图去换更强大的服务器,对大型网站而言,不管多么强大的服务器,都满足不了网站持续增长的业务需求.这种情况下,更恰当的做法是增加一台服务器分担原有服务器的访问及存储压力.通过负载均衡调度服务器,将来自浏览器的访问请求分发到应用服务器集群中的任何一台服务器上,如果有更多的用户,就在集群中加入更多的应用服务器,使应用服务器的负载压力不再成为整个网站的瓶颈. 摘自<大型网站技术架构_核心原理与案例分析>

Nginx将请求分发到各web应用

介绍了VMWare12虚拟机.Linux(CentOS7)系统安装.部署Nginx1.6.3代理服务做负载均衡.接下来介绍通过Nginx将请求分发到各web应用处理服务. 一.Web应用开发 1.asp.net mvc5开发 (1)新建一个MVC5工程,新建一个Controller,在Index方法实现将当前时间保存到Session["mysession"],并写Cookies["mycookies"]存储主机名和当前时间. 1 2 3 4 5 6 7 8 9 10

linux 内核网络数据包接收流程

转:https://segmentfault.com/a/1190000008836467 本文将介绍在Linux系统中,数据包是如何一步一步从网卡传到进程手中的. 如果英文没有问题,强烈建议阅读后面参考里的两篇文章,里面介绍的更详细. 本文只讨论以太网的物理网卡,不涉及虚拟设备,并且以一个UDP包的接收过程作为示例. 本示例里列出的函数调用关系来自于kernel 3.13.0,如果你的内核不是这个版本,函数名称和相关路径可能不一样,但背后的原理应该是一样的(或者有细微差别) 网卡到内存 网卡需

【nginx】【转】Nginx启动框架处理流程

Nginx启动过程流程图: ngx_cycle_t结构体: Nginx的启动初始化在src/core/nginx.c的main函数中完成,当然main函数是整个Nginx的入口,除了完成启动初始化任务以外,也必定是所有功能模块的入口之处.Nginx的初始化工作主要围绕一个类型为ngx_cycle_t类型的全局变量(cycle)展开. ngx_cycle_t结构体类型: typedef struct ngx_cycle_s ngx_cycle_t; struct ngx_cycle_s { voi

【Nginx】请求上下文

上下文与全异步web服务器的关系 请求上下文指在一个请求的处理过程中,把一些关键的信息保存下来的类似struct这样的结构体.每个http模块都可以有自己的上下文结构体,一般都是在刚开始处理请求时在内存池上分配它,之后当经由epoll.http框架再次调用到http模块的处理方法时,这个http模块可以由请求上下文结构体中获取信息.请求结束时就会销毁该请求的内存池,自然也就销毁了上下文结构体. Nginx是全异步处理的web服务器,http模块可能会多次反复处理同一个请求,所以必须定义上下文结构