網絡基礎 -- 應用層HTTP協議

目錄

應用層(TCP/IP協議中的應用層/HTTP協議)

HTTP協議

URL -- 統一資源定位符

UrlEncode / UrlDecode

HTTP協議格式

概述 

HTTP 請求消息Request / 響應消息Response

首行

請求(Request)首行

響應(Response)首行

頭部 

正文 

實現一個簡單的HTTP服務器


應用層(TCP/IP協議中的應用層/HTTP協議)

應用層 : 程序員自己編寫程序, 應用程序之間如何進行數據溝通(數據發送的格式, 以及接收到數據之後如何解析)的約定, 也稱之爲應用層協議.  它由程序員自己制定,  程序員可以自定義協議, 稱爲私有協議, 也可以選擇一些現成的非常好用的知名協議.

序列化 : 將數據對象按照指定的協議, 即按約定的格式持久化存儲或格式化數據傳輸

反序列化 : 持久化存儲的數據或網絡數據傳輸的數據按照指定協議解析出數據對象化得過程

來看個私有協議的例子

網絡計算器的例子 : 客戶端將要計算的兩個數字以及運算符發送給服務端, 有服務端進行計算, 最終返回結果.

方案一 : 

  • 客戶端發送一個形如 "1+1" 的字符串
  • 這個字符串有兩個操作數, 都是整型
  • 兩個整型之間有一個字符是運算符嗎只能是是 '+' 
  • 數字和運算符之間沒有空格

這種方式相對來說是非常麻煩的, 因爲將一串字符解析成我們想要的結果並不是那麼的簡潔, 所以我們來看下面的結構化傳輸

方案二 :

  • 定義結構體來表示我們需要出傳輸的數據
  • 發送時將結構體按照字符串發送, 接收到的數據再按照相同的規則將這一串字符串轉換爲結構體
    (因爲在內存中都是以二進制傳輸的, 所以傳啥都是傳二進制, 解析時就可以還原)
  • 這個過程就是序列化和反序列化的過程

核心代碼

#定義格式化數據
typedef struct{
    int num1;
    int num2;
    char c;//運算符
}data;

#服務端
data buf;
recv(ser_fd, &buf, sizeof(buf), 0);
if(buf.c == '+'){
    cout << buf.num1 << buf.c << buf.num2 << " = " << buf.num1 + buf.num2 <<endl;
}

#客戶端
data buf;
cout << "請依次輸入num1, num2\n";
cin >> buf.num1;
cin >> buf.num2;
buf.c ='+';
send(cli_fd, &buf, sizeof(buf), 0);

序列化的方法還有json序列化, jsio序列化用途廣泛, 還衍生了阿里的fastjson, 美團的MSON, 谷歌的GSON等更加優秀的轉碼工具. protobuf序列化是谷歌的一款開源項目,性能好,效率高,並且支持多種語言,例如:java,C++,python等. 優點:轉碼性能高,支持多語言.  缺點:中文文檔少,使用相對複雜.

接下來再來看知名協議, http協議


HTTP協議

HTTP協議(Hyper Text Transfer Protocol, 超文本傳輸協議) 的縮寫, 是一種簡單的請求-響應協議, 是應用層協議. 通常運行在TCP之上, 是萬維網 (WWW, World Wide Web) 數據通信的基礎,  所有的 WWW 文件都必須遵守這個標準.

HTTP的發展是由蒂姆·伯納斯·李 於1989年在歐洲核子研究組織(CERN)所發起. HTTP的標準制定由萬維網協會(World Wide Web Consortium,W3C) 和互聯網工程任務組 (Internet Engineering Task Force,IETF) 進行協調, 最終發佈了一系列的RFC, 其中最著名的是1999年6月公佈的 RFC 2616, 定義了HTTP協議中現今廣泛使用的一個版本—— HTTP 1.1.

2014年12月, 互聯網工程任務組 (IETF) 的Hypertext Transfer Protocol Bis(httpbis)工作小組將 HTTP/2 標準提議遞交至IESG進行討論, 於2015年2月17日被批准.  HTTP/2 標準於2015年5月以RFC 7540正式發表, 今後將取代HTTP 1.1成爲HTTP的實現標準 .

