518雲盒:教你怎麼實現一個Http伺服器
我始終覺得,天生的出身很重要,但後天的努力更加重要,所以如今的很多“科班”往往不如後天努力的“非科班”。所以,我們需要重新給“專業”和“專家”下一個定義:所謂專業,就是別人不搞你搞,這就是你的“專業”;你和別人同時搞,你比別人搞的好,就是“專家”。
說到http協議和http請求,很多人都知道,但是他們真的“知道”嗎?我面試過很多求職者,一說到http協議,他們能滔滔不絕,然後我問他http協議的具體格式是啥樣子的?很多人不清楚,不清楚就不清楚吧,他甚至能將http協議的頭扯到html文件頭部。當我問http GET和POST請求的時候,GET請求是什麼形式一般人都可以答出來,但是POST請求的資料放在哪裡, 如何識別和解析這些POST資料,很多人又說不清道不明瞭。當說到http伺服器時,很多人離開了apache、Nginx這樣現成的http server之外,自己實現一個http伺服器無從下手,如果實際應用場景有需要使用到一些簡單http請求時,使用apache、Nginx這樣重量級的http伺服器程式實在勞師動眾,你可以嘗試自己實現一個簡單的。
上面提到的問題,如果您不能清晰地回答出來,可以閱讀一下這篇文章,這篇文章在不僅介紹http的格式,同時帶領大家從零實現一個簡單的http伺服器程式。
二、http協議介紹
1. http協議是應用層協議,一般建立在tcp協議的基礎之上(當然你的實現非要基於udp也是可以的),也就是說http協議的資料收發是透過tcp協議的。
2. http協議也分為head和body兩部分,但是我們一般說的html中的和標記不是http協議的頭和身體,它們都是http協議的body部分。
那麼http協議的頭到底長啥樣子呢?我們來介紹一下http協議吧。
http協議的格式如下:
1GET或POST 請求的url路徑(一般是去掉域名的路徑) HTTP協議版本號\r\n 2欄位1名: 欄位1值\r\n 3欄位2名: 欄位2值\r\n 4 … 5欄位n名 : 欄位n值\r\n 6\r\n 7http協議包體內容
也就是說http協議由兩部分組成:包頭和包體,包頭與包體之間使用一個\r\n分割,由於http協議包頭的每一行都是以\r\n結束,所以http協議包頭一般以\r\n\r\n結束。
舉個例子,比如我們在瀏覽器中請求這個網址,這是一個典型的GET方法,瀏覽器組裝的http資料包格式如下:
GET /index_2013.php HTTP/1.1\r\n 2Host: \r\n 3Connection: keep-alive\r\n 4Upgrade-Insecure-Requests: 1\r\n 5User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n 6Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\n 7Accept-Encoding: gzip, deflate\r\n 8Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n 9\r\n
上面這個請求只有包頭沒有包體,http協議的包體不是必須的,也就是說GET請求一般沒有包體。
如果GET請求帶引數,那麼一般是附加在請求的url後面,引數與引數之間使用&分割,例如請求?param1=value1¶m2=value2¶m3=value3,我們看下這個請求組裝的的http協議包格式:
GET /index_2013.php?param1=value1¶m2=value2¶m3=value3 HTTP/1.1\r\n 2Host: \r\n 3Connection: keep-alive\r\n 4Upgrade-Insecure-Requests: 1\r\n 5User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n 6Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\n 7Accept-Encoding: gzip, deflate\r\n 8Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n 9\r\n
對比一下,你現在知道http協議的GET引數放在協議包的什麼位置了吧。
那麼POST的資料放在什麼位置呢?我們再12306網站中登陸輸入使用者名稱和密碼:
然後發現瀏覽器以POST方式組裝了http協議包傳送了我們的使用者名稱、密碼和其他一些資訊,組裝的包格式如下:
POST /passport/web/login HTTP/1.1\r\n 2Host: kyfw.12306.cn\r\n 3Connection: keep-alive\r\n 4Content-Length: 55\r\n 5Accept: application/json, text/javascript, */*; q=0.01\r\n 6Origin: \r\n 7X-Requested-With: XMLHttpRequest\r\n 8User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n 9Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n 10Referer: \r\n 11Accept-Encoding: gzip, deflate, br\r\n 12Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n 13Cookie: _passport_session=0b2cc5b86eb74bcc976bfa9dfef3e8a20712; _passport_ct=18d19b0930954d76b8057c732ce4cdcat8137; route=6f50b51faa11b987e576cdb301e545c4; RAIL_EXPIRATION=1526718782244; RAIL_DEVICEID=QuRAhOyIWv9lwWEhkq03x5Yl_livKZxx7gW6_-52oTZQda1c4zmVWxdw5Zk79xSDFHe9LJ57F8luYOFp_yahxDXQAOmEV8U1VgXavacuM2UPCFy3knfn42yTsJM3EYOy-hwpsP-jTb2OXevJj5acf40XsvsPDcM7; BIGipServerpool_passport=300745226.50215.0000; BIGipServerotn=1257243146.38945.0000; BIGipServerpassport=1005060362.50215.0000\r\n 14\r\n 15username=balloonwj%40qq.com&password=iloveyou&appid=otn
其中username=balloonwj%40qq.com&password=iloveyou&appid=otn就是我們的POST資料,但是大家需要注意的以下幾種,不要搞錯:
1. 我的使用者名稱是balloonwj@qq.com,到POST裡面變成balloonwj%40qq.com,其中%40是@符號的16進位制轉碼形式。這個碼錶可以參考這裡:
2.這裡有三個變數,分別是username、password和appid,他們之間使用&符號分割,但是請注意的是,這不意味著傳遞多個POST變數時必須使用&符號分割,只不過這裡是瀏覽器html表單(輸入使用者名稱和密碼的文字框是html表單的一種)分割多個變數採用的預設方式而已。你可以根據你的需求,來自由定製,只要讓伺服器知道你的解析方式即可。比如可以這麼分割:
方法一
username=balloonwj%40qq.com|password=iloveyou|appid=otn
方法二
username:balloonwj%40qq.com\r\n 2password:iloveyou\r\n 3appid:otn\r\n
方法三
username,password,appid=balloonwj%40qq.com,iloveyou,otn
不管怎麼分割,只要你能自己按一定的規則解析出來就可以了。
不知道你注意到沒有,上面的POST資料放在http包體中,伺服器如何解析呢?可能你沒明白我的意思,看下圖:
如上圖所示,由於http協議是基於tcp協議的,tcp協議是流式協議,包頭部分可以透過多出的\r\n來分界,包體部分如何分界呢?這是協議本身要解決的問題。目前一般有兩種方式,第一種方式就是在包頭中有個content-Length欄位,這個欄位的值的大小標識了POST資料的長度,上圖中55就是資料username=balloonwj%40qq.com&password=iloveyou&appid=otn的長度,伺服器收到一個資料包後,先從包頭解析出這個欄位的值,再根據這個值去讀取相應長度的作為http協議的包體資料。還有一個格式叫做http chunked技術(分塊),大致意思是將大包分成小包,具體的詳情有興趣的讀者可以自行搜尋學習。
三、http客戶端實現
如果您能掌握以上說的http協議,你就可以自己透過程式碼組裝http協議傳送http請求了(也是各種開源http庫的做法)。我們先簡單地介紹一下如何模擬傳送http。舉個例子,我們要請求,那麼我們可以先透過域名得到ip地址,即透過socket API gethostbyname()得到的ip地址,由於http伺服器預設的埠號是80,有了域名和ip地址之後,我們使用socket API connect()去連線伺服器,然後根據上面介紹的格式組裝成http協議包,利用socket API send()函式發出去,如果伺服器有應答,我們可以使用socket API recv()去接受資料,接下來就是解析資料(先解析包頭和包體)。
四、http伺服器實現
我們這裡簡化一些問題,假設客戶端傳送的請求都是GET請求,當客戶端發來http請求之後,我們拿到http包後就做相應的處理。我們以為我們的flamingo伺服器實現一個支援http格式的註冊請求為例。假設使用者在瀏覽器裡面輸入以下網址,就可以實現一個註冊功能:
{"username": "13917043329", "nickname": "balloon", "password": "123"}
這裡我們的http協議使用的是12345埠號而不是預設的80埠。如何偵聽12345埠,這個是非常基礎的知識了,這裡就不介紹了。當我們收到資料以後:
1void HttpSession::OnRead(const std::shared_ptr<TcpConnection>& conn, Buffer* pBuffer, Timestamp receivTime) 2{ 3 //LOG_INFO << "Recv a http request from " << conn->peerAddress().toIpPort(); 4 5 string inbuf; 6 //先把所有資料都取出來 7 inbuf.append(pBuffer->peek(), pBuffer->readableBytes()); 8 //因為一個http包頭的資料至少\r\n\r\n,所以大於4個字元 9 //小於等於4個字元,說明資料未收完,退出,等待網路底層接著收取 10 if (inbuf.length() <= 4) 11 return; 12 13 //我們收到的GET請求資料包一般格式如下: 14 /* 15 GET /register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22} HTTP/1.1\r\n 16 Host: 120.55.94.78:12345\r\n 17 Connection: keep-alive\r\n 18 Upgrade-Insecure-Requests: 1\r\n 19 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n 20 Accept-Encoding: gzip, deflate\r\n 21 Accept-Language: zh-CN, zh; q=0.9, en; q=0.8\r\n 22 \r\n 23 */ 24 //檢查是否以\r\n\r\n結束,如果不是說明包頭不完整,退出 25 string end = inbuf.substr(inbuf.length() - 4); 26 if (end != "\r\n\r\n") 27 return; 28 29 //以\r\n分割每一行 30 std::vector<string> lines; 31 StringUtil::Split(inbuf, lines, "\r\n"); 32 if (lines.size() < 1 || lines[0].empty()) 33 { 34 conn->forceClose(); 35 return; 36 } 37 38 std::vector<string> chunk; 39 StringUtil::Split(lines[0], chunk, " "); 40 //chunk中至少有三個字串:GET+url+HTTP版本號 41 if (chunk.size() < 3) 42 { 43 conn->forceClose(); 44 return; 45 } 46 47 LOG_INFO << "url: " << chunk[1] << " from " << conn->peerAddress().toIpPort(); 48 //inbuf = /register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22} 49 std::vector<string> part; 50 //透過?分割成前後兩端,前面是url,後面是引數 51 StringUtil::Split(chunk[1], part, "?"); 52 //chunk中至少有三個字串:GET+url+HTTP版本號 53 if (part.size() < 2) 54 { 55 conn->forceClose(); 56 return; 57 } 58 59 string url = part[0]; 60 string param = part[1].substr(2); 61 62 if (!Process(conn, url, param)) 63 { 64 LOG_ERROR << "handle http request error, from:" << conn->peerAddress().toIpPort() << ", request: " << pBuffer->retrieveAllAsString(); 65 } 66 67 //短連線,處理完關閉連線 68 conn->forceClose(); 69}
程式碼註釋都寫的很清楚,我們先利用\r\n分割得到每一行,其中第一行的資料是:
GET /register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22} HTTP/1.1
其中%22是雙引號的url轉碼形式,%20是空格的url轉碼形式,然後我們根據空格分成三段,其中第二段就是我們的網址和引數:
/register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22}
然後我們根據網址與引數之間的問號將這個分成兩段:第一段是網址,第二段是引數:
1bool HttpSession::Process(const std::shared_ptr<TcpConnection>& conn, const std::string& url, const std::string& param) 2{ 3 if (url.empty()) 4 return false; 5 6 if (url == "/register.do") 7 { 8 OnRegisterResponse(param, conn); 9 } 10 else if (url == "/login.do") 11 { 12 OnLoginResponse(param, conn); 13 } 14 else if (url == "/getfriendlist.do") 15 { 16 17 } 18 else if (url == "/getgroupmembers.do") 19 { 20 21 } 22 else 23 return false; 24 25 26 return true; 27}
然後我們根據url匹配網址,如果是註冊請求,會走註冊處理邏輯:
void HttpSession::OnRegisterResponse(const std::string& data, const std::shared_ptr<TcpConnection>& conn) 2{ 3 string retData; 4 string decodeData; 5 URLEncodeUtil::Decode(data, decodeData); 6 BussinessLogic::RegisterUser(decodeData, conn, false, retData); 7 if (!retData.empty()) 8 { 9 std::string response; 10 URLEncodeUtil::Encode(retData, response); 11 MakeupResponse(retData, response); 12 conn->send(response); 13 14 LOG_INFO << "Response to client: cmd=msg_type_register" << ", data=" << retData << conn->peerAddress().toIpPort();; 15 } 16}
註冊結果放在retData中,為了發給客戶端,我們將結果中的特殊字元如雙引號轉碼,如返回結果是:
{"code":0, "msg":"ok"}
會被轉碼成:
{%22code%22:0,%20%22msg%22:%22ok%22}
然後,將資料組裝成http協議發給客戶端,給客戶端的應答協議與http請求協議有一點點差別,就是將請求的url路徑換成所謂的http響應碼,如200表示應答正常返回、404頁面不存在。應答協議格式如下:
GET或POST 響應碼 HTTP協議版本號\r\n 2欄位1名: 欄位1值\r\n 3欄位2名: 欄位2值\r\n 4 … 5欄位n名 : 欄位n值\r\n 6\r\n 7http協議包體內容
舉個例子如:
HTTP/1.1 200 OK\r\n Content-Type: text/html\r\n Content-Length:42\r\n \r\n {%22code%22:%200,%20%22msg%22:%20%22ok%22}
注意,包頭中的Content-Length長度必須正好是包體{%22code%22:%200,%20%22msg%22:%20%22ok%22}的長度,這裡是42。這也符合我們瀏覽器的返回結果:
當然,需要注意的是,我們一般說http連線一般是短連線,這裡我們也實現了這個功能(看上面的程式碼:conn->forceClose();),不管一個http請求是否成功,伺服器處理後立馬就關閉連線。
當然,這裡還有一些沒處理好的地方,如果你仔細觀察上面的程式碼就會發現這個問題,就是不滿足一個http包頭時的處理,如果某個客戶端(不是使用瀏覽器)透過程式模擬了一個連線請求,但是遲遲不發含有\r\n\r\n的資料,這路連線將會一直佔用。我們可以判斷收到的資料長度,防止別有用心的客戶端給我們的伺服器亂髮資料。我們假定,我們能處理的最大url長度是2048,如果使用者傳送的資料累積不含\r\n\r\n,且超過2048個,我們認為連線非法,將連線斷開。程式碼修改成如下形式:
void HttpSession::OnRead(const std::shared_ptr<TcpConnection>& conn, Buffer* pBuffer, Timestamp receivTime) { //LOG_INFO << "Recv a http request from " << conn->peerAddress().toIpPort(); string inbuf; //先把所有資料都取出來 inbuf.append(pBuffer->peek(), pBuffer->readableBytes()); //因為一個http包頭的資料至少\r\n\r\n,所以大於4個字元 //小於等於4個字元,說明資料未收完,退出,等待網路底層接著收取 if (inbuf.length() <= 4) return; //我們收到的GET請求資料包一般格式如下: /* GET /register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22} HTTP/1.1\r\n Host: 120.55.94.78:12345\r\n Connection: keep-alive\r\n Upgrade-Insecure-Requests: 1\r\n User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n Accept-Encoding: gzip, deflate\r\n Accept-Language: zh-CN, zh; q=0.9, en; q=0.8\r\n \r\n */ //檢查是否以\r\n\r\n結束,如果不是說明包頭不完整,退出 string end = inbuf.substr(inbuf.length() - 4); if (end != "\r\n\r\n") return; //超過2048個字元,且不含\r\n\r\n,我們認為是非法請求 else if (inbuf.length() >= MAX_URL_LENGTH) { conn->forceClose(); return; } //以\r\n分割每一行 std::vector<string> lines; StringUtil::Split(inbuf, lines, "\r\n"); if (lines.size() < 1 || lines[0].empty()) { conn->forceClose(); return; } std::vector<string> chunk; StringUtil::Split(lines[0], chunk, " "); //chunk中至少有三個字串:GET+url+HTTP版本號 if (chunk.size() < 3) { conn->forceClose(); return; } LOG_INFO << "url: " << chunk[1] << " from " << conn->peerAddress().toIpPort(); //inbuf = /register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22} std::vector<string> part; //透過?分割成前後兩端,前面是url,後面是引數 StringUtil::Split(chunk[1], part, "?"); //chunk中至少有三個字串:GET+url+HTTP版本號 if (part.size() < 2) { conn->forceClose(); return; } string url = part[0]; string param = part[1].substr(2); if (!Process(conn, url, param)) { LOG_ERROR << "handle http request error, from:" << conn->peerAddress().toIpPort() << ", request: " << pBuffer->retrieveAllAsString(); } //短連線,處理完關閉連線 conn->forceClose(); }
但這隻能解決傳送非法資料的情況,如果一個客戶端連上來不給我們發任何資料,這段邏輯就無能為力了。如果不斷有客戶端這麼做,會浪費我們大量的連線資源,所以我們還需要一個定時器去定時檢測哪些http連線超過一定時間內沒給我們發資料,找到後將連線斷開。這又涉及到伺服器定時器如何設計了,關於這部分請參考我寫的其他文章。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69917053/viewspace-2642006/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 518雲盒伺服器:網路攻擊與網路入侵伺服器
- Node.js實現一個HTTP伺服器Node.jsHTTP伺服器
- 手把手教你怎麼使用雲伺服器伺服器
- C#實現一個最簡單的HTTP伺服器C#HTTP伺服器
- 雲控怎麼實現預想
- 如何實現一個詞雲
- 教你10分鐘完成一個自己的微信商城雲伺服器伺服器
- 華納雲:怎麼獲取伺服器真實ip伺服器
- 該怎麼實現伺服器後臺?伺服器
- 怎麼實現一個3d翻書效果3D
- 雲伺服器出現502錯誤怎麼辦伺服器
- 怎麼會wwwhj518vip18587065666
- 雲伺服器怎麼搭建lnmp伺服器LNMP
- 手摸手教你實現一個簡單的PromisePromise
- 手把手教你實現一個引導動畫動畫
- 手把手教你實現一個完整的 PromisePromise
- 教你如何快速實現一個圖片爬蟲爬蟲
- 前端er,什麼時候,你想寫一個 HTTP 伺服器?前端HTTP伺服器
- Golang如何實現HTTP代理伺服器GolangHTTP伺服器
- 恆訊科技教你:http怎麼直接跳轉到https?HTTP
- 實現一個websocket伺服器-實踐篇Web伺服器
- netty寫一個http伺服器NettyHTTP伺服器
- Lua-http庫寫一個爬蟲程式怎麼樣 ?HTTP爬蟲
- 這個演算法怎麼實現???演算法
- 【BI 視覺化外掛】怎麼做? 手把手教你實現視覺化
- 手把手教你用 Go 實現一個 mTLSGoTLS
- 阿里雲伺服器怎麼樣?阿里伺服器
- 雲伺服器有什麼作用,怎麼使用?伺服器
- 怎麼檢視伺服器的埠 雲伺服器的埠怎麼修改伺服器
- 教你一招:基於Redis實現一個分散式鎖Redis分散式
- 你真的知道怎麼實現一個延遲佇列嗎?佇列
- 怎麼理解前端路由? 當然是自己實現一個啦!前端路由
- 【移動適配】一個畫素的border怎麼實現
- Netty 實現HTTP檔案伺服器NettyHTTP伺服器
- 使用 Java 11 HTTP Client API 實現 HTTP/2 伺服器推送JavaHTTPclientAPI伺服器
- 如何從零實現一個詞雲效果
- 手把手教你實現一個canvas智繪畫板Canvas
- 手把手教你實現一個 Vue 進度條元件!Vue元件