【網絡】第三章-應用層協議

應用層協議

  在這個章節中將會進一步詳細討論應用層協議及其知名協議HTTP協議。

協議

  應用層負責程序之間的數據溝通,其中協議大概分爲兩類,自定製協議和知名協議。

自定製協議

  自定製協議就是程序員自己定義的協議,用來對應用程序發送的數據進行整理或者加密,對端只有瞭解這種協議才能對數據進行解析。
  這裏利用自定製簡單實現一個網絡版計算器。客戶端將兩個數字和一個運算符傳輸給服務端,服務端對接收到的信息進行解析,得到數字和運算符運算出結果後將結果返回給客戶端。
  在開始之前我們要先自定製一個協議方便我們的客戶端與服務端之間進行數據通信。
  我們可以將一個表達式解析成如下形式再發送給服務端:1 + 1 -> 1; 2; +。這樣的協議形式我們只需要讓服務端對數據進行相應切割就可以找到兩個操作數和運算符,但是解析過程略微繁瑣,所以我們還有第二種方式。
  我們利用一個整形數據四個字節一個運算符一個字節的特點,一共分配九個字節大小的內存,約定前八個字節存放兩個操作數,最後一個字節存放運算符,一起發送到服務端服務端再對發送的數據分塊解析也可以得到數據。
  第一種方式易於實現,只用發送一個字符串即可,第二種方式可以用內存拷貝的方式實現,不過最方便的還是利用一個結構體。

tcp_socket.hpp
/**                                                                                   
 * 封裝一個tcpsocket類,向外提供簡單接口能夠實現客戶端服務端編程流程
 * 1、創建套接字                                  
 * 2、綁定地址信息            
 * 3、開始監聽/發起連接請求                      
 * 4、獲取已完成連接                            
 * 5、發送數據                                                
 * 6、接收數據                 
 * 7、關閉套接字
 **/                                             
                                                
#include <iostream>
#include <string>            
#include <netinet/in.h>                        
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>    
#define CHECK_RET(q) if(q == false) {return -1;}
struct calc_t                                     
{                    
  int num1;                                
  int num2;      
  char op;        
};                                             
class TcpSocket      
{               
  public:         
    TcpSocket()       
    {                                
                       
    }                                                 
    ~TcpSocket() 
    {     
      Close();                                 
    }
    //創建套接字
    bool Socket()
    {
      //這裏首先創建的時皮條套接字
      _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
      if(_sockfd < 0)
      {
        std::cerr << "socket error" << std::endl;
        return false;
      }
      return true;
    }
    //綁定地址信息
    bool Bind(const std::string& ip, uint16_t port)
    {
      struct sockaddr_in addr;
      addr.sin_family = AF_INET;
      addr.sin_port = htons(port);
      addr.sin_addr.s_addr = inet_addr(&ip[0]);
      socklen_t len = sizeof(struct sockaddr_in);
      int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
      if(ret < 0)
      {
        std::cerr << "bind error" << std::endl;
        return false;
      }
      return true;                                                      
    }
    //服務端開始監聽
    bool Listen(int backlog = 5)
    {
      int ret = listen(_sockfd, backlog);
      if(ret < 0)
      {
        std::cerr << "listen error" << std::endl;
        return false;
      }
      return true;
    }
    //連接服務端
    bool Connect(const std::string& ip, uint16_t port)
    {
      int ret;
      struct sockaddr_in addr;
      addr.sin_family = AF_INET;
      addr.sin_port = htons(port);
      addr.sin_addr.s_addr = inet_addr(&ip[0]);
      socklen_t len = sizeof(struct sockaddr_in);
      ret = connect(_sockfd, (struct sockaddr*)&addr, len);
      if(ret < 0)
      {
        std::cerr << "connet error" << std::endl;
        return false;
      }
      return true;
    }
    //設置套接字
    void SetFd(int fd)
    {
      _sockfd = fd;
    }
    //獲取新的套接字                                                             
    bool Accept(TcpSocket& newsock)
    {
      struct sockaddr_in addr;
      socklen_t len = sizeof(struct sockaddr_in);
      //這裏fd是皮條套接字新創建出來的連接套接字
      int fd = accept(_sockfd, (struct sockaddr*)&addr, &len);
      if(fd < 0)
      {
        std::cerr << "accept error" << std::endl;
        return false;
      }
      //newsock._sockfd = fd;
      newsock.SetFd(fd);
      return true;
    }
    //發送數據
    bool Send(void* buf, int len)
    {

      int ret = send(_sockfd, buf, len, 0);
      if(ret < 0)
      {
        std::cerr << "send error" << std::endl;
        return false;
      }
      return true;
    }
    bool Send(const std::string& buf)
    {                                                                     
      int ret = send(_sockfd, &buf[0], buf.size(), 0);
      if(ret < 0)
      {
        std::cerr << "send error" << std::endl;
        return false;
      }
      return true;
    }
    //接收數據
    bool Recv(void* buf, int len)
    {
      int ret = recv(_sockfd, buf, len, 0);
      if(ret < 0)
      {
        std::cerr << "recv error" << std::endl;
        return false;
      }
      else if(ret == 0)
      {
        std::cerr << "peer shutdown" << std::endl;
        return false;
      }
      return true;
    }
    bool Recv(std::string& buf)
    {
      char tmp[4096] = {0};
      int ret = recv(_sockfd, &tmp[0], 4096, 0);
      if(ret < 0)
      {                                                                 
        std::cerr << "recv error" << std::endl;
        return false;
      }
      else if(ret == 0)
      {
        std::cerr << "peer shutdown" << std::endl;
        return false;
      }
      buf = tmp;
      return true;
    }
    //關閉
    bool Close()
    {
      if(_sockfd >= 0)
      {
        close(_sockfd);
      }
    }
  private:
    int _sockfd;
};
tcp_cli.cpp
#include "tcp_socket.hpp"
#include <stdlib.h>
/**
 * 實現客戶端
 * 1、創建套接字
 * 2、綁定地址信息(客戶端不需要手動綁定)
 * 3、向服務端發起連接請求
 * 4、發送數據
 * 5、接收數據
 * 6、關閉套接字
 **/
