uhttpd源碼分析

來源:互聯網
上載者:User

標籤: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必須指定,別的都有預設值。一般情況下我們可以用這樣的參數來啟動伺服器:

1 ./uhttpd -p 8080

伺服器預設是運行在背景,可以使用“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源碼分析

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.