小型web服务器thttpd的学习总结(上)

1、软件的主要架构

软件的文件布局比较清晰,主要分为6个模块,主模块是thttpd.c文件,这个文件中包含了web server的主要逻辑,并调用了其他模块的函数。其他的5个模块都是单一的功能模块,之间没有任何耦合。

  • 其中包括多路IO复用的抽象模块fdwatch.h/c,这个模块中将常用的IO复用接口,如poll/select抽象为一类接口,从而保证了接口的单一性和软件的可移植性。
  • libhttpd模块包含的是libhttpd.h/c文件,主要的功能是完成地提供http请求的解析和处理服务,对外提供相应的接口。
  • match模块则是对外提供了一个match.c用来做为关键词的匹配作用,用于cgi符号匹配。
  • mmc模块包块的也是mmc.h/c文件,用来进行文件存储的缓存管理。
  • 另外一个就是timer.h/c,自己实现的一个定时器模块,主要用来做请求接收,发送和清理内存的操作定时。

2、各个模块代码分析

2.1 fdwatch模块

该模块对外提供了6个函数,就是对一般的select/poll类函数的使用方法进行了相应的抽象,包括

//获得系统可使用的fd的最大数目,并初始化数据结构
extern int fdwatch_get_nfiles(void)

//清除fdwatch中的数据结构
extern void fdwatch_clear( void )

//对fd set中的fds进行操作,其中rw表示是否可读可写
extern int fdwatch_add_fd(int fd, int rw)
extern int fdwatch_del_fd(int fd, int rw)

//fd多路复用的主循环函数 参数是超时时间
extern int fdwatch(long timeout_msecs)

//对fd状态的检查
extern void fdwatch_check_fd(int fd, int rw)

//提取出fdwatch当前的状态
extern void fdwatch(long* nselectP, long* npollP)

如果想自己简单实现的话,可以按照如下进行实现:

//首先设置模块全局变量
static int nfiles;   //可以watch的最大fd数目
static int maxfd;    //当前watch的最大fd值
static int conn_nums;  //当前连接的数目
static long nselect;	//当前select的次数

由于如果使用select的话,需要首先有一个fd_set来标记需要关注哪些fd可读,关注哪些fd可写。而将标记fd_set传入之后,该fd_set返回的指将是当前可读或者可写的fd列表,会改变标记set的值,因此,这里设置了两个fd_set,一个用于标记需要关注的fd,另一个用于传入select函数,获得当前可处理的fd情况。

//标记 set
static fd_set master_rfdset;   
static fd_set master_wfdset;
//工作 set
static fd_set working_rfdset;
static fd_set working_wfdset;

//内部函数的声明
static int fdwatch_select(long timeout_msecs);

该函数用于获得当前可以复用的fd的最大个数,这个最大个数受制于几个因素,一个是进程可以打开的最大的文件描述符数,getdtablesize()返回的值,还有资源限制的最大fd数,另外还不能超过fd_setsize值,一般现在的fd_set类型都是long int的数组,每一位代表一个fd的读写情况,取值一般为1024。

int fdwatch_get_nfiles( void )
{
#ifdef RLIMIT_NOFILE
	struct rlimit rl;
#endif
	//进程所能打开的最大文件描述符数
	nfiles = getdtablesize();

	//设置资源限制的最大fd值	
#ifdef RLIMIT_NOFILE
	if(getrlimit(RLIMIT_NOFILE, &rl) == 0)
	{
		nfiles = rl.rlim_cur;
		if( rl.rlim_max == RLIM_INFINITY )
			rl.rlim_cur = 8192;
		else
			rl.rlim_cur = rl.rlim_max;
		if( setrlimit( RLIMIT_NOFILE, &rl) == 0 )
			nfiles = rl.rlim_cur;
	}
#endif
//如果是SELECT不能超过FD_SETSIZE的值
	nfiles = MIN(nfiles, FD_SETSIZE);

	nselect = 0;

	return nfiles; 
}

清除标志位,直接调用FD_ZERO函数:

void fdwatch_clear( void )
{
	maxfd = -1;
	conn_nums = 0;
	FD_ZERO( &master_wfdset );
	FD_ZERO( &master_rfdset );
}

增加标志位,则是根据rw的情况调用FD_SET函数:

void fdwatch_add_fd( int fd, int rw )
{
	conn_nums++;
	if(fd > maxfd)
		maxfd = fd;
	switch( rw )
	{
		case FD_READ:
			FD_SET(fd, &master_rfdset);
		case FD_WRITE:
			FD_SET(fd, &master_wfdset);
		default:
			return;
	}

}

检查标志位,同样根据rw的情况调用FD_ISSET函数:

