Nginx作为一款开源的、高性能的HTTP服务器和反向代理服务器而闻名,本文基于nginx-1.15.0,将为读者简要介绍其HTTP处理流程。
通常nginx配置文件如下所示:
代码语言:javascript复制worker_processes 1;
events {
worker_connections 1024;
}
http{
access_log logs/access.log main;
server {
listen 80;
server_name example.com;
location ~ .php$ {
fastcgi_pass 127.0.0.1:9000;
}
}
}
- Nginx采用master-worker编模式,master初始化配置,创建Socket并监听端口,启动并管理worker进程,worker进程负责接收客户端请求并提供服务;其中worker_processes配置的就是worker进程的数目;
- events指令块用于配置事件处理相关,比如worker_connections用于配置每个worker进程最大维护的socket链接数目;
- http指令块用于配置http请求处理相关,比如access_log用于配置access日志文件路径;
- server指令块用于配置virtual server,通常会在一台机器配置多个virtual server,监听不同端口号,映射到不同文件目录;比如listen可配置监听端口号;
- location指令块配置不同路径请求处理方式,比如proxy_pass可配置将请求按照http协议格式转发给上游,fastcgi_pass可配置将请求按照fastcgi协议转发给fpm处理。
Nginx高度模块化,每个模块实现某一具体功能,比如ngx_http_limit_req_module模块实现按请求速率限流功能,ngx_http_fastcgi_module模块实现fastcgi协议通信功能。每个模块都需要解析配置文件中相关配置,每个模块需要解析的所有配置都定义为ngx_command_t数组。
例如ngx_http_module模块,其ngx_command_t数定义如下:
代码语言:javascript复制struct ngx_command_s {
ngx_str_t name;
ngx_uint_t type;
char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
...
};
static ngx_command_t ngx_http_commands[] = {
{ ngx_string("http"),
NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
ngx_http_block,
...
},
};
- name指令名称,解析配置文件时按照名称能匹配查找;
- type指令类型,NGX_CONF_NOARGS标识该配置无参数,NGX_CONF_BLOCK该配置是一个配置块,NGX_MAIN_CONF表示配置可以出现在哪些位置(NGX_MAIN_CONF、NGX_HTTP_SRV_CONF、NGX_HTTP_LOC_CONF);
- set指令处理函数;
初始化服务器
http指令块用于配置http请求处理相关,解析http指令的处理函数为ngx_http_block,实现如下:
代码语言:javascript复制static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
//解析main配置
//解析server配置
//解析location配置
//初始化HTTP处理流程所需的handler
//初始化listening
if (ngx_http_optimize_servers(cf, cmcf, cmcf->ports) != NGX_OK) {
return NGX_CONF_ERROR;
}
}
函数ngx_http_block主要解析http块内部的main配置、server配置与location配置;同时会初始化HTTP处理流程所需的handler;以及初始化所有监听端口。
函数ngx_http_optimize_servers将所有配置的IP端口进一步解析,并存储在conf->cycle->listening字段,这是一个数组,后续操作会遍历此数组,创建socket并监听。
conf->cycle->listening数组元素类型为ngx_listening_t,创建该ngx_listening_t对象时,同时会设置其处理handler为函数ngx_http_init_connection,当接受到客户端链接请求时,会调用此handler。
那么什么时候启动监听呢?全局搜索关键字cycle->listening可以找到。main方法会调用ngx_init_cycle,其完成了服务器初始化的大部分工作,其中就包括启动监听(ngx_open_listening_sockets):
代码语言:javascript复制ngx_int_t ngx_open_listening_sockets(ngx_cycle_t *cycle)
{
for (i = 0; i < cycle->listening.nelts; i ) {
s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0);
bind(s, ls[i].sockaddr, ls[i].socklen);
listen(s, ls[i].backlog);
}
}
假设nginx使用epoll处理所有socket事件,那么什么时候将监听事件添加到epoll呢?同样全局搜索关键字cycle->listening可以找到。ngx_event_core_module模块是事件处理核心模块,初始化此模块时会执行ngx_event_process_init函数,从而将监听事件添加到epoll:
代码语言:javascript复制static ngx_int_t ngx_event_process_init(ngx_cycle_t *cycle)
{
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i ) {
//设置读事件处理handler
rev->handler = ngx_event_accept;
ngx_add_event(rev, NGX_READ_EVENT, 0);
}
}
注意到向epoll添加读事件时,设置该读事件处理函数为ngx_event_accept,即接收到客户端socket连接请求事件时会调用该处理函数。
HTTP请求解析
基础结构体
结构体ngx_connection_t存储socket连接相关信息;nginx预先创建若干个ngx_connection_t对象,存储在全局变量ngx_cycle->free_connections,称之为连接池;当新生成socket时,会尝试从连接池中获取空闲connection连接,如果获取失败,则会直接关闭此socket。
指令worker_connections用于配置连接池最大连接数目,配置在events指令块中,由ngx_event_core_module解析。
代码语言:javascript复制events {
use epoll;
worker_connections 60000;
}
当nginx作为HTTP服务器时(从用户的角度,http 1.1协议下,浏览器默认使用两个并发连接),最大客户端数目maxClient=worker_processes X worker_connections/2;当nginx作为反向代理服务器时,最大客户端数目maxClient=worker_processes X worker_connections/4。其worker_processes为用户配置的worker进程数目。
结构体ngx_connection_t定义如下:
代码语言:javascript复制struct ngx_connection_s {
//空闲连接池中,data指向下一个连接,形成链表;取出来使用时,data指向请求结构体ngx_http_request_s
void *data;
//读写事件结构体
ngx_event_t *read;
ngx_event_t *write;
ngx_socket_t fd; //socket fd
ngx_recv_pt recv; //socket接收数据函数指针
ngx_send_pt send; //socket发送数据函数指针
ngx_buf_t *buffer; //输入缓冲区
struct sockaddr *sockaddr; //客户端地址
socklen_t socklen;
ngx_listening_t *listening; //监听的ngx_listening_t对象
struct sockaddr *local_sockaddr; //本地地址
socklen_t local_socklen;
…………
}
这里需要重点关注几个字段:
- data:void类型为指针;当该链接空闲时,data指向下一个空闲链接,以此形成链表;当该链接被分配之后,data指向对应的请求结构体ngx_http_request_s;
- read和write:读写事件结构体,类型为ngx_event_t;事件结构体中需要重点关注handler字段,标识为事件处理函数;
- recv和send指向socket接收/发送数据函数;
结构体ngx_http_request_t存储整个HTTP请求处理流程所需的所有信息,字段非常多,这里只进行简要说明:
代码语言:javascript复制struct ngx_http_request_s {
//链接
ngx_connection_t *connection;
//读写事件处理handler
ngx_http_event_handler_pt read_event_handler;
ngx_http_event_handler_pt write_event_handler;
//请求头缓冲区
ngx_buf_t *header_in;
//解析后的请求头
ngx_http_headers_in_t headers_in;
//请求体结构体
ngx_http_request_body_t *request_body;
//请求行
ngx_str_t request_line;
//解析后的若干请求行
ngx_uint_t method;
ngx_uint_t http_version;
ngx_str_t uri;
ngx_str_t args;
…………
}
- connection指向底层对应的ngx_connection_t链接对象;
- read_event_handler和write_event_handler指向HTTP请求读写事件处理函数;
- headers_in存储解析后的请求头;
- request_body请求体结构体;
- request_line接受到的请求行;
- method和http_version等为解析后的如干请求行;
请求行与请求体解析相对比较简单,这里重点讲述请求头的解析,解析后的请求头信息都存储在ngx_http_headers_in_t结构体中。
ngx_http_request.c文件中定义了所有的HTTP头部,存储在ngx_http_headers_in数组,数组的每个元素是一个ngx_http_header_t结构体,主要包含三个字段,头部名称、头部解析后字段存储在ngx_http_headers_in_t的偏移量,解析头部的处理函数。
代码语言:javascript复制ngx_http_header_t ngx_http_headers_in[] = {
{ ngx_string("Host"), offsetof(ngx_http_headers_in_t, host),
ngx_http_process_host },
{ ngx_string("Connection"), offsetof(ngx_http_headers_in_t, connection),
ngx_http_process_connection },
…………
}
typedef struct {
ngx_str_t name;
ngx_uint_t offset;
ngx_http_header_handler_pt handler;
} ngx_http_header_t;
解析请求头时,只需从ngx_http_headers_in数组中查找请求头ngx_http_header_t对象,调用处理函数handler,存储到r->headers_in对应字段即可。以解析Connection头部为例,ngx_http_process_connection实现如下:
代码语言:javascript复制static ngx_int_t ngx_http_process_connection(ngx_http_request_t *r, ngx_table_elt_t *h, ngx_uint_t offset)
{
if (ngx_strcasestrn(h->value.data, "close", 5 - 1)) {
r->headers_in.connection_type = NGX_HTTP_CONNECTION_CLOSE;
} else if (ngx_strcasestrn(h->value.data, "keep-alive", 10 - 1)) {
r->headers_in.connection_type = NGX_HTTP_CONNECTION_KEEP_ALIVE;
}
return NGX_OK;
}
输入参数offset在此处并没有什么作用。注意到第二个输入参数类型为ngx_table_elt_t,存储了当前请求头的键值对信息:
代码语言:javascript复制typedef struct {
ngx_uint_t hash; //请求头key的hash值
ngx_str_t key;
ngx_str_t value;
u_char *lowcase_key; //请求头key转为小写字符串(可以看到HTTP请求头解析时key不区分大小写)
} ngx_table_elt_t;
再思考一个问题,从ngx_http_headers_in数组中查找请求头对应ngx_http_header_t对象时,需要遍历,每个元素都需要进行字符串比较,效率低下。因此nginx将ngx_http_headers_in数组转换为哈希表,哈希表的键即为请求头的key,方法ngx_http_init_headers_in_hash实现了数组到哈希表的转换,转换后的哈希表存储在cmcf->headers_in_hash字段。
基础结构体关系示意图如下所示:
解析HTTP请求
"初始化服务器"小节提到,在创建socket启动监听时,会添加可读事件到epoll,事件处理函数为ngx_event_accept,用于接收socket连接,分配connection连接,并调用ngx_listening_t对象的处理函数(ngx_http_init_connection)。
代码语言:javascript复制void ngx_event_accept(ngx_event_t *ev)
{
s = accept4(lc->fd, (struct sockaddr *) sa, &socklen, SOCK_NONBLOCK);
ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;
c = ngx_get_connection(s, ev->log);
ls->handler(c);
}
- 客户端socket连接成功时,都需要分配connection连接,如果分配失败则会直接关闭此socket;
- 而每个worker进程连接池的最大连接数目是固定的,当不存在空闲连接时,此worker进程accept的所有socket都会被拒绝;
- 多个worker进程通过抢锁竞争是否注册监听端口的事件;而当ngx_accept_disabled大于0时,会直接放弃此次竞争,同时ngx_accept_disabled减1。
- 通过ngx_accept_disabled计算方式可以看到,当worker进程的空闲连接过少时,可以减少其抢锁成功的次数;
socket连接成功后,nginx会等待客户端发送HTTP请求,默认会有60秒的超时时间,即60秒内没有接收到客户端请求时,断开此连接,打印错误日志。函数ngx_http_init_connection用于设置读事件处理函数,以及超时定时器。
代码语言:javascript复制void ngx_http_init_connection(ngx_connection_t *c)
{
c->read = ngx_http_wait_request_handler;
c->write->handler = ngx_http_empty_handler;
ngx_add_timer(rev, c->listening->post_accept_timeout);
}
全局搜索post_accept_timeout字段,可以查找到,该字段值可通过配置文件中的client_header_timeout修改(可在http配置块或者server配置块中设置)。
函数ngx_http_wait_request_handler为解析HTTP请求的入口函数,实现如下:
代码语言:javascript复制static void ngx_http_wait_request_handler(ngx_event_t *rev)
{
//读事件已经超时
if (rev->timedout) {
ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");
ngx_http_close_connection(c);
return;
}
n = c->recv(c, b->last, size);
//创建请求对象ngx_http_request_t,HTTP请求整个处理过程都有用;
c->data = ngx_http_create_request(c);
//设置读事件处理函数(此次请求行可能没有读取完)
rev->handler = ngx_http_process_request_line;
ngx_http_process_request_line(rev);
}
注意到当读事件超时时,nginx会直接关闭该链接;函数ngx_http_create_request创建并初始化ngx_http_request_t对象;解析请求行处理函数为ngx_http_process_request_line。
解析请求行与请求头的代码较为繁琐,重点在于读取socket数据,解析字符串,这里不做详述。HTTP请求解析过程主要函数调用如下图所示:
注意,解析完成请求行与请求头,nginx就开始处理HTTP请求,并没有等到解析完请求体再处理。处理请求入口为ngx_http_process_request。
HTTP请求处理阶段
HTTP请求处理的11个阶段
nginx将HTTP请求处理流程分为11个阶段,绝大多数HTTP模块都会将自己的handler添加到某个阶段(将handler添加到全局唯一的数组phases中),nginx处理HTTP请求时会挨个调用每个阶段的handler。需要注意的是其中有4个阶段不能添加自定义handler。11个阶段定义如下:
代码语言:javascript复制typedef enum {
NGX_HTTP_POST_READ_PHASE = 0,
NGX_HTTP_SERVER_REWRITE_PHASE, //server块中配置了rewrite指令,重写url
NGX_HTTP_FIND_CONFIG_PHASE, //查找匹配的location配置;不能自定义handler;
NGX_HTTP_REWRITE_PHASE, //location块中配置了rewrite指令,重写url
NGX_HTTP_POST_REWRITE_PHASE, //检查是否发生了url重写,如果有,重新回到FIND_CONFIG阶段;不能自定义handler;
NGX_HTTP_PREACCESS_PHASE, //访问控制,比如限流模块会注册handler到此阶段
NGX_HTTP_ACCESS_PHASE, //访问权限控制,比如基于ip黑白名单的权限控制,基于用户名密码的权限控制等
NGX_HTTP_POST_ACCESS_PHASE, //根据访问权限控制阶段做相应处理;不能自定义handler;
NGX_HTTP_TRY_FILES_PHASE, //只有配置了try_files指令,才会有此阶段;不能自定义handler;
NGX_HTTP_CONTENT_PHASE, //内容产生阶段,返回响应给客户端
NGX_HTTP_LOG_PHASE //日志记录
} ngx_http_phases;
- NGX_HTTP_POST_READ_PHASE:第一个阶段,ngx_http_realip_module模块会注册handler到该阶段(nginx作为代理服务器时有用,后端以此获取客户端原始IP),而该模块默认不会开启,需要通过--with-http_realip_module启动;
- NGX_HTTP_SERVER_REWRITE_PHASE:server块中配置了rewrite指令时,该阶段会重写url;
- NGX_HTTP_FIND_CONFIG_PHASE:查找匹配的location配置;该阶段不能自定义handler;
- NGX_HTTP_REWRITE_PHASE:location块中配置了rewrite指令时,该阶段会重写url;
- NGX_HTTP_POST_REWRITE_PHASE:该阶段会检查是否发生了url重写,如果有,重新回到FIND_CONFIG阶段,否则直接进入下一个阶段;该阶段不能自定义handler;
- NGX_HTTP_PREACCESS_PHASE:访问控制,比如限流模块ngx_http_limit_req_module会注册handler到该阶段;
- NGX_HTTP_ACCESS_PHASE:访问权限控制,比如基于ip黑白名单的权限控制,基于用户名密码的权限控制等;
- NGX_HTTP_POST_ACCESS_PHASE:该阶段会根据访问权限控制阶段做相应处理,不能自定义handler;
- NGX_HTTP_TRY_FILES_PHASE:只有配置了try_files指令,才会有此阶段,不能自定义handler;
- NGX_HTTP_CONTENT_PHASE:内容产生阶段,返回响应给客户端;ngx_http_fastcgi_module模块就处于该阶段;
- NGX_HTTP_LOG_PHASE:该阶段会记录日志;
nginx使用结构体ngx_module_s表示一个模块,其中字段ctx,是一个指向模块上下文结构体的指针(上下文结构体的字段都是一些函数指针);nginx的HTTP模块上下文结构体大多都有字段postconfiguration,负责注册本模块的handler到某个处理阶段。11个阶段在解析完成http配置块指令后初始化。
代码语言:javascript复制static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
//解析http配置块
//初始化11个阶段的phases数组,注意多个模块可能注册到同一个阶段,因此phases是一个二维数组
if (ngx_http_init_phases(cf, cmcf) != NGX_OK) {
return NGX_CONF_ERROR;
}
//遍历所有HTTP模块,注册handler
for (m = 0; ngx_modules[m]; m ) {
if (ngx_modules[m]->type != NGX_HTTP_MODULE) {
continue;
}
module = ngx_modules[m]->ctx;
if (module->postconfiguration) {
if (module->postconfiguration(cf) != NGX_OK) {
return NGX_CONF_ERROR;
}
}
}
//将二维数组转换为一维数组,从而遍历执行数组所有handler
if (ngx_http_init_phase_handlers(cf, cmcf) != NGX_OK) {
return NGX_CONF_ERROR;
}
}
- 多个模块可能注册handler到同一个阶段,因此phases是一个二维数组;
- for循环遍历所有HTTP类型的模块,调用其postconfiguration函数,注册handler到相应阶段;
- ngx_http_init_phase_handlers函数会将二维数组phase转换为一维数组,后续遍历执行该数组所有handler;
- 以限流模块ngx_http_limit_req_module模块为例,postconfiguration方法简单实现如下:
static ngx_int_t ngx_http_limit_req_init(ngx_conf_t *cf)
{
h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers);
*h = ngx_http_limit_req_handler;
//ngx_http_limit_req_module模块的限流方法;nginx处理HTTP请求时,都会调用此方法判断应该继续执行还是拒绝请求
return NGX_OK;
}
GDB调试,断点到ngx_http_block方法执行所有HTTP模块注册handler之后,打印phases数组
代码语言:javascript复制p cmcf->phases[*].handlers
p *(ngx_http_handler_pt*)cmcf->phases[*].handlers.elts
11个阶段注册的handler如下图所示:
11个阶段初始化
上面提到HTTP的11个处理阶段handler存储在phases数组,但由于多个模块可能注册handler到同一个阶段,使得phases是一个二维数组,因此需要转换为一维数组,转换后存储在cmcf->phase_engine字段,phase_engine的类型为ngx_http_phase_engine_t,定义如下:
代码语言:javascript复制typedef struct {
ngx_http_phase_handler_t *handlers; //一维数组,存储所有handler
ngx_uint_t server_rewrite_index; //记录NGX_HTTP_SERVER_REWRITE_PHASE阶段handler的索引值
ngx_uint_t location_rewrite_index; //记录NGX_HTTP_REWRITE_PHASE阶段handler的索引值
} ngx_http_phase_engine_t;
struct ngx_http_phase_handler_t {
ngx_http_phase_handler_pt checker; //执行handler之前的校验函数
ngx_http_handler_pt handler;
ngx_uint_t next; //下一个待执行handler的索引(通过next实现handler跳转执行)
};
//cheker函数指针类型定义
typedef ngx_int_t (*ngx_http_phase_handler_pt)(ngx_http_request_t *r, ngx_http_phase_handler_t *ph);
//handler函数指针类型定义
typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);
- handlers字段为存储所有handler的一维数组;
- server_rewrite_index字段记录NGX_HTTP_SERVER_REWRITE_PHASE阶段handler的索引值;
- location_rewrite_index字段记录NGX_HTTP_REWRITE_PHASE阶段handler的索引值;
- ngx_http_phase_handler_t结构体中的checker字段为执行handler之前的校验函数;next字段为下一个待执行handler的索引(通过next实现handler跳转执行);
- 数组转换功能由函数ngx_http_init_phase_handlers实现,代码逻辑比较长但是相对简单,这里不做过多详述;
GDB打印出转换后的数组如下图所示,第一列是cheker字段,第二列是handler字段,箭头表示next跳转;图中有个返回的箭头,即NGX_HTTP_POST_REWRITE_PHASE阶段可能返回到NGX_HTTP_FIND_CONFIG_PHASE;原因在于只要NGX_HTTP_REWRITE_PHASE阶段产生了url重写,就需要重新查找匹配location。
处理HTTP请求
上面提到HTTP请求的处理入口函数是ngx_http_process_request,其主要调用ngx_http_core_run_phases实现11个阶段的执行流程;
ngx_http_core_run_phases遍历预先设置好的cmcf->phase_engine.handlers数组,调用其checker函数,逻辑如下:
代码语言:javascript复制void ngx_http_core_run_phases(ngx_http_request_t *r)
{
ph = cmcf->phase_engine.handlers;
//phase_handler初始为0,表示待处理handler的索引;cheker内部会根据ph->next字段修改phase_handler
while (ph[r->phase_handler].checker) {
rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]);
if (rc == NGX_OK) {
return;
}
}
}
checker内部就是调用handler,并设置下一步要执行handler的索引;比如说ngx_http_core_generic_phase实现如下:
代码语言:javascript复制ngx_int_t ngx_http_core_generic_phase(ngx_http_request_t *r, ngx_http_phase_handler_t *ph)
{
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "rewrite phase: %ui", r->phase_handler);
rc = ph->handler(r);
if (rc == NGX_OK) {
r->phase_handler = ph->next;
return NGX_AGAIN;
}
}
内容产生阶段
内容产生阶段NGX_HTTP_CONTENT_PHASE是HTTP请求处理的第10个阶段,一般情况有3个模块注册handler到此阶段:ngx_http_static_module、ngx_http_autoindex_module和ngx_http_index_module。
但是当我们配置了proxy_pass和fastcgi_pass时,情况会有所不同。
使用proxy_pass配置上游时,ngx_http_proxy_module模块会设置其处理函数到配置类conf;使用fastcgi_pass配置时,ngx_http_fastcgi_module会设置其处理函数到配置类conf。例如:
代码语言:javascript复制static char * ngx_http_fastcgi_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_core_loc_conf_t *clcf;
clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
clcf->handler = ngx_http_fastcgi_handler;
}
阶段NGX_HTTP_FIND_CONFIG_PHASE查找匹配的location,并获取此ngx_http_core_loc_conf_t对象,将其handler赋值给ngx_http_request_t对象的content_handler字段(内容产生阶段处理函数)。
而在执行内容产生阶段的checker函数时,会检测执行content_handler指向的函数;查看ngx_http_core_content_phase函数实现(内容产生阶段的checker函数):
代码语言:javascript复制ngx_int_t ngx_http_core_content_phase(ngx_http_request_t *r,
ngx_http_phase_handler_t *ph)
{
if (r->content_handler) { //如果请求对象的content_handler字段不为空,则调用
r->write_event_handler = ngx_http_request_empty_handler;
ngx_http_finalize_request(r, r->content_handler(r));
return NGX_OK;
}
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "content phase: %ui", r->phase_handler);
rc = ph->handler(r); //否则执行内容产生阶段handler
}