Write your own http server
author : Kevin Lynx
Why write your own?
看這個(gè)問(wèn)題的人證明你知道什么是http server,世界上有很多各種規(guī)模的http server,為什么要自己實(shí)現(xiàn)一個(gè)?其實(shí)沒(méi)什么
理由。我自己?jiǎn)栕约海杏X(jué)就是在自己娛樂(lè)自己,或者說(shuō)只是練習(xí)下網(wǎng)絡(luò)編程,或者是因?yàn)槟橙瘴铱吹侥硞€(gè)庫(kù)宣稱(chēng)自己附帶一個(gè)小
型的http server時(shí),我不知道是什么東西,于是就想自己去實(shí)現(xiàn)一個(gè)。
What's httpd ?
httpd就是http daemon,這個(gè)是類(lèi)unix系統(tǒng)上的名稱(chēng),也就是http server。httpd遵循HTTP協(xié)議,響應(yīng)HTTP客戶端的request,
然后返回response。
那么,什么是HTTP協(xié)議?最簡(jiǎn)單的例子,就是你的瀏覽器與網(wǎng)頁(yè)服務(wù)器之間使用的應(yīng)用層協(xié)議。雖然官方文檔說(shuō)HTTP協(xié)議可以
建立在任何可靠傳輸?shù)膮f(xié)議之上,但是就我們所見(jiàn)到的,HTTP還是建立在TCP之上的。
httpd最簡(jiǎn)單的response是返回靜態(tài)的HTML頁(yè)面。在這里我們的目標(biāo)也只是一個(gè)響應(yīng)靜態(tài)網(wǎng)頁(yè)的httpd而已(也許你愿意加入CGI
特性)。
More details about HTTP protocol
在這里有必要講解HTTP協(xié)議的更多細(xì)節(jié),因?yàn)槲覀兊膆ttpd就是要去解析這個(gè)協(xié)議。
關(guān)于HTTP協(xié)議的詳細(xì)文檔,可以參看rfc2616。但事實(shí)上對(duì)于實(shí)現(xiàn)一個(gè)簡(jiǎn)單的響應(yīng)靜態(tài)網(wǎng)頁(yè)的httpd來(lái)說(shuō),完全沒(méi)必要讀這么一
分冗長(zhǎng)的文檔。在這里我推薦<HTTP Made Really Easy>,以下內(nèi)容基本取自于本文檔。
- HTTP協(xié)議結(jié)構(gòu)
HTTP協(xié)議無(wú)論是請(qǐng)求報(bào)文(request message)還是回應(yīng)報(bào)文(response message)都分為四部分:
* 報(bào)文頭 (initial line )
* 0個(gè)或多個(gè)header line
* 空行(作為header lines的結(jié)束)
* 可選body
HTTP協(xié)議是基于行的協(xié)議,每一行以\r\n作為分隔符。報(bào)文頭通常表明報(bào)文的類(lèi)型(例如請(qǐng)求類(lèi)型),報(bào)文頭只占一行;header line
附帶一些特殊信息,每一個(gè)header line占一行,其格式為name:value,即以分號(hào)作為分隔;空行也就是一個(gè)\r\n;可選body通常
包含數(shù)據(jù),例如服務(wù)器返回的某個(gè)靜態(tài)HTML文件的內(nèi)容。舉個(gè)例子,以下是一個(gè)很常見(jiàn)的請(qǐng)求報(bào)文,你可以截獲瀏覽器發(fā)送的數(shù)據(jù)
包而獲得:
1 GET /index.html HTTP/1.1
2 Accept-Language: zh-cn
3 Accept-Encoding: gzip, deflate
4 User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; MAXTHON 2.0)
5 Host: localhost
6 Connection: Keep-Alive
7
我為每一行都添加了行號(hào),第1行就是initial line,2-6行是header lines,7行是一個(gè)header line的結(jié)束符,沒(méi)有顯示出來(lái)。
以下是一個(gè)回應(yīng)報(bào)文:
1 HTTP/1.1 200 OK
2 Server: klhttpd/0.1.0
3 Content-Type: text/html
4 Content-Length: 67
5
6 <head><head><title>index.html</title></head><body>index.html</body>
第6行就是可選的body,這里是index.html這個(gè)文件的內(nèi)容。
- HTTP request method
因?yàn)槲覀冏龅氖路?wù)器端,所以我們重點(diǎn)對(duì)請(qǐng)求報(bào)文做說(shuō)明。首先看initial line,該行包含幾個(gè)字段,每個(gè)字段用空格分開(kāi),例
如以上的GET /index.html HTTP/1.1就可以分為三部分:GET、/index.html、HTTP/1.1。其中第一個(gè)字段GET就是所謂的request
method。它表明請(qǐng)求類(lèi)型,HTTP有很多method,例如:GET、POST、HEAD等。
就我們的目標(biāo)而言,我們只需要實(shí)現(xiàn)對(duì)GET和HEAD做響應(yīng)即可。
GET是最普遍的method,表示請(qǐng)求一個(gè)資源。什么是資源?諸如HTML網(wǎng)頁(yè)、圖片、聲音文件等都是資源。順便提一句,HTTP協(xié)議
中為每一個(gè)資源設(shè)置一個(gè)唯一的標(biāo)識(shí)符,就是所謂的URI(更寬泛的URL)。
HEAD與GET一樣,不過(guò)它不請(qǐng)求資源內(nèi)容,而是請(qǐng)求資源信息,例如文件長(zhǎng)度等信息。
- More detail
繼續(xù)說(shuō)說(shuō)initial line后面的內(nèi)容:
對(duì)應(yīng)于GET和HEAD兩個(gè)method,緊接著的字段就是資源名,其實(shí)從這里可以看出,也就是文件名(相對(duì)于你服務(wù)器的資源目錄),例
如這里的/index.html;最后一個(gè)字段表明HTTP協(xié)議版本號(hào)。目前我們只需要支持HTTP1.1和1.0,沒(méi)有多大的技術(shù)差別。
然后是header line。我們并不需要關(guān)注每一個(gè)header line。我只羅列有用的header line :
- Host : 對(duì)于HTTP1.1而言,請(qǐng)求報(bào)文中必須包含此header,如果沒(méi)有包含,服務(wù)器需要返回bad request錯(cuò)誤信息。
- Date : 用于回應(yīng)報(bào)文,用于客戶端緩存數(shù)據(jù)用。
- Content-Type : 用于回應(yīng)報(bào)文,表示回應(yīng)資源的文件類(lèi)型,以MIME形式給出。什么是MIME?它們都有自己的格式,例如:
text/html, image/jpg, image/gif等。
- Content-Length : 用于回應(yīng)報(bào)文,表示回應(yīng)資源的文件長(zhǎng)度。
body域很簡(jiǎn)單,你只需要將一個(gè)文件全部讀入內(nèi)存,然后附加到回應(yīng)報(bào)文段后發(fā)送即可,即使是二進(jìn)制數(shù)據(jù)。
- 回應(yīng)報(bào)文
之前提到的一個(gè)回應(yīng)報(bào)文例子很典型,我們以其為例講解。首先是initial line,第一個(gè)字段表明HTTP協(xié)議版本,可以直接以請(qǐng)求
報(bào)文為準(zhǔn)(即請(qǐng)求報(bào)文版本是多少這里就是多少);第二個(gè)字段是一個(gè)status code,也就是回應(yīng)狀態(tài),相當(dāng)于請(qǐng)求結(jié)果,請(qǐng)求結(jié)果
被HTTP官方事先定義,例如200表示成功、404表示資源不存在等;最后一個(gè)字段為status code的可讀字符串,你隨便給吧。
回應(yīng)報(bào)文中最好跟上Content-Type、Content-Length等header。
具體實(shí)現(xiàn)
正式寫(xiě)代碼之前我希望你能明白HTTP協(xié)議的這種請(qǐng)求/回應(yīng)模式,即客戶端發(fā)出一個(gè)請(qǐng)求,然后服務(wù)器端回應(yīng)該請(qǐng)求。然后繼續(xù)
這個(gè)過(guò)程(HTTP1.1是長(zhǎng)連接模式,而HTTP1.0是短連接,當(dāng)服務(wù)器端返回第一個(gè)請(qǐng)求時(shí),連接就斷開(kāi)了)。
這里,我們無(wú)論客戶端,例如瀏覽器,發(fā)出什么樣的請(qǐng)求,請(qǐng)求什么資源,我們都回應(yīng)相同的數(shù)據(jù):

