情人节在家很无聊,很随便地就找了这么一个号称 安全 的web服务器:thttpd,闲的蛋疼,于是看了下代码.
主要数据结构
thttpd代码量很小,wc -l看了一下,11,000行左右,除去1k行左右的几个cgi演示性质的代码,大概有15个源文件,主要的数据结构如下:
各结构体的作用简单描述如下: TimerStruct(typedef成Timer)主要作为一个定时器,它包含了一个TimerProc*元素作为它到时之后的处理函数.结构体httpd_server表示一个绑定到某网址的server,而结构体httpd_conn就是一个从客户端(浏览器)到服务器(thttpd)的连接信息,这些连接信息也就是客户端看到的HTTP协议下指定的GET查询,POST更改操作对应的HTTP请求信息和响应信息,以及服务器看到的accept()返回的connfd信息,显然,结构体connecttab表示的就是被定时器绑定的连接信息和对应.
下面,分别介绍一下这些结构体对应的数据结构和算法:
Timer
由上图可知,Timer有两个指针,分别是prev和next,这样设计,就可以像timers.c下的 static Timer* free_timers; 一样,创建一个双向链表,元素的个数可以用一个int元素表示.这是低阶的实现,更高级的应用在这一行代码: Timer *timers[HASH_SIZE]; ,这里定义了一个数组,每个数组元素却是由多个Timer元素组成的双向链表,数组的下标根据Timer元素值的hash指定.
这么设计有什么作用呢? 实际上,在thttpd的实现中,timers不外乎就是拿来插入和删除一个Timer元素的,稍微有点不一样的是,作者对这个数组的每一个元素(也就是链表)做了排序.对双向链表排序看起来有点无厘头,因为它是顺序取值的,又不是数组那样可以随机读写.实际上,这是作者对Timer应用的思考: 不管用什么数据结构来组织,既然Timer是一个计时器,那么就总是需要从一堆Timer里面找出离某个时间最近的那个Timer的. 在thttp的实现中,因为与客户端的连接有很多,也就是accept()返回的connfd数量有很多,根据这个最近的Timer,就可以从这一堆connfd中select出已经ready(可读可写或异常)的那个connfd,然后对此connfd操作(写或读).好吧,既然关键点出来了,那么将链表排序也就是可以理解了的: 要找出最小的那个Timer,只要从times[]数组里面找出头元素最小的那个链表的下标,然后在此下标对应的链表中顺序比较找出最小的那个就可以了,这么一来,就省去了所有元素都比较一遍的麻烦.转化成代码如下:
Timer* tmr_mstimeout( ) { register Timer* min = timers[0]; for ( h = 1; h < HASH_SIZE; ++h ) { //timers在插入或删除元素时自然是要用心维护好顺序的,这里不打出来 if ( timers[h] < min ) min = timers[h]; } return min; }
至于为什么不直接设计成一个由很多Timer组成的数组呢(即: 元素是Timer而不是由Timer组成的链表,也就是 Timer timers[HASH_SIZE]) ,这么设计就可以直接用二分查找了.我觉得,这应该是因为作者认为哈希后每个链表的元素很少的缘故.
至于前面提到的 static Timer* free_timers; ,它实际上起的是类似于内存池的作用,当需要一个Timer元素时,从free_timers分配,当timers里需要cancel一个Timer时,由free_timers接收,省去了分配内存和释放内存的时间(C语言还好,C++的话分配是需要三步骤的,明显的吃力不讨好.).分配操作转换成代码如下:
Timer* tmr_create( ) { Timer* t; //free_timers相当于内存池 if ( free_timers != (Timer*) 0 ) { t = free_timers; free_timers = t->next; --free_count; } else { t = (Timer*) malloc( sizeof(Timer) ); if ( t == (Timer*) 0 ) return (Timer*) 0; ++alloc_count; } t->hash = hash( t ); //将新分配的元素插入timers l_add( t ); ++active_count; return t; }
文件mmc.c下结构体MapStruct还有thttpd.c下结构体connecttab实现思想上和Timer都差不多,不加赘述.
fdwatch.c
fdwatch.c下select poll devpoll kqueue操作维护了各自的数据结构(保存文件描述符fd)和几种操作(add_fd,del_fd等),作者把这些操作封装成界面通用的宏,成功的隐藏了内部细节.比如说,fdwatch.c的用户(主要是thttp.c文件)只要用一句DEL_FD宏就可以实现添加fd的功能,而无需考虑背后fd是添加到select poll devpoll kqueue对应的哪个数据结构中,这里不多说.
下面看看select,很明显,它需要维护一个数据结构,用来存储一堆文件描述符fd,这个数据结构需要支持add和del操作.第一选择是一维数组,add时就是push(),del时也不过排序后二分查找即可.作者采用的是另外一种思路,开两个数组,维持 a[i]=j, b[j]=i 的关系.这样,要在a[]数组中找到j,可以很简单先到辅助数组b[]中找出j在a[]数组中的下标,省去了排序和二分查找.这是小技巧,直接看一下add和del的代码:
//nselect_fds维持为数组select_fds最后那个元素的下标 static void select_add_fd (int fd) { select_fds[nselect_fds] = fd; select_fdidx[fd] = nselect_fds; ++nselect_fds; } static void select_del_fd (int fd) { int idx = select_fdidx[fd]; --nselect_fds; select_fds[idx] = select_fds[nselect_fds]; select_fdidx[select_fds[idx]] = idx; select_fds[nselect_fds] = -1; select_fdidx[fd] = -1; }
thttpd.c
这个没什么好说的,看一下main()函数流程就是了:
int main(int argc, char * argv[]) { ... //很无聊的初始化各种数据结构(timer,connecttab,httpd_server等等),读配置,转daemon,注册信号处理函数等操作 ... //真正开始干活了. while(...) { //看一下有多少个文件描述符是ready了的. num_ready = fdwatch( tmr_mstimeout( &tv ) ); //找到需要处理的connection while ( c = (connecttab*) fdwatch_get_next_client_data() ) { switch ( c->conn_state ) { case CNST_READING: handle_read( c, &tv ); break; case CNST_SENDING: handle_send( c, &tv ); break; case CNST_LINGERING: handle_linger( c, &tv ); break; } } //所有计时器都跑的很欢快. tmr_run(...); } //主循环结束 exit(0); }
唉,人家都是分析架构的,我只看得懂一点点代码.杯具.