一個HTTP服務器的基本配置無非是
接下來我們寫一個可以解析配置文件的小模塊函數(shù)
struct init_config_from_file {
int core_num; /* CPU Core numbers */
#define PORT_SIZE 10
char listen_port[PORT_SIZE]; /* */
#define ADDR_SIZE IPV6_LENGTH_CHAR
char use_addr[ADDR_SIZE]; /* NULL For Auto select(By Operating System) */
#define PATH_LENGTH 256
char root_path[PATH_LENGTH]; /* page root path */
};
typedef struct init_config_from_file wsx_config_t;
這個是配置文件的所有屬性,可以將讀取的參數(shù),存進這個結(jié)構(gòu)體中,與主線程交互
/*
* Read the config file "wsx.conf" in particular path
* and Put the data to the config object
* @param config is aims to be a parameter set
* @return 0 means Success
* */
int init_config(wsx_config_t * config);
交互的接口,我的配置文件叫做 wsx.conf
對于配置文件存放位置而言,可以靈活一些,例如可以額外添加一個命令行參數(shù),用來指定本次需要使用的配置文件路徑:
./httpd -f /path/to/wsx.conf當然這用在開發(fā)版本可以方便調(diào)試,實際上的HTTP服務器并不行,參見守護進程的定義最經(jīng)典的做法還是指定默認路徑,將配置文件都存放在某個地方,可以多設定幾個,并設定優(yōu)先級
#開頭的都是注釋,這點十分容易做到。上代碼
static const char * config_path_search[] = {CONFIG_FILE_PATH, "./wsx.conf", "/etc/wushxin/wsx.conf", NULL};
int init_config(wsx_config_t * config){
const char ** roll = config_path_search;
FILE * file;
for (int i = 0; roll[i] != NULL; ++i) {
file = fopen(roll[i], "r");
if (file != NULL)
break;
}
if (NULL == file) {
#if defined(WSX_DEBUG)
fprintf(stderr, "Check For the Config file, does it stay its life?\n"
"In Such Path: \n%s\n%s\n%s\n", config_path_search[0], config_path_search[1], config_path_search[2]);
#endif
exit(-1);
}
...未結(jié)束
這是很簡單的文件操作,包括打開文件,驗證是否成功,可以選擇將其封裝成一個inline函數(shù),來模塊化這個邏輯。
char buf[PATH_LENGTH] = {"\0"};
char * ret;
ret = fgets(buf, PATH_LENGTH, file);
while (ret != NULL) {
char * pos = strchr(buf, ':');
char * check = strchr(buf, '#'); /* Start with # will be ignore */
if (check != NULL)
*check = '\0';
if (pos != NULL) {
*pos++ = '\0';
if (0 == strncasecmp(buf, "thread", 6)) {
sscanf(pos, "%d", &config->core_num);
}
else if (0 == strncasecmp(buf, "root", 4)) {
sscanf(pos, "%s", &config->root_path);
/* End up without "/", Add it */
if ((config->root_path)[strlen(config->root_path)-1] != '/') {
strncat(config->root_path, "/", 1);
}
}
else if (0 == strncasecmp(buf, "port", 4)) {
sscanf(pos, "%s", &config->listen_port);
}
else if (0 == strncasecmp(buf, "addr", 4)) {
sscanf(pos, "%s", &config->use_addr);
}
} /* if pos != NULL */
ret = fgets(buf, PATH_LENGTH, file);
} /* while */
fclose(file);
return 0;
}
真正的核心代碼沒幾行,四個if,使用strncasecmp函數(shù),檢測參數(shù)。但是并沒有 驗證參數(shù)的正確性。
當然你也可以寫成 json 的形式,再用第三方庫,比如c-json之類的解析,但 那不是要依賴第三方了嗎?所以我的建議還是自己寫一個解析的函數(shù)。
如果沒能理解這小段代碼,建議翻一下C語言的入門教材,回顧一下語法。
配置文件的樣式
# Just Edit this Config file Or
# You can Create a new one and save the Old to
# Back up
# But Remember that , that file can only parse
# the FOUR CONFIGURATION :
# thread root port address
# Watch out the case sensitive !!!
# thread -- For the Worker thread number
# root -- For the WebSite's root path
# port -- Listen Port
# address -- Host's address(Note it If you can)
# Or empty For the auto select by Operating System
thread:8
# Using shell Command (pwd) to show your root Path!
root:/root/ClionProjects/httpd3/
port:9998 # That is a port
address:192.168.141.149
listen)了?。鞒虉D中沒有畫出listen,過于冗余,但卻必不可少)可以將 創(chuàng)建,綁定合并成一個函數(shù),在成功之后,再執(zhí)行listen。
/*
* Open The Listen Socket With the specific host(IP address) and port
* That must be compatible with the IPv6 And IPv4
* host_addr could be NULL
* port MUST NOT BE NULL !!!
* sock_type is the pointer to a memory ,which comes from the Outside(The Caller)
* */
int open_listenfd(const char * restrict host_addr,const char * restrict port, int * restrict sock_type);
可以看出來,需要一個IP, 一個PORT, 第三個參數(shù)是套接字類型擔不是傳入?yún)?shù),而是傳出參數(shù)。
int open_listenfd(const char * restrict host_addr, const char * restrict port, int * restrict sock_type){
int listenfd = 0; /* listen the Port, To accept the new Connection */
struct addrinfo info_of_host;
struct addrinfo * result;
struct addrinfo * p;
/* 實際上這一行完全可以在上面使用 初始化來達到目的。
* struct addrinfo info_of_host = {0}; 需要c99
*/
memset(&info_of_host, 0, sizeof(info_of_host));
info_of_host.ai_family = AF_UNSPEC; /* Unknown Socket Type */
info_of_host.ai_flags = AI_PASSIVE; /* Let the Program to help us fill the Message we need */
info_of_host.ai_socktype = SOCK_STREAM; /* TCP */
int error_code;
if(0 != (error_code = getaddrinfo(host_addr, port, &info_of_host, &result))){
fputs(gai_strerror(error_code), stderr);
return ERR_GETADDRINFO; /* -2 */
}
for(p = result; p != NULL; p = p->ai_next) {
listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if(-1 == listenfd)
continue; /* Try the Next Possibility */
optimizes(listenfd);
if(-1 == bind(listenfd, p->ai_addr, p->ai_addrlen)){
close(listenfd);
continue; /* Same Reason */
}
break; /* If we get here, it means that we have succeed to do all the Work */
}
freeaddrinfo(result);
if (NULL == p) {
fprintf(stderr, "In %s, Line: %d\nError Occur while Open/ Binding the listen fd\n",__FILE__, __LINE__);
return ERR_BINDIND;
}
fprintf(stderr, "DEBUG MESG: Now We(%d) are in : %s , listen the %s port Success\n", listenfd,
inet_ntoa(((struct sockaddr_in *)p->ai_addr)->sin_addr), port);
*sock_type = p->ai_family;
set_nonblock(listenfd);
return listenfd;
}
其中有一個optimizes,是用來設置一些套接字選項的,現(xiàn)在只需要知道有這些選項就行
套接字選項分別是TCP_NODELAY 和 SO_REUSEADDR。
細看之下,和前面介紹的幾個接口幾乎是完全一致的用法。但如果認為網(wǎng)絡編程就是這樣接口調(diào)用的話,那就是大錯特錯。
就這樣,如果你的配置文件中,<IP, PORT>沒什么差錯的話,我們就完成了打開服務器套接字的工作,這時候你可以組織并且運行一下前面說的這些代碼,看看是否如此。
運行成功與否可以通過你的終端是否顯示上述的調(diào)試信息看出來:
DEBUG MESG: Now We(x) are in : %s , listen the xx port Success
寫到這里,實際上整個主函數(shù)的代碼已經(jīng)接近尾聲,來看看全部的過程調(diào)用
int main(int argc, char * argv[]) {
wsx_config_t config = {0};
init_config(&config)
int sock_type = 0;
int listenfd = open_listenfd(config.use_addr, config.listen_port, &sock_type);
listen(listenfd, SOMAXCONN);
signal(SIGPIPE, SIG_IGN);
handle_loop(listenfd, sock_type, &config);
return 0;
}
這個邏輯已經(jīng)十分清晰,為了方便我省去了錯誤檢查,在代碼中應該自己添加,這里面有兩個新事物: signal(), handle_loop()
來解釋一下signal(SIGPIPE, SIG_IGN)是什么以及為什么
signal是信號函數(shù),還記得之前的章節(jié)用它來當做函數(shù)指針類型的一個練習思考題嗎?它的作用就是在本進程/線程接收到該信號(SIGPIPE)時候,會進行這樣的(SIG_IGN)處理sigation,比較復雜但是也比較推薦你用它,這里為了減少概念,就用了最原始的signal。SIGPIPE是一個關(guān)于寫的錯誤,觸發(fā)條件是向一個發(fā)送了RST的對端進行寫操作,默認行為就是結(jié)束本進程,我們當然不愿意結(jié)束了,明明是對方的錯,怎么要我們死。最基本的做法就是忽略它SIG_IGN。SIGPIPE,模擬一下情形,這里需要對TCP的工作方式有一定了解,不了解的可以跳過:
RST的對端進行寫操作的話,就會觸發(fā)SIGPIPE,信號這個東西就是全局的,所以如果你想知道哪個線程觸發(fā)了這個信號,還需要檢查寫操作是否返回了EPIPE錯誤handle_loop 是一個事件循環(huán)的入口
epoll監(jiān)聽服務器套接字,用來建立新連接handle_loop就干了兩件事
幾個全局變量
static int * epfd_group = NULL; /* Workers' epfd set */
static int epfd_group_size = 0; /* Workers' epfd set size */
static int workers = 0; /* Number of Workers */
static int listeners = MAX_LISTEN_EPFD_SIZE; /* Number of Listenner */
static conn_client * clients; /* Client set */
handle_loop()
void handle_loop(int file_dsption, int sock_type, const wsx_config_t * config) {
workers = config->core_num - listeners;
int listen_epfd = epoll_create1(0);
{ /* Register listen fd to the listen_epfd */
struct epoll_event event;
event.data.fd = file_dsption;
event.events = EPOLLET | EPOLLERR | EPOLLIN;
/* 以ET方式監(jiān)聽file_dsption的讀事件,錯誤事件 */
epoll_ctl(listen_epfd, EPOLL_CTL_ADD, file_dsption, &event);
}
/* Prepare Workers Sources */
prepare_workers(config);
pthread_t listener_set[listeners];
pthread_t worker_set[workers];
for (int i = 0; i < listeners; ++i)
pthread_create(&listener_set[i], NULL, listen_thread, (void*)listen_epfd);
for (int j = 0; j < workers; ++j) {
pthread_create(&worker_set[j], NULL, workers_thread, (void*)(epfd_group[j]));
pthread_detach(worker_set[j]);
}
for (int k = 0; k < listeners; ++k)
pthread_join(listener_set[k], NULL);
destroy_resouce();
}
使用了最原始的線性數(shù)組來存儲所有的連接信息(conn_client),這其實弊端很大,比如最明顯的數(shù)量以及預分配的資源過大。但關(guān)鍵是夠簡單,且效率最高。
整個的原理就是,在接受到新連接以后,按照某種規(guī)則分配給第i個子線程,每個子線程中有一個工作epoll(epoll_group[i-1]),用來監(jiān)聽新連接的事件,并處理。
prepare_workers 就是分配內(nèi)存空間的相關(guān)工作。這段代碼,同樣省略了錯誤檢查,希望自己添加。
{}里面可以看出來怎么向epoll實例中注冊監(jiān)聽實體,以及監(jiān)聽事件。
整段代碼的后半部分,是關(guān)于線程的啟動,操作,銷毀。pthread_detach意味著放棄線程的資源回收權(quán),用通俗的話來說就是:“撒丫子跑吧,我管不著你了!”。