URL -- 統一資源定位符

平時我們所說的 "網址" 其實就是URL(Uniform Resource Locator, 統一資源定位符)

                  

來看個栗子 :
 

其中目錄中 1. 語法是一個超鏈接, 鏈接爲 : 
https://baike.baidu.com/item/URL%E6%A0%BC%E5%BC%8F/10056474?fr=aladdin#1

登錄信息 : 也就是用戶名和密碼, 由於安全性的原因, 一般已經很少早URL中出現了. 是非必要部分

服務器地址 : 是服務器IP地址的另一種表達方式, 方便人們記憶, 但最終還是要經過域名解析得到服務器的IP地址才能訪問服務器. 一個域名只能對應一個IP地址, 但一個IP地址可以對應多個域名. 如我們熟知的 www.baidu.com 的對應的IP 是 14.215.177.39  , 也可以用IP地址作爲域名.

端口號 : 端口不是一個URL必須的部分, 如果省略端口部分, 將採用默認端口.

帶層次文件路徑 :  也叫虛擬目錄部分, 從域名後的第一個 "/" 開始到最後一個 "/" 爲止, 是虛擬目錄部分. 虛擬目錄也不是一個URL必須的部分. "/" 開頭但並不根目錄, 只是服務器中的一個子目錄, 因爲如果是根目錄的話對服務器太過危險.  從域名後的最後一個 "/" 開始到 "?" 爲止, 是文件名部分. 如果沒有 "?" , 則是從域名後的最後一個 "?" 開始到 "#" 爲止, 是文件部分. 如果沒有 "?" 和 "#", 那麼從域名後的最後一個 "/" 開始到結束, 都是文件名部分. 文件名部分也不是一個URL必須的部分, 如果省略該部分, 則使用默認的文件名.

查詢字符串 : 從 "?" 到 "#" 之間, 都是查詢字符串部分, 是客戶端提交給服務器的數據, 有一個個key=val的鍵值對組成, 鍵值對之間用&間隔

片段標識符 : 從#開始到最後, 都是片段標識部分, 即網頁上點擊跳轉的標籤, 如上面連接中最後的 #1


UrlEncode / UrlDecode

可以看到, 在URL中, "/", "#", "?", "&" 等字符已經具有了特殊含義, 所以, 提交給服務器的數據中如果有這些字符就會存在歧義, 到底是字符本身還是具有特定意義呢? 這時就有了url編碼和url解碼, 來解決這個問題

UrlEncode (Url編碼)

將字符的每個字節轉化爲16進制, 例如 : 字符串 "c++" 其中 '+' 是特殊字符轉換後爲 c2B2B, 在特殊字符轉換之後在前面加上%號來標識這是一個特殊字符, c++也就變成了 c%2B%2B 

還有像中文也需要Url編碼,   在線編碼 : http://tool.chinaz.com/tools/urlencode.aspx

"中文" 這兩個字編碼後如圖 : 

UrlDecode (Url解碼)

按照相同的規則, 將url編碼後的字符轉換爲原字符 . 


HTTP協議格式

概述 

HTTP是一個客戶端終端(用戶)和服務器端(網站)請求和應答的標準. 通過使用網頁瀏覽器、網絡爬蟲或者其它的工具,客戶端發起一個HTTP請求到服務器上指定端口(默認端口爲80). 我們稱這個客戶端爲用戶代理程序(user agent). 應答的服務器上存儲着一些資源,比如HTML文件和圖像. 我們稱這個應答服務器爲源服務器(origin server). 在用戶代理和源服務器中間可能存在多個“中間層”,比如代理服務器、網關或者隧道(tunnel).

儘管TCP/IP協議是傳輸層最流行的協議簇, 但在HTTP協議中, 並沒有規定其下層(傳輸層)必須用什麼協議. 事實上,HTTP可以在任何互聯網協議上,或其他網絡上實現. HTTP假定其下層協議提供可靠的傳輸. 因此,任何能夠提供這種保證的協議都可以被其使用. 因此也就是其在TCP/IP協議族使用TCP作爲其傳輸層。

