一個(gè)HTTP服務(wù)器的基本配置無非是
接下來我們寫一個(gè)可以解析配置文件的小模塊函數(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;
這個(gè)是配置文件的所有屬性,可以將讀取的參數(shù),存進(jìn)這個(gè)結(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
對(duì)于配置文件存放位置而言,可以靈活一些,例如可以額外添加一個(gè)命令行參數(shù),用來指定本次需要使用的配置文件路徑:
./httpd -f /path/to/wsx.conf
當(dāng)然這用在開發(fā)版本可以方便調(diào)試,實(shí)際上的HTTP服務(wù)器并不行,參見守護(hù)進(jìn)程的定義最經(jīng)典的做法還是指定默認(rèn)路徑,將配置文件都存放在某個(gè)地方,可以多設(shè)定幾個(gè),并設(shè)定優(yōu)先級(jí)
#
開頭的都是注釋,這點(diǎn)十分容易做到。上代碼
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é)束
這是很簡單的文件操作,包括打開文件,驗(yàn)證是否成功,可以選擇將其封裝成一個(gè)inline
函數(shù),來模塊化這個(gè)邏輯。
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;
}
真正的核心代碼沒幾行,四個(gè)if
,使用strncasecmp
函數(shù),檢測(cè)參數(shù)。但是并沒有 驗(yàn)證參數(shù)的正確性。
當(dāng)然你也可以寫成 json
的形式,再用第三方庫,比如c-json
之類的解析,但 那不是要依賴第三方了嗎?所以我的建議還是自己寫一個(gè)解析的函數(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
)了!(流程圖中沒有畫出listen
,過于冗余,但卻必不可少)可以將 創(chuàng)建,綁定合并成一個(gè)函數(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);
可以看出來,需要一個(gè)IP, 一個(gè)PORT, 第三個(gè)參數(shù)是套接字類型擔(dān)不是傳入?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;
/* 實(shí)際上這一行完全可以在上面使用 初始化來達(dá)到目的。
* 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;
}
其中有一個(gè)optimizes,是用來設(shè)置一些套接字選項(xiàng)的,現(xiàn)在只需要知道有這些選項(xiàng)就行
套接字選項(xiàng)分別是TCP_NODELAY
和 SO_REUSEADDR
。
細(xì)看之下,和前面介紹的幾個(gè)接口幾乎是完全一致的用法。但如果認(rèn)為網(wǎng)絡(luò)編程就是這樣接口調(diào)用的話,那就是大錯(cuò)特錯(cuò)。
就這樣,如果你的配置文件中,<IP, PORT>沒什么差錯(cuò)的話,我們就完成了打開服務(wù)器套接字的工作,這時(shí)候你可以組織并且運(yùn)行一下前面說的這些代碼,看看是否如此。
運(yùn)行成功與否可以通過你的終端是否顯示上述的調(diào)試信息看出來:
DEBUG MESG: Now We(x) are in : %s , listen the xx port Success
寫到這里,實(shí)際上整個(gè)主函數(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;
}
這個(gè)邏輯已經(jīng)十分清晰,為了方便我省去了錯(cuò)誤檢查,在代碼中應(yīng)該自己添加,這里面有兩個(gè)新事物: signal()
, handle_loop()
來解釋一下signal(SIGPIPE, SIG_IGN)
是什么以及為什么
signal
是信號(hào)函數(shù),還記得之前的章節(jié)用它來當(dāng)做函數(shù)指針類型的一個(gè)練習(xí)思考題嗎?它的作用就是在本進(jìn)程/線程接收到該信號(hào)(SIGPIPE
)時(shí)候,會(huì)進(jìn)行這樣的(SIG_IGN
)處理sigation
,比較復(fù)雜但是也比較推薦你用它,這里為了減少概念,就用了最原始的signal
。SIGPIPE
是一個(gè)關(guān)于寫的錯(cuò)誤,觸發(fā)條件是向一個(gè)發(fā)送了RST
的對(duì)端進(jìn)行寫操作,默認(rèn)行為就是結(jié)束本進(jìn)程,我們當(dāng)然不愿意結(jié)束了,明明是對(duì)方的錯(cuò),怎么要我們死。最基本的做法就是忽略它SIG_IGN
。SIGPIPE
,模擬一下情形,這里需要對(duì)TCP的工作方式有一定了解,不了解的可以跳過:
RST
的對(duì)端進(jìn)行寫操作的話,就會(huì)觸發(fā)SIGPIPE
,信號(hào)這個(gè)東西就是全局的,所以如果你想知道哪個(gè)線程觸發(fā)了這個(gè)信號(hào),還需要檢查寫操作是否返回了EPIPE
錯(cuò)誤handle_loop
是一個(gè)事件循環(huán)的入口
epoll
監(jiān)聽服務(wù)器套接字,用來建立新連接handle_loop
就干了兩件事
幾個(gè)全局變量
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的讀事件,錯(cuò)誤事件 */
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ù)組來存儲(chǔ)所有的連接信息(conn_client
),這其實(shí)弊端很大,比如最明顯的數(shù)量以及預(yù)分配的資源過大。但關(guān)鍵是夠簡單,且效率最高。
整個(gè)的原理就是,在接受到新連接以后,按照某種規(guī)則分配給第i個(gè)子線程,每個(gè)子線程中有一個(gè)工作epoll
(epoll_group[i-1]),用來監(jiān)聽新連接的事件,并處理。
prepare_workers
就是分配內(nèi)存空間的相關(guān)工作。這段代碼,同樣省略了錯(cuò)誤檢查,希望自己添加。
{}
里面可以看出來怎么向epoll
實(shí)例中注冊(cè)監(jiān)聽實(shí)體,以及監(jiān)聽事件。
整段代碼的后半部分,是關(guān)于線程的啟動(dòng),操作,銷毀。pthread_detach
意味著放棄線程的資源回收權(quán),用通俗的話來說就是:“撒丫子跑吧,我管不著你了!”。