標籤:blog http io ar os 使用 sp for on
uhttpd是openwrt上預設的Web伺服器,支援CGI,lua指令碼,以及靜態檔案的服務。它是一個精簡的伺服器,一般適合作為路由器這樣的嵌入式裝置使用,或者Web伺服器的入門學習。
uhttpd的源碼可以用svn到這裡下載。
概述
uhttpd.png
首先,在uhttpd啟動的時候,它會先讀取參數,進行伺服器的配置。參數可以由命令列輸入,其中port參數必須制定,其他都有預設值。
配置完參數之後,伺服器會進入uh_mainloop,等待請求。
在主迴圈中,uhttpd採用select進行輪詢,而不是採用fork進行並發,一定程度上降低了並發能力,但是很適合這樣的小型伺服器。
當select檢測到有用戶端請求,uhttpd就會先接受請求,再進行解析,之後再調用uh_dispatch_request去分發請求。其中,lua請求比較特殊,不由uh_dispatch_request分發。
在分發過程(不包括lua請求)當中,會根據path的首碼來判斷是CGI請求還是靜態檔案請求,預設的CGI首碼是/cgi-bin。CGI請求進入uh_cgi_request,檔案請求進入uh_file_request,lua請求則會進入lua_request。
在三種handler中,就會進行請求的處理了。lua_request會調用lua解譯器進行處理,file_request直接讀取檔案並且返回,CGI請求比較複雜,之後會詳細說明。
在三種request處理之後,都會返回給用戶端。一次迴圈到此結束。
啟動伺服器配置
啟動入口的main函數位於uhttpd.c,它接受命令列參數,進行伺服器配置,並且啟動伺服器。讓我們先來看看它有哪些配置。
help
其中port必須指定,別的都有預設值。一般情況下我們可以用這樣的參數來啟動伺服器:
伺服器預設是運行在背景,可以使用“ps -A | grep uhttp”看到它的運行情況,用nmap掃描一下本地連接埠也可以看到它已經在監聽8080連接埠了。
nmap
uhttpd的配置用“struct config”儲存,成員也很豐富:
123456789101112131415161718192021222324252627282930313233343536373839 |
struct config {char docroot[PATH_MAX]; char *realm;char *file; char *index_file; char *error_handler;int no_symlinks;int no_dirlists;int network_timeout; int rfc1918_filter;int tcp_keepalive;#ifdef HAVE_CGIchar *cgi_prefix;#endif#ifdef HAVE_LUAchar *lua_prefix;char *lua_handler;lua_State *lua_state;lua_State * (*lua_init) (const char *handler);void (*lua_close) (lua_State *L);void (*lua_request) (struct client *cl, struct http_request *req, lua_State *L);#endif#if defined(HAVE_CGI) || defined(HAVE_LUA)int script_timeout;#endif#ifdef HAVE_TLSchar *cert;char *key;SSL_CTX *tls;SSL_CTX * (*tls_init) (void);int (*tls_cert) (SSL_CTX *c, const char *file);int (*tls_key) (SSL_CTX *c, const char *file);void (*tls_free) (struct listener *l);int (*tls_accept) (struct client *c);void (*tls_close) (struct client *c);int (*tls_recv) (struct client *c, void *buf, int len);int (*tls_send) (struct client *c, void *buf, int len);#endif}; |
連接埠綁定
伺服器的連接埠綁定沒有寫在config裡面,而是直接用uh_socket_bind進行了連接埠的綁定。
12345 |
/* bind sockets */bound += uh_socket_bind(&serv_fds, &max_fd, bind[0] ? bind : NULL, port,&hints,(opt == ‘s‘), &conf); |
uh_socket_bind:
1234 |
static int uh_socket_bind(fd_set *serv_fds, int *max_fd, const char *host, const char *port,struct addrinfo *hints, int do_tls, struct config *conf) |
此函數進行連接埠綁定並且把listener加到了一個全域的鏈表中,於是我們可以綁定多個連接埠。
123456 |
/* add listener to global list */if( ! (l = uh_listener_add(sock, conf)) ){fprintf(stderr, "uh_listener_add(): Failed to allocate memory\n");goto error;} |
比較有意思的是,uhttpd把一些資訊存在了鏈表裡,用add函數在表頭插入。C語言沒有現成的集合架構,但是自己寫一個鏈表也是很輕鬆的。這些工具函數都在uhttpd-utils.c裡。
1234 |
static struct client *uh_clients = NULL;struct client * uh_client_add(int sock, struct listener *serv);struct client * uh_client_lookup(int sock);void uh_client_remove(int sock); |
設定檔
光用命令列的話肯定太麻煩,uhttpd也可以用設定檔來進行配置。
12 |
/* config file */uh_config_parse(&conf); |
但是設定檔的選項好像不是很多,最好的方式還是寫一個啟動指令碼。
正式啟動
在一系列的配置之後,uhttpd終於要正式啟動了。它預設是後台啟動,fork一個子進程,父進程退出,子進程帶著設定檔和伺服器的FD進入了mainloop。
12 |
/* server main loop */uh_mainloop(&conf, serv_fds, max_fd); |
等待請求
uh_mainloop函數也在uhttp.c裡,最外層是一個大的迴圈。
1234567 |
while (run) {if( select(max_fd + 1, &read_fds, NULL, NULL, NULL) == -1 ) {...}...} |
select
select函數起到了阻塞請求的作用,並且和accept不用的是,它使用輪詢機制,而不是fork,更加適合嵌入式裝置。
12345 |
if( select(max_fd + 1, &read_fds, NULL, NULL, NULL) == -1 ){perror("select()");exit(1);} |
最後一個參數是設定逾時時間,如果設定成NULL,則無限逾時,直到FD有變動。
獲得請求
之後會進入一個嵌套複雜的“if-else”語句,數了一下最深有六層if嵌套。主要的功能就是遍曆所有的FD,分別找到服務端和用戶端的FD,在服務端,accept並且把client加入鏈表。在用戶端的FD中,處理請求。
用uh_http_header_recv獲得請求之後,用uh_http_header_parse解析,得到一個http_request的結構體。
12345678 |
struct http_request {intmethod;float version;int redirect_status;char *url;char *headers[UH_LIMIT_HEADERS];struct auth_realm *realm;}; |
請求分發
得到http_request之後,就可以根據URL來進行請求的分發了。帶有lua首碼的給lua_request,否則交給uh_dispatch_request。
1234567891011121314 |
/* Lua request? */if( conf->lua_state && uh_path_match(conf->lua_prefix, req->url) ){conf->lua_request(cl, req, conf->lua_state);}else/* dispatch request */if( (pin = uh_path_lookup(cl, req->url)) != NULL ){/* auth ok? */if( !pin->redirected && uh_auth_check(cl, req, pin) )uh_dispatch_request(cl, req, pin);} |
dispatch也只是做了一個簡單的判斷然後就交給下一級了。
123456789 |
if( uh_path_match(cl->server->conf->cgi_prefix, pin->name) ||(ipr = uh_interpreter_lookup(pin->phys)) ){uh_cgi_request(cl, req, pin, ipr);}else{uh_file_request(cl, req, pin);} |
處理請求
lua請求暫時不說了,這裡只說檔案和CGI請求。
靜態檔案
file_request在uhttpd-file.c中。成員如下
1234567891011121314151617181920212223 |
uhttpd-file.c macro _XOPEN_SOURCE _BSD_SOURCE function uh_file_mime_lookup uh_file_mktag uh_file_date2unix uh_file_unix2date uh_file_header_lookup uh_file_response_ok_hdrs uh_file_response_200 uh_file_response_304 uh_file_response_412 uh_file_if_match uh_file_if_modified_since uh_file_if_none_match uh_file_if_range uh_file_if_unmodified_since uh_file_scandir_filter_dir uh_file_dirlist uh_file_request |
既然是靜態檔案請求,自然是先看看本地有沒有這個檔案,有的話就讀取內容發給用戶端,沒有就404。
這裡有一個有趣的函數,
123456 |
/* test preconditions */if(ok) ensure_out(uh_file_if_modified_since(cl, req, &pi->stat, &ok));if(ok) ensure_out(uh_file_if_match(cl, req, &pi->stat, &ok));if(ok) ensure_out(uh_file_if_range(cl, req, &pi->stat, &ok));if(ok) ensure_out(uh_file_if_unmodified_since(cl, req, &pi->stat, &ok));if(ok) ensure_out(uh_file_if_none_match(cl, req, &pi->stat, &ok)); |
處理請求的過程中大量使用了ensure_out,它應該是保證關閉FD的。如果網路發生異常或者檔案讀寫異常,需要保證FD被正確關閉。實現很簡單,一個類函數宏就搞定了。
123456 |
#define ensure_out(x) \do { if((x) < 0) goto out; } while(0) out:if( fd > -1 )close(fd); |
CGI請求
處理CGI請求稍微複雜一點。
uh_cgi_request函數位於uhttpd-cgi.c。成員如下。
123456 |
uhttpd-cgi.c function uh_cgi_header_parse uh_cgi_header_lookup uh_cgi_error_500 uh_cgi_request |
雖然成員很少,但是總體還是有600多行。
CGI的處理過程,基本上就是調用CGI程式,獲得它的處理結果,然後返回給用戶端。但CGI程式和主調函數,肯定是兩個進程,它們之間如何通訊,如何傳遞資料,這才是關鍵。
uhttpd採用了管道和CGI程式進行通訊,有兩個管道,實現雙向通訊。一個管道負責從父進程寫資料到CGI程式,主要是用戶端的POST資料。另一個就是讀取CGI程式的處理結果。同時,按照CGI的標準,HTTP要求標頭都是通過環境變數的方式傳給CGI程式的,CGI程式是fork和exec的,所以會繼承環境變數,達到傳遞資料的目的。
在子進程中,則用dup2進行了一個重新導向,把輸入輸出資料流都定向到了管道。
123 |
/* patch stdout and stdin to pipes */dup2(rfd[1], 1);dup2(wfd[0], 0); |
之後就用了大段的代碼設定環境變數。
123456 |
setenv("SERVER_NAME", sa_straddr(&cl->servaddr), 1);setenv("SERVER_ADDR", sa_straddr(&cl->servaddr), 1);setenv("SERVER_PORT", sa_strport(&cl->servaddr), 1);setenv("REMOTE_HOST", sa_straddr(&cl->peeraddr), 1);setenv("REMOTE_ADDR", sa_straddr(&cl->peeraddr), 1);setenv("REMOTE_PORT", sa_strport(&cl->peeraddr), 1); |
之後才真正地調用CGI程式。
1234 |
if( ip != NULL )execl(ip->path, ip->path, pi->phys, NULL);elseexecl(pi->phys, pi->phys, NULL); |
與此同時,父進程則焦急地等待著管道另一頭的迴音。它等來等去等的不耐煩了,於是它又機制地給自己設定了一個timeout,過了這個時間它就離開了。
12345678910111213 |
ensure_out(rv = select_intr(fd_max, &reader,(content_length > -1) ? &writer : NULL, NULL, &timeout));....../* read it from socket ... */ensure_out(buflen = uh_tcp_recv(cl, buf,min(content_length, sizeof(buf))));...../* ... and write it to child‘s stdin */if( write(wfd[1], buf, buflen) < 0 )perror("write()");....../* read data from child ... */if( (buflen = read(rfd[0], buf, sizeof(buf))) > 0 ) |
從CGI程式讀完了資料之後,它還是不放心,又解析了一下回應標頭,確認正確之後,才發給了用戶端。
到這裡,整個處理過程才算結束。
總結
第一次看伺服器的源碼,所以找了一個比較簡單的伺服器。大致能夠理解它的原理,但是很多細節還是不明白,可能只有自己親自去實現才能對它有一個深刻的理解。uhttd的代碼並不多,其中很多的代碼都用來處理錯誤,可見處理異常情況也是很重要的。有機會的話,希望自己能親自實現一個伺服器。
uhttpd源碼分析