前面講了lighttpd的fdevent系統,從這一篇開始,我們將進入lighttpd的狀態機。狀態機可以說是lighttpd最核心的部分。lighttpd將一個連接在不同的時刻分成不同的狀態,狀態機則根據連接當前的狀態,決定要對連接進行的處理以及下一步要進入的狀態。下面這幅圖描述了lighttpd的狀態機:
(在lighttpd源碼文件夾中的doc目錄中有個state.dot文件,通過dot命令可以生成上面的圖:dot -Tpng state.dot -o state.png。)
圖中的各個狀態對應於下面的一個枚舉類型:
1 typedef enum
2 {
3 CON_STATE_CONNECT, //connect 連接開始
4 CON_STATE_REQUEST_START, //reqstart 開始讀取請求
5 CON_STATE_READ, //read 讀取並解析請求
6 CON_STATE_REQUEST_END, //reqend 讀取請求結束
7 CON_STATE_READ_POST, //readpost 讀取post數據
8 CON_STATE_HANDLE_REQUEST, //handelreq 處理請求
9 CON_STATE_RESPONSE_START, //respstart 開始回復
10 CON_STATE_WRITE, //write 回復寫數據
11 CON_STATE_RESPONSE_END, //respend 回復結束
12 CON_STATE_ERROR, //error 出錯
13 CON_STATE_CLOSE //close 連接關閉
14 } connection_state_t;
在每個連接中都會保存這樣一個枚舉類型變量,用以表示當前連接的狀態。connection結構體的第一個成員就是這個變量。
在連接建立以後,在connections.c/connection_accpet()函數中,lighttpd會調用 connection_set_state()函數,將新建立的連接的狀態設置為CON_STATE_REQUEST_START。在這個狀態中,lighttpd記錄連接建立的時間等信息。
下面先來說一說整個狀態機的核心函數───connections.c/ connection_state_machine()函數。函數很長,看著比較嚇人。。。其實,這裡我們主要關心的是函數的主體部分:while循環和其中的那個大switch語句,刪減之後如下:
1 int connection_state_machine(server * srv, connection * con)
2 {
3 int done = 0, r;
4 while (done == 0)
5 {
6 size_t ostate = con -> state;
7 int b;
8 //這個大switch語句根據當前狀態機的狀態進行相應的處理和狀態轉換。
9 switch (con->state)
10 {
11 case CON_STATE_REQUEST_START: /* transient */
12 case CON_STATE_REQUEST_END: /* transient */
13 case CON_STATE_HANDLE_REQUEST:
14 case CON_STATE_RESPONSE_START:
15 case CON_STATE_RESPONSE_END: /* transient */
16 case CON_STATE_CONNECT:
17 case CON_STATE_CLOSE:
18 case CON_STATE_READ_POST:
19 case CON_STATE_READ:
20 case CON_STATE_WRITE:
21 case CON_STATE_ERROR: /* transient */
22 default:
23 break;
24 }//end of switch(con -> state) ...
25 if (done == -1)
26 {
27 done = 0;
28 }
29 else if (ostate == con->state)
30 {
31 done = 1;
32 }
33 }
34 return 0;
35 }
程序進入這個函數以後,首先根據當前的狀態進入對應的switch分支執行相應的動作。然後,根據情況,進入下一個狀態。跳出switch語句之後,如果連接的狀態沒有改變,說明連接讀寫數據還沒有結束,但是需要等待IO事件,這時,跳出循環,等待IO事件。對於done==-1的情況,是在 CON_STATE_HANDLE_REQUEST狀態中的問題,後面再討論。如果在處理的過程中沒有出現需要等待IO事件的情況,那麼在while循環中,連接將被處理完畢並關閉。
接著前面的話題,在建立新的連接以後,程序回到 network.c/network_server_handle_fdevent()函數中的for循環在中後,lighttpd對這個新建立的連接調用了一次connection_state_machine()函數。如果這個連接沒有出現需要等待IO事件的情況,那麼在這次調用中,這個連接請求就被處理完畢。但是實際上,在連接第一次進入CON_STATE_READ狀態時,幾乎是什麼都沒做,保持這個狀態,然後跳出了while循環。在循環後面,還有一段代碼:
1 switch (con->state)
2 {
3 case CON_STATE_READ_POST:
4 case CON_STATE_READ:
5 case CON_STATE_CLOSE:
6 fdevent_event_add(srv->ev, &(con->fde_ndx), con->fd, FDEVENT_IN);
7 break;
8 case CON_STATE_WRITE:
9 if (!chunkqueue_is_empty(con->write_queue) &&
10 (con->is_writable == 0)&& (con->traffic_limit_reached == 0))
11 {
12 fdevent_event_add(srv->ev, &(con->fde_ndx), con->fd, FDEVENT_OUT);
13 }
14 else
15 {
16 fdevent_event_del(srv->ev, &(con->fde_ndx), con->fd);
17 }
18 break;
19 default:
20 fdevent_event_del(srv->ev, &(con->fde_ndx), con->fd);
21 break;
22 }
這段代碼前面已經介紹過,這個連接的連接fd被加入到fdevent系統中,等待IO事件。當有數據可讀的時候,在main函數中,lighttpd調用這個fd對應的handle函數,這裡就是connection_handle_fdevent()函數。這個函數一開始將連接加入到了joblist(作業隊列)中。前面已經說過,這個函數僅僅做了一些標記工作。程序回到main函數中時,執行了下面的代碼:
1 for (ndx = 0; ndx < srv->joblist->used; ndx++)
2 {
3 connection *con = srv->joblist->ptr[ndx];
4 handler_t r;
5 connection_state_machine(srv, con);
6 switch (r = plugins_call_handle_joblist(srv, con))
7 {
8 case HANDLER_FINISHED:
9 case HANDLER_GO_ON:
10 break;
11 default:
12 log_error_write(srv, __FILE__, __LINE__, "d", r);
13 break;
14 }
15 con->in_joblist = 0;//標記con已經不在隊列中。
16 }
這段代碼就是對joblist中的所有連接,依次對其調用connection_state_machine()函數。在這次調用中,連接開始真正的讀取數據。lighttpd調用connection_handle_read_state()函數讀取數據。在這個函數中,如果數據讀取完畢或出錯,那麼連接進入相應的狀態,如果數據沒有讀取完畢那麼連接的狀態不變。(PS:在connection_handle_read_state()讀取的數據其實就是HTTP頭,在這個函數中根據格式HTTP頭的格式判斷HTTP頭是否已經讀取完畢,包括POST數據。)上面說到,在 connection_state_machile()函數的while循環中,如果連接的狀態沒有改變,那麼將跳出循環。繼續等待讀取數據。
讀取完數據,連接進入CON_STATE_REQUEST_END。在這個狀態中lighttpd對HTTP頭進行解析。根據解析的結果判斷是否有POST 數據。如果有,則進入CON_STATE_READ_POST狀態。這個狀態的處理和CON_STATE_READ一樣。如果沒有POST數據,則進入 CON_STATE_HANDLE_REQUEST狀態。在這個狀態中lighttpd做了整個連接最核心的工作:處理連接請求並准備response數據。
處理完之後,連接進入CON_STATE_RESPONSE_START。在這個狀態中,主要工作是准備response頭。准備好後,連接進入CON_STATE_WRITE狀態。顯然,這個狀態是向客戶端回寫數據。第一次進入WRITE狀態什麼都不做,跳出循環後將連接fd加入 fdevent系統中並監聽寫事件(此時僅僅是修改要監聽的事件)。當有寫事件發生時,和讀事件一樣調用 connection_handle_fdevent函數做標記並把連接加入joblist中。經過若干次後,數據寫完。連接進入 CON_STATE_RESPONSE_END狀態,進行一些清理工作,判斷是否要keeplive,如果是則連接進入 CON_STATE_REQUEST_START狀態,否則進入CON_STATE_CLOSE。進入CLOSE後,等待客戶端掛斷,執行關閉操作。這裡順便說一下,在將fd加到fdevent中時,默認對每個fd都監聽錯誤和掛斷事件。
連接關閉後,connection結構體並沒有刪除,而是留在了server結構體的connecions成員中。以便以後再用。
關於joblist有一個問題。在每次將連接加入的joblist中時,通過connection結構體中的in_joblist判斷是否連接已經在 joblist中。但是,在joblist_append函數中,並沒有對in_joblist進行賦值,在程序的運行過程中,in_joblist始終是0.也就是說,每次調用joblist_append都會將連接加入joblist中,不論連接是否已經加入。還有,當連接已經處理完畢後,程序也沒有將對應的connection結構體指針從joblist中刪除,雖然這樣不影響程序運行,因為斷開後,對應的connection結構體的狀態被設置成 CON_STATE_CONNECT,這個狀態僅僅是清理了一下chunkqueue。但這將導致joblist不斷增大,造成輕微的內存洩漏。在最新版(1.4.26)中,這個問題依然沒有修改。
就先說到這。後面將詳細介紹各個狀態的處理。