int fdwatch_check_fd( int fd, int rw)
{
	switch( rw )
		{
			case FD_READ:
				return FD_ISSET(fd, &working_rfdset);
			case FD_WRITE:
				return FD_ISSET(fd, &working_wfdset);
			default:
				return 0;
		}
}

在大循环中,将master_fdset的值赋值给working_fdset然后调用select传入working_fdset进行检测,检测的时候由参数timeout_msecs决定。

int fdwatch( long timeout_msecs )
{
	return fdwatch_select( timeout_msecs );
}

static int fdwatch_select( long timeout_msecs )
{
	struct  timeval timeout;

	++nselect;
	working_rfdset = master_rfdset;
	working_wfdset = master_wfdset;
	if(timeout_msecs == INFTIM)
	{
		if((maxfd + 1) <= nfiles)
			return select(maxfd +1, &working_rfdset, &working_wfdset, NULL, (struct timeval*)0);
		else
		{
			perror("maxfd out of range");
			return -1;
		}
	}
	else
	{
		timeout.tv_sec = timeout_msecs / 1000L;
		timeout.tv_usec = timeout_msecs % 1000L * 1000L;
		if((maxfd + 1) <= nfiles)
			return select(maxfd + 1, &working_rfdset, &working_wfdset, NULL, &timeout);
		else
		{
			perror("maxfd out of range");
			return -1;
		}
	}	
}

下面两个函数主要是永远检测当前select模块的情况,方便后面打log。

void fdwatch_status( long* nselectP )
{
	*nselectP = nselect;
	nselect = 0;
}

int fdwatch_get_conn_nums(void)
{
	return conn_nums;
}

可以看到fdwatch模块仅仅是对select做了一个简单的封装,从而可以更加灵活的使用接口进行fd复用的操作,从而可以正常的处理小规模的服务器并发。

2.1 定时器模块

定时器模块主要是提供一个定时服务,而采用的时间则是从gettimeofday库函数来获得。建立起定时器模块,主要需要首先确定下模块中的几个结构体,如定时器触发后,响应的函数和该函数的参数选择:

//响应函数定义 
typedef void timeout_func(timeout_args args, struct timeval* now);
//传入参数
typedef union
{
	void* p;
	int i;
	long int l;
}timeout_args;

这里将参数定义为一个联合体,从而可以传入多样类型的值,同时节省空间。所以,定时器结构的定义为:

typedef struct timer_struct
{
	timeout_func* timer_proc;    //响应函数
	timeout_args args;			  //响应函数参数
	struct timeval time;         //定时器触发时间
	long msecs;					  //定时多长时间
	int periodic;				  //周期性标志

	struct timer_struct* next;   //做成链表
} Timer;

然后可以看下定时器模块需要提供的模块接口,大抵也就是如下几种,创建一个定时器,运行一个定时器,重置一个定时器,取消一个定时器,这里还提供了一个查看最近定时器的触发时间的接口,用来在这段时间内通过select进行查看各个连接的情况,也就是说这个时间作为上述fdwatch函数的参数传入。此外在本定时器模块中,实际上是建立了两个链表,一个是当前定时器的列表,一个是被取消的定时器的列表。因此,还提供了tmr_clean函数用于合理释放无用定时器所占用的内存。而tmr_destroy函数则是销毁所有的定时器结构。

//创建一个定时器
extern Timer* tmr_create(timeout_func* timer_proc, 	timeout_args args, struct timeval* now, long msecs, int periodic);

//运行一个定时器
extern void tmr_run(struct timeval* now);

//查看最近的定时器触发时间-毫秒
extern long tmr_timeout_ms(struct timeval* now);

//查看最近的定时器触发时间-struct timeval
extern struct timeval* tmr_timeout(struct timeval* now);

//重置一个定时器
extern void tmr_reset(Timer* timer, struct timeval* now);

//取消一个定时器
extern void tmr_cancle(Timer* timer);

//清除定时器结构
extern void tmr_clean(void);

//销毁所以定时器内存
extern void tmr_destroy(void);

具体的函数实现,这里就简单的阐述一下过程,不展开代码叙述了。创建定时器时,如果无用定时器列表中有内容,就直接使用其数据,否则malloc一个,然后初始化后,插入列表中。运行一个定时器,则是根据当前时刻的时间,在列表中依次比对,对于超时的定时器运行其回调函数,接着根据周期性选择回收这个定时器还是重新设置这个定时器。最近触发时间也是在链表中找出最近的时间返回。重置定时器就是根据传入的时间,重新确定定时器的触发时间。取消定时器就是将该定时器从当前定时器列表转移到无用定时器列表中。清除定时器和销毁定时器上面已经介绍过了,就是销毁某些内存。

一次性写很多真的看着都烦呀,那另外两个主要的模块就在下篇来介绍吧。
另注:本文中的代码是自己手写,和原代码并非都是一致的。

原文地址:https://www.cnblogs.com/nearmeng/p/4362022.html