Lighttpd1.4.20源码分析之状态机(1)---状态机总览
前面讲了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)中,这个问题依然没有修改。
就先说到这。后面将详细介绍各个状态的处理。