菜鳥學習Nginx之HTTP會話
從本篇開始,介紹Nginx中HTTP相關。Nginx做為Web Server那麼HTTP必然是重中之重。本篇不打算介紹太深入,本篇最主要內容是如何與事件驅動關聯起來。
一、監聽埠
Nginx預設監聽埠是80埠,那麼Nginx是如何將80埠的listening事件註冊到事件驅動(epoll)中呢?這裡簡單回顧一下,具體內容可參考《菜鳥學習nginx之核心模組ngx_events_module》。
worker程序的程序主函式是ngx_worker_process_cycle。進入該函式會先呼叫函式ngx_worker_process_init進行初始化流程,當ngx_worker_process_init返回之後則進入無限迴圈即投入服務。那麼在ngx_worker_process_init函式就會把listening socket加入到事件驅動epoll中。然而真正加入到epoll物件中是函式ngx_event_process_init函式,部分程式碼如下:
/** * for each listening socket * 迴圈遍歷listening 將listen socket 與connection物件進行繫結 */ ls = cycle->listening.elts; for (i = 0; i < cycle->listening.nelts; i++) { #if (NGX_HAVE_REUSEPORT) if (ls[i].reuseport && ls[i].worker != ngx_worker) { continue; } #endif /* 返回可用連線物件 這裡listening socket也會佔用一個connection物件 */ c = ngx_get_connection(ls[i].fd, cycle->log); if (c == NULL) { return NGX_ERROR; } c->type = ls[i].type; c->log = &ls[i].log; c->listening = &ls[i]; ls[i].connection = c; rev = c->read; rev->log = c->log; rev->accept = 1; /* 表示當前讀事件是accept事件 用於區別正常資料報文讀取還是Accept事件讀取 */ #if (NGX_HAVE_DEFERRED_ACCEPT) rev->deferred_accept = ls[i].deferred_accept; #endif if (!(ngx_event_flags & NGX_USE_IOCP_EVENT)) { if (ls[i].previous) { /* * delete the old accept events that were bound to * the old cycle read events array */ old = ls[i].previous->connection; if (ngx_del_event(old->read, NGX_READ_EVENT, NGX_CLOSE_EVENT) == NGX_ERROR) { return NGX_ERROR; } old->fd = (ngx_socket_t)-1; } } #if (NGX_WIN32) ... #else rev->handler = (c->type == SOCK_STREAM) ? ngx_event_accept : ngx_event_recvmsg; #if (NGX_HAVE_REUSEPORT) if (ls[i].reuseport) { if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) { return NGX_ERROR; } continue; } #endif if (ngx_use_accept_mutex) { continue; } //註冊讀事件 主要是用於listen監聽 if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) { return NGX_ERROR; } #endif }
迴圈遍歷listenings陣列,通過ngx_add_event函式,逐個將listening socket加入到epoll物件中且設定事件型別為READ事件。
至此Nginx完成了listening socket註冊到epoll事件驅動中。當客戶端發來請求時就會呼叫回撥函式--ngx_event_accept。
二、Accept事件
註冊listening socket到epoll的流程中有這樣一行程式碼,用於設定回撥函式,具體如下:
/* TCP連線的回撥函式是ngx_event_accept,其他的回撥函式則是ngx_event_recv_msg */ rev->handler = (c->type == SOCK_STREAM) ? ngx_event_accept : ngx_event_recvmsg;
這裡使用的協議是TCP,所以當客戶端的連線請求到來時,會觸發回撥函式ngx_event_accept函式執行,具體如何執行可參考《菜鳥學習nginx之事件模組epoll(1)》。
2.1、流程圖
從流程圖中可以看出,這部分程式碼邏輯還是比較繁瑣,下面我們分片進行分析解讀。
2.2、超時處理
/**
* 處理Accept事件
* @param ev 待讀事件
*/
void
ngx_event_accept(ngx_event_t *ev)
{
socklen_t socklen;
ngx_err_t err;
ngx_log_t *log;
ngx_uint_t level;
ngx_socket_t s;
ngx_event_t *rev, *wev;
ngx_sockaddr_t sa;
ngx_listening_t *ls;
ngx_connection_t *c, *lc;
ngx_event_conf_t *ecf;
#if (NGX_HAVE_ACCEPT4)
static ngx_uint_t use_accept4 = 1;
#endif
if (ev->timedout) {//表示超時 重新新增listen socket到事件驅動中
if (ngx_enable_accept_events((ngx_cycle_t *) ngx_cycle) != NGX_OK) {
return;
}
ev->timedout = 0;
}
//獲取event配置項
ecf = ngx_event_get_conf(ngx_cycle->conf_ctx, ngx_event_core_module);
if (!(ngx_event_flags & NGX_USE_KQUEUE_EVENT)) {
ev->available = ecf->multi_accept;
}
//從使用者私有資料中獲取當前事件所對應的connection以及listening物件
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);
這程式碼主要用於判斷當前事件是否為超時事件。超時事件的產生條件是在定時器週期內沒有新的請求到來,此時需要重新註冊listening socket到epoll中。下面進入主流程do while迴圈。
2.3、呼叫accept接收使用者請求
do {
socklen = sizeof(ngx_sockaddr_t);
/* 接收客戶端連線建立請求 */
#if (NGX_HAVE_ACCEPT4)
if (use_accept4) {
s = accept4(lc->fd, &sa.sockaddr, &socklen, SOCK_NONBLOCK);//預設建立的socket是非阻塞socket
} else {
s = accept(lc->fd, &sa.sockaddr, &socklen);
}
#else
s = accept(lc->fd, &sa.sockaddr, &socklen);
#endif
/* 連線建立失敗 */
if (s == (ngx_socket_t) -1) {
err = ngx_socket_errno;
if (err == NGX_EAGAIN) {
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, err,
"accept() not ready");
return;
}
level = NGX_LOG_ALERT;
if (err == NGX_ECONNABORTED) {
level = NGX_LOG_ERR;
} else if (err == NGX_EMFILE || err == NGX_ENFILE) {
level = NGX_LOG_CRIT;
}
#if (NGX_HAVE_ACCEPT4)
ngx_log_error(level, ev->log, err,
use_accept4 ? "accept4() failed" : "accept() failed");
if (use_accept4 && err == NGX_ENOSYS) {
use_accept4 = 0;
ngx_inherited_nonblocking = 0;//嘗試用accept介面處理新請求
continue;
}
#else
ngx_log_error(level, ev->log, err, "accept() failed");
#endif
if (err == NGX_ECONNABORTED) {
if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
ev->available--;
}
if (ev->available) {
continue;
}
}
if (err == NGX_EMFILE || err == NGX_ENFILE) {
if (ngx_disable_accept_events((ngx_cycle_t *) ngx_cycle, 1)
!= NGX_OK)
{
return;
}
if (ngx_use_accept_mutex) {
if (ngx_accept_mutex_held) {
ngx_shmtx_unlock(&ngx_accept_mutex);
ngx_accept_mutex_held = 0;
}
ngx_accept_disabled = 1;
} else {
ngx_add_timer(ev, ecf->accept_mutex_delay);
}
}
return;
}
根據不同的錯誤碼,進行特殊處理,大部分場景最後都是return操作。
2.4、建立connection物件
當accept返回成功,則表新的tcp連線已經建立成功,Nginx需要對其進行封裝成connection物件並對connection結構成員進行初始化操作,例如socket操作設定等,具體程式碼如下:
/* 負數 負載均衡*/
ngx_accept_disabled = ngx_cycle->connection_n / 8
- ngx_cycle->free_connection_n;
c = ngx_get_connection(s, ev->log);//獲取新的connection物件
if (c == NULL) {
if (ngx_close_socket(s) == -1) {
ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_socket_errno,
ngx_close_socket_n " failed");
}
return;
}
c->type = SOCK_STREAM;
#if (NGX_STAT_STUB)
(void) ngx_atomic_fetch_add(ngx_stat_active, 1);
#endif
/* 建立連線級記憶體池 */
c->pool = ngx_create_pool(ls->pool_size, ev->log);
if (c->pool == NULL) {
ngx_close_accepted_connection(c);
return;
}
if (socklen > (socklen_t) sizeof(ngx_sockaddr_t)) {
socklen = sizeof(ngx_sockaddr_t);
}
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;
}
/* set a blocking mode for iocp and non-blocking mode for others */
if (ngx_inherited_nonblocking) {
if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
if (ngx_blocking(s) == -1) {
ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_socket_errno,
ngx_blocking_n " failed");
ngx_close_accepted_connection(c);
return;
}
}
} else {
if (!(ngx_event_flags & NGX_USE_IOCP_EVENT)) {
if (ngx_nonblocking(s) == -1) {
ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_socket_errno,
ngx_nonblocking_n " failed");
ngx_close_accepted_connection(c);
return;
}
}
}
*log = ls->log;
/* 初始化connection物件 參考ngx_linux_io */
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;
#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;
#if (NGX_SOLARIS)
/* Solaris's sendfilev() supports AF_NCA, AF_INET, and AF_INET6 */
c->sendfile = 0;
#endif
}
#endif
rev = c->read;
wev = c->write;
wev->ready = 1;
if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
rev->ready = 1;
}
if (ev->deferred_accept) {
rev->ready = 1;
#if (NGX_HAVE_KQUEUE || NGX_HAVE_EPOLLRDHUP)
rev->available = 1;
#endif
}
rev->log = log;
wev->log = log;
/*
* TODO: MT: - ngx_atomic_fetch_add()
* or protection by critical section or light mutex
*
* TODO: MP: - allocated in a shared memory
* - ngx_atomic_fetch_add()
* or protection by critical section or light mutex
*/
c->number = ngx_atomic_fetch_add(ngx_connection_counter, 1);
#if (NGX_STAT_STUB)
(void) ngx_atomic_fetch_add(ngx_stat_handled, 1);
#endif
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;
}
}
2.5、HTTP框架初始化connection物件
下面這段程式碼最主要功能是對connection物件初始化流程,如下:
/**
* ngx_add_conn不空且沒有設定NGX_USE_EPOLL_EVENT標誌位
* epoll模型不會進入此分支
*/
if (ngx_add_conn && (ngx_event_flags & NGX_USE_EPOLL_EVENT) == 0) {
if (ngx_add_conn(c) == NGX_ERROR) {
ngx_close_accepted_connection(c);
return;
}
}
log->data = NULL;
log->handler = NULL;
/*
* 此回撥函式用於處理新的連線 handler賦值由HTTP框架設定
* ngx_http_init_connection 這個函式主要功能是將當前socket註冊到事件驅動中
*/
ls->handler(c);
if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
ev->available--;
}
} while (ev->available);
三、註冊事件驅動
上面章節,Nginx是如何處理Accept事件,但是有一點疑問是accept函式返回的socket是如何加入到epoll中呢?listening socket是如何再次加入到epoll中呢(每次accept事件之後需要重新加入)?
3.1、新socket加入epoll事件驅動
在上一節中有這一行程式碼:
/*
* 此回撥函式用於處理新的連線 handler賦值由HTTP框架設定
* ngx_http_init_connection 這個函式主要功能是將當前socket註冊到事件驅動中
*/
ls->handler(c);
該程式碼是HTTP框架用於初始化連線 ,回撥函式是ngx_http_init_connection,該函式用於將客戶端與服務端通訊的socket加入到事件驅動中:
/**
* 初始化http連線
* @param c TCP連線
*/
void ngx_http_init_connection(ngx_connection_t *c)
{
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
/* 在記憶體池中申請http connection物件 */
hc = ngx_pcalloc(c->pool, sizeof(ngx_http_connection_t));
if (hc == NULL)
{
ngx_http_close_connection(c);
return;
}
c->data = hc;//save
/* 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;
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;
/* 設定讀寫事件handler回撥函式 */
rev = c->read;
rev->handler = ngx_http_wait_request_handler; /* 讀事件回撥函式 */
c->write->handler = ngx_http_empty_handler; /* 寫事件回撥函式 再未收到client請求不會主動傳送資料 */
#if (NGX_HTTP_V2)
if (hc->addr_conf->http2)
{
rev->handler = ngx_http_v2_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";
}
if (rev->ready)
{
/* the deferred accept(), iocp */
if (ngx_use_accept_mutex)
{
ngx_post_event(rev, &ngx_posted_events);
return;
}
rev->handler(rev);
return;
}
/*
* 將事件新增到定時器和epoll事件驅動中
* 回撥函式都是ngx_http_wait_request_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;
}
}
通過上述程式碼可知:
1、讀事件處理函式為ngx_http_wait_request_handler,寫事件處理函式為ngx_http_empty_handler。
2、將當前socket加入到epoll以及定時器中。
四、總結
至此,HTTP會話流程已經建立完成。HTTP會話主要是初始化connection物件以及將通訊socket加入到事件驅動中即可。