首页 > Nginx > Nginx upstream原理分析【1】新连接的处理过程

Nginx upstream原理分析【1】新连接的处理过程

2013年5月26日 发表评论 阅读评论 9601次阅读    

在上一篇“Nginx upstream原理分析【0】指令解析”中介绍了nginx对于upstream的指令解析,初始化的逻辑。这里介绍一个请求从一开始到后面是怎么处理的。

0、Accept接收新连接:

首先,nginx在ngx_init_cycle解析完配置后,就已经打开了所有的监听端口,然后会调用各个进程初始化函数,这样event模块的进程初始化函数为ngx_event_process_init,这个函数会将所有的listening端口结构跟ngx_connection_t结构进行关联,互相指向,并且为这个连接注册了读写监控事件,这个事件的句柄正式:ngx_event_accept函数,这样,当listen端口有新连接到来的时候会调用ngx_event_accept函数。

下面来简单看一下ngx_event_accept函数的逻辑,函数首先循环最多5次调用accept()系统函数去接收这个连接,并计算一下本进程的负载,来进行简单的负载均衡:

//当有新连接后,会调用读ngx_event_t结构read的handler回调,监听socket会设置为这个函数。
//工作进程初始化的时候会调用ngx_event_process_init模块初始化函数设置为ngx_event_accept,当做accept钩子
//有新连接的时候会调用这里进行accept.
//这里会将新连接放入epoll,监听可读可写事件,然后调用ngx_http_init_connection
void ngx_event_accept(ngx_event_t *ev)
{
    socklen_t          socklen;
    ngx_err_t          err;
    ngx_log_t         *log;
    ngx_socket_t       s;
    ngx_event_t       *rev, *wev;
    ngx_listening_t   *ls;
    ngx_connection_t  *c, *lc;
    ngx_event_conf_t  *ecf;
    u_char             sa[NGX_SOCKADDRLEN];

    ecf = ngx_event_get_conf(ngx_cycle->conf_ctx, ngx_event_core_module);//先得到ngx_events_module,然后再得到里面的core模块
    if (ngx_event_flags & NGX_USE_RTSIG_EVENT) {
        ev->available = 1;
    } else if (!(ngx_event_flags & NGX_USE_KQUEUE_EVENT)) {
        ev->available = ecf->multi_accept;//一次尽量接完,默认为0的
    }
    lc = ev->data;//得到这个事件所属的连接
    ls = lc->listening;//从而得到这个连接所指的listening 结构
    ev->ready = 0;
    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, ev->log, 0,"accept on %V, ready: %d", &ls->addr_text, ev->available);
    do {//这个连接有可读事件了,那可能可以读很多了,所以得有循环
        socklen = NGX_SOCKADDRLEN;
        s = accept(lc->fd, (struct sockaddr *) sa, &socklen);//接一个新连接
        if (s == -1) {//失败
            err = ngx_socket_errno;
            if (err == NGX_EAGAIN) {//没有了这回
                ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, err, "accept() not ready");
                return;
            }
            ngx_log_error((ngx_uint_t) ((err == NGX_ECONNABORTED) ? NGX_LOG_ERR : NGX_LOG_ALERT), ev->log, err, "accept() failed");
            if (err == NGX_ECONNABORTED) {
                if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
                    ev->available--;//kqueue的话不能接多个
                }
                if (ev->available) {
                    continue;
                }
            }
            return;
        }
//accept成功
#if (NGX_STAT_STUB)
        (void) ngx_atomic_fetch_add(ngx_stat_accepted, 1);
#endif
        ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;
//当已使用的连接数占到在nginx.conf里配置的worker_connections总数的7/8以上时,ngx_accept_disabled为大于0,
//此后在主循环里面就不会再进行accept,而是递减1,这样相当于让我这个进程丢掉一点accept的机会吧。
//不过这个只在accept_mutex on 配置打开时才有效,否则的话是默认会不断监听的

        c = ngx_get_connection(s, ev->log);//拿到一个空闲的连接

接收到一个连接后,我们就得到了一个新的SOCKET FD,以后的所有交互就都在这个新的连接上操作了。于是就为这个连接新建一个内存池(nginx的内存池是基于连接的,连接关闭就可以释放了)。紧接着就是为这个连接设置相关的读写相关的句柄,加入epoll读写事件中,当然,这里并没有设置相关的读写事件回调函数,为什么呢?因为现在还不知道,现在只是刚刚接受了这个连接,还没有进行协议什么的处理,如果后面是http,https是有区别的。然后调用了ls->handler(c);,也就是调用这个监听端口上面的handler回调函数,这个回调函数一般为ngx_http_init_connection,也就是初始化连接函数,这个我们一会介绍。

这个handler是什么时候设置的呢?答案是:在nginx解析完http{}块后,调用ngx_http_optimize_servers这个函数对listening和connection相关的变量进行了初始化和调优,并最终在 ngx_http_add_listening (被ngx_http_init_listening调用) 中注册了listening 的 handler 为 ngx_http_init_connection。
我们来看一下ngx_event_accept连接接受函数的余下代码:

        *log = ls->log;
        c->recv = ngx_recv;//k ngx_unix_recv  ,其实还有ngx_ssl_recv
        c->send = ngx_send;//k ngx_unix_send , 其实还有ngx_ssl_write
        c->recv_chain = ngx_recv_chain;//k ngx_readv_chain
        c->send_chain = ngx_send_chain;//k ngx_writev_chain
/*ngx_io = ngx_os_io ;//相当于这个IO是跟os相关的。
ngx_os_io_t ngx_os_io = {
    ngx_unix_recv,
    ngx_readv_chain,
    ngx_udp_unix_recv,
    ngx_unix_send,
    ngx_writev_chain,
    0
};*/
        c->log = log;
        c->pool->log = log;
        c->socklen = socklen;
        c->listening = ls;//刚申请的连接,回指一下这个连接所属的listening结构。指向我是从哪个listenSOCK accept出来的
        c->local_sockaddr = ls->sockaddr;
        c->unexpected_eof = 1;
#if (NGX_HAVE_UNIX_DOMAIN)
        if (c->sockaddr->sa_family == AF_UNIX) {
            c->tcp_nopush = NGX_TCP_NOPUSH_DISABLED;
            c->tcp_nodelay = NGX_TCP_NODELAY_DISABLED;
        }
#endif
        rev = c->read;//这个新连接的读写事件
        wev = c->write;
        wev->ready = 1;// 写事件,表示已经accept了 ?
        if (ngx_event_flags & (NGX_USE_AIO_EVENT|NGX_USE_RTSIG_EVENT)) {
            /* rtsig, aio, iocp */
            rev->ready = 1;
        }
        if (ev->deferred_accept) {
//如果采用deferred模式,内核在三次握手建立连接后,不会立即通知程序监听连接可读,而是等待到第一个可读数据包才通知,因此,此时是有可读事件的
            rev->ready = 1;//这回可以读的
        }
//······
        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, log, 0, "*%d accept: %V fd:%d", c->number, &c->addr_text, s);
        if (ngx_add_conn && (ngx_event_flags & NGX_USE_EPOLL_EVENT) == 0) {
            if (ngx_add_conn(c) == NGX_ERROR) {//现在加入了,但还没设置回调呢,不过没事,反正单进程,不会有事的。待会就加
//如果使用epoll,我喜欢.ngx_epoll_add_connection 采用边缘触发,注册EPOLLIN|EPOLLOUT|EPOLLET
                ngx_close_accepted_connection(c);
                return;
            }
        }
        log->data = NULL;
        log->handler = NULL;
//注意,这个链接的读写事件回调句柄暂时还没有设置,为什么呢? 因为此处是通用的,
//我只负责接受连接,加入epoll,具体句柄,看具体的类型了,是http还是ftp还是https啥的。具体的就得看这个listen sock是用于什么了,比如http,ftp啥的。
//比如说: 接收一个连接后,应该怎么办呢,应该进行对应的初始化。那怎么初始化? 解析时碰到什么,就怎么初始化吧
        ls->handler(c);//指向ngx_http_init_connection,最开头是在ngx_http_commands -> ngx_http_block设置的
// ngx_http_block 里面调用了 ngx_http_optimize_servers ,这个函数对listening和connection相关的变量进行了初始化和调优,
//并最终在 ngx_http_add_listening (被ngx_http_init_listening调用) 中注册了listening 的 handler 为 ngx_http_init_connection
        if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
            ev->available--;
        }

    } while (ev->available);//一次可以接多个,直到没有可读的了
}

1、连接初始化ngx_http_init_connection/ngx_http_init_request:

上面介绍到nginx accept了一个新的连接,并且加入了epoll的监听事件里面了,但是其并没有设置这个连接的读写事件回调,他只是调用了listening连接上面的handler句柄,来进行对应业务的初始化,对于http,那就是ngx_http_init_connection函数,这个函数做了以下几件事:

首先将这个客户端连接的读事件回到设置为ngx_http_init_request,写事件回调设置为ngx_http_empty_handler空操作;

如果是TCP_DEFER_ACCEPT,那就说明数据已经到来,内核才通知nginx有新连接的,而不是三次握手就通知了,那么将这个事件挂入ngx_posted_events待会解开accept锁后再处理事件,如果没有ngx_use_accept_mutex锁,那就直接调用ngx_http_init_request处理请求;

然后调用ngx_add_timer设置这个连接在发起请求到发送header之间的超时事件,也就配置:“client_header_timeout xxxxs"指定的时间。

