91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

怎么實現一個Http服務器

發布時間:2021-11-23 21:32:09 來源:億速云 閱讀:129 作者:柒染 欄目:云計算

這期內容當中小編將會給大家帶來有關怎么實現一個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連接超過一定時間內沒給我們發數據,找到后將連接斷開。

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

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

海宁市| 华亭县| 安多县| 和林格尔县| 兰西县| 额尔古纳市| 太仆寺旗| 泾源县| 德兴市| 台南市| 合肥市| 廊坊市| 汝州市| 龙山县| 稻城县| 班戈县| 教育| 天祝| 泸溪县| 姚安县| 临桂县| 商丘市| 巴东县| 太和县| 桐乡市| 谢通门县| 康马县| 南雄市| 象山县| 平山县| 永平县| 衢州市| 黔江区| SHOW| 福州市| 休宁县| 赫章县| 赣榆县| 宁乡县| 石棉县| 伊通|