久久精品人人爽,华人av在线,亚洲性视频网站,欧美专区一二三

怎么實現一個Http服務器

191次閱讀
沒有評論

共計 11529 個字符,預計需要花費 29 分鐘才能閱讀完成。

這期內容當中丸趣 TV 小編將會給大家帶來有關怎么實現一個 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 協議吧。

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 結束。

舉個例子,比如我們在瀏覽器中請求 http://www.hootina.org/index_2013.php 這個網址,這是一個典型的 GET 方法,瀏覽器組裝的 http 數據包格式如下:

GET /index_2013.php HTTP/1.1\r\n 2Host: www.hootina.org\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 后面,參數與參數之間使用 分割,例如請求 http://www.hootina.org/index_2013.php?param1=value1?m2=value2?m3=value3,我們看下這個請求組裝的的 http 協議包格式:

GET /index_2013.php?param1=value1 param2=value2 param3=value3 HTTP/1.1\r\n 2Host: www.hootina.org\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 網站 https://kyfw.12306.cn/otn/login/init 中登陸輸入用戶名和密碼:

怎么實現一個 Http 服務器

然后發現瀏覽器以 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: https://kyfw.12306.cn\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: https://kyfw.12306.cn/otn/login/init\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 進制轉碼形式。這個碼表可以參考這里:http://www.w3school.com.cn/tags/html_ref_urlencode.html

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 服務器

如上圖所示,由于 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。舉個例子,我們要請求 http://www.hootina.org/index_2013.php,那么我們可以先通過域名得到 ip 地址,即通過 socket API gethostbyname() 得到 www.hootina.org 的 ip 地址,由于 http 服務器默認的端口號是 80,有了域名和 ip 地址之后,我們使用 socket API connect()去連接服務器,然后根據上面介紹的格式組裝成 http 協議包,利用 socket API send()函數發出去,如果服務器有應答,我們可以使用 socket API recv()去接受數據,接下來就是解析數據(先解析包頭和包體)。

http 服務器實現

我們這里簡化一些問題,假設客戶端發送的請求都是 GET 請求,當客戶端發來 http 請求之后,我們拿到 http 包后就做相應的處理。我們以為我們的 flamingo 服務器實現一個支持 http 格式的注冊請求為例。假設用戶在瀏覽器里面輸入以下網址,就可以實現一個注冊功能:

http://120.55.94.78:12345/register.do?p={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 服務器

當然,需要注意的是,我們一般說 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 連接超過一定時間內沒給我們發數據,找到后將連接斷開。

上述就是丸趣 TV 小編為大家分享的怎么實現一個 Http 服務器了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注丸趣 TV 行業資訊頻道。

正文完
 
丸趣
版權聲明:本站原創文章,由 丸趣 2023-08-25發表,共計11529字。
轉載說明:除特殊說明外本站除技術相關以外文章皆由網絡搜集發布,轉載請注明出處。
評論(沒有評論)
主站蜘蛛池模板: 威海市| 壤塘县| 革吉县| 鄂托克旗| 南昌县| 蒙城县| 汕头市| 武邑县| 鸡泽县| 陆良县| 灵川县| 广州市| 枣强县| 敦煌市| 盱眙县| 澎湖县| 张家港市| 巴塘县| 普兰店市| 龙胜| 子长县| 沙雅县| 商城县| 南江县| 苍山县| 芜湖县| 剑河县| 阳信县| 临沧市| 新巴尔虎左旗| 凤台县| 镇巴县| 桃江县| 海盐县| 平和县| 德兴市| 台北县| 建水县| 定远县| 连南| 东山县|