int main(int argc, char* argv[])
{
  if(argc != 3)
  {
    std::cerr << "./tcp_cli srvip srvport" << std::endl;
    return -1;
  }
  TcpSocket sock;
  CHECK_RET(sock.Socket());
  std::string ip = argv[1];
  uint16_t port = atoi(argv[2]);
  CHECK_RET(sock.Connect(ip, port));
  while(1)
  {
    calc_t tmp;
    tmp.num1 = 11;                                      
    tmp.num2 = 22;
    tmp.op = '+';
    sock.Send((void*)&tmp, sizeof(calc_t));
  }
  sock.Close();
}
tcp_srv.cpp
/**                                                                                
 * 服務端實現
 * 1、創建套接字
 * 2、綁定地址信息
 * 3、開始監聽
 * 4、獲取新連接
 * 5、接收數據
 * 6、發送數據
 * 7、關閉套接字
 **/
#include "tcp_socket.hpp"
#include <stdlib.h>
int main(int argc, char* argv[])
{
  if(argc != 3)
  {
    std::cerr << "./tcp_srv 192.169.11.128 9000" << std::endl;
    return -1;
  }
  std::string ip = argv[1];
  uint16_t port = atoi(argv[2]);
  TcpSocket sock;
  CHECK_RET(sock.Socket());
  CHECK_RET(sock.Bind(ip, port));
  CHECK_RET(sock.Listen());
  //這個新的套接字要放在循環外部,否則一次循環結束變量銷燬會關閉套接字連接就會斷開
  TcpSocket newsock;
  while(1)
  {
    bool ret = sock.Accept(newsock);
    if(ret == false)
    {
      continue;
    }
    calc_t buf;
    ret = newsock.Recv((void*)&buf, sizeof(calc_t));
    if(ret == false)
    {
      std::cerr << "Recv error" << std::endl;
      return -1;
    }
    std::cout << buf.num1 << " " << buf.num2 << " " << buf.op << std::endl;
  }
  sock.Close();
}                               

  使用:

[misaki@localhost netbase]$ ./tcp_srv 192.168.11.128 9000
11 22 +

  我們的服務端就收到了指定的數據信息。
  在對於自定製協議的使用中有兩條重要概念,序列化與反序列化。
  序列化:將數據對象按照指定協議在內存中進行排布成爲可持久化存儲。
  反序列化:將數據傳按照指定的協議進行解析得到各個數據對象。
  對於序列化與反序列化有幾種常用工具。json序列化,protobuf序列化,二進制序列化

知名協議(HTTP協議)

  應用層知名協議有很多不過最常用的就是HTTP協議。在HTTP協議中包含一個重要信息統一資源定位符(URL),URL中又包含哪些信息呢?

URL

  其實URL就是我們常說的網址。
  在URL中有登錄信息,服務器地址端口號,文件地址信息,查詢字符串和片段標識符。
  這裏給出一篇博客對URL的組成形式進行了講解。
  https://www.jianshu.com/p/406d19dfabd3
  在URL中要注意的是文件地址信息以結束往後的是查詢字符串及片段標識符這兩個信息。
  查詢字符串是客戶端提交給服務器的數據。查詢字符串是一個個key=val的鍵值對,並且以&作爲間隔,以#作爲結尾。並且提交的數據中不能出現特殊字符,因爲會和URL中的分隔符造成二義,造成URL解析失敗,因此若提交的數據中有特殊字符就必須進行轉義。在URL中如果字符前出現了%則認爲這個字符經過了URL的轉碼。URL轉碼規則爲將需要轉碼的字符轉爲16進制,然後從右到左,取4位(不足4位直接處理),每2位做一位,前面加上%,編碼成%XY 格式。片段標識符是對資源的補充,可以理解爲書籤。

HTTP協議格式

  HTTP協議分爲以下幾個大的組成部分。

1、首行

  首行又分爲請求首行和響應首行。
  請求首行格式請求方法 URL 協議版本\r\n
  請求方法有很多,其中常用的有GET/POST方法。GET方法多用於向服務器請求資源,不過也可以提交數據,只不過提交的數據在查詢字符串中以明文方式傳輸,十分不安全並且長度有限。POST方法是專門用於向服務器提交表單數據的,提交的數據在正文中。除此之外還有很多種請求方法,HEAD/PUT/DELETE/CONNECT/OPTIONS/TRACE/PATCH,這裏簡單介紹幾個。
  HEAD請求方法和GET一樣是向服務器請求資源,不過HEAD請求只會接收服務器的回覆中的首行及頭部信息,而不要正文信息。
  PUT請求會用正文信息替代服務端指定文件中的數據。
  DELETE是刪除服務器指定文件。
  URL已經介紹過。關於協議版本目前常見的有http/0.9 http/1.0 http/1.1 http/2。0.9版本是HTTP最早期的協議,只有一個GET方法,用於向服務器獲取數據展示HTML文件。並且0.9版本的連接是短連接,客戶端向服務端建立連接發送請求,服務端應答完畢就會關閉套接字。與之對應的是長連接,即一輪信息交互後不會關閉客戶端,可以繼續通信,這樣就不用頻繁建立套接字,這個長連接在1.0版本中得以實現,但是這個長連接還是一來一回的連接方式,並且在1.0版本中默認關閉長連接,需要在頭部信息中開啓,在這個版本中還加入了POSTHEAD方法。之後便是1.1版本,並且是現在最常用的版本,這個版本中長連接默認開啓,並且使用了管線化連接方式,即客戶端可以連續發送多個請求,服務端一一進行回覆,更加靈活方便,在這個版本中還將請求方法增加到現在的9個方法。
  響應首行格式協議版本 響應狀態碼 響應狀態碼描述\r\n
  協議版本與請求首行中的協議版本是一樣的。這裏着重介紹響應狀態碼。
  響應狀態碼是服務端對請求處理結果的表述,一共分爲五大類:

1xx:表示描述性信息
2xx:處理正確響應,如200表示請求已經正確處理完畢
3xx:表示重定向,如301永久重定向,302臨時重定向。
                永久重定向指下次如果還請求此路徑則直接訪問重定向之後的網址,不再請求原網址
                臨時重定向表示知識臨時重定向到新網址,下次訪問還是先訪問原網址
4xx:客戶端錯誤,如404請求資源未找到,400請求格式有誤
5xx:服務端錯誤,如502網關錯誤,500服務器內部錯誤。

  響應狀態碼描述就是對響應狀態碼響應的文字描述信息。

2、頭部

  頭部是以一個一個鍵值對的形式存在的,格式key: value,標識着連接的屬性和一些信息。一對鍵值對獨佔有一行,以\r\n作爲結尾。這裏簡單介紹幾種頭信息,但其實頭信息非常非常的多。

//請求頭部:
Connection:開啓/關閉長連接
Content-Length:正文長度
Content-Type:正文數據類型,有很多類型,包括圖片,HTML超文本等
User-Agent:瀏覽器版本信息及系統版本信息
Accept:告訴服務端自己可以接收哪些數據
Accept-Encoding:能接收的數據編碼
Cookie:向服務端發送當前用戶的Cookie信息即sessionid
//響應頭部:
Server:服務端版本信息
Date:事件
Content-Type:正文數據類型
Transfer-Encoding:傳輸格式,如果爲chunked表示爲分塊傳輸,此時每個分塊的大小寫在正文第一行,最後一個分塊大小爲0
Location:重定向後的新位置
Set-Coolie:向客戶端發送屬於當前用戶的Cookie信息即sessionid,還有一些其他信息,如Cookie超時失效日期
Referer:從哪個頁面跳轉到當前頁面

  在請求頭部和響應頭部中都有關於Cookie的屬性信息,那麼什麼是Cookie?這裏舉個例子,假如說我們在網上購物,此時我們看中一件商品要將其加入購物車,因此爲了區分用戶需要進行登錄,那麼本次請求就是加入購物車請求我們需要登錄一次,然後請求成功了,隨後我們想要購買購物車中的商品,但是HTTP協議是無狀態協議,本次請求與上次請求無任何關係,於是爲了購買我們不得不再登陸一次,才能確認我們用戶的身份,我們每次進行用戶操作都要進行登錄,十分麻煩。
  之後大佬們想要讓HTTP協議能夠進行狀態維持,於是加入了Cookie幫助我們臨時保存一些驗證信息,於是情況就改變了。在我們第一次登錄完畢後,服務端會在服務器內爲客戶端建立一個會話,生成一個會話id(sessionid),並將會話id和用戶信息保存起來,此時服務端會給客戶端一個響應,響應信息的頭部中就會有Set-Cookie信息,其中存儲着sessionid和一些其他相關信息例如超時失效信息。客戶端在收到服務端的sessionid信息後會將信息保存在瀏覽器自己的Cookie文件中,在下次再向服務器發信息時會先將與這個服務器對應的Cookie文件中的信息全部讀出放入請求信息中,這個信息就存放在請求頭部的Cookie中,其中主要就是sessionid。在服務端獲取sessionid後就能通過這個id找到對應用戶的用戶信息,從而避免需要重複登錄的情況發生。
  這裏還要關注一個問題Cookiesession的區別是什麼?Cookie是保存在客戶端的,每次發起請求時會發送給服務端去尋找屬於當前用戶的session;而session是保存在服務端的,是爲每個用戶創建的一個會話,其中保存着對應的sessionid和用戶信息。

3、正文

  在頭部與正文之間用一個空行作爲間隔,\r\n\r\n即表示一個空行,當遇到兩個連續的\r\n時則認爲頭部結束了。正文則是一些數據信息。

實現HTTP協議服務器

  不管什麼信息發送至服務端,服務端同一回覆學習是一種態度
  HTTP服務器實際上是一個TCP服務器,接收到數據後打印出接收的數據,然後統一按照HTTP協議格式回覆信息即可。

/**
 * 實現一個簡單的http服務器
 * 這個代碼用到了我們之前封裝額tcp頭文件
 **/
#include "tcp_socket.hpp"
#include <iostream>
#include <sstream>
int main()
{
  TcpSocket sock;
  CHECK_RET(sock.Socket());
  CHECK_RET(sock.Bind("0.0.0.0", 9000));
  CHECK_RET(sock.Listen());
  while(1)
  {
    TcpSocket cliSock;
    if(sock.Accept(cliSock) == false)
    {
      continue;
    }
    std::string buf;
    cliSock.Recv(buf);
    std::cout << "req:[" << buf << "]" << std::endl;;
    std::string body = "<html><body><h1>學習是一種態度</h1></body></html>";
    body += "<meta http-equiv='content-type' content='text/html;charset=utf-8'>";
    std::string first = "HTTP/1.1 200 OK";
    std::stringstream ss;
    ss << "Content-Length: " << body.size() << "\r\n";
    ss << "Content-Type: " << "text/html" << "\r\n";
    std::string head = ss.str();
    std::string blank = "\r\n";
    cliSock.Send(first);
    cliSock.Send(head);
    cliSock.Send(blank);
    cliSock.Send(body);
    cliSock.Close();
  }
  sock.Close();
}                       

  我們用瀏覽器訪問我們的服務器,服務端會打出以下數據。

