这章正式开始分析 nginx 处理请求的流程。从简单到复杂,先完整地分析下 nginx 作为静态 web 服务器的处理流程。该分析流程中,我使用的配置文件如下:
events {
}
http {
server {
listen 80;
location / {
root html;
index index.html index.htm;
}
}
}
启动 nginx 后,使用 curl 127.0.0.1
命令就可以看到返回的静态页面 index.html 中的内容。
由于整个流程代码量比较大,我将分几篇文章分析。分析代码时,我只提比较重要的地方,一些错误处理和这个流程不相关的细枝末节的都略去了。当了解了整个流程后,就可以去探究每部分的细节。后续有时间的话,我也会写些文章,讲述一些关键的数据结构和一些很关键的细节。
ngx_event_accept
在启动流程文章最后我说过,在每个 worker 进程的循环中,会调用 ngx_process_events_and_timers
函数来处理所有事件。在这个函数内部,则会调用ngx_epoll_process_events
来处理 epoll 监测到的网络事件。
当 listening socket 上出现读事件(新的TCP连接请求)时,ngx_event_accept
函数会被调用处理这个事件。这个函数的原型如下:
void ngx_event_accept(ngx_event_t *ev);
它的参数是 ngx_event_t 结构
,它表示一个事件。在 nginx 中,事件分为网络事件或定时器事件,每种事件又分为读事件和写事件。
lc = ev->data;
ls = lc->listening;
ev->ready = 0;
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, ev->log, 0,
"accept on %V, ready: %d", &ls->addr_text, ev->available);
在该函数处理流程中,lc = ev->data 指向代表 listening socket 的 ngx_connection_t
结构,
ls = lc->listening 指向 ngx_listening_t
结构。
ev->ready = 0 将事件的 ready 标志清零,告诉 epoll 该读事件已处理。
s = accept(lc->fd, &sa.sockaddr, &socklen);
接受新连接请求,返回一个新的 socket 。
c = ngx_get_connection(s, ev->log);
给这个 socket 分配一个 ngx_connection_t
结构,这个结构代表一个连接(表示一个连接已经建立了)。然后
c->type = SOCK_STREAM;
将连接的类型设置为 SOCK_STREAM
,也就是 TCP 连接。
c->pool = ngx_create_pool(ls->pool_size, ev->log);
if (c->pool == NULL) {
ngx_close_accepted_connection(c);
return;
}
c->sockaddr = ngx_palloc(c->pool, socklen);
if (c->sockaddr == NULL) {
ngx_close_accepted_connection(c);
return;
}
ngx_memcpy(c->sockaddr, &sa, socklen);
log = ngx_palloc(c->pool, sizeof(ngx_log_t));
if (log == NULL) {
ngx_close_accepted_connection(c);
return;
}
*log = ls->log;
为该连接创建一个内存池,在内存池中分配一个 ngx_sockaddr_t
结构保存源IP地址和端口信息,再分配一个表示日志的 ngx_log_t
结构。
c->recv = ngx_recv;
c->send = ngx_send;
c->recv_chain = ngx_recv_chain;
c->send_chain = ngx_send_chain;
c->log = log;
c->pool->log = log;
c->socklen = socklen;
c->listening = ls;
c->local_sockaddr = ls->sockaddr;
c->local_socklen = ls->socklen;
初始化 ngx_connection_t
结构中一些关键的成员。c->recv 、c->send、c->recv_chain、c->send_chain 分别指向四个函数,ngx_unix_recv
,ngx_readv_chain
,ngx_unix_send
,ngx_linux_sendfile_chain
(linux 支持 sendfile() 系统调用时使用) 或 ngx_writev_chain
函数。这几个函数的原型如下:
/* 从 socket 上读取数据到一个缓冲区 */
ssize_t ngx_unix_recv(ngx_connection_t *c, u_char *buf, size_t size);
/* 从 socket 上读取数据到多个缓冲区 */
ssize_t ngx_readv_chain(ngx_connection_t *c, ngx_chain_t *entry, off_t limit);
/* 往 socket 上发送数据,要发送的数据在一个缓冲区 */
ssize_t ngx_unix_send(ngx_connection_t *c, u_char *buf, size_t size);
/* 往 socket 上发送数据,要发送的数据在多个缓冲区 */
ngx_chain_t *ngx_linux_sendfile_chain(ngx_connection_t *c, ngx_chain_t *in,
off_t limit);
ngx_chain_t *ngx_writev_chain(ngx_connection_t *c, ngx_chain_t *in,
off_t limit);
后面再讲它们的实现。
rev = c->read;
wev = c->write;
wev->ready = 1;
rev->log = log;
wev->log = log;
初始化这个连接的读写事件,wev->ready = 1 表示写事件已经就绪。因为连接刚建立,发送缓冲区是空的,所以可以立即往这个 socket 上发送数据。
c->number = ngx_atomic_fetch_add(ngx_connection_counter, 1);
然后,给这个连接分配一个ID,主要用来打日志。然后
if (ls->addr_ntop) {
c->addr_text.data = ngx_pnalloc(c->pool, ls->addr_text_max_len);
if (c->addr_text.data == NULL) {
ngx_close_accepted_connection(c);
return;
}
c->addr_text.len = ngx_sock_ntop(c->sockaddr, c->socklen,
c->addr_text.data,
ls->addr_text_max_len, 0);
if (c->addr_text.len == 0) {
ngx_close_accepted_connection(c);
return;
}
}
将客户端的IP地址和端口号,转换成字符串形式,保存到 addr_text 成员中。
log->data = NULL;
log->handler = NULL;
ls->handler(c);
调用 ls->handler,即 ngx_http_init_connection
函数,这个事件就处理完成了。这个handler 是在解析 http{} 配置时,ngx_http_add_listening
函数设置的。
ngx_http_init_connection
它的原型如下:
void ngx_http_init_connection(ngx_connection_t *c)
首先
hc = ngx_pcalloc(c->pool, sizeof(ngx_http_connection_t));
if (hc == NULL) {
ngx_http_close_connection(c);
return;
}
c->data = hc;
分配一个 ngx_http_connection_t
结构。
/* find the server configuration for the address:port */
port = c->listening->servers;
if (port->naddrs > 1) {
/*
* there are several addresses on this port and one of them
* is an "*:port" wildcard so getsockname() in ngx_http_server_addr()
* is required to determine a server address
*/
if (ngx_connection_local_sockaddr(c, NULL, 0) != NGX_OK) {
ngx_http_close_connection(c);
return;
}
switch (c->local_sockaddr->sa_family) {
#if (NGX_HAVE_INET6)
case AF_INET6:
sin6 = (struct sockaddr_in6 *) c->local_sockaddr;
addr6 = port->addrs;
/* the last address is "*" */
for (i = 0; i < port->naddrs - 1; i++) {
if (ngx_memcmp(&addr6[i].addr6, &sin6->sin6_addr, 16) == 0) {
break;
}
}
hc->addr_conf = &addr6[i].conf;
break;
#endif
default: /* AF_INET */
sin = (struct sockaddr_in *) c->local_sockaddr;
addr = port->addrs;
/* the last address is "*" */
for (i = 0; i < port->naddrs - 1; i++) {
if (addr[i].addr == sin->sin_addr.s_addr) {
break;
}
}
hc->addr_conf = &addr[i].conf;
break;
}
} else {
switch (c->local_sockaddr->sa_family) {
#if (NGX_HAVE_INET6)
case AF_INET6:
addr6 = port->addrs;
hc->addr_conf = &addr6[0].conf;
break;
#endif
default: /* AF_INET */
addr = port->addrs;
hc->addr_conf = &addr[0].conf;
break;
}
}
/* the default server configuration for the address:port */
hc->conf_ctx = hc->addr_conf->default_server->ctx;
接下来,根据接受该连接的IP地址和端口号,找到处理这个请求的默认服务。conf_ctx 成员指向这个默认服务的配置上下文(ngx_http_conf_ctx_t
结构)。以上代码对照这个图就能看明白了:
ctx = ngx_palloc(c->pool, sizeof(ngx_http_log_ctx_t));
if (ctx == NULL) {
ngx_http_close_connection(c);
return;
}
ctx->connection = c;
ctx->request = NULL;
ctx->current_request = NULL;
c->log->connection = c->number;
c->log->handler = ngx_http_log_error;
c->log->data = ctx;
c->log->action = "waiting for request";
c->log_error = NGX_ERROR_INFO;
分配一个 HTTP 日志上下文 ngx_http_log_ctx_t
结构,将连接的日志结构中的 data 成员指向它。并设置 handler 为 ngx_http_log_error
函数。这样调用 ngx_log_error
函数输出错误日志时就会调用这个 handler 。
rev = c->read;
rev->handler = ngx_http_wait_request_handler;
c->write->handler = ngx_http_empty_handler;
将读事件的 handler 设置为 ngx_http_wait_request_handler
函数。写事件 handler 设置为 ngx_http_empty_handler
函数,这个函数实际上什么都不做。
ngx_add_timer(rev, c->listening->post_accept_timeout);
ngx_reusable_connection(c, 1);
if (ngx_handle_read_event(rev, 0) != NGX_OK) {
ngx_http_close_connection(c);
return;
}
ngx_add_timer
为这个读事件添加一个定时器,定时器超时后,就会调用它的 handler ngx_http_wait_request_handler
函数。
ngx_reusable_connection
将这个连接置为可重用。因为该连接上还没有请求到来,所以当连接池中的连接不够用时,就可以重用这个连接。
ngx_handle_read_event
这个函数判断如果该读事件没有被 epoll 监测并且没有就绪,那么就让 epoll 监测它。它的代码如下:
/* kqueue, epoll */
if (!rev->active && !rev->ready) {
if (ngx_add_event(rev, NGX_READ_EVENT, NGX_CLEAR_EVENT)
== NGX_ERROR)
{
return NGX_ERROR;
}
}
return NGX_OK;
初始时,读事件满足这两点要求,所以会被 epoll 监测。
到这里,函数返回,整个建立 TCP 连接的读事件就处理完成了。
当连接上有请求的数据到达时,就会由 ngx_http_wait_request_handler
函数处理。