1. 所在的結構體定義
struct mg_server {
sock_t listening_sock;
union socket_address lsa; // Listening socket address
struct ll active_connections;
struct ll uri_handlers;
char *config_options[NUM_OPTIONS];
void *server_data;
void *ssl_ctx; // SSL context
sock_t ctl[2]; // Control socketpair. Used to wake up from select() call
};
active_connections是一個雙向循環鏈表:
struct ll { struct ll *prev, *next; };
2. 變量初始化
mg_create_server函數:
LINKED_LIST_INIT(&server->active_connections);
#define LINKED_LIST_INIT(N) ((N)->next = (N)->prev = (N))
3. 向鏈表中添加元素
accept_new_connection函數
該函數又是一個高級的if else if else的寫法,作者的意圖操作是:
- if(執行一個語句並判斷)
- else if(再執行一個語句並判斷) {異常處理}
- else if(再執行一個語句並判斷) {異常處理}
- else {最終執行一些操作}
這裏的厲害之處就在於:
每次if或者else if執行完語句後,故意讓預期表達式爲false,同而可以執行下一個分支,這樣正常情況下,上面的三個if或else if判斷中的表達式都會執行到,同時保證最終進入到else中完成所有的處理。
下面來分析函數的具體作用:
- sock = accept(server->listening_sock, &sa.sa, &len) accept一個連接
- check_acl(server->config_options[ACCESS_CONTROL_LIST], ntohl(* (uint32_t *) &sa.sin.sin_addr) 檢查連接不是acl ACCESS_CONTROL_LIST
- conn = (struct connection *) calloc(1, sizeof(*conn)) calloc一個struct connection大小的空間,將指針賦給conn
- set_close_on_exec(sock); and set_non_blocking_mode(sock); 給sock設置參數
- 然後賦值:conn->server、conn->client_sock、conn->mg_conn.remote_ip、conn->mg_conn.remote_port以及conn->mg_conn.server_param
- LINKED_LIST_ADD_TO_FRONT(&server->active_connections, &conn->link); 將conn->link添加到server->active_connections循環鏈表中
注意:這裏只是將conn->link添加到server->active_connections循環鏈表中,並沒有添加和conn的指針相關的元素,這裏又是一個技巧。獲取conn的地方使用的另外一個宏是LINKED_LIST_ENTRY:
#define LINKED_LIST_ENTRY(P,T,N) ((T *)((char *)(P) - offsetof(T, N)))
調用時,傳遞的三個參數是:struct ll *變量、struct connection結構體以及link成員變量,一個使用的例子如下:
conn = LINKED_LIST_ENTRY(lp, struct connection, link);
LINKED_LIST_ENTRY宏定義分解來看:
- (char *)(P) 將P指針強轉爲char *
- offsetof(T, N) 計算結構體T中成員N在該結構體中的偏移量
- ((char *)(P) - offsetof(T, N)) 將P的指針往前減少相應的偏移量,即獲得結構體T的指針
- ((T *)((char *)(P) - offsetof(T, N))) 最後強轉爲(T *)類型
4. 輪詢所有的active_connections
mg_poll_server函數中
LINKED_LIST_FOREACH(&server->active_connections, lp, tmp) {
conn = LINKED_LIST_ENTRY(lp, struct connection, link);
#define LINKED_LIST_FOREACH(H,N,T) \
for (N = (H)->next, T = (N)->next; N != (H); N = (T), T = (N)->next)
該函數中有三處使用到這種遍歷server->active_connections並取得struct connection *類型的指針的地方:
- add_to_set for socket select
- Read/write from clients
- Close expired connections and those that need to be closed
其中,close_conn(conn);時,會執行下述語句來移除server->active_connections列表中對應的節點:
LINKED_LIST_REMOVE(&conn->link);
5. 從鏈表中移除節點
從代碼上看,只有在close_conn函數中才有調用到LINKED_LIST_REMOVE(&conn->link)操作,但是close_conn函數只在mg_poll_server中有調用。
#define LINKED_LIST_REMOVE(N) do { ((N)->next)->prev = ((N)->prev); \
((N)->prev)->next = ((N)->next); LINKED_LIST_INIT(N); } while (0)
LINKED_LIST_REMOVE宏完成了將conn從server->active_connections列表中移除的功能。
然後closesocket(conn->client_sock)並依次free掉conn結構體中申請的空間以及最後free掉自身。
mg_destroy_server函數中,只是遍歷所有的active_connections,然後free掉conn,但是並沒有完成其它釋放操作,如closesocket(conn->client_sock)、free(conn->request)等等,因此,此處需要改爲也調用close_conn(conn)來完成所有的釋放和銷燬動作。
6. close_conn函數調用處分析
既然server->active_connections鏈表實際上是用來保存struct connection *數據的,那麼要了解什麼時候釋放節點,則必須分析close_conn(conn)的調用處。
調用close_conn函數的地方有三處:
mg_poll_server函數:
else if (conn->flags & CONN_CLOSE) {
close_conn(conn);
}
如果conn->flags標記中包含CONN_CLOSE,那麼輪詢處理中就會關閉該connection。
mg_poll_server函數:
if (conn->flags & CONN_CLOSE || conn->last_activity_time < expire_time) {
close_conn(conn);
}
如果conn->flags標記中包含CONN_CLOSE或者連接的最後活動時間小於失效時間,則輪詢處理中也會關閉該connection。
mg_destroy_server函數:
close_conn(conn);
此處本來就是要銷燬server,調用close_conn來關閉和銷燬資源。
6.1 conn->flags中CONN_CLOSE標記
write_to_client函數:
int n = conn->ssl == NULL ? send(conn->client_sock, io->buf, io->len, 0) : 0;
if (is_error(n)) {
conn->flags |= CONN_CLOSE;
}
如果發送數據失敗,置conn->flags中CONN_CLOSE標記
write_to_client函數:
if (io->len == 0 && conn->flags & CONN_SPOOL_DONE) {
conn->flags |= CONN_CLOSE;
}
如果待發送數據爲0(亦即發送數據完畢),並且CONN_SPOOL_DONE標記也爲true,置conn->flags中CONN_CLOSE標記
read_from_client函數:
n = recv(conn->client_sock, buf, sizeof(buf), 0);
if (is_error(n)) {
conn->flags |= CONN_CLOSE;
}
如果接收數據失敗,置conn->flags中CONN_CLOSE標記
close_local_endpoint函數:
if (keep_alive) {
process_request(conn); // Can call us recursively if pipelining is used
} else {
conn->flags |= conn->remote_iobuf.len == 0 ? CONN_CLOSE : CONN_SPOOL_DONE;
}
如果不是keep_alive,並且數據已經發送完畢,置conn->flags中CONN_CLOSE標記
6.2 conn->flags中CONN_SPOOL_DONE標記
open_file_endpoint函數:
if (!strcmp(conn->mg_conn.request_method, "HEAD")) {
conn->flags |= CONN_SPOOL_DONE;
close(conn->endpoint.fd);
conn->endpoint_type = EP_NONE;
}
如果HTTP請求方法是HEAD(類似於GET, 但是不返回body信息,用於檢查對象是否存在,以及得到對象的元數據),則置conn->flags中CONN_SPOOL_DONE標記
send_options函數:
conn->flags |= CONN_SPOOL_DONE;
如果HTTP請求方法是OPTIONS(詢問可以執行哪些方法),則置conn->flags中CONN_SPOOL_DONE標記
close_local_endpoint函數:
if (keep_alive) {
process_request(conn); // Can call us recursively if pipelining is used
} else {
conn->flags |= conn->remote_iobuf.len == 0 ? CONN_CLOSE : CONN_SPOOL_DONE;
}
如果不是keep_alive,並且數據尚未發送完畢,置conn->flags中CONN_SPOOL_DONE標記
綜上分析,當HTTP HEAD或HTTP OPTIONS請求,或者需要進行半關閉(HTTP 300重定向或數據讀取出錯等等),如果這個時候數據沒有傳輸完畢,需要置CONN_SPOOL_DONE標記