小型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一個,然后初始化后,插入列表中。運行一個定時器,則是根據當前時刻的時間,在列表中依次比對,對於超時的定時器運行其回調函數,接着根據周期性選擇回收這個定時器還是重新設置這個定時器。最近觸發時間也是在鏈表中找出最近的時間返回。重置定時器就是根據傳入的時間,重新確定定時器的觸發時間。取消定時器就是將該定時器從當前定時器列表轉移到無用定時器列表中。清除定時器和銷毀定時器上面已經介紹過了,就是銷毀某些內存。

一次性寫很多真的看着都煩呀,那另外兩個主要的模塊就在下篇來介紹吧。
另注:本文中的代碼是自己手寫,和原代碼並非都是一致的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM