nginx學習筆記七(nginx HTTP框架的執行流程)
之前已經介紹過nginx的事件框架。那麼,對於client發出的一個http的請求,nginx的http框架是如何一步步解析這個http請求?http框架又是如何和之前介紹過得epoll事件模組結合起來的,下面來簡要介紹下。
注:我手頭上的nginx工程是nginx-1.9.14的,與《深入理解nginx》的版本不一致,在http框架這塊的程式碼上也有著較大的區別。
一.ngx_http_init_connection
在http框架初始化的時候(參見《深入理解nginx》第10章),會將每個ngx_listening_t結構體的handler方法設為ngx_http_init_connection,這是框架初始化的時候完成的工作。在整個系統正常工作起來之後,client每發出一個新的http連線請求,nginx的事件模組會對這個請求進行處理,最後在ngx_event_accept函式裡面會呼叫accept系統呼叫來接收這個請求。而在ngx_event_accept函式的最後會呼叫ls->handler即ngx_http_init_connection函式。這樣一來,新的http連線就會來到http框架中的函式來進行後續的解析和處理。
這個函式最核心的地方是設定c->read->handler和c->write->handler。因為此時TCP連線已經建立了,後續當epoll_wait返回事件的時候,需要完成的就不是TCP連線操作而是資料接收處理操作。所以這裡把handler設定成了ngx_http_wait_request_handler二.ngx_http_wait_request_handlervoid ngx_http_init_connection(ngx_connection_t *c) //當建立連線後開闢ngx_http_connection_t結構,這裡面儲存該伺服器端ip:port所在server{}上下文配置資訊,和server_name資訊等,然後讓 //ngx_connection_t->data指向該結構,這樣就可以通過ngx_connection_t->data獲取到伺服器端的serv loc 等配置資訊以及該server{}中的server_name資訊 { ngx_uint_t i; ngx_event_t *rev; struct sockaddr_in *sin; ngx_http_port_t *port; ngx_http_in_addr_t *addr; ngx_http_log_ctx_t *ctx; ngx_http_connection_t *hc; #if (NGX_HAVE_INET6) struct sockaddr_in6 *sin6; ngx_http_in6_addr_t *addr6; #endif //注意ngx_connection_t和ngx_http_connection_t的區別,前者是建立連線accept前使用的結構,後者是連線成功後使用的結構 hc = ngx_pcalloc(c->pool, sizeof(ngx_http_connection_t)); if (hc == NULL) { ngx_http_close_connection(c); return; } //在伺服器端accept客戶端連線成功(ngx_event_accept)後,會通過ngx_get_connection從連線池獲取一個ngx_connection_t結構,也就是每個客戶端連線對於一個ngx_connection_t結構, //並且為其分配一個ngx_http_connection_t結構,ngx_connection_t->data = ngx_http_connection_t,見ngx_http_init_connection c->data = hc; /* 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 */ //說明listen ip:port存在幾條沒有bind選項,並且存在萬用字元配置,如listen *:port,那麼就需要通過ngx_connection_local_sockaddr來確定 //究竟客戶端是和那個本地ip地址建立的連線 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 "*" */ //根據上面的ngx_connection_local_sockaddr函式獲取到客戶端連線到本地,本地IP地址獲取到後,遍歷ngx_http_port_t找到對應 //的IP地址和埠,然後賦值給ngx_http_connection_t->addr_conf,這裡面儲存有server_name配置資訊以及該ip:port對應的上下文資訊 for (i = 0; i < port->naddrs - 1; i++) { if (addr[i].addr == sin->sin_addr.s_addr) { break; } } /* 這裡也體現了在ngx_http_init_connection中獲取http{}上下文ctx,如果客戶端請求中帶有host引數,則會繼續在ngx_http_set_virtual_server 中重新獲取對應的server{}和location{},如果客戶端請求不帶host頭部行,則使用預設的server{},見 ngx_http_init_connection */ 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 */ //listen add:port對於的 server{}配置塊的上下文ctx hc->conf_ctx = hc->addr_conf->default_server->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; c->log->data = ctx; c->log->action = "waiting for request"; c->log_error = NGX_ERROR_INFO; rev = c->read; rev->handler = ngx_http_wait_request_handler; c->write->handler = ngx_http_empty_handler; #if (NGX_HTTP_SPDY) if (hc->addr_conf->spdy) { rev->handler = ngx_http_spdy_init; } #endif #if (NGX_HTTP_SSL) { ngx_http_ssl_srv_conf_t *sscf; sscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_ssl_module); if (sscf->enable || hc->addr_conf->ssl) { c->log->action = "SSL handshaking"; if (hc->addr_conf->ssl && sscf->ssl.ctx == NULL) { ngx_log_error(NGX_LOG_ERR, c->log, 0, "no \"ssl_certificate\" is defined " "in server listening on SSL port"); ngx_http_close_connection(c); return; } hc->ssl = 1; rev->handler = ngx_http_ssl_handshake; } } #endif if (hc->addr_conf->proxy_protocol) { hc->proxy_protocol = 1; c->log->action = "reading PROXY protocol"; } /* 如果新連線的讀事件ngx_event_t結構體中的標誌位ready為1,實際上表示這個連線對應的套接字快取上已經有使用者發來的資料, 這時就可呼叫上面說過的ngx_http_init_request方法處理請求。 */ //這裡只可能是當listen的時候添加了defered引數並且核心支援,在ngx_event_accept的時候才會置1,才可能執行下面的if裡面的內容,否則不會只需if裡面的內容 if (rev->ready) { /* the deferred accept(), iocp */ if (ngx_use_accept_mutex) { //如果是配置了accept_mutex,則把該rev->handler延後處理, //實際上執行的地方為ngx_process_events_and_timers中的ngx_event_process_posted ngx_post_event(rev, &ngx_posted_events); return; } rev->handler(rev); //ngx_http_wait_request_handler return; }
當進入這個函式的時候,一定是客戶端開始往client發實際的資料了(像HTTP頭,請求行等等)。那麼在這個函式裡面會先呼叫recv來接收下。由於nginx裡面的recv都是非阻塞的,因此當前的recv可能會沒接收到資料(比如出現client資料還沒傳送完這樣的情況,此時recv會返回EAGAIN,這個並不是出錯,而是讓程式過一會再來recv看看。在非阻塞程式裡面比較常見。)當出現EAGAIN的時候,需要再次把該事件註冊到epoll裡面去。這是因為nginx裡面的epoll採用的是ET觸發模式,epoll_wait模式將無法再次獲取該事件,所以需要重新進行註冊。然後函式會直接return,將控制權交換給HTTP框架。//客戶端建立連線後,只有第一次讀取客戶端資料到資料的時候,執行的handler指向該函式,因此當客戶端連線建立成功後,只有第一次讀取 //客戶端資料才會走該函式,如果在保活期內又收到客戶端請求,則不會再走該函式,而是執行ngx_http_process_request_line,因為該函式 //把handler指向了ngx_http_process_request_line static void ngx_http_wait_request_handler(ngx_event_t *rev) { u_char *p; size_t size; ssize_t n; ngx_buf_t *b; ngx_connection_t *c; ngx_http_connection_t *hc; ngx_http_core_srv_conf_t *cscf; c = rev->data; ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http wait request handler"); if (rev->timedout) { //如果tcp連線建立後,等了client_header_timeout秒一直沒有收到客戶端的資料包過來,則關閉連線 ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out"); ngx_http_close_connection(c); return; } if (c->close) { ngx_http_close_connection(c); return; } hc = c->data; cscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_core_module); size = cscf->client_header_buffer_size; //預設1024 b = c->buffer; if (b == NULL) { b = ngx_create_temp_buf(c->pool, size); if (b == NULL) { ngx_http_close_connection(c); return; } c->buffer = b; } else if (b->start == NULL) { b->start = ngx_palloc(c->pool, size); if (b->start == NULL) { ngx_http_close_connection(c); return; } b->pos = b->start; b->last = b->start; b->end = b->last + size; } //這裡如果一次沒有把所有客戶端的資料讀取完,則在ngx_http_process_request_line中會繼續讀取 //與ngx_http_read_request_header配合讀 n = c->recv(c, b->last, size); //讀取客戶端來的資料 執行ngx_unix_recv if (n == NGX_AGAIN) { //nginx裡面採用的都是非阻塞的recv,因此當執行recv時候可能會出現還沒傳送完的情形,這時候recv實際上就會返回EAGAIN錯誤 if (!rev->timer_set) { ngx_add_timer(rev, c->listening->post_accept_timeout, NGX_FUNC_LINE); ngx_reusable_connection(c, 1); } if (ngx_handle_read_event(rev, 0, NGX_FUNC_LINE) != NGX_OK) { ngx_http_close_connection(c); return; } /* * We are trying to not hold c->buffer's memory for an idle connection. */ if (ngx_pfree(c->pool, b->start) == NGX_OK) { b->start = NULL; } return; } if (n == NGX_ERROR) { ngx_http_close_connection(c); return; } if (n == 0) { ngx_log_error(NGX_LOG_INFO, c->log, 0, "client closed connection"); ngx_http_close_connection(c); return; } b->last += n; if (hc->proxy_protocol) { hc->proxy_protocol = 0; p = ngx_proxy_protocol_read(c, b->pos, b->last); if (p == NULL) { ngx_http_close_connection(c); return; } b->pos = p; if (b->pos == b->last) { c->log->action = "waiting for request"; b->pos = b->start; b->last = b->start; ngx_post_event(rev, &ngx_posted_events); return; } } c->log->action = "reading client request line"; ngx_reusable_connection(c, 0); //從新讓c->data指向新開闢的ngx_http_request_t c->data = ngx_http_create_request(c); if (c->data == NULL) { ngx_http_close_connection(c); return; } rev->handler = ngx_http_process_request_line; ngx_http_process_request_line(rev); }
ps. 有一個疑惑:如果epoll提示監聽的讀fd上有資料來了,但是取出該讀fd, 使用recv系統呼叫的返回值是EAGAIN。這具體是什麼原因導致的?epoll既然提示,那麼該fd的接收快取中應該存有一定的可讀資料才對?
如果recv返回的結果是n>0。說明此時接收到client傳來的資料了,但是隻recv一次可能沒法讀取到所有的資料,而且TCP傳送端的快取區也很可能存不下整個HTTP請求行。所以需要採取額外的措施來繼續接收資料,並且判斷是否接收到了完成的HTTP請求行。nginx是專門實現了一個函式ngx_http_process_request_line來完成這個事,本函式後來把handler指向了ngx_http_process_request_line。
三.ngx_http_process_request_line
static void
ngx_http_process_request_line(ngx_event_t *rev) //gx_http_process_request_line方法來接收HTTP請求行
{
ssize_t n;
ngx_int_t rc, rv;
ngx_str_t host;
ngx_connection_t *c;
ngx_http_request_t *r;
c = rev->data;
r = c->data;
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, rev->log, 0,
"http process request line");
/*
檢查這個讀事件是否已經超時,超時時間仍然是nginx.conf配置檔案中指定的client_header_timeout。如果ngx_event_t事件的timeout標誌為1,
則認為接收HTTP請求已經超時,呼叫ngx_http_close_request方法關閉請求,同時由ngx_http_process_request_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;
}
rc = NGX_AGAIN;
//讀取一行資料,分析出請求行中包含的method、uri、http_version資訊。然後再一行一行處理請求頭,並根據請求method與請求頭的資訊來決定
//是否有請求體以及請求體的長度,然後再去讀取請求體
for ( ;; ) {
if (rc == NGX_AGAIN) {
n = ngx_http_read_request_header(r);
if (n == NGX_AGAIN || n == NGX_ERROR) {
//如果核心中的資料已經讀完,但這時候頭部欄位還沒有解析完畢,則把控制器交還給HTTP,當資料到來的時候觸發
//ngx_http_process_request_line,因為該函式外面rev->handler = ngx_http_process_request_line;
return;
}
}
rc = ngx_http_parse_request_line(r, r->header_in);
if (rc == NGX_OK) { //請求行解析成功
/* the request line has been parsed successfully */
//請求行內容及長度 //GET /sample.jsp HTTP/1.1整行
r->request_line.len = r->request_end - r->request_start;
r->request_line.data = r->request_start;
r->request_length = r->header_in->pos - r->request_start;
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
"http request line: \"%V\"", &r->request_line);
//請求方法 GET POST等 //GET /sample.jsp HTTP/1.1 中的GET
r->method_name.len = r->method_end - r->request_start + 1;
r->method_name.data = r->request_line.data;
//GET /sample.jsp HTTP/1.1 中的HTTP/1.1
if (r->http_protocol.data) {
r->http_protocol.len = r->request_end - r->http_protocol.data;
}
if (ngx_http_process_request_uri(r) != NGX_OK) {
return;
}
if (r->host_start && r->host_end) {
host.len = r->host_end - r->host_start;
host.data = r->host_start;
rc = ngx_http_validate_host(&host, r->pool, 0);
if (rc == NGX_DECLINED) {
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 (rc == NGX_ERROR) {
ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}
if (ngx_http_set_virtual_server(r, &host) == NGX_ERROR) {
return;
}
r->headers_in.server = host;
}
if (r->http_version < NGX_HTTP_VERSION_10) { //1.0以下版本沒有請求頭部欄位,
/*
使用者請求的HTTP版本小於1.0(如HTTP 0.9版本),其處理過程將與HTTP l.0和HTTP l.1的完全不同,它不會有接收HTTP
頭部這一步驟。這時將會呼叫ngx_http_find_virtual_server方法尋找到相應的虛擬主機? */
if (r->headers_in.server.len == 0
&& ngx_http_set_virtual_server(r, &r->headers_in.server) //http0.9應該是從請求行獲取虛擬主機?
== NGX_ERROR)
{
return;
}
ngx_http_process_request(r);
return;
}
//初始化用於存放http頭部行的空間,用來存放http頭部行
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;
}
c->log->action = "reading client request headers";
rev->handler = ngx_http_process_request_headers;
ngx_http_process_request_headers(rev);//開始解析http頭部行
return;
}
if (rc != NGX_AGAIN) {//讀取完畢核心該套接字上面的資料,頭部行不全,則說明頭部行不全關閉連線
/* there was error while a request line parsing */
ngx_log_error(NGX_LOG_INFO, c->log, 0,
ngx_http_client_errors[rc - NGX_HTTP_CLIENT_ERROR]);
ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
return;
}
//表示該行內容不夠,例如recv讀取的時候,沒有把整行資料讀取出來,返回後繼續recv,然後接著上次解析的位置繼續解析直到請求行解析完畢
/* NGX_AGAIN: a request line parsing is still incomplete */
/*
如果ngx_http_parse_request_line方法返回NGX_AGAIN,則表示需要接收更多的字元流,這時需要對header_in緩衝區做判斷,檢查
是否還有空閒的記憶體,如果還有未使用的記憶體可以繼續接收字元流,則跳轉到第2步,檢查緩衝區是否有未解析的字元流,否則呼叫
ngx_http_alloc_large_header_buffer方法分配更大的接收緩衝區。到底分配多大呢?這由nginx.conf檔案中的large_client_header_buffers配置項指定。
*/
if (r->header_in->pos == r->header_in->end) {
rv = ngx_http_alloc_large_header_buffer(r, 1);
if (rv == NGX_ERROR) {
ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}
if (rv == NGX_DECLINED) {
r->request_line.len = r->header_in->end - r->request_start;
r->request_line.data = r->request_start;
ngx_log_error(NGX_LOG_INFO, c->log, 0,
"client sent too long URI");
ngx_http_finalize_request(r, NGX_HTTP_REQUEST_URI_TOO_LARGE);
return;
}
}
//表示頭部行沒有解析完成,繼續讀資料解析
}
}
在ngx_http_process_request_line這個函式裡面,首先是判斷是否超時。如果不超時的話,那麼接下來首先是進入 n = ngx_http_read_request_header(r);函式。 在這個函式裡面,先是判斷當前使用者態快取區裡面是否有一些還沒解析的資料。如果存在一些未解析的資料,那麼會繼續下去呼叫ngx_http_parse_request_line來進行解析請求行。如果解析成功,則後面會繼續解析請求頭。但是也可能解析失敗,因為TCP是位元組流的服務,當前收到的位元組可能還沒有涵蓋整個請求行。所以rc的狀態會再變成EAGAIN,然後再次進入 ngx_http_read_request_header(r),在這個函式裡面會嘗試呼叫recv來接收資料。直至接收到足夠的資料,以成功解析請求行。