Nginx upstream原理分析【1】新连接的处理过程
在上一篇“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,函数只要是如下几个功能:
- 初始化一个HTTP请求的相关结构,重点是ngx_http_request_t结构,然后读取请求第一个数据包的数据,并设置相关的回调函数
- 设置rev->handler = ngx_http_process_request_line; , 读事件的回调函数。
- 初始化数据后,调用读数据回调函数回调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中情况:
- 返回NGX_OK,解析出了一行。下面需要将这个HEADER放入哈希表中,然后调用对应的ngx_http_headers_in里面的回调(ngx_http_headers_in里面的函数)。
- 返回NGX_HTTP_PARSE_HEADER_DONE,全部请求的HEADER已经处理完毕,碰到了空行\r\n\r\n,那就调用ngx_http_process_request_header查找对应的虚拟主机,然后调用ngx_http_process_request进入请求处理间断,里面会进入ngx_http_handler->phrases。
- 返回NGX_AGAIN,表示一个请求header还没有解析完成,没有数据了需要读取一些数据才行,continue继续循环。
- 否则,失败,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后面介绍。
近期评论