http://www.cnblogs.com/kernel_hcy/archive/2010/03/24/1694203.html
前面讲了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)中,这个问题依然没有修改。
就先说到这。后面将详细介绍各个状态的处理。