Nginx学习笔记系列第三篇-深度理解请求流程

184 阅读7分钟

这是我参与更文挑战的第21天,活动详情查看: 更文挑战

背景

这是Nginx学习笔记汇总的第三篇,把Nginx的处理请求的流程和底层机制,以及对应的C语言中的数据结构总结一下

请求流程

request概念

request在Nginx中指的是http请求,Nginx中的数据结构是ngx_http_request_t,它是对一个http请求的封装,一个http请求,包含请求行、请求头、请求体、响应行、响应头、响应体

http请求是典型的请求-响应类型的的网络协议,而http是文本协议,所以在分析请求行与请求头,以及输出响应行与响应头,往往是一行一行的进行处理,读取一行数据,分析出请求行中包含的method、uri、http_version信息,然后再一行一行处理请求头,并根据请求method与请求头的信息来决定是否有请求体以及请求体的长度,然后再去读取请求体,得到请求后,我们处理请求产生需要输出的数据,然后再生成响应行,响应头以及响应体,在将响应发送给客户端之后,一个完整的请求就处理完

Nginx的特殊处理机制是:当请求头读取完成后,就开始进行请求的处理了,Nginx通过ngx_http_request_t来保存解析请求与输出响应相关的数据

Nginx的完整请求处理流程

一个请求是从ngx_http_init_request开始,在这个函数中,会设置读事件为ngx_http_process_request_line,即接下来的网络事件会由ngx_http_process_request_line来执行,它是用来处理请求行的,通过ngx_http_read_request_header来读取请求数据,然后调用ngx_http_parse_request_line函数来解析请求行,Nginx为提高效率,采用状态机来解析请求行,而且在进行method的比较时,没有直接使用字符串比较,而是将四个字符转换成一个整型,然后一次比较以减少cpu的指令数

一个请求行包含请求的方法,uri,版本,也可以包含host信息,比如一个请求GET www.taobao.com/uri HTTP/1.0这样一个请求行也是合法的,而且host是www.taobao.com,此时,Nginx会忽略请求头中的host域,而以请求行中的这个为准来查找虚拟主机;而对于http0.9版来说,是不支持请求头的,需要特别处理;所以,在后面解析请求头时,协议版本都是1.0或1.1,整个请求行解析到的参数,会保存到ngx_http_request_t结构当中

在解析完请求行后,Nginx会设置读事件的handler为ngx_http_process_request_headers,然后后续的请求就在 ngx_http_process_request_headers中进行读取与解析,ngx_http_process_request_headers函数用来读取请求头,跟请求行一样,还是调用ngx_http_read_request_header来读取请求头,调用ngx_http_parse_header_line来解析一行请求头,解析到的请求头会保存到ngx_http_request_t的域headers_in中,headers_in是一个链表结构,保存所有的请求头,而HTTP中有些请求是需要特别处理的,这些请求头与请求处理函数存放在一个映射表里面,即ngx_http_headers_in,在初始化时,会生成一个hash表,当每解析到一个请求头后,就会先在这个hash表中查找,如果有找到,则调用相应的处理函数来处理这个请求头,比如:Host头的处理函数是ngx_http_process_host

当Nginx解析到两个回车换行符时,就表示请求头的结束,此时就会调用ngx_http_process_request来处理请求了,ngx_http_process_request会设置当前的连接的读写事件处理函数为ngx_http_request_handler,然后再调用ngx_http_handler来真正开始处理一个完整的http请求;注意,读写事件处理函数都是ngx_http_request_handler,因为在这个函数中,会根据当前事件是读事件还是写事件,分别调用ngx_http_request_t中的read_event_handler或write_event_handler,由于此时请求头已经读取完成了,Nginx先不读取请求body,这里设置read_event_handler为ngx_http_block_reading,即不读取数据了

真正开始处理数据是在ngx_http_handler函数里面,这个函数会设置write_event_handler为ngx_http_core_run_phases,并执行ngx_http_core_run_phases函数,ngx_http_core_run_phases函数将执行多阶段请求处理,Nginx将一个http请求的处理分为多个阶段,这个函数就是执行这些阶段来产生数据,因为ngx_http_core_run_phases最后会产生数据,所以前面设置写事件的处理函数为ngx_http_core_run_phases

函数的调用逻辑:最终是调用ngx_http_core_run_phases来处理请求,产生的响应头会放在ngx_http_request_t的headers_out中,Nginx的各种阶段会对请求进行处理,最后会调用filter来过滤数据,对数据进行加工,如truncked传输、gzip压缩等,这里的filter包括header filter与body filter,即对响应头或响应体进行处理;filter是一个链表结构,分别有header filter与body filter,先执行header filter中所有的filter,然后再执行body filter中所有的filter,在header filter中的最后一个filter,即ngx_http_header_filter将会遍历所有的响应头,最后需要输出的响应头在一个连续的内存,然后调用ngx_http_write_filter进行输出,ngx_http_write_filter是body filter中的最后一个,所以Nginx首先的body信息,在经过一系列的body filter之后,最后也会调用ngx_http_write_filter来进行输出

要注意的是,Nginx会将整个请求头都放在一个buffer里面,这个buffer的大小通过配置项client_header_buffer_size来设置,如果用户的请求头太大,这个buffer装不下,Nginx就会重新分配一个新的更大的buffer来装请求头,这个大buffer可以通过large_client_header_buffers来设置,这个large_buffer这一组buffer,比如配置48k,就是表示有四个8k大小的buffer可以用,为了保存请求行或请求头的完整性,一个完整的请求行或请求头,需要放在一个连续的内存里面,所以一个完整的请求行或请求头只会保存在一个buffer里面,这样,如果请求行大于一个buffer的大小,就会返回414错误,如果一个请求头大小大于一个buffer大小,就会返回400错误

在实际业务应用场景,开发者就需要根据实际的需求来调整上面的buffer参数,来优化程序

keepalive和keepalive_timeout

keepalive用于http连接中表示是否关掉长连接,若需要长连接,Nginx在输出完响应体后,会设置当前连接的keepalive属性,然后等待客户端下一次请求

Nginx不可能一直等待下去,当Nginx设置keepalive等待下一次的请求时,同时也会设置一个最大等待时间,这个时间是通过选项keepalive_timeout来配置的,如果配置为0,则表示关掉keepalive

对于请求量比较大的Nginx,关掉keepalive会产生比较多的time-wait状态的socket,当客户端的一次访问需要多次访问同一个server时,打开keepalive的优势非常大,比如图片服务器,通常一个网页会包含很多个图片,打开keepalive也会大量减少time-wait的数量

pipe

http1.1引入了一种新的特性:pipeline,pipeline其实就是流水线作业,它可以看作为keepalive的一种升华,因为pipeline也是基于长连接的,目的就是利用一个连接做多次请求,如果客户端要提交多个请求,对于keepalive来说,那么第二个请求,必须要等到第一个请求的响应接收完全后,才能发起,这和TCP的停止等待协议是一样的,得到两个响应的时间至少为2RTT,而对pipeline来说,客户端不必等到第一个请求处理完后,就可以马上发起第二个请求,得到两个响应的时间可能能够达到1RTT

Nginx是直接支持pipeline的,但是Nginx对pipeline中的多个请求的处理却不是并行的,依然是一个请求接一个请求的处理,只是在处理第一个请求的时候,客户端就可以发起第二个请求,这样Nginx利用pipeline减少了处理完一个请求后,等待第二个请求的请求头数据的时间

Nginx的做法很简单,Nginx在读取数据时,会将读取的数据放到一个buffer里面,所以如果Nginx在处理完前一个请求后,如果发现buffer里面还有数据,就认为剩下的数据是下一个请求的开始,然后就接下来处理下一个请求,否则就设置keepalive