朋友們、伙計們,我們又見面了,本期來給大家帶來應用層自定義協議相關的知識點,如果看完之后對你有一定的啟發,那么請留下你的三連,祝大家心想事成!
C 語 言 專 欄:C語言:從入門到精通
數據結構專欄:數據結構
個? 人? 主? 頁?:stackY、
C + + 專 欄? ?:C++
Linux 專?欄? :Linux
目錄
1. 協議
2. 自定義協議
2.1 預備工作?
3. 序列與反序列化
3.1?報頭的添加與解析
3.2 計算業務
4. 功能完善
4.1 服務端
4.2 客戶端?
5. 成熟的序列反序列化方案
1. 協議
前面說過,協議其實就是一種約定,我們實現的tcp通信時,都是按照字符串的方式進行發送的,那么對方收到的也是字符串,那么如果我們需要發送一些具體化的數據呢?
就比如:現在使用的這些聊天軟件,我們在發送數據時,有昵稱、時間、具體的消息內容,因此,在發送數據時,不僅僅是將消息內容發送過去,而是將這三樣東西發送過去了,這是一種結構化的數據;
- 所以在發送類似與這種結構化字段的數據就要制定一種協議;
- 協議其實就是雙方在通信時約定好的一種結構化字段;
在應用層這里我們發送時并不是直接將這個結構化的字段發送給對方:
- 因為在應用層很可能雙方系統有所差異,對于結構體的計算不統一,導致數據的不準確;
- 所以,在應用層這里,我們要發送結構化的字段,必須要將結構化字段進行序列化成為字節流(“字符串”),將字節流發送給對方,對方通過反序列化將字節流轉化為結構化字段;
- 序列化的目的是為了更好的網絡發送,反序列化的目的是為了上層更好的對數據進行有效字段的提取;
- 序列化和反序列化的方式雙方可以進行統一的約定;
2. 自定義協議
在自定協議這里我們直接實現一個網絡版本的計算器來提現一下自定義協議的過程;
我們采用分模塊來實現:
- Socket.hpp:對網絡套接字進行封裝
- TcpServer.hpp:實現Tcp的服務器
- TcpServerMain.cc:測試Tcp服務器
- TcpClientMain.cc:完成客戶端
- Protocol.hpp:自定義協議
- Calculate.hpp:實現計算的業務
2.1 預備工作?
既然要進行網絡通信,那么就少不了需要套接字接口,前面已經寫過好多次套接字的接口了,這里對套接字進行封裝,將服務器和客戶端各自使用的接口整合在一起,我們對封裝好的套接字提供一些我們需要的接口接口;
我們之前發送數據使用的read和write,其實還有兩個接口:
Socket.hpp:#pragma once#include <iostream> #include <string> #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h>#define Convert(addrptr) ((struct sockaddr *)addrptr) // 套接字中對于地址類型強轉的宏namespace Net_Work {const static int defaultsockfd = -1;const int backlog = 5;enum // 對于一些錯誤碼的設置{SocketError = 1,BindError,ListenError,};// 封裝一個基類,Socket接口類// 設計模式:模版方法類class Socket{public:virtual ~Socket() {}virtual void CreateSocketorDie() = 0; // 創建套接字virtual void BindSocketorDie(uint16_t port) = 0; // 綁定virtual void ListenSocketorDie(int backlog) = 0; // 監聽virtual Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) = 0; // 獲取連接virtual bool ConnectServer(std::string &serverip, uint16_t &serverport) = 0; // 建立連接virtual int GetSockFd() = 0; // 獲取套接字virtual void SetSockFd(int sockfd) = 0; // 設置套接字virtual void CloseSocket() = 0; // 關閉套接字virtual bool Recv(std::string *buffer, int size) = 0; // 讀取信息virtual void Send(std::string &send_str) = 0; // 發送信息public:// 創建監聽套接字----Servervoid BuildListenSocketMethod(uint16_t port, int blacklog){CreateSocketorDie();BindSocketorDie(port);ListenSocketorDie(blacklog);}// 創建連接套接字---Clientbool BuildConnectSocketMethod(std::string &serverip, uint16_t serverport){CreateSocketorDie();return ConnectServer(serverip, serverport);}void BuildNormalSocketMethod(int sockfd){SetSockFd(sockfd);}};class TcpSocket : public Socket{public:TcpSocket(int sockfd = defaultsockfd) : _sockfd(sockfd){}~TcpSocket() {}void CreateSocketorDie() override{_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0)exit(SocketError);}void BindSocketorDie(uint16_t port) override{struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;int n = ::bind(_sockfd, Convert(&local), sizeof(local));if (n < 0)exit(BindError);}void ListenSocketorDie(int backlog) override{int n = ::listen(_sockfd, backlog);if (n < 0)exit(ListenError);}Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) override{struct sockaddr_in peer;socklen_t len = sizeof(peer);uint16_t newsockfd = ::accept(_sockfd, Convert(&peer), &len);if (newsockfd < 0)return nullptr;*peerip = inet_ntoa(peer.sin_addr);*peerport = ntohs(peer.sin_port);Socket *s = new TcpSocket(newsockfd);return s;}bool ConnectServer(std::string &serverip, uint16_t &serverport) override{struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(serverip.c_str());server.sin_port = htons(serverport);int n = ::connect(_sockfd, Convert(&server), sizeof(server));if (n == 0)return true;elsereturn false;}int GetSockFd() override{return _sockfd;}void SetSockFd(int sockfd) override{_sockfd = sockfd;}void CloseSocket() override{if (_sockfd > defaultsockfd)::close(_sockfd);}bool Recv(std::string *buffer, int size) override{char inbuffer[size];ssize_t n = recv(_sockfd, inbuffer, size - 1, 0);if(n > 0){inbuffer[n] = 0;*buffer += inbuffer;return true;}else if (n == 0) return false;else return false;}void Send(std::string &send_str) override{// 發送信息這里我們簡略的寫了;send(_sockfd, send_str.c_str(), send_str.size(), 0);}private:int _sockfd;};}
為了先測試一下客戶端與服務器,我們先來簡單的定制一下協議:
我們想要實現一個網絡版本的計算器,來自己實現一下自定義協議;
協議方法:
- 請求:參數1?運算符號 參數2
- 響應:運算結果 運算狀態(結果是否可靠)
請求和相應是兩個結構化的字段,因為今天在同一臺主機上進行測試,所以我們先直接發送結構化的字段,未來客戶端和服務器分別include這個協議,至此雙方都可以看到同一份結構化字段,這就是一種自定義的協議;
另外,我們定制好的協議我也想給他們設置一個工廠模式,為了后面方便使用;
接下來我們對客戶端和服務器進行簡單的實現,因為服務器與客戶端在前面UDP和TCP通信那里細致的說多了,這里就直接展示代碼了:
TcpServer.hpp:
?對于服務器我們想采用多線程的方式對任務進行處理;
TcpServerMain.cc:
這里對與任務函數的編寫就簡單一點,為了測試能否正常通信,以及直接傳遞結構化字段;
客戶端:
TcpClientMain.cc:
客戶端這里我們也是直接發送結構化字段;
網絡版本計算器的基本流程:
服務器啟動之后,先創建連接套接字,構建一個計算的請求,然后向服務器發起請求;
因為是本地測試,所以我們可以直接傳遞結構體,先完成基本的通信,后面再實現序列化的過程;
服務器啟動之后,先創建監聽套接字設置回調方法,然后獲取新連接,使用多線程執行任務;
基本測試:
上面的代碼通信時直接發送的是結構體字段,這種發送方式只在限定情況下可以使用,就比如我們上面的測試代碼是在本地上演示的,所以不會有什么問題,為了考慮更全面,接下來就需要進行序列、反序列操作;?
在進行序列化之前再來對TCP協議進行一下深入的概念性了解:
- 其實我們在進行TCP通信的時候,我們使用的發送(write/send)和接收(read/recv)的接口,并不是直接將數據通過網絡發送給對方,因為這些接口是用戶層接口,在內核層雙方還會存在兩個緩沖區:一個是發送緩沖區,一個是接收緩沖區;
- write/send和read/recv接口只是將數據從用戶緩沖區拷貝到內核緩沖區,本質就是一個拷貝函數;
- 那么至于什么時候發送,發多少,怎么發,發送出錯了怎么辦等等,這些都不需要用戶去考慮,這是由內核決定的,換句話說是由TCP協議決定的!
- TCP協議在進行通信的時候,將發送緩沖區中的數據通過網絡拷貝至對方接收緩沖區中,其實,是雙方的OS之間進行通信;
- 這也就解釋了,write或者read在某些條件下會發生阻塞的問題;當接收緩沖區中沒有數據時,read就會阻塞,因為他不具備接收條件;當發送緩沖區寫滿的時候,write就會阻塞,因為他不具備發送條件;
- 像上面這種有人寫就有人拿的模型其實就是一種生產者消費者模型;
- 因為雙方在接收和發送時是兩個獨立的模塊,所以可以進行同時通信,所以說TCP協議是全雙工協議;
在TCP通信中,在發送時,對方發送了多少數據,并不意味著我就要接收多少數據,這完全由TCP協議來決定;
那么這就存在一種問題:我要讀取對方發送的數據,怎么保證我就能讀到對方發送的一個完整的數據報文呢?
此時就需要明確報文與報文之前的邊界(代碼中體現)
3. 序列與反序列化
為了完整我們的代碼,我們就需要在發送數據與解析數據時進行序列與反序列化的工作;
序列反序列化也是雙方進行的一種約定,也就是自定義的一種協議;
在這里我們想定制的協議是:
- 未來要發送數據時將結構化字段全部轉化為一個字符串“_data_x op _data_y”;
- 這里需要注意op的長度是固定的(+ - * /)但是兩邊的操作數的長度是不固定的,所以為了反序列化更方便,我們需要添加報頭,其中報頭表示的含義就是這個字符串有效內容的長度“len_data_x op _data_y”,這個報頭就叫做報文的自描述字段;
- 為了讓報頭和有效載荷易于區分,并且為了讓報文與報文之間易于區分,我們需要在報頭和有效載荷的中間添加特殊字符(\n),在報文末尾添加特殊字符(\n)
- “len\n_data_x op _data_y\n”;
- 未來在讀取報文的時候,首先讀到的就是報頭,讀到\n時就知道前面的是報頭,根據報頭所表示的有效內容的長度,再向后讀取指定大小的字符即可;
- 這里添加\n是為了應付多種場景,我們現在的場景是四則運算,有效載荷中不可能出現\n,但是如果場景是一個聊天信息呢,里面可能會出現\n,但是這個消息的長度不可能有\n,所以用\n來區分報頭與有效載荷的邊界,當然也可以使用其他特殊字符;
- 另外,我們在報文最后添加的\n不僅僅用于區分報文和報文之間的邊界,還可以幫助我們在寫代碼的時候打印調試;
- 上面是對請求進行的序列化,對于響應也是一樣的“len\n_result _code”。
因為請求和相應都需要添加報頭,所以序列化與報頭我們分開處理;
Request的序列化與反序列化:
未來的客戶端與服務器都需要遵守這樣的約定來進行數據的交互與處理,這就是一種自定義的協議,有用戶來決定的;
Response序列化與反序列化:
3.1?報頭的添加與解析
添加報頭:
未來我們相對這種類型"_data_x op _data_y"的字符串添加報頭,所以我們依舊采用字符串的操作,這里就不詳細解釋了;
拼接特殊字符即可;
解析報頭:
因為我們不確定報文的完整性,所以在使用解析報頭時我們采用循環調用的方式;
首先我們需要找到區分報頭和有效載荷的特殊字符;
然后截取特殊字符前面的報頭來確定有效載荷的長度;
因為報文的不確定性,所以我們需要通過前面對有效載荷的長度以及報文長度的已知值來確定出一個完整報文的總長度;
然后根據傳入的package與這個長度比較,想要至少有一個完整的報文那么就必須大于或者等于這個總長度;
然后通過特殊字符的位置進行截取到有效載荷的信息;
然后將我們已經截取到的完整報文丟棄掉,繼續處理下一個報文;
3.2 計算業務
有了序列化與反序列化,接下來就需要對數據進行業務處理了,我們拿到數據先對數據進行處理,獲取到其中的運算符(+ - * / %),然后根據不同的運算符來截取對應的操作數執行運算,然后將結果返回,所以在使用計算業務的時候,傳入的是一個請求,返回的是一個相應;
4. 功能完善
4.1 服務端
上面實現的添加報頭與解包分用其實就可以進行通信了,但是,我們想實現的是,把發送數據和接收數據放在TcpServer底層,此時我只負責發送和接收,不管發送和接收的數據是什么,將網絡和業務進行解耦;
此時就需要對執行任務的函數進行簡單的調整,未來我們發送一個字節流,對字節流進行業務處理,然后將處理完成的結果再序列化為字節流再返回給我即可,還可以再帶一個參數,表示的是業務執行過程中是否出錯;
首先我們來實現一下這個業務處理的函數HandlerRequest,在調用時,傳入一個待處理的字節流,我們需要對這個字節流進行處理,獲取到一個完整的報文,然后對報文反序列化,將有效載荷進行業務處理,處理完成之后的結果我們需要再進行序列化以及添加報頭,然后返回出去;
緊接著我們需要在ThreadRun函數中對數據進行接收和發送的操作:
4.2 客戶端?
客戶端這里的代碼就不封裝了,直接編寫實現通信;
在客戶端這里我們首先需要構建一些需要計算的請求,然后對其進行序列化并添加報頭,然后發送給服務器:
我們可以來梳理一下這個解耦的邏輯:
?我們既然能發送,當然也可以進行讀取我們發送之后計算完成的結果,所以需要對返回的響應進行解析并反序列化得到最終的結構化字段的Response:
#include "Protocol.hpp" #include "Socket.hpp"#include <iostream> #include <ctime> #include <cstdlib> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h>using namespace Protocol;int main(int argc, char *argv[]) {if (argc != 3){std::cout << "Usage: " << " serverip serverport" << std::endl;return 1;}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 創建套接字Net_Work::Socket *conn = new Net_Work::TcpSocket();if (!conn->BuildConnectSocketMethod(serverip, serverport)){std::cerr << "connect " << serverip << ":" << serverport << " failed" << std::endl;}std::cout << "connect " << serverip << ":" << serverport << " success" << std::endl;// 使用工廠模式std::unique_ptr<Protocol::Factory> factory = std::make_unique<Protocol::Factory>();srand(time(nullptr) ^ getpid()); // 建立隨機數的種子const std::string opers = "+-*/%";while (true){// 構建請求,遵守協議int x = rand() % 100; // [0, 99)usleep(rand() % 1234);int y = rand() % 100; // [0, 99)char op = opers[rand() % opers.size()];// 創建請求std::shared_ptr<Protocol::Request> req = factory->BuildRequest(x, y, op);// 對請求序列化std::string request_str;req->Serialize(&request_str);std::cout << request_str << std::endl;// for teststd::string testreq = request_str;testreq += " ";testreq += "= ";// 添加報頭request_str = Encode(request_str);// 發送請求conn->Send(request_str);std::string response_str;while(true){// 讀取響應if(!conn->Recv(&response_str, 1024)) break;// 解析響應報文std::string response;if(!Decode(response_str, &response))continue;// 反序列化auto resp = factory->BuildResponse();resp->Deserialize(response);// 得到了結果std::cout << testreq << resp->GetResult() << "[" << resp->GetCode() << "]" << std::endl;break;}sleep(1);}conn->CloseSocket();return 0; }
5. 成熟的序列反序列化方案
上面我們是手寫的序列反序列化,這樣子寫也可以,但是畢竟是我們手寫的,我們可以使用一下成熟的方案,比如:json、protobuf、xml;
我們想使用一下json來替換我們手寫的序列和反序列化;
想細致了解json的使用可以去搜一些博客看一下,這里我們先使用json進行簡單的演示:
// ubuntu 安裝jsoncppsudo apt-get install libjsoncpp-dev
演示代碼:
#include <iostream> #include <string>#include "jsoncpp/json/json.h"int main() {// 創建Json對象// Json::Value 萬能的類型Json::Value root;// 添加kv映射數據root["k1"] = 100;root["k2"] = 100;root["hello"] = "world";root["bit"] = 8;// 序列化Json::FastWriter writer;std::string s = writer.write(root);std::cout << s << std::endl;// 反序列化int k1, k2, bit;std::string hello;Json::Value _root;Json::Reader reader;if (reader.parse(s, _root)){k1 = _root["k1"].asInt();k2 = _root["k2"].asInt();hello = _root["hello"].asCString();bit = _root["bit"].asInt();}std::cout << k1 << " " << k2 << " " << hello << " " << bit << std::endl;return 0; }
接下來我們就將json引入到我們的代碼中:
我們使用條件編譯,也可以將我們自己實現的序列反序列化的過程保留下來
這到這里我們的代碼已經完結了,其實也可以將服務器變成守護進程;
源碼鏈接:?https://gitee.com/yue-sir-bit/linux/tree/master/Network_version_calculator
當我們自己手寫協議之后,再回頭看一下OSI定義的七層網絡協議棧,就可以與我們本節實現的代碼可以結合起來了: