nginx 源码分析:http 请求处理流程——建立 TCP 连接


这章正式开始分析 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_recvngx_readv_chainngx_unix_sendngx_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 函数处理。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值