通常,由HTTP客戶端發起一個請求,創建一個到服務器指定端口(默認是80端口)的TCP連接。HTTP服務器則在那個端口監聽客戶端的請求. 一旦收到請求,服務器會向客戶端返回一個狀態,比如"HTTP/1.1 200 OK",以及返回的內容,如請求的文件、錯誤消息、或者其它信息. 

那麼具體客戶端是如何向服務端請求或者服務端如何響應客戶端呢?


HTTP 請求消息Request / 響應消息Response

Request 和 Response 的格式都是

首行 
頭部/報頭  
正文 

//在頭部與正文之間有一個空格(實際是\r\n), 所以我們看到的應該是下面的格式

首行 
頭部/報頭  
空格
正文 

     


首行

請求(Request)首行

首行 : 共三條信息, 以空格進行間隔, 以\r\n作爲結尾, 具體是:   

            請求方法 URL 協議版本\r\n

我們在瀏覽器中訪問 www.cplusplus.com , 用Fiddler 抓包工具抓包, 如下 :

請求信息如下 :

請求方法: 

序號 方法 描述
1 GET 請求指定的頁面信息, 並返回實體主體, 主要用於獲取資源
2 HEAD 類似於 GET 請求, 只不過返回的響應中沒有具體的內容(沒有正文), 用於獲取頭部/報頭
3 POST 向指定資源提交數據進行處理請求(例如提交表單或者上傳文件). 數據被包含在請求體中. POST 請求可能會導致新的資源的建立和/或已有資源的修改. 
4 PUT 從客戶端向服務器傳送的數據取代指定的文檔的內容
5 DELETE 請求服務器刪除指定的頁面
6 CONNECT HTTP/1.1 協議中預留給能夠將連接改爲管道方式的代理服務器
7 OPTIONS 允許客戶端查看服務器的性能
8 TRACE 回顯服務器收到的請求,主要用於測試或診斷
9 PATCH 是對 PUT 方法的補充,用來對已知資源進行局部更新

URL : 統一資源定位符 -- 包含像哪個服務器請求哪個資源等信息

協議版本 :  HTTP/0.9  HTTP/1.0  HTTP/1.1  HTTP/2

HTTP/0.9 : 0.9是第一個版本的HTTP協議,已過時。它的組成極其簡單,只允許客戶端發送GET這一種請求,且不支持請求頭。由於沒有協議頭,造成了HTTP/0.9協議只支持一種內容,即純文本. 不過網頁仍然支持用HTML語言格式化,同時無法插入圖片. 0.9版本只支持短連接, 即一次傳輸後立即斷開

HTTP/1.0 : HTTP協議的第二個版本,第一個在通訊中指定版本號的HTTP協議版本,至今仍被廣泛採用。相對於HTTP/0.9 增加了如下主要特性:

  • 請求與響應支持頭域
  • 響應對象以一個響應狀態行開始
  • 響應對象不只限於超文本
  • 開始支持客戶端通過POST方法向Web服務器提交數據,支持GET、HEAD、POST方法
  • 支持長連接(但默認還是使用短連接, 長連接 : 一次連接(TCP三次握手後)可以傳輸多次), 緩存機制, 以及身份認證 

HTTP/1.1HTTP協議的第三個版本, 是目前使用廣泛的協議版本 . HTTP 1.1是目前主流的HTTP協議版本

支持長連接(默認就爲長連接), chunked編碼傳輸,字節範圍請求,請求流水線, 支持管線化傳輸等.

chunked(分塊)編碼 : 該編碼將實體分塊傳送並逐塊標明長度,直到長度爲0塊表示傳輸結束, 這在實體長度未知時特別有用(比如由數據庫動態產生的數據)

字節範圍請求 : HTTP/1.1支持傳送內容的一部分. 比方說, 當客戶端已經有內容的一部分, 爲了節省帶寬, 可以只向服務器請求一部分. 該功能通過在請求消息中引入了range頭域來實現, 它允許只請求資源的某個部分. 在響應消息中Content-Range頭域聲明瞭返回的這部分對象的偏移值和長度. 如果服務器相應地返回了對象所請求範圍的內容,則響應碼206(Partial Content)

