前面介紹了lighttpd使用的watcher-worker模型, 它對IO事件處理的封裝, 現(xiàn)在可以把這些結合起來看看這大概的流程.
首先, 服務器創(chuàng)建監(jiān)聽socket, 然后在server.c中調用函數(shù)network_register_fdevents將監(jiān)聽socket注冊到IO事件處理器中:
int network_register_fdevents(server *srv) {
size_t i;
if (-1 == fdevent_reset(srv->ev)) {
return -1;
}
/* register fdevents after reset */
for (i = 0; i < srv->srv_sockets.used; i++) {
server_socket *srv_socket = srv->srv_sockets.ptr[i];
fdevent_register(srv->ev, srv_socket->fd, network_server_handle_fdevent, srv_socket);
fdevent_event_add(srv->ev, &(srv_socket->fde_ndx), srv_socket->fd, FDEVENT_IN);
}
return 0;
}
在這里, 調用函數(shù)fdevent_register注冊fd到IO事件處理器中, 對于服務器監(jiān)聽fd而言,
它在fdnode中的回調函數(shù)handler是函數(shù)network_server_handle_fdevent, 而ctx則是srv_socket.
接著調用函數(shù)fdevent_event_add, 其中傳入的第三個參數(shù)是FDEVENT_IN, 也就是當該fd上有可讀數(shù)據(jù)時觸發(fā)調用, 對于所有監(jiān)聽的fd而言,
有可讀事件就意味著有新的連接到達.
然后服務器創(chuàng)建子進程worker, 服務器父進程自己成為watcher, 自此下面的工作由子進程進行處理,
每個子進程所完成的工作都是一樣的.有的書上說有多個進程在等待accept連接的時候會造成所謂的驚群現(xiàn)象,在lighttpd的代碼中,
沒有看到在accaept之前進行加鎖操作, 這是否會造成驚群不得而知.
現(xiàn)在, 在IO事件處理器中僅有一個fd等待觸發(fā), 就是前面注冊的監(jiān)聽fd, 我們看看當一個連接到來的時候處理的流程, 首先看我們曾經(jīng)說過的
輪詢fd進行處理的主循環(huán):
// 輪詢FD
if ((n = fdevent_poll(srv->ev, 1000)) > 0) {
/* n is the number of events */
int revents;
int fd_ndx;
fd_ndx = -1;
do {
fdevent_handler handler;
void *context;
handler_t r;
// 獲得處理這些事件的函數(shù)指針 fd等
// 獲得下一個fd在fdarray中的索引
fd_ndx = fdevent_event_next_fdndx (srv->ev, fd_ndx);
// 獲得這個fd要處理的事件類型
revents = fdevent_event_get_revent (srv->ev, fd_ndx);
// 獲取fd
fd = fdevent_event_get_fd (srv->ev, fd_ndx);
// 獲取回調函數(shù)
handler = fdevent_get_handler(srv->ev, fd);
// 獲取處理相關的context(對server是server_socket指針, 對client是connection指針)
context = fdevent_get_context(srv->ev, fd);
/* connection_handle_fdevent needs a joblist_append */
// 進行處理
switch (r = (*handler)(srv, context, revents)) {
case HANDLER_FINISHED:
case HANDLER_GO_ON:
case HANDLER_WAIT_FOR_EVENT:
case HANDLER_WAIT_FOR_FD:
break;
case HANDLER_ERROR:
/* should never happen */
SEGFAULT();
break;
default:
log_error_write(srv, __FILE__, __LINE__, "d", r);
break;
}
} while (--n > 0);
當一個連接到來的時候, 調用fdevent_poll返回值是1, 因為這個函數(shù)的返回值表示的是有多少網(wǎng)絡IO事件被觸發(fā)了, 接著由于n>0, 進入循環(huán)中
獲得被觸發(fā)的fd, 回調函數(shù), 以及ctx指針, 在這里由于是監(jiān)聽fd被觸發(fā), 那么返回的回調函數(shù)是前面提到的network_server_handle_fdevent,
接著就要調用這個函數(shù)處理IO事件了:
// 這個函數(shù)是處理server事件的函數(shù), 與connection_handle_fdevent對應
handler_t network_server_handle_fdevent(void *s, void *context, int revents) {
server *srv = (server *)s;
server_socket *srv_socket = (server_socket *)context;
connection *con;
int loops = 0;
UNUSED(context);
if (revents != FDEVENT_IN) {
log_error_write(srv, __FILE__, __LINE__, "sdd",
"strange event for server socket",
srv_socket->fd,
revents);
return HANDLER_ERROR;
}
/* accept()s at most 100 connections directly
*
* we jump out after 100 to give the waiting connections a chance */
// 一次最多接受100個鏈接
for (loops = 0; loops < 100 && NULL != (con = connection_accept(srv, srv_socket)); loops++) {
handler_t r;
// 這里馬上進入狀態(tài)機中進行處理僅僅對應狀態(tài)為CON_STATE_REQUEST_START這一段
// 也就是保存連接的時間以及設置一些計數(shù)罷了
connection_state_machine(srv, con);
switch(r = plugins_call_handle_joblist(srv, con)) {
case HANDLER_FINISHED:
case HANDLER_GO_ON:
break;
default:
log_error_write(srv, __FILE__, __LINE__, "d", r);
break;
}
}
return HANDLER_GO_ON;
}
我給這段代碼加了一些注釋, 有幾個地方做一些解釋:
1)UNUSED(context)是一個宏, 擴展開來就是( (void)(context) ), 實際上是一段看似無用的代碼, 因為沒有起什么明顯的作用, 是一句廢話,
在這個函數(shù)中, 實際上沒有使用到參數(shù)context, 如果在比較嚴格的編譯器中, 這樣無用的參數(shù)會產(chǎn)生一條警告, 說有一個參數(shù)沒有使用到, 加上了
這么一句無用的語句, 就可以避免這個警告.那么, 有人就會問了, 為什么要傳入這么一個無用的參數(shù)呢?回答是, 為了滿足這個接口的需求,
來看看回調函數(shù)的類型定義:
typedef handler_t (*fdevent_handler)(void *srv, void *ctx, int revents);
這個函數(shù)指針要求的第二個參數(shù)是一個ctx指針, 對于監(jiān)聽fd的回調函數(shù)network_server_handle_fdevent而言, 它是無用的, 但是對于處理連接fd
的回調函數(shù)而言, 這個指針是有用的.
2) 在函數(shù)的前面, 首先要判斷傳入的event事件是否是FDEVENT_IN, 也就是說, 只可能在fd有可讀數(shù)據(jù)的時候才觸發(fā)該函數(shù), 其它的情況都是錯誤.
3)函數(shù)在最后進入一個循環(huán), 循環(huán)的最多次數(shù)是100次, 并且當connection_accept函數(shù)返回NULL時也終止循環(huán), 也就是說, 當監(jiān)聽fd被觸發(fā)時,
服務器盡量的去接收新的連接, 最多接收100個新連接, 這樣有一個好處, 假如服務器監(jiān)聽fd是每次觸發(fā)只接收一個新的連接, 那么效率是比較低的,
不如每次被觸發(fā)的時候"盡力"的去接收, 一直到接收了100個新的連接或者沒有可接收的連接之后才返回.接著來看看負責接收新連接的函數(shù)
connection_accept做了什么:
// 接收一個新的連接
connection *connection_accept(server *srv, server_socket *srv_socket) {
/* accept everything */
/* search an empty place */
int cnt;
sock_addr cnt_addr;
socklen_t cnt_len;
/* accept it and register the fd */
/**
* check if we can still open a new connections
*
* see #1216
*/
// 如果正在使用的連接數(shù)大于最大連接數(shù) 就返回NULL
if (srv->conns->used >= srv->max_conns) {
return NULL;
}
cnt_len = sizeof(cnt_addr);
if (-1 == (cnt = accept(srv_socket->fd, (struct sockaddr *) &cnt_addr, &cnt_len))) {
switch (errno) {
case EAGAIN:
#if EWOULDBLOCK != EAGAIN
case EWOULDBLOCK:
#endif
case EINTR:
/* we were stopped _before_ we had a connection */
case ECONNABORTED: /* this is a FreeBSD thingy */
/* we were stopped _after_ we had a connection */
break;
case EMFILE:
/* out of fds */
break;
default:
log_error_write(srv, __FILE__, __LINE__, "ssd", "accept failed:", strerror(errno), errno);
}
return NULL;
} else {
connection *con;
// 當前使用的fd數(shù)量+1
srv->cur_fds++;
/* ok, we have the connection, register it */
// 打開的connection+1(這個成員貌似沒有用)
srv->con_opened++;
// 獲取一個新的connection
con = connections_get_new_connection(srv);
// 保存接收到的fd
con->fd = cnt;
// 索引為-1
con->fde_ndx = -1;
#if 0
gettimeofday(&(con->start_tv), NULL);
#endif
// 注冊函數(shù)指針和connection指針
fdevent_register(srv->ev, con->fd, connection_handle_fdevent, con);
// 狀態(tài)為可以接收請求
connection_set_state(srv, con, CON_STATE_REQUEST_START);
// 保存接收連接的時間
con->connection_start = srv->cur_ts;
// 保存目標地址
con->dst_addr = cnt_addr;
buffer_copy_string(con->dst_addr_buf, inet_ntop_cache_get_ip(srv, &(con->dst_addr)));
// 保存server_socket指針
con->srv_socket = srv_socket;
// 設置一下接收來的FD, 設置為非阻塞
if (-1 == (fdevent_fcntl_set(srv->ev, con->fd))) {
log_error_write(srv, __FILE__, __LINE__, "ss", "fcntl failed: ", strerror(errno));
return NULL;
}
#ifdef USE_OPENSSL
/* connect FD to SSL */
if (srv_socket->is_ssl) {
if (NULL == (con->ssl = SSL_new(srv_socket->ssl_ctx))) {
log_error_write(srv, __FILE__, __LINE__, "ss", "SSL:",
ERR_error_string(ERR_get_error(), NULL));
return NULL;
}
SSL_set_accept_state(con->ssl);
con->conf.is_ssl=1;
if (1 != (SSL_set_fd(con->ssl, cnt))) {
log_error_write(srv, __FILE__, __LINE__, "ss", "SSL:",
ERR_error_string(ERR_get_error(), NULL));
return NULL;
}
}
#endif
return con;
}
}
拋開出錯處理這部分不解釋, 一旦出錯, 就返回NULL指針, 這時可以終止上面那個循環(huán)接收新連接的過程,下面重點看看接收了一個新的連接之后需要
做哪些事情, 在上面的代碼中我加了一些簡單的注釋, 下面加一些更加詳細些的解釋:
1)要將服務器已經(jīng)接收的fd數(shù)量(成員cur_fds)加一, 這個數(shù)量用于判斷是否可以接收新的連接的, 超過一定的數(shù)量時, 服務器就暫停接收,
等一些fd釋放之后才能繼續(xù)接收
2) 調用函數(shù)connections_get_new_connection返回一個connection指針, 用于保存新到的連接, 獲得這個指針之后要保存接收這個連接的時間
(成員connection_start中), 保存新到連接的地址(成員dst_addr和dst_addr_buf中), 此外還要保存一個server指針, 并且調用函數(shù)fdevent_fcntl_set
將該fd設置為非阻塞的, 最后別忘了要調用fdevent_register函數(shù)將該fd注冊到IO事件處理器中, 另外該fd當前的狀態(tài)通過connection_set_state設置為
CON_STATE_REQUEST_START, 這是后面進入狀態(tài)機處理連接的基礎.
了解了這個函數(shù)的處理過程, 回頭看看上面的循環(huán):
for (loops = 0; loops < 100 && NULL != (con = connection_accept(srv, srv_socket)); loops++) {
handler_t r;
// 這里馬上進入狀態(tài)機中進行處理僅僅對應狀態(tài)為CON_STATE_REQUEST_START這一段
// 也就是保存連接的時間以及設置一些計數(shù)罷了
connection_state_machine(srv, con);
switch(r = plugins_call_handle_joblist(srv, con)) {
case HANDLER_FINISHED:
case HANDLER_GO_ON:
break;
default:
log_error_write(srv, __FILE__, __LINE__, "d", r);
break;
}
}
我們已經(jīng)分析完了函數(shù)connection_accept, 當一個新的連接調用這個函數(shù)成功返回的時候, 這個循環(huán)執(zhí)行函數(shù)connection_state_machine
進行處理.這是一個非常關鍵的函數(shù), 可以說, 我們后面講解lighttpd的很多筆墨都將花費在這個函數(shù)上, 這也是我認為lighttpd實現(xiàn)中最精妙的
地方之一, 在這里我們先不進行講解, 你所需要知道的是, 在這里, connection_state_machine調用了函數(shù)fdevent_event_add, 傳入的事件參數(shù)仍然是
FDEVENT_IN, 也就是說, 對于新加入的fd, 它所首先關注的IO事件也是可讀事件.
我們大體理一理上面的流程, 省略去對watcher-worker模型的描述:
創(chuàng)建服務器監(jiān)聽fd-->
調用fdevent_register函數(shù)將監(jiān)聽fd注冊到IO事件處理器中-->
調用fdevent_event_add函數(shù)添加FDEVENT_IN到監(jiān)聽fd所關注的事件中-->
當一個新的連接到來時:
IO事件處理器輪詢返回一個>0的值-->
IO事件處理返回被觸發(fā)的fd, 回調函數(shù), ctx指針,在這里就是監(jiān)聽fd,回調函數(shù)則是network_server_handle_fdevent->
調用監(jiān)聽fd注冊的回調函數(shù)network_server_handle_fdevent-->
network_server_handle_fdevent函數(shù)盡力接收新的連接, 除非已經(jīng)接收了100個新連接, 或者沒有新連接到來-->
對于新到來的連接, 同樣是調用fdevent_register函數(shù)將它注冊到IO事件處理器中, 同樣調用fdevent_event_add函數(shù)添加該fd所關注的事件是FDEVENT_IN
以上, 就是lighttpd監(jiān)聽fd處理新連接的大體流程.
我們知道, fd分為兩種:一種是服務器自己創(chuàng)建的監(jiān)聽fd, 負責監(jiān)聽端口, 接收新到來的連接;
另一種, 就是由監(jiān)聽fd調用accept函數(shù)返回的連接fd, 這兩種fd在處理時都會注冊到IO事件處理器中(調用fdevent_register函數(shù)),
同時添加它們所關注的IO事件(可讀/可寫等)(調用fdevent_event_add函數(shù)).
也就是說,對IO事件處理器而言, 它并不關注所處理的fd是什么類型的, 你要使用它, 那么就把你的fd以及它的回調函數(shù)注冊到其中, 同時添加你所關注的IO事件是什么, 當一個fd所關注的IO事件被觸發(fā)時, IO事件處理器自動會根據(jù)你所注冊的回調函數(shù)進行回調處理, 這是關鍵點, 如果你沒有明白, 請回頭看看前面提到的IO事件處理器.
這些的基礎就是我們前面提到IO事件處理器, 前面我們提到過, lighttpd對IO事件處理的封裝很漂亮, 每個具體實現(xiàn)都按照接口的規(guī)范進行處理.
我們在講解時, 也沒有涉及到任何一個具體實現(xiàn)的細節(jié), 這也是因為lighttpd的封裝很好, 以至于我們只需要了解它們對外的接口不需要深入細節(jié)就
可以明白其運行的原理.在本節(jié)中, 我們結合IO事件處理器, 對上面提到的第一種fd也就是監(jiān)聽fd的處理流程做了介紹, 在后面的內(nèi)容中, 將重點講解對
連接fd的處理.