// ngx_http_block 里面调用了 ngx_http_optimize_servers ,这个函数对listening和connection相关的变量进行了初始化和调优,
//并最终在 ngx_http_add_listening (被ngx_http_add_listening调用) 中注册了listening 的 handler 为 ngx_http_init_connection
void ngx_http_init_connection(ngx_connection_t *c)
{//注册这个新连接的回调函数,因为我是HTTP,因此对这个刚accept的连接,我需要注册我需要的读写事件回调。
//之前已经加入读写事件到EPOLL了的,在ngx_event_accept里面,就是一个监听端口有新连接事件,调用ngx_event_accept,
//然后它ACCEPT一个或多个连接,然后调用这个监听listening结构的handler,然后我们想到,在HTTP服务的时候,也就是ngx_http_block这个cmd会知道的
//我的连接,我要自己init,也就是ngx_http_init_connection的回调到listening->handler
    ngx_event_t         *rev;
    ngx_http_log_ctx_t  *ctx;

    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;//错误日志打印的句柄,自定义,专门用来打HTTP的日志
    c->log->data = ctx;
    c->log->action = "reading client request line";//标注我正在干啥事,打日志时好用

    c->log_error = NGX_ERROR_INFO;

    rev = c->read;//读事件结构
    rev->handler = ngx_http_init_request;//如果待会有可读事件,那应该调用的handler为ngx_http_init_request,表示刚开始读数据时的状态
    c->write->handler = ngx_http_empty_handler;//空操作

#if (NGX_STAT_STUB)
    (void) ngx_atomic_fetch_add(ngx_stat_reading, 1);
#endif

    if (rev->ready) {
//如果设置了TCP_DEFER_ACCEPT,那说明accept的时候,实际上数据已经到来.内核此时才通知我们有新连接,其实是还有数据
        /* the deferred accept(), rtsig, aio, iocp */
        if (ngx_use_accept_mutex) {//如果用了锁,那这里先不读了,挂到后面,退出返回后再读取。
            ngx_post_event(rev, &ngx_posted_events);
//把这个事件放到后面进行处理,相当于accept的时候,因为有accept锁,我们已经拿到锁了,所以这里先不读了,后续再读
            return;
        }
        ngx_http_init_request(rev);//果断去读数据
        return;
    }
    ngx_add_timer(rev, c->listening->post_accept_timeout);//等于client_header_timeout,就是客户端发送头部的延迟超时时间
    if (ngx_handle_read_event(rev, 0) != NGX_OK) {
//如果没在epoll里面,加入。实际上在ngx_event_accept已经调用了ngx_epoll_add_connection
#if (NGX_STAT_STUB)
        (void) ngx_atomic_fetch_add(ngx_stat_reading, -1);
#endif
        ngx_http_close_connection(c);
        return;
    }
}

从上面可以看出,ngx_http_init_connection做了一些初始化工作,然后主要的工作移步到了ngx_http_init_request函数,顾名思义,其作用是初始化一个http的请求,也就是请求的header到来的时候,进行初始化。虽然 rev->handler = ngx_http_init_request;,但是在后者里面会更改这个读写事件句柄的。nginx之所以不在ngx_http_init_connection里面初始化ngx_http_request_t等结构,原因是想把这个动嘴延迟到请求的数据真正到来的时候再进行,算是一种优化,不然如果一个客户端发送大堆的连接,却不发送数据,那我们就会为这些空连接建立很多请求的数据结构,太费了。

下面看看真正的请求初始化函数ngx_http_init_request,函数只要是如下几个功能:

  1. 初始化一个HTTP请求的相关结构,重点是ngx_http_request_t结构,然后读取请求第一个数据包的数据,并设置相关的回调函数
  2. 设置rev->handler = ngx_http_process_request_line; , 读事件的回调函数。
  3. 初始化数据后,调用读数据回调函数回调rev->handler(rev);//ngx_http_process_request_line。

最后一步相当于在ACCEPT一个连接后,先设置读数据回调函数为ngx_http_init_request,然后这里面其实就是想第一次嘛,初始化一个请求的相关数据,然后立马将读数据回调函数归正,设置为正常的读HTTP读请求函数ngx_http_process_request_line。

函数比较长,大都是进行初始化,这里我们摘出主要的了解一下:

static void ngx_http_init_request(ngx_event_t *rev)
{
//·····
    c->data = r;//掉换一下指向,SOCK连接结构的data指向ngx_http_request_t
    r->http_connection = hc;//ngx_http_request_t的http_connection索引所属的HTTP连接。那怎么从这里找到对应的SOCK连接 ?r->connection = c;

    c->sent = 0;
    r->signature = NGX_HTTP_MODULE;
    /* find the server configuration for the address:port */
    port = c->listening->servers;//找到这个连接对应的监听端口对应的servers
    r->connection = c;//索引所对应的SOCK连接
//···
    r->virtual_names = addr_conf->virtual_names;
    /* the default server configuration for the address:port */
    cscf = addr_conf->default_server;
//下面就是拿到这个连接对应的配置结构
    r->main_conf = cscf->ctx->main_conf;//指向对应的配置结构
    r->srv_conf = cscf->ctx->srv_conf;
    r->loc_conf = cscf->ctx->loc_conf;

    rev->handler = ngx_http_process_request_line;//设置这个SOCK的下一次可读事件的句柄
    r->read_event_handler = ngx_http_block_reading;
//····
//可以看出ngx_http_init_request之所以在ngx_http_init_connection里面会设置为读事件回调函数,就是想再可读事件到来的时候,再开始初始化相关数据
//免得一开始接受一个连接就分配数据,太费了。然后初始化后到正常状态,设置为ngx_http_process_request_line
    rev->handler(rev);//ngx_http_process_request_line
}

可以看到,这个函数完成ngx_http_request_t结构体的初始化,设置了连接的各个字段关系等,然后就是调用ngx_http_process_request_line函数去处理连接上面的GET、POST请求行。


2、处理HTTP请求行ngx_http_process_request_line:

这个函数由上面的init_request函数设置为连接的读事件结构的回调,当客户端一开始有数据发送过来的时候,负责处理HTTP协议的第一行,也就是GET、POST行,也就是GET /UII HTTP 1.1 , 读取完毕后,会调用ngx_http_process_request_headers读取头部数据。

现在来一步步看这个读取请求行的函数,函数主题是一个大循环,也就是循环处理头部的数据,调用ngx_http_read_request_header看看有没有可读的数据,如果有就调用ngx_http_parse_request_line解析头部,如果成功了就进行对应的处理,否则返回继续。下面看一下函数前半部分:

static void ngx_http_process_request_line(ngx_event_t *rev)
{//读取客户端发送的第一行数据,也就是GET /UII HTTP 1.1 , 读取完毕后,会调用ngx_http_process_request_headers读取头部数据
//ngx_event_t的data记录所属的连接connection_t,连接里面目前指向http_request_t
    u_char                    *host;
    ssize_t                    n;
    ngx_int_t                  rc, rv;
    ngx_connection_t          *c;
    ngx_http_request_t        *r;
    ngx_http_core_srv_conf_t  *cscf;

    c = rev->data;//在ngx_http_init_request里面设置的
    r = c->data;
//····
    rc = NGX_AGAIN;
    for ( ;; ) {
        if (rc == NGX_AGAIN) {
            n = ngx_http_read_request_header(r);//读HTTP头部,返回读到的数据的大小。或者失败
            if (n == NGX_AGAIN || n == NGX_ERROR) {
                return;
            }
        }
        rc = ngx_http_parse_request_line(r, r->header_in);//解析请求的第一行,也就是: GET /index.html HTTP 1.1
        if (rc == NGX_OK) {
            /* the request line has been parsed successfully */
            r->request_line.len = r->request_end - r->request_start;//记录请求头的缓冲位置
            r->request_line.data = r->request_start;
            if (r->args_start) {//如果有?开始的部分,就是后面的参数
                r->uri.len = r->args_start - 1 - r->uri_start;
            } else {
                r->uri.len = r->uri_end - r->uri_start;
            }
            if (r->complex_uri || r->quoted_uri) {//URI上有.%#/等符号,就定义为复杂的URI
                r->uri.data = ngx_pnalloc(r->pool, r->uri.len + 1);
                if (r->uri.data == NULL) {
                    ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
                    return;
                }
				//解析参数部分,因为含有复杂的字符. 解析结果放入uri.data里面
                cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
                rc = ngx_http_parse_complex_uri(r, cscf->merge_slashes);//解析请求头的参数部分等。
                if (rc == NGX_HTTP_PARSE_INVALID_REQUEST) {
                    ngx_log_error(NGX_LOG_INFO, c->log, 0,  "client sent invalid request");
                    ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
                    return;
                }
            } else {
                r->uri.data = r->uri_start;//都是正常字符,直接改变指向就行。内存也不用释放了。
            }

上面ngx_http_read_request_header读取请求数据的函数比较简单,它看看有没有数据在header_in的缓冲区,如果有,则返回大小,否则读一些,返回大小读取第一行GET、POST的时候会调用这里获取数据,读取header的时候也会调用这里。
读取连接数据是用c->recv()函数进行的,读取到r->header_in里面,recv是在ngx_event_accept接受一个连接的时候设置的读事件回调,写事件回调,函数列表在ngx_os_io结构里面,c->recv = ngx_recv;//k ngx_unix_recv。下面看一下ngx_http_read_request_header函数:

static ssize_t ngx_http_read_request_header(ngx_http_request_t *r)
{//看看有没有数据在header_in的缓冲区,如果有,则返回大小,否则读一些,返回大小
//读取第一行GET、POST的时候会调用这里获取数据,读取header的时候也会调用这里。
    ssize_t                    n;
    ngx_event_t               *rev;
    ngx_connection_t          *c;
    ngx_http_core_srv_conf_t  *cscf;

    c = r->connection;//根据http请求结构,得到所绑定的连接,然后得到连接的read读事件结构体。
    rev = c->read;
    n = r->header_in->last - r->header_in->pos;//看看buf里面是否还有数据,如果有,返回数据长度
    if (n > 0) {
        return n;
    }
    if (rev->ready) {
//在ngx_event_accept接受一个连接的时候设置的读事件回调,写事件回调,函数列表在ngx_os_io结构里面
//        c->recv = ngx_recv;//k ngx_unix_recv  ,其实还有ngx_ssl_recv
        n = c->recv(c, r->header_in->last, r->header_in->end - r->header_in->last);
    } else {//什么情况下会这样?一个连接没有准备好 .ngx_unix_recv里面设置过为0,表示没有数据可以读了
        n = NGX_AGAIN;//暂时没有可读数据,待会可能有读的
    }

    if (n == NGX_AGAIN) {//如果刚才没有读到数据
        if (!rev->timer_set) {
            cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
            ngx_add_timer(rev, cscf->client_header_timeout);
//设置一个读去header头的超时定时器,如果超时了,就会调用ngx_http_process_request_line进行读取头部,然后一开始就失败超时了的
        }

        if (ngx_handle_read_event(rev, 0) != NGX_OK) {//刚才没读到,现在就加入可读事件epoll中
            ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
            return NGX_ERROR;
        }

        return NGX_AGAIN;
    }

    if (n == 0) {//如果recv返回0,表示连接被中断了。
        ngx_log_error(NGX_LOG_INFO, c->log, 0, "client closed prematurely connection");
    }

    if (n == 0 || n == NGX_ERROR) {
        c->error = 1;//有错误发生,关闭连接
        c->log->action = "reading client request headers";
        ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
        return NGX_ERROR;
    }
    r->header_in->last += n;//缓冲区往后增加n个字符。返回数据大小
    return n;
}

ngx_http_parse_request_line函数比较重要,是用来进行请求行解析的,函数比较长,主要就是一个巨大的状态机,用来解析请求行。HTTP的请求行格式为:

Request-Line   = Method SP Request-URI SP HTTP-Version CRLF

Request-URI    = "*" | absoluteURI | abs_path | authority

其中absoluteURI 比较复杂,可能是个url,http 1.1支持这种,比如GET http://www.w3.org/pub/WWW/TheProject.html HTTP/1.1,这里面是带有host,port等的。解析比较麻烦,这里就简单举个例子列一下代码吧:

/* gcc, icc, msvc and others compile these switches as an jump table */
/*HTTP请求结构:http://www.ietf.org/rfc/rfc2616.txt
					Request   = Request-Line              ; Section 5.1
                        *(( general-header        ; Section 4.5
                         | request-header         ; Section 5.3
                         | entity-header ) CRLF)  ; Section 7.1
                        CRLF
                        [ message-body ]          ; Section 4.3*/
ngx_int_t
ngx_http_parse_request_line(ngx_http_request_t *r, ngx_buf_t *b)
{//解析请求的第一行,也就是: GET /index.html HTTP 1.1,模式为Request-Line   = Method SP Request-URI SP HTTP-Version CRLF
//请注意,请求行是可能带host等全URL的,比如:  GET http://www.w3.org/pub/WWW/TheProject.html HTTP/1.1
    u_char  c, ch, *p, *m;
//····
    state = r->state;//这个给力,记录一下上次的状态,君子报仇十年不晚
    for (p = b->pos; p < b->last; p++) {//一个个的扫描缓冲区
        ch = *p;
        switch (state) {
        /* HTTP methods: GET, HEAD, POST */
        case sw_start:
            r->request_start = p;//记录一下开头
            if (ch == CR || ch == LF) {
                break;//忽略前面的换行
            }
            if ((ch < 'A' || ch > 'Z') && ch != '_') {//初略的判断一下是否有问题。下划线是干嘛的?莫非是合法的头部字符?
                return NGX_HTTP_PARSE_INVALID_METHOD;
            }
            state = sw_method;//设置为下一步获取方法GET/POST等,那ch这个字符待会就丢了吧,没事的, r->request_start已经记录了开头部分
            break;

        case sw_method:
            if (ch == ' ') {//如果在解析方法的时候,碰到了空格,GOOD,只是得到了方法,GET/POST
                r->method_end = p - 1;//HTTP 的方法 碰到空格算结尾
                m = r->request_start;//从sw_start里面记录的尾部,
//······
    b->pos = p;//记住这个位子,刚才我扫到了这里
    r->state = state;//我的下一个状态是这个。我应该处理这个了。,下回有数据,将从这里开始。至于刚才读了啥,没事,*_start指针记着了的
    return NGX_AGAIN;//还没有结束,下回继续

done:
    b->pos = p + 1;
    if (r->request_end == NULL) {//已经读取完毕,如果没有记录request_end,记录一下,结尾部分在此
        r->request_end = p;
    }
    r->http_version = r->http_major * 1000 + r->http_minor;//版本
    r->state = sw_start;//这是啥意思,莫非其他地方也有类似的状态机。对,比如ngx_http_parse_header_line,相当于告诉别人,你的状态从此开始
    if (r->http_version == 9 && r->method != NGX_HTTP_GET) {
        return NGX_HTTP_PARSE_INVALID_09_METHOD;
    }
    return NGX_OK;
}

请求行解析完成 后,设置一些相关的字段,uri等数据的处理,如果处理到了host_start,也就是URI中带了host,那就验证一下是否合法等。然后就修改客户端连接的读事件回调为ngx_http_process_request_headers并调用之,进行请求的header的处理。也就是说,ngx_http_process_request_line处理完请求行后,就会修改连接上的可读事件结构,设置为处理HEADER的ngx_http_process_request_headers,后续的可读事件由其接管。下面过一下ngx_http_process_request_line函数余下的代码,具体见注释中。

			//HOST处理
            if (r->host_start && r->host_end) {
			//请注意,请求行是可能带host等全URL的,比如:  GET http://www.w3.org/pub/WWW/TheProject.html HTTP/1.1
                host = r->host_start;//设置了HOST,验证一下是否合法,结果放入host 临时变量
                n = ngx_http_validate_host(r, &host,  r->host_end - r->host_start, 0);
                if (n == 0) {
                    ngx_log_error(NGX_LOG_INFO, c->log, 0, "client sent invalid host in request line");
                    ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
                    return;
                }
                if (n < 0) {                     ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);                     return;                 }                 r->headers_in.server.len = n;
                r->headers_in.server.data = host;//保存一下HOST字符串
            }
            if (r->http_version < NGX_HTTP_VERSION_10) {//HTTP 1.0版本      		/*http://blog.csdn.net/forgotaboutgirl/article/details/6936982      		在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。      		但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。 			HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。 			此外,服务器应该接受以绝对路径标记的资源请求。*/                 if (ngx_http_find_virtual_server(r, r->headers_in.server.data, r->headers_in.server.len) == NGX_ERROR)
                {//找出对应的虚拟主机。那好吧,如果到后面看到Host的HTTP HEADER,还会再换一下,再找的。
                    ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
                    return;
                }
                ngx_http_process_request(r);//1.0就直接处理了。啥意思
                return;
            }

            if (ngx_list_init(&r->headers_in.headers, r->pool, 20, sizeof(ngx_table_elt_t)) != NGX_OK){
                ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
                return;
            }
            if (ngx_array_init(&r->headers_in.cookies, r->pool, 2, sizeof(ngx_table_elt_t *))  != NGX_OK){
                ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
                return;
            }
            c->log->action = "reading client request headers";
            rev->handler = ngx_http_process_request_headers;
            ngx_http_process_request_headers(rev);//下面去读取请求的头部数据,第一行的GET在此读取成功。下面读取头部数据了。

3、处理Headers头部数据ngx_http_process_request_headers:

ngx_http_process_request_line调用这里,此时已经读取完了请求的第一行GET /uri http 1.0.下面开始循环读取请求的头部headers数据.

跟读取请求行一样,调用ngx_http_read_request_header函数读取一部分数据放到r->header_in里面,有数据放在header_in了,下面进行处理解析header行,每次只解析一行。GET/POST行已经在ngx_http_parse_request_line进行处理了。这个HEADER的解析由函数ngx_http_parse_header_line完成,ngx_http_parse_header_line解析结果有如下4中情况:

  1. 返回NGX_OK,解析出了一行。下面需要将这个HEADER放入哈希表中,然后调用对应的ngx_http_headers_in里面的回调(ngx_http_headers_in里面的函数)。
  2. 返回NGX_HTTP_PARSE_HEADER_DONE,全部请求的HEADER已经处理完毕,碰到了空行\r\n\r\n,那就调用ngx_http_process_request_header查找对应的虚拟主机,然后调用ngx_http_process_request进入请求处理间断,里面会进入ngx_http_handler->phrases。
  3. 返回NGX_AGAIN,表示一个请求header还没有解析完成,没有数据了需要读取一些数据才行,continue继续循环。
  4. 否则,失败,ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);关闭连接。

ngx_http_parse_header_line比较简单,也是基于状态的解析,返回对应的上述状态。

ngx_http_process_request_header函数用来对请求的头部数据进行简单的处理,其会调用ngx_http_find_virtual_server函数找到对应host的虚拟主机名字,并将虚拟主机对应的配置设置到当前请求结构里面,更新srv_conf,loc_conf。虚拟主机的查找就在此,也就在读取完成所有header的时候,查找对应的虚拟主机。看一下ngx_http_process_request_headers的代码:

static void ngx_http_process_request_headers(ngx_event_t *rev)
{//ngx_http_process_request_line调用这里,此时已经读取完了请求的第一行GET /uri http 1.0.
//下面开始循环读取请求的头部headers数据
    u_char                     *p;
    size_t                      len;
    ssize_t                     n;
    ngx_int_t                   rc, rv;
    ngx_table_elt_t            *h;
    ngx_connection_t           *c;
    ngx_http_header_t          *hh;
    ngx_http_request_t         *r;
    ngx_http_core_srv_conf_t   *cscf;
    ngx_http_core_main_conf_t  *cmcf;

    c = rev->data;//从可读事件结构中得到对应的HTTP连接
    r = c->data;//然后得到连接对应的数据结构,对于HTTP,就是ngx_http_request_t
    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, rev->log, 0, "http process request header line");

    if (rev->timedout) {//超时是永恒的话题
        ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");
        c->timedout = 1;//待会打日志时用,标记为超时
        ngx_http_close_request(r, NGX_HTTP_REQUEST_TIME_OUT);
        return;
    }
	//在ngx_http_process_request_line处理请求的GET/POST里面,会根据HOST域设置对应的虚拟主机数据配置
    cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
    cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
    rc = NGX_AGAIN;

    for ( ;; ) {
        if (rc == NGX_AGAIN) {
            if (r->header_in->pos == r->header_in->end) {
//如果header_in缓冲里面没有数据结构了,那就得去读一点了。不然就读一点,处理一点。
                rv = ngx_http_alloc_large_header_buffer(r, 0);
//申请一块大的缓冲区,保留之前的数据。后面的0表示现在不是在处理请求行的过程中
//······
            }

            n = ngx_http_read_request_header(r);//尝试去读一点数据出来,这个函数负责读取get行和header数据。
            if (n == NGX_AGAIN || n == NGX_ERROR) {
                return;//如果没有读数据了,或者失败了。都会设置相应的错误码的
            }
        }
		//有数据放在header_in了,下面进行处理解析header行,每次只解析一行。GET/POST行已经在ngx_http_parse_request_line进行处理了。
        rc = ngx_http_parse_header_line(r, r->header_in, cscf->underscores_in_headers);
        if (rc == NGX_OK) {//解析出了一行。下面需要将这个HEADER放入哈希表中,然后调用对应的ngx_http_headers_in里面的回调。
            if (r->invalid_header && cscf->ignore_invalid_headers) {
                /* there was error while a header line parsing */
                ngx_log_error(NGX_LOG_INFO, c->log, 0, "client sent invalid header line: \"%*s\"", r->header_end - r->header_name_start, r->header_name_start);
                continue;
            }
            /* a header line has been parsed successfully */
            h = ngx_list_push(&r->headers_in.headers);//在r->headers_in.headers里面申请一个位置,用来放置请求头部
            if (h == NULL) {
                ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
                return;
            }
			//下面就拿到了一对K-V了。
            h->hash = r->header_hash;
            h->key.len = r->header_name_end - r->header_name_start;//header的名字长度
            h->key.data = r->header_name_start;//记录名字开始。
            h->key.data[h->key.len] = '\0';//标志结尾
            h->value.len = r->header_end - r->header_start;
            h->value.data = r->header_start;
            h->value.data[h->value.len] = '\0';

            h->lowcase_key = ngx_pnalloc(r->pool, h->key.len);
            if (h->lowcase_key == NULL) {
                ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
                return;
            }

            if (h->key.len == r->lowcase_index) {
                ngx_memcpy(h->lowcase_key, r->lowcase_header, h->key.len);

            } else {
                ngx_strlow(h->lowcase_key, h->key.data, h->key.len);
            }
			//找到这个请求头部对应的处理函数,调用之。这个哈希表在ngx_http_headers_in里面
			//只是简单设置一下变量,并未进行实际的操作,处理。实际处理在后面的ngx_http_process_request_header
            hh = ngx_hash_find(&cmcf->headers_in_hash, h->hash, h->lowcase_key, h->key.len);
            if (hh && hh->handler(r, h, hh->offset) != NGX_OK) {
                return;
            }
            ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,"http header: \"%V: %V\"", &h->key, &h->value);
            continue;//继续下一个请求头部
        }
        if (rc == NGX_HTTP_PARSE_HEADER_DONE) {//全部请求的HEADER已经处理完毕,碰到了空行
            /* a whole header has been parsed successfully */
            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "http header done");
            r->request_length += r->header_in->pos - r->header_in->start;
            r->http_state = NGX_HTTP_PROCESS_REQUEST_STATE;//下一个步骤,处理请求状态。全部状态在这里:ngx_http_state_e
            rc = ngx_http_process_request_header(r);//简单处理一下HEADER域,设置虚拟主机等。
            if (rc != NGX_OK) {
                return;
            }
            ngx_http_process_request(r);//然后进入请求处理间断,里面会进入ngx_http_handler->phrases。
            return;
        }

        if (rc == NGX_AGAIN) {
            /* a header line parsing is still not complete */
            continue;
        }
        /* rc == NGX_HTTP_PARSE_INVALID_HEADER: "\r" is not followed by "\n" */
        ngx_log_error(NGX_LOG_INFO, c->log, 0,  "client sent invalid header line: \"%*s\\r...\"",
                      r->header_end - r->header_name_start, r->header_name_start);
        ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
        return;
    }
}

到这里的情景我们回顾一下:读取完毕了请求行,headers,并且查找到了虚拟主机,设置好了相关srv/loc_conf。下面就是进行真正请求的处理啦。


4、Phrase过程处理部分(rewrite,index,content等)ngx_http_process_request:

ngx_http_process_request的工作比较简单,设置了客户端连接的读写事件回调为ngx_http_request_handler,这样有可读,可写事件的时候会调用这个函数。然后就进入ngx_http_handler进行处理了,看看代码:

static void
ngx_http_process_request(ngx_http_request_t *r)
{//ngx_http_process_request_headers函数调用这里,目前情景我们回顾一下:读取完毕了请求行,headers,并且查找到了虚拟主机,
//设置好了相关srv/loc_conf。下面就是进行真正请求的处理啦。
    ngx_connection_t  *c;
    c = r->connection;//拿到当前请求的连接结构
    if (r->plain_http) {//是否通过SSL发送明文请求。如果是,关闭连接
        ngx_log_error(NGX_LOG_INFO, c->log, 0, "client sent plain HTTP request to HTTPS port");
        ngx_http_finalize_request(r, NGX_HTTP_TO_HTTPS);
        return;
    }
#if (NGX_HTTP_SSL)
//······
#endif
    if (c->read->timer_set) {//这是干嘛,关闭定时器吗?
        ngx_del_timer(c->read);
    }
#if (NGX_STAT_STUB)//如果编译的时候设置了STUB,择这里需要增加统计计数
    (void) ngx_atomic_fetch_add(ngx_stat_reading, -1);
    r->stat_reading = 0;
    (void) ngx_atomic_fetch_add(ngx_stat_writing, 1);
    r->stat_writing = 1;
#endif
    c->read->handler = ngx_http_request_handler;//设置这个连接的读事件结构。有可读事件就调动这个函数
    c->write->handler = ngx_http_request_handler;
    r->read_event_handler = ngx_http_block_reading;//

    ngx_http_handler(r);//请求的头部数据已经读取处理完了,准备长袍,进行头部解析,重定向,
    //以及content phrase,这个时候会触发ngx_http_fastcgi_handler等内容处理模块,
    //其里面会调用ngx_http_read_client_request_body->ngx_http_upstream_init从而进入FCGI的处理阶段或者proxy处理阶段。
    ngx_http_run_posted_requests(c);//ngx_http_run_posted_requests函数是处理子请求的。是么
}

下面来看一下ngx_http_handler,这个函数被上面的ngx_http_process_request和ngx_http_internal_redirect调用,用来启动nginx著名的请求过程处理循环。主要就是调用了一下ngx_http_core_run_phases函数。
另外这个函数还判断了一下r->internal是否为1,如果是旧表示目前这个函数是通过ngx_http_internal_redirect调用过来的,后者会设置r->internal=1,以通知ngx_http_handler目前是在内部重定向的过程中,下面就不需要进行realip_handler的处理了。也就是r->phase_handler设置为cmcf->phase_engine.server_rewrite_index。

void ngx_http_handler(ngx_http_request_t *r)
{//ngx_http_process_request调用这里,进入处理过程的解析,重定向等。此时HEADER已经读取完毕。
//	ngx_http_process_request和ngx_http_internal_redirect调用这里。

    ngx_http_core_main_conf_t  *cmcf;
    r->connection->log->action = NULL;
    r->connection->unexpected_eof = 0;
    if (!r->internal) {//刚开始internal为0,表示不是在内部重定向过程中。
        switch (r->headers_in.connection_type) {////连接是keep-alive还是要close
        case 0://没有设置。则进行默认。需要看是否是HTTP 1.1。 HTTP 1.0不支持keepalive
 //····
        if (r->keepalive) {//如果要保持连接。如下几种情况不支持,比如IE6的POST,safari浏览器。
//·····
        }
        if (r->headers_in.content_length_n > 0) {
            r->lingering_close = 1;
//lingering_close用于在请求完成后,接收客户端发送的额外数据并全部忽略掉,防止因关闭连接时接收缓冲区中仍有数据而停止发送响应并返回RST。
        } else {
            r->lingering_close = 0;
        }
        r->phase_handler = 0;//因为不是在内部重定向循环里面,因此肯定是第一次进入,那就从第一个阶段开始处理吧。
    } else {
    //为1表示:设置请求为内部重定向状态。通知ngx_http_handler,进行间断选择的时候从server_rewrite_index开始进行循环处理,不然又回去了
        cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
        r->phase_handler = cmcf->phase_engine.server_rewrite_index;//获取rewrite过程的下标,从那里开始解析。
    }
    r->valid_location = 1;
#if (NGX_HTTP_GZIP)
    r->gzip_tested = 0;
    r->gzip_ok = 0;
    r->gzip_vary = 0;
#endif
    r->write_event_handler = ngx_http_core_run_phases;//请求的可写事件处理设置为这个,这样就调起了过程处理。
    ngx_http_core_run_phases(r);
}

到这里后,就进入ngx_http_core_run_phases函数进行处理了,这是个nginx phrase过程处理函数(暂且让我这么叫吧,不知道怎么翻译)。其会做重定向,配置查找,内容处理等,这部分内容比较长,后续另起文章介绍了。


总结一下nginx是怎么处理一个连接的(前面部分):

  • accept这个连接;
  • 然后调用listening->handler()也就是ngx_http_init_connection->ngx_http_init_request()初始化这个连接的数据;
  • ngx_http_process_request_line读取请求行;
  • ngx_http_process_request_headers读取Headers头部数据,查找虚拟主机,更新srv_conf/loc_conf配置;
  • ngx_http_process_request开始准备进入phrase过程处理循环,进行重定向,配置更新,内容处理等;

关于nginx后面复杂的phrase过程处理,业也就是ngx_http_core_run_phases后面介绍。

Share
分类: Nginx 标签: , , ,
  1. 本文目前尚无任何评论.
  1. 本文目前尚无任何 trackbacks 和 pingbacks.

注意: 评论者允许使用'@user空格'的方式将自己的评论通知另外评论者。例如, ABC是本文的评论者之一,则使用'@ABC '(不包括单引号)将会自动将您的评论发送给ABC。使用'@all ',将会将评论发送给之前所有其它评论者。请务必注意user必须和评论者名相匹配(大小写一致)。