[misaki@localhost httpserver]$ ./httpserver 
req:[GET / HTTP/1.1
Host: 192.168.11.128:9000
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate
Accept-Language: zh,zh-TW;q=0.9,en-US;q=0.8,en;q=0.7

]
req:[GET /favicon.ico HTTP/1.1
Host: 192.168.11.128:9000
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Accept: image/webp,image/apng,image/*,*/*;q=0.8
Referer: http://192.168.11.128:9000/
Accept-Encoding: gzip, deflate
Accept-Language: zh,zh-TW;q=0.9,en-US;q=0.8,en;q=0.7

  由於我們打印了客戶端發來的數據所以我們可以看到很多http請求數據。瀏覽器也會顯示以下頁面。
http
  之後我們更改一下代碼,用一下重定向。

/**                                                                                   
 * 實現一個簡單的http服務器
 **/                     
#include "tcp_socket.hpp"
#include <iostream>
#include <sstream>
int main()
{                
  TcpSocket sock;          
  CHECK_RET(sock.Socket());             
  CHECK_RET(sock.Bind("0.0.0.0", 9000));
  CHECK_RET(sock.Listen());
  while(1)
  {                   
    TcpSocket cliSock;               
    if(sock.Accept(cliSock) == false)
    {          
      continue;
    }               
    std::string buf;  
    cliSock.Recv(buf);                               
    std::cout << "req:[" << buf << "]" << std::endl;;                      
    std::string body = "<html><body><h1>學習是一種態度</h1></body></html>";      
    body += "<meta http-equiv='content-type' content='text/html;charset=utf-8'>";
    //std::string first = "HTTP/1.1 200 OK";
    //這類改爲重定向                      
    std::string first = "HTTP/1.1 302 OK";
    std::stringstream ss;                             
    ss << "Content-Length: " << body.size() << "\r\n";
    ss << "Content-Type: " << "text/html" << "\r\n";
    ss << "Location: http://www.taobao.com/\r\n";
    std::string head = ss.str();
    std::string blank = "\r\n";
    cliSock.Send(first);
    cliSock.Send(head);
    cliSock.Send(blank);
    cliSock.Send(body);
    cliSock.Close();
  }
  sock.Close();
}                             

  再次使用瀏覽器訪問,則會跳轉到淘寶頁面。
http
  之後我們再次更改代碼,這次使用404狀態碼。

/**
 * 實現一個簡單的http服務器
 **/
#include "tcp_socket.hpp"
#include <iostream>
#include <sstream>
int main()
{
  TcpSocket sock;
  CHECK_RET(sock.Socket());
  CHECK_RET(sock.Bind("0.0.0.0", 9000));
  CHECK_RET(sock.Listen());
  while(1)
  {
    TcpSocket cliSock;
    if(sock.Accept(cliSock) == false)
    {
      continue;
    }
    std::string buf;
    cliSock.Recv(buf);
    std::cout << "req:[" << buf << "]" << std::endl;;
    std::string body = "<html><body><h1>學習是一種態度</h1></body></html>";
    body += "<meta http-equiv='content-type' content='text/html;charset=utf-8'>";
    //std::string first = "HTTP/1.1 200 OK";
    //這類改爲重定向
    //std::string first = "HTTP/1.1 302 OK";
    //這次改爲客戶端錯誤
    std::string first = "HTTP/1.1 404 OK";                                           
    std::stringstream ss;
    ss << "Content-Length: " << body.size() << "\r\n";
    ss << "Content-Type: " << "text/html" << "\r\n";
    ss << "Location: http://www.taobao.com/\r\n";
    std::string head = ss.str();
    std::string blank = "\r\n";
    cliSock.Send(first);
    cliSock.Send(head);
    cliSock.Send(blank);
    cliSock.Send(body);
    cliSock.Close();
  }
  sock.Close();
}                                 

  還是使用瀏覽器訪問顯示以下畫面。
http
  我們的返回狀態碼是404爲什麼也可以顯示頁面?瀏覽器默認錯誤頁面是可以自定製的,我們這裏返回的相當於是一個錯誤頁面,於是瀏覽器也幫我們顯示了出來。
  其他狀態碼這裏就不一一演示了。

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