/**//* 阻塞地接受一個(gè)客戶端連接 */
SOCKET con = accept( s, 0, 0 );

/**//* recv request */

char request[1024] =
{ 0 };
ret = recv( con, request, sizeof( request ), 0 );
printf( request );

/**//* whatever we recv, we send 200 response */

{
char content[] = "<head><head><title>index.html</title></head><body>index.html</body>";
char response[512];
sprintf( response, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n%s", strlen( content ), content );
ret = send( con, response, strlen( response ), 0 );
}
closesocket( con );
程序以最簡(jiǎn)單的阻塞模式運(yùn)行,我們可以將重點(diǎn)放在協(xié)議的分析上。運(yùn)行程序,在瀏覽器里輸入http://localhost:8080/index.html
,然后就可以看到瀏覽器正常顯示content中描述的HTML文件。假設(shè)程序在8080端口監(jiān)聽(tīng)。
現(xiàn)在你基本上明白了整個(gè)工作過(guò)程,我們可以把代碼寫(xiě)得更全面一點(diǎn),例如根據(jù)GET的URI來(lái)載入對(duì)應(yīng)的文件然后回應(yīng)給客戶端。
其實(shí)這個(gè)很簡(jiǎn)單,只需要從initial line里解析出(很一般的字符串解析)URI字段,然后載入對(duì)應(yīng)的文件即可。例如以下函數(shù):
void http_response( SOCKET con, const char *request )


{

/**//* get the method */
char *token = strtok( request, " " );
char *uri = strtok( 0, " " );
char file[64];
sprintf( file, ".%s", uri );


{

/**//* load the file content */
FILE *fp = fopen( file, "rb" );
if( fp == 0 )

{

/**//* response 404 status code */
char response[] = "HTTP/1.1 404 NOT FOUND\r\n\r\n";
send( con, response, strlen( response ), 0 );
}
else

{

/**//* response the resource */

/**//* first, load the file */
int file_size ;
char *content;
char response[1024];
fseek( fp, 0, SEEK_END );
file_size = ftell( fp );
fseek( fp, 0, SEEK_SET );
content = (char*)malloc( file_size + 1 );
fread( content, file_size, 1, fp );
content[file_size] = 0;

sprintf( response, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n%s", file_size, content );
send( con, response, strlen( response ), 0 );
free( content );
}
}
}


其他
要將這個(gè)簡(jiǎn)易的httpd做完善,我們還需要注意很多細(xì)節(jié)。包括:對(duì)不支持的method返回501錯(cuò)誤;對(duì)于HTTP1.1要求有Host這個(gè)
header;為了支持客戶端cache,需要添加Date header;支持HEAD請(qǐng)求等。
相關(guān)下載中我提供了一個(gè)完整的httpd library,純C的代碼,在其上加上一層資源載入即可實(shí)現(xiàn)一個(gè)簡(jiǎn)單的httpd。在這里我將
對(duì)代碼做簡(jiǎn)要的說(shuō)明:
evbuffer.h/buffer.c : 取自libevent的buffer,用于緩存數(shù)據(jù);
klhttp-internal.h/klhttp-internal.c :主要用于處理/解析HTTP請(qǐng)求,以及創(chuàng)建回應(yīng)報(bào)文;
klhttp-netbase.h/klhttp-netbase.c :對(duì)socket api的一個(gè)簡(jiǎn)要封裝,使用select模型;
klhttp.h/klhttp.c :庫(kù)的最上層,應(yīng)用層主要與該層交互,這一層主要集合internal和netbase。
test_klhttp.c :一個(gè)測(cè)試?yán)印?
相關(guān)下載:
klhttpd
文中相關(guān)代碼
參考資料:
http://www.w3.org/Protocols/rfc2616/rfc2616.html
http://jmarshall.com/easy/http/
http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html