請求流水線 : 支持持久連接的客戶端可以 "流水線" 處理它的請求(例如 : 發送多個請求, 而不需要等待每個響應). 服務器必須按照接收請求的順序將其響應發送到這些請求

支持管線化傳輸 : 管線化機制須通過長連接完成, 管線化傳輸是將多個HTTP請求 (request) 整批提交的技術, 而在傳送過程中不需先等待服務端的迴應.

另外還新增這些特性 :
     請求消息和響應消息都應支持主機(Host)頭域, 在HTTP/1.0中認爲每臺服務器都綁定一個唯一的IP地址, 因此,請求消息中的URL並沒有傳遞主機名(hostname). 但隨着虛擬主機技術的發展,在一臺物理服務器上可以存在多個虛擬主機 (Multi-homed Web Servers), 並且它們共享一個IP地址. 因此, Host頭的引入就很有必要了.

     新增了一批請求方法 (Request method) :  OPTIONS,PUT, DELETE, TRACE, CONNECT方法

HTTP/2 : 最新HTTP協議,目前應用還非常少.

主要特點 : 
HTTP/2 最大的特點是, 不會改動HTTP 的語義, HTTP 方法, 狀態碼, URI 及首部字段等等, 在這些核心概念上一如往常, 卻能致力於突破上一代標準的性能限制, 改進傳輸性能, 實現低延遲和高吞吐量. 而之所以叫2, 是在於新增的二進制分幀層.

服務器端推流: Server Push, 支持服務器主動向客戶端推送消息. 目前服務器需要瀏覽器解析頁面後再發送新請求來獲取js, css, 圖片等資源. HTTP/2爲了優化這個開銷, 可以提前將這些資源 "推送" 到客戶端的緩存中.

隨時復位 : HTTP/1.1一個缺點是當HTTP信息有一定長度大小數據傳輸時, 不能方便地隨時停止它, 中斷TCP連接的代價是比較昂貴的. 使用HTTP/2的 RST_STREAM將能方便停止一個信息傳輸, 啓動新的信息, 在不中斷連接的情況下提高帶寬利用效率.

頭部壓縮 : 現在網頁加載是資源密集型的, 一個頁面通常有很多資源要加載, 每次請求的頭部數據不可忽視 (尤其是Cookies),  加上TCP的Slow Start機制(一種擁塞控制機制)會導致往返次數加大. 壓縮可以有效的減少包分組的數量, 從而減少延遲, 尤其是在移動端上. 因爲GZIP壓縮有安全性隱患, 所以HTTP/2自己實現了一套壓縮算法——HPACK.


響應(Response)首行

共三條信息, 以空格進行間隔, 以\r\n作爲結尾, 具體是:   

            協議版本 響應狀態碼 狀態碼描述\r\n

接着上面的例子, 響應信息如下:

協議版本: 還是上面所說的HTTP/0.9  HTTP/1.0  HTTP/1.1  HTTP/2

響應狀態碼 : 告訴客戶端針對這次請求, 是一個什麼樣的處理結果, 共有五大類

HTTP狀態碼分類
分類 分類描述
1xx 信息,服務器收到請求,需要請求者繼續執行操作
2xx 成功,操作被成功接收並處理
3xx 重定向,需要進一步的操作以完成請求
4xx 客戶端錯誤,請求包含語法錯誤或無法完成請求
5xx 服務器錯誤,服務器在處理請求的過程中發生了錯誤
常見HTTP狀態碼列表
狀態碼 狀態碼英文 中文描述
200 OK 請求成功. 一般用於GET與POST請求
301 Moved Permanently 永久移動. 請求的資源已被永久的移動到新URI, 返回信息會包括新的URI, 瀏覽器會自動定向到新URL. 今後任何新的請求都應使用新的URI代替
302 Found 臨時移動. 與301類似. 但資源只是臨時被移動.客戶端應繼續使用原有URI
303 See Other 查看其它地址. 與301類似. 使用GET和POST請求查看
400 Unauthorized 客戶端請求的語法錯誤,服務器無法理解
404 Not Found 服務器無法根據客戶端的請求找到資源(網頁).通過此代碼,網站設計人員可設置 "您所請求的資源無法找到" 的個性頁面
500 Internal Server Error 服務器內部錯誤,無法完成請求
502 Bad Gateway 作爲網關或者代理工作的服務器嘗試執行請求時,從遠程服務器接收到了一個無效的響應
504 Gateway Time-out 充當網關或代理的服務器,未及時從遠端服務器獲取請求

響應狀態碼描述 : 對於狀態碼的解釋

並不固定, 我們可以根據具體的問題設置具體的解釋, 一般都用狀態碼的英文名稱

更多狀態碼, 戳鏈接( ̄︶ ̄)↗ : https://www.runoob.com/http/http-status-codes.html


頭部 

由一個個 key: val 鍵值對組成, 鍵值對以 \r\n作爲結尾.

           key: val\r\n
           key: val\r\n
           key: val\r\n 
           key: val\r\n 

還是接着上面的例子 , 下面的圖片是請求報文 : 

頭部字段 (key): 值(val)

Connection -- 本次請求是否是長連接, 是 val=keep-Alive, 不是長連接val=close
Content-Length -- 描述正文的數據長度, 讓對端知道處理多大的數據
Content-Type -- 描述正文數據的類型, 好讓對端知道正文數據如何處理, 如上圖中的text/html, 即需要接收html類型數據, 編碼是
                           UTF-8
Content-Language -- 用來說明訪問者希望採用的語言或語言組合, 但正文是什麼語音並不由Content-Language決定
                                    例如中文是: zh-CN
User-Agent -- 簡稱UA, 是一種向訪問網站提供客戶端所使用的瀏覽器類型及版本, 操作系統及版本, 瀏覽器內核, 等信息的標識.
                        UA可以僞裝.

Accept -- 告訴對端我能接收處理什麼樣的數據, Accept: text/html
Accept-Language -- 表示瀏覽器所支持的語言類型, 如 Accept-Language: zh-CN, zh;q=0.9  表示瀏覽器支持簡體中文和繁體中
                                   文, 簡體中文優先,  q是權重係數, 範圍 [0, 1], q 值越大, 請求越傾向於獲得其“;”之前的類型表示的內容, 若沒
                                   有指定 q 值, 則默認爲1, 若被賦值爲0, 則用於提醒服務器哪些是瀏覽器不接受的內容類型.

Referer -- 在百度中搜谷歌, 然後點擊谷歌的鏈接, 就能抓取到如下, 請求信息, 可以看到, 谷歌是通過百度搜索進入的, 谷歌趕緊給
                 百度打了5毛錢, 買網頁廣告的人就知道自己廣告買的值不值了, Referer 是referrer拼寫錯誤的, 但也就這樣沿用下來來, 
                    
Transfer_Encoding -- 傳輸編碼, Transfer-Encoding: chunked 表示分塊傳輸, 在每個塊前添加16進制的塊大小(字節), 最後以
                                     0\r\n\r\n結束, 主要用於傳輸比較大的數據或未知大小的數據
Location -- 響應頭指示URL的頁面重定向到. 它僅在提供3xx狀態響應時纔有意義.
Expires -- 表示應該在什麼時候認爲文檔已經過期,從而不再緩存它.
Cookie/Set-Cookie -- 早期的HTTP是短連接, 請求/響應完畢連接則會斷開, 就比如我們要剁手網購, 買一個東西就要登錄一次, 剁
                 都不快樂了. 對這個問題, 解決方案就是Cookie, 客戶登錄一次, 服務端爲客戶端創建一個session(會話), session中保                     存當前的會話信息, 客戶端的認證信息等, 存儲到數據庫中,在登錄成功響應時服務端使用Set-Cookie告訴客戶端會話
                 id(session_id)是多少. 客戶端收到這個響應後, 將Cookie中的信息保存起來. 下一次客戶端請求服務器時, 就會自動保存
                 Cookie信息, 讀取出來, 通過Cookie發送給服務器, 服務器接收到Cookie之後取出信息, 即取出session_id, 通過這個id在
                 數據庫中找到會話信息, 就知道當前這個客戶端是誰了
Cookie和Session區別 : Session是服務端爲每個客戶端建立的會話, 將session_id作爲Cookie信息返回給客戶端, 也就是說
                  Session保存在服務端.  Cookie保存在客戶端. Cookie中保存的Session_id就是服務端得到時可以找到對應的Session
                  從而識別出客戶端身份.

更多頭部字段的信息, 戳鏈接( ̄︶ ̄)↗ : https://www.php.cn/manual/view/35521.html

正文 

Request : 客戶端向服務端的請求數據

Response : 響應正文, 服務器返回給客戶端的數據


實現一個簡單的HTTP服務器

我們實現HTTP服務器通常是TCP服務器, 即在傳輸層使用TCP協議.  在應用層的數據通信採用HTTP協議格式

具體步驟 : 

1. 搭建一個TCP服務器, 等待客戶端連接

2. 客戶端新連接到來, 服務端新建套接字

3. 通過這個新建套接字接收數據(這個數據是HTTP協議格式的數據)

接收數據的時候如何保證能夠接收一個完整的HTTP請求?

   1). 接收HTTP請求頭部 : 頭部與正文之間以\r\n作爲間隔, 最後一個頭部信息以\r\n作爲結尾, 所以當出現\r\n\r\n時, 就認爲頭部到
        此結束.
   2). 根據頭部信息中的Content-Length確定正文長度, 然後接收具體長度的正文就可以了.

4. 服務器解析客戶端請求

5. 根據具體業務請求, 進行處理並響應

封裝socket_tcp.hpp

#include<iostream>
#include<string>
#include<unistd.h> //close包含在這個頭文件中
#include<netinet/in.h>//地址結構體
#include<arpa/inet.h>//字節序轉換接口
#include<sys/socket.h>//套接字接口

using namespace std;
#define BACKLOG 10
#define CHECK_RET(ret){if((ret) == false) return -1;} 

class TcpSocket{
    int m_sockfd;

    void Addr(struct sockaddr_in*, const string&, const uint16_t) const;
public:
    TcpSocket():m_sockfd(-1){}
    bool Socket();
    int GetFd();
    bool Bind(const string& ip, const uint16_t port) const;
    bool Listen(int backlog = BACKLOG) const;
    bool Connect(const string ip, const uint16_t port) const;
    bool Accept(TcpSocket* sock, string* ip = nullptr, uint16_t* port = nullptr) const;
    bool Recv(string* buf) const;
    bool Send(const string& data) const;
    bool Close() const;
};

bool TcpSocket::Socket(){
    m_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(m_sockfd < 0){
        perror("socket error");
        return false;
    }
    return true;
}
int TcpSocket::GetFd(){
    return m_sockfd;
}
void TcpSocket::Addr(struct sockaddr_in* addr, const string& ip, const uint16_t port) const{
    addr->sin_family = AF_INET;
    addr->sin_port = htons(port);
    inet_pton(AF_INET, ip.c_str(), &addr->sin_addr.s_addr);
}
bool TcpSocket::Bind(const string& ip, const uint16_t port) const{
    struct sockaddr_in addr;
    Addr(&addr, ip, port);
    socklen_t len = sizeof(struct sockaddr_in);
    int ret = bind(m_sockfd, (struct sockaddr*)&addr, len);
    if(ret < 0){
        perror("bind error");
        return false;
    }
    return true;
}

bool TcpSocket::Listen(int backlog) const{
    int ret = listen(m_sockfd, backlog);
    if(ret < 0){
        perror("listen error");
        return false;
    }
    return true;
}

bool TcpSocket::Connect(const string ip, const uint16_t port) const{
    struct sockaddr_in addr;//定義一個IPv4的地址結構
    Addr(&addr, ip, port);//向這個結構中綁定地址與端口
    socklen_t len = sizeof(struct sockaddr_in);
    int ret = connect(m_sockfd, (struct sockaddr*)&addr, len);
    //將服務端信息描述到socket中, 並向服務端發起連接請求
    if(ret < 0){
        perror("connect error");
        return false;
    }
    return true;
}

bool TcpSocket::Accept(TcpSocket* sock, string* ip, uint16_t* port) const{
    struct sockaddr_in cli_addr;
    socklen_t len = sizeof(struct sockaddr_in);
    int newsockfd = accept(m_sockfd, (struct sockaddr*)&cli_addr, &len);
    if(newsockfd < 0){
        perror("accept error");
        return false;
    }
    sock->m_sockfd = newsockfd;
    char str[INET_ADDRSTRLEN];//IPv6用INET6_ADDRSTRLEN
    if(ip != nullptr){
       *ip = inet_ntop(AF_INET, &cli_addr.sin_addr, str, sizeof(str));
    }
    if(port != nullptr){
        *port = ntohs(cli_addr.sin_port);
    }
    return true;
}

bool TcpSocket::Recv(string* buf) const{
    char tmp[4096] = { 0 };
    ssize_t ret = recv(m_sockfd, tmp, 4095, 0);
    if(ret < 0){
        perror("recv error");
        return false;
    }
    else if(ret == 0){
        cout << "connection break\n";
        return false;
    }
    buf->assign(tmp, ret);
    return true;
}
bool TcpSocket::Send(const string& data) const{
    size_t slen = 0, ret = 0;
    size_t size = data.size();
    while(slen < size) {
        ret = send(m_sockfd, &data[slen], size - slen, 0);
        if(ret < 0){
            perror("send error");
            return false;                                                  
        }
        slen += ret;                              
    }
    return true;
}
bool TcpSocket::Close()const {
    close(m_sockfd);
    return true;
}

http_ser.cpp

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<sstream>
#include<pthread.h>
#include"socket_tcp.hpp"

#define CHECK_RET_(ret, ptr){if((ret) == false){delete ptr; return NULL;}}

void* thr_start(void* arg) {
    pthread_detach(pthread_self());
    TcpSocket* sock = (TcpSocket*)arg;
    string buf;
    CHECK_RET_(sock->Recv(&buf), sock);
    cout << "http req:\n" << buf;
    string body = "<html><body><h1>Hello World!</h1></body></html>";
    string blank = "\r\n";
    stringstream header;
    header << "Content-Length: " << body.size() << "\r\n";
    header << "Content-Type: text/html\r\n";
    //header << "Location: http://www.baidu.com/\r\n";
    //string first = "HTTP/1.1 302 Found\r\n";
    string first = "HTTP/1.1 200 OK\r\n";
    CHECK_RET_(sock->Send(first), sock);
    CHECK_RET_(sock->Send(header.str()), sock)
    CHECK_RET_(sock->Send(blank), sock);
    CHECK_RET_(sock->Send(body), sock);
    delete sock;
    return NULL;
}
int main(int argc, char* argv[]){
    if(argc != 3){
        cout << "Should input ./ser_tcp [ip] [port]\n";
        return -1;
    }
    TcpSocket ser;
    CHECK_RET(ser.Socket());
    string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    CHECK_RET(ser.Bind(ip, port));
    CHECK_RET(ser.Listen());
    string buf;
    while(1){
        TcpSocket* newsock = new TcpSocket;
        string ip;
        uint16_t port;
        bool ret = ser.Accept(newsock, &ip, &port);
        if(ret == false){delete newsock;  continue; }//服務端並不會因爲一次失敗而退出, 而是繼續獲取下一個連接
        printf("new connection[ip: %s][port: %d]\n", ip.c_str(), port);
        pthread_t tid;
        if(pthread_create(&tid, NULL, thr_start, (void*)newsock)){
            fprintf(stderr, "pthread_create: %s\n", strerror(ret));
            delete newsock;
            return -1;
        }
    }
    return 0;
}

瀏覽器要想能訪問我們虛擬機中的服務器程序, 就需要關閉Linux防火牆, 命令如下 : 

sudo systemctl stop firewalld
sudo systemctl disable firewalld

還需要將虛擬機的網絡設置爲橋接模式, 在局域網下其他的主機也能訪問到 .

電腦瀏覽器 :

                 

 

 手機瀏覽器 :

                                                          

可以看到, 操作系統 是Android 9, 手機型號是 HONOR HLK-AL00, 瀏覽器是UC. 感情啥都給我傳過去了.....

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章