計算機網絡 -- 序列化與反序列化

一 協議的重要性

? 我們都知道,在進行網絡通信的過程中,通信的雙方可以是不同的設備,不同的平臺,不同的平臺,比如說,手機用戶和電腦用戶進行通信,ios系統和安卓系統進行通信。

? 自己的數據,如何保證對方端能安全接收到呢,假設linux為服務端,Windows為客戶端,如何確保數據能被正確接收呢?

? 就像我們中國人用中文進行交流一樣,假設我們和某個外國人進行交流,母語的差異導致雙方無法正常交流,信息也無法傳達,于是我們只能打開手機上的?同聲傳譯功能,可以將信息轉換為對方能聽懂的語言,最終實現交流。?

??同聲傳譯?這個功能可以看做一種?協議(可以確保對端能理解自己傳達的信息),協議?的出現解決了主機間的交流問題。

??也就是說,我們通過協議,規定了網絡通信的雙方,必須按照某種規則來對傳輸的內容進行解析或者是打包。

??對于網絡來說,協議是雙方通信的基石,如果沒有協議,那么即使數據傳輸的再完美也無法使用,比如下面這個就是一個簡單的?兩正整數運算協議。

  • 協議要求:發送的數據必須由兩個操作數(正整數)和一個運算符組成,并且必須遵循?x op y?這樣的運算順序。
int x;
int y;
char op; // 運算符

? 主機A在發送消息時需要將 操作數x、操作數y和運算符op 進行傳遞,只要主機A和主機B都遵循這個 協議,那么主機B在收到消息后一定清楚這是兩個操作數和一個運算符

? 現在的問題是如何傳遞?

方案一:將兩個操作數和一個運算符拼接在一起直接傳遞
方案二:將兩個操作數和一個運算符打包成一個結構體傳遞

方案一:直接拼接 xopy方案二:封裝成結構體
struct Mssage{int x;int y;char op;
};

? 無論是方案一還是方案二都存在問題,前者是對端接收到消息后無法解析,后者則是存在平臺兼容問題(不同平臺的結構體內存規則可能不同,會導致讀取數據出錯)

? 要想確保雙方都能正確理解?協議,還需要進行?序列化與反序列化?處理。

二 .什么是序列化與反序列化?

? 序列化是指?將一個或多個需要傳遞的數據,按照協議的格式,拼接為一條字節流數據,反序列化則是?將收到的數據按照格式解析。?

? 可見,反序列化和序列化就是協議的一部分。

??比如主機A想通過?兩正整數運算協議?給主機B發送這樣的消息:

//1+1
int x = 1;
int y = 1;
char op = '+';

可以根據格式(這里使用?(空格))進行?序列化,序列化后的數據長這樣:

// 經過序列化后得到
string msg = "1 + 1";

在經過網絡傳輸后,主機B收到了消息,并根據?(空格)進行?反序列化,成功獲取了主機A發送的信息。

string msg = "1 + 1";// 經過反序列化后得到
int x = 1;
int y = 1;
char op = '+';

? ?這里可以將需要傳遞的數據存儲在結構體中,傳遞/接收?時將數據填充至類中,類中提供?序列化與反序列化?的相關接口即可。

??

class Request
{
public:void Serialization(string* str){}void Deserialization(const sting& str){}public:int _x;int _y;char _op;
};

? 以上就是一個簡單的?序列化和反序列化?流程,簡單來說就是?協議?定制后不能直接使用,需要配合?序列化與反序列化?這樣的工具理解,接下來我們就基于?兩正整數運算協議?編寫一個簡易版的網絡計算器,重點在于?理解協議、序列化和反序列化。

三 相關程序的實現框架

我們接下來要編寫的程序從實現功能來看是十分簡單的:

? 客戶端給出兩個正整數和一個運算符,服務器計算出結果后返回

整體框架為:

客戶端獲取正整數與運算符 -> 將這些數據構建出 Request 對象 -> 序列化 -> 將結果(數據包)傳遞給服務器 ->

服務器進行反序列化 -> 獲取數據 -> 根據數據進行運算 -> 將運算結果構建出 Response 對象(回響對象) -> 序列化 -> 將結果(數據包)傳遞給客戶端 -> 客戶端反序列后獲取最終結果。

?既然這是一個基于網絡的簡易版計算器,必然離不開網絡相關接口,在編寫?服務器?與?客戶端?的邏輯之前,需要先將?socket?接口進行封裝,方面后續的使用。

四 程序實現

4.1 封裝socket相關操作

注:當前實現的程序是基于?TCP?協議的

? ?簡單回顧下,服務器需要 創建套接字、綁定IP地址和端口號、進入監聽連接狀態、等待客戶端連接,至于客戶端需要 創建套接字、由操作系統綁定IP地址和端口號、連接服務器,等客戶端成功連上服務器后,雙方就可以正常進行網絡通信了。

? ? 為了讓客戶端和服務器都能使用同一個頭文件,我們可以把客戶端和服務器需要的所有操作都進行實現,各自調用即可。

Sock.hpp 套接字相關接口頭文件

#pragma once#include "Log.hpp"
#include "Err.hpp"#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>class Sock{const static int default_sock = -1;const static int default_backlog = 32;
public:Sock():_sock(default_sock){}// 創建套接字void Socket(){_sock = socket(AF_INET, SOCK_STREAM, 0);if(_sock == -1){logMessage(Fatal, "Creater Socket Fail! [%d]->%s", errno, strerror(errno));exit(SOCKET_ERR);}logMessage(Debug, "Creater Socket Success");}// 綁定IP與端口號void Bind(const uint16_t& port){struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) == -1){logMessage(Fatal, "Bind Socket Fail! [%d]->%s", errno, strerror(errno));exit(BIND_ERR);}logMessage(Debug, "Bind Socket Success");}// 進入監聽狀態void Listen(){if(listen(_sock, default_backlog) == -1) {logMessage(Fatal, "Listen Socket Fail! [%d]->%s", errno, strerror(errno));exit(LISTEN_ERR);}}// 嘗試處理連接請求int Accept(std::string* ip, uint16_t* port){struct sockaddr_in client;socklen_t len = sizeof(client);int retSock = accept(_sock, (struct sockaddr*)&client, &len)
;if(retSock < 0)logMessage(Warning, "Accept Fail! [%d]->%s", errno, strerror(errno));else{*ip = inet_ntoa(client.sin_addr);*port = ntohs(client.sin_port);logMessage(Debug, "Accept [%d -> %s:%d] Success", retSock, ip->c_str(), *port);}return retSock;}// 嘗試進行連接int Connect(const std::string& ip, const uint16_t& port){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(port);server.sin_addr.s_addr = inet_addr(ip.c_str());return connect(_sock, (struct sockaddr*)&server, sizeof(server));}// 獲取sockint GetSock(){return _sock;}// 關閉sockvoid Close(){if(_sock != default_sock){close(_sock);}logMessage(Debug, "Close Sock Success");}~Sock(){}
private:int _sock; // 既可以是監聽套接字,也可以是連接成功后返回的套接字
};

Err.hpp?錯誤碼頭文件

#pragma onceenum{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR,CONNECT_ERR,FORK_ERR,SETSID_ERR,CHDIR_ERR,OPEN_ERR,READ_ERR,};

Log.hpp?日志輸出頭文件?

#pragma once#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdarg.h>using namespace std;enum{Debug = 0,Info,Warning,Error,Fatal
};static const string file_name = "./tcp_log";string getLevel(int level){vector<string> vs = {"<Debug>", "<Info>", "<Warning>", "<Error>", "<Fatal>", "<Unknown>"};//避免非法情況if(level < 0 || level >= vs.size() - 1){return vs[vs.size() - 1];}return vs[level];
}string getTime(){time_t t = time(nullptr);   //獲取時間戳struct tm *st = localtime(&t);    //獲取時間相關的結構體char buff[128];snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d", st->tm_year + 1900, st->tm_mon + 1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec);return buff;
}//處理信息
void logMessage(int level, const char* format, ...)
{//日志格式:<日志等級> [時間] [PID] {消息體}string logmsg = getLevel(level);    //獲取日志等級logmsg += " " + getTime();  //獲取時間logmsg += " [" + to_string(getpid()) + "]";    //獲取進程PID//截獲主體消息char msgbuff[1024];va_list p;va_start(p, format);    //將 p 定位至 format 的起始位置vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自動根據格式進行讀取va_end(p);logmsg += " {" + string(msgbuff) + "}";    //獲取主體消息// 直接輸出至屏幕上 方便進行測試cout << logmsg << endl;// //持久化。寫入文件中// FILE* fp = fopen(file_name.c_str(), "a");   //以追加的方式寫入// if(fp == nullptr) return;   //不太可能出錯// fprintf(fp, "%s\n", logmsg.c_str());// fflush(fp); //手動刷新一下// fclose(fp);// fp = nullptr;
} 

有了?Sock.hpp?頭文件后,服務器/客戶端就可以專注于邏輯編寫了.

4.2 服務器

? 首先準備好?TcpServer.hpp?頭文件,其中實現了服務器初始化、服務器啟動、序列化與反序列化等功能。

server.hpp?服務器頭文件

#pragma once#include "Sock.hpp"
#include <iostream>
#include <string>
#include <pthread.h>namespace My_Server{class server;// 線程所需要的信息類class ThreadDate{public:ThreadDate(int& sock, std::string& ip, uint16_t& port, TcpServer* ptsvr):_sock(sock),_ip(ip),_port(port),_ptsvr(ptsvr){}~ThreadDate(){}int _sock;std::string _ip;uint16_t _port;TcpServer* _ptsvr; // 回指指針};class server{const static uint16_t default_port = 8888;private:// 線程的執行函數static void* threadRoutine(void* args){// 線程剝離pthread_detach(pthread_self());ThreadDate* td = static_cast<ThreadDate*>(args);td->_ptsvr->ServiceIO(td->_sock, td->_ip, td->_port);delete td;return nullptr;}// 進行IO服務的函數void ServiceIO(const int& sock, const std::string ip, const uint16_t& port){// TODO}public:server(const uint16_t port = default_port):_port(port){}// 初始化服務器void Init(){_listen_sock.Socket();_listen_sock.Bind(_port);_listen_sock.Listen();}// 啟動服務器void Start(){while(true){std::string ip;uint16_t port;int sock = _listen_sock.Accept(&ip, &port);if(sock == -1){continue;}// 創建子線程,執行業務處理pthread_t tid;ThreadDate* td = new ThreadDate(sock, ip, port, this);pthread_create(&tid, nullptr, threadRoutine, td);}}~server(){_listen_sock.Close();}private:Sock _listen_sock; // 監聽套接字uint16_t _port;    // 服務器端口號};
}

server.cc?簡易計算器服務器源文件

#include <iostream>
#include <memory>
#include "server.hpp"using namespace std;int main(){unique_ptr<My_Server::server> tsvr(new My_Server::server());tsvr->Init();tsvr->Start();return 0;
}

Makefile?自動編譯腳本

.PHONY:all
all:server 
# //clientserver:server.ccg++ -o $@ $^ -std=c++11 -lpthread# client:client.cc
# 	g++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -rf server 
# client

編譯并運行程序,同時查看網絡使用情況:
?

netstat -nltp

此時就證明前面寫的代碼已經沒有問題了,接下來是填充?ServiceIO()?函數

4.3 序列化和反序列化

ServiceIO()?函數需要做這幾件事

  • 讀取數據
  • 反序列化
  • 業務處理
  • 序列化
  • 發送數據

除了?序列化和反序列化?外,其他步驟之前都已經見過了,所以我們先來看看如何實現?序列化與反序列化。

ServiceIO()?函數 — 位于 server.hpp?頭文件中的?server?類中

// 進行IO服務的函數
void ServiceIO(const int& sock, const std::string ip, const uint16_t& port){// 1.讀取數據// 2.反序列化// 3.業務處理// 4.序列化// 5.發送數據
}

? 需要明白我們當前的?協議?為?兩正整數運算,分隔符為?(空格),客戶端傳給服務器兩個操作數和一個運算符,服務器在計算完成后將結果返回,為了方便數據的讀寫,可以創建兩個類:Request?(客戶端發送的一串待求的運算字符串)和?Response(服務器發送給客戶端的結果),類中的成員需要遵循協議要求,并在其中支持?序列化與反序列化。

? 但這兩個函數,明顯重復率有點高,我們的分隔符同樣為一個空格,需要進行提取的,也都是數字,因此我們可以寫個工具類,從而方便序列化和反序列化。

Util.hpp?工具類

#pragma once
#include <string>
#include <vector>class Util{
public://數字轉化為字符串static std::string IntToStr(int val){// 特殊處理if(val == 0)return "0";std::string str;while(val){str += (val % 10) + '0';val /= 10;}int left = 0;int right = str.size() - 1;while(left < right){std::swap(str[left++], str[right--]);}return str;}//字符串轉化為數字static int StrToInt(const std::string& str) {int ret = 0;for(auto e : str){ret = (ret * 10) + (e - '0');}return ret;}// 將給定的字符串用分隔符進行分割static void StringSplit(const std::string& str, const std::string& sep, std::vector<std::string>* result){size_t left = 0;size_t right = 0;while(right < str.size()){// 每次right都查找到下一個分隔符right = str.find(sep, left); if(right == std::string::npos){break;}//left到right之間即為要提取的數字result->push_back(str.substr(left, right - left));//left指向分割符下一個數字left = right + sep.size();}//會漏掉最后一個數字if(left < str.size()){result->push_back(str.substr(left));}}
};

??Protocol.hpp?協議處理相關頭文件。

#pragma once
#include <string>
#include"Util.hpp"
#include<iostream>
namespace My_protocol{// 協議的分隔符 這里我們自己設定為" "
const char* SEP= " ";
//分隔符長度
const int SEP_LEN = strlen(SEP);//對運算進行序列和反序列化
class Request{
public:Request(int x = 0, int y = 0, char op = '+'): _x(x), _y(y), _op(op){}// 序列化bool Serialization(std::string *outStr){*outStr = ""; // 清空std::string left = Util::IntToStr(_x);std::string right = Util::IntToStr(_y);*outStr = left + SEP + _op + SEP + right;return true;}// 反序列化bool Deserialization(const std::string &inStr){std::vector<std::string> result;Util::StringSplit(inStr, SEP, &result);// 協議規定:只允許存在兩個操作數和一個運算符if(result.size() != 3){return false;}// 規定:運算符只能為一個字符if(result[1].size() != 1){return false;}_x = Util::StrToInt(result[0]);_y = Util::StrToInt(result[2]);_op = result[1][0];return true;}~Request(){}public:int _x;int _y;char _op;
};//業務處理函數class Response{public:Response(int result = 0, int code = 0):_result(result), _code(code){}// 序列化bool Serialization(std::string *outStr) {*outStr = ""; // 清空std::string left = Util::IntToStr(_result);std::string right = Util::IntToStr(_code);*outStr = left + SEP + right;return true;}// 提取結果和錯誤碼bool Deserialization(const std::string &inStr){std::vector<std::string> result;Util::StringSplit(inStr, SEP, &result);if(result.size() != 2)return false;_result = Util::StrToInt(result[0]);_code = Util::StrToInt(result[1]);return true;}~Response(){}public:int _result; // 結果int _code;   // 錯誤碼};}

4.4 業務的實際處理

? server?中的業務處理函數由?CalcServer.cc?傳遞,規定業務處理函數的類型為?void(Request&, Response*)

Calculate()?函數 — 位于server.cc

#include "server.hpp"
#include "Protocol.hpp"#include <iostream>
#include <memory>
#include <functional>
#include <unordered_map>using namespace std;void Calculate(My_protocol::Request& req, My_protocol::Response* resp){// 這里只是簡單的計算而已int x = req._x;int y = req._y;char op = req._op;unordered_map<char, function<int()>> hash = {{'+', [&](){ return x + y; }},{'-', [&](){ return x - y; }},{'*', [&](){ return x * y; }},{'/', [&](){if(y == 0){resp->_code = 1;return 0;} return x / y; }},{'%', [&](){ if(y == 0){resp->_code = 2;return 0;}return x % y;}}};if(hash.count(op) == 0)resp->_code = 3;elseresp->_result = hash[op]();
}int main(){unique_ptr<My_Server::server> tsvr(new My_Server::server(Calculate));tsvr->Init();tsvr->Start();return 0;
}

既然?CalcServer?中傳入了?Calculate()?函數對象,server?類中就得接收并使用,也就是業務處理.

server.hpp?頭文件

#pragma once#include "Sock.hpp"
#include <iostream>
#include <string>
#include <pthread.h>
#include "Protocol.hpp"
#include<functional>namespace My_Server
{class server;// 線程所需要的信息類class ThreadDate{public:ThreadDate(int &sock, std::string &ip, uint16_t &port, server *ptsvr): _sock(sock), _ip(ip), _port(port), _ptsvr(ptsvr){}~ThreadDate(){}int _sock;std::string _ip;uint16_t _port;server *_ptsvr; // 回指指針};using func_t = std::function<void(My_protocol::Request&, My_protocol::Response*)>;class server{const static uint16_t default_port = 8088;private:// 線程的執行函數static void *threadRoutine(void *args){// 線程剝離pthread_detach(pthread_self());ThreadDate *td = static_cast<ThreadDate *>(args);td->_ptsvr->ServiceIO(td->_sock, td->_ip, td->_port);delete td;return nullptr;}// 進行IO服務的函數void ServiceIO(const int &sock, const std::string ip, const uint16_t &port){while (true){// 1.讀取數據std::string package; // 假設這是已經讀取到的數據包,格式為 "1 + 1"// 2.反序列化My_protocol::Request req;if (req.Deserialization(package) == false){logMessage(Warning, "Deserialization fail!");continue;}// 3.業務處理// TODOMy_protocol::Response resp; // 業務處理完成后得到的響應對象_func(req, &resp);// 4.序列化std::string sendMsg;resp.Serialization(&sendMsg);std::cout<<sendMsg<<std::endl;// 5.發送數據}}public:server(func_t fun,const uint16_t port = default_port): _port(port),_func(fun){}// 初始化服務器void Init(){_listen_sock.Socket();_listen_sock.Bind(_port);_listen_sock.Listen();}// 啟動服務器void Start(){while (true){std::string ip;uint16_t port;int sock = _listen_sock.Accept(&ip, &port);if (sock == -1){continue;}// 創建子線程,執行業務處理pthread_t tid;ThreadDate *td = new ThreadDate(sock, ip, port, this);pthread_create(&tid, nullptr, threadRoutine, td);}}~server(){_listen_sock.Close();}private:Sock _listen_sock; // 監聽套接字uint16_t _port;    // 服務器端口號func_t _func;      // 上層傳入的業務處理函數};
}

? 這就做好業務處理了,ServiceIO() 函數已經完成了 50% 的工作,接下來的重點是如何讀取和發送數據?

? TCP 協議是面向字節流的,這也就意味著數據在傳輸過程中可能會因為網絡問題,分為多次傳輸,這也就意味著我們可能無法將其一次性讀取完畢,需要制定一個策略,來確保數據全部遞達.

4.5 報頭處理

如何確認自己已經讀取完了所以數據?答案是提前知道目標數據的長度,邊讀取邊判斷

數據在發送時,是需要在前面添加 長度 這個信息的,通常將其稱為 報頭,而待讀取的數據稱為 有效載荷,報頭 和 有效載荷 的關系類似于快遞單與包裹的關系,前者是后者成功遞達的保障

最簡單的 報頭 內容就是 有效載荷 的長度

問題來了,如何區分?報頭?與?有效載荷?呢?

  • 當前可以確定的是,我們的報頭中只包含了長度這個信息
  • 可以通過添加特殊字符,如?\r\n?的方式進行區分
  • 后續無論有效載荷變成什么內容,都不影響我們通過報頭進行讀取

報頭處理屬于協議的一部分

所以在正式讀寫數據前,需要解決?報頭?的問題(收到數據后移除報頭,發送數據前添加報頭)

ReadPackage()?讀取函數 — 位于?Protocol.hpp?頭文件

在?Protocol.hpp?中完成報頭的添加和移除

#define HEAD_SEP "\r\n"
#define HEAD_SEP_LEN strlen(HEAD_SEP)// 添加報頭
void AddHeader(std::string& str){// 先計算出長度size_t len = str.size();std::string strLen = Util::IntToStr(len);// 再進行拼接str = strLen + HEAD_SEP + str;
}// 移除報頭
void RemoveHeader(std::string& str, size_t len){// len 表示有效載荷的長度str = str.substr(str.size() - len);
}

報頭+有效載荷需要通過?read()?或者?recv()?函數從網絡中讀取,并且需要邊讀取邊判斷。

ReadPackage()?讀取函數 — 位于?Protocol.hpp?頭文件

#define BUFF_SIZE 1024
// 讀取數據
int ReadPackage(int sock, std::string& inBuff, std::string* package){// 也可以使用 read 函數char buff[BUFF_SIZE];int n = recv(sock, buff, sizeof(buff) - 1, 0);if(n < 0)return -1; // 表示讀取失敗else if(n == 0)return 0; // 需要繼續讀取buff[n] = '\0';inBuff += buff;// 判斷 inBuff 中是否存在完整的數據包(報頭\r\n有效載荷)int pos = inBuff.find(HEAD_SEP);if(pos == std::string::npos)return -1;std::string strLen = inBuff.substr(0, pos); // 有效載荷的長度int packLen = strLen.size() + HEAD_SEP_LEN + Util::StrToInt(strLen); // 這是 報頭+分隔符+有效載荷 的總長度if(inBuff.size() < packLen)return -1;*package = inBuff.substr(0, packLen); // 獲取 報頭+分隔符+有效載荷 ,也就是數據包inBuff.erase(0, packLen); // 從緩沖區中取走字符串return Util::StrToInt(strLen);
}

完整代碼:
?

#pragma once
#include <string>
#include"Util.hpp"
#include<iostream>namespace My_protocol{#define HEAD_SEP "\r\n"
#define HEAD_SEP_LEN strlen(HEAD_SEP)// 添加報頭
void AddHeader(std::string& str){// 先計算出長度size_t len = str.size();std::string strLen = Util::IntToStr(len);// 再進行拼接str = strLen + HEAD_SEP + str;
}// 移除報頭
void RemoveHeader(std::string& str, size_t len){// len 表示有效載荷的長度str = str.substr(str.size() - len);
}#define BUFF_SIZE 1024
// 讀取數據
int ReadPackage(int sock, std::string& inBuff, std::string* package){// 也可以使用 read 函數char buff[BUFF_SIZE];int n = recv(sock, buff, sizeof(buff) - 1, 0);if(n < 0)return -1; // 表示讀取失敗else if(n == 0)return 0; // 需要繼續讀取buff[n] = '\0';inBuff += buff;// 判斷 inBuff 中是否存在完整的數據包(報頭\r\n有效載荷)int pos = inBuff.find(HEAD_SEP);if(pos == std::string::npos)return -1;std::string strLen = inBuff.substr(0, pos); // 有效載荷的長度int packLen = strLen.size() + HEAD_SEP_LEN + Util::StrToInt(strLen); // 這是 報頭+分隔符+有效載荷 的總長度if(inBuff.size() < packLen)return -1;*package = inBuff.substr(0, packLen); // 獲取 報頭+分隔符+有效載荷 ,也就是數據包inBuff.erase(0, packLen); // 從緩沖區中取走字符串return Util::StrToInt(strLen);
}// 協議的分隔符 這里我們自己設定為" "
const char* SEP= " ";
//分隔符長度
const int SEP_LEN = strlen(SEP);//對運算進行序列和反序列化
class Request{
public:Request(int x = 0, int y = 0, char op = '+'): _x(x), _y(y), _op(op){}// 序列化bool Serialization(std::string *outStr){*outStr = ""; // 清空std::string left = Util::IntToStr(_x);std::string right = Util::IntToStr(_y);*outStr = left + SEP + _op + SEP + right;return true;}// 反序列化bool Deserialization(const std::string &inStr){std::vector<std::string> result;Util::StringSplit(inStr, SEP, &result);// 協議規定:只允許存在兩個操作數和一個運算符if(result.size() != 3){return false;}// 規定:運算符只能為一個字符if(result[1].size() != 1){return false;}_x = Util::StrToInt(result[0]);_y = Util::StrToInt(result[2]);_op = result[1][0];return true;}~Request(){}public:int _x;int _y;char _op;
};//業務處理函數class Response{public:Response(int result = 0, int code = 0):_result(result), _code(code){}// 序列化bool Serialization(std::string *outStr) {*outStr = ""; // 清空std::string left = Util::IntToStr(_result);std::string right = Util::IntToStr(_code);*outStr = left + SEP + right;return true;}// 提取結果和錯誤碼bool Deserialization(const std::string &inStr){std::vector<std::string> result;Util::StringSplit(inStr, SEP, &result);if(result.size() != 2)return false;_result = Util::StrToInt(result[0]);_code = Util::StrToInt(result[1]);return true;}~Response(){}public:int _result; // 結果int _code;   // 錯誤碼};}

此時對于?ServiceIO()?函數來說,核心函數都已經準備好了,只差拼裝了。

ServiceIO()?函數 — 位于?server.hpp?頭文件中的server?類中

 // 進行IO服務的函數void ServiceIO(const int &sock, const std::string ip, const uint16_t &port){std::string inBuff;while (true){// 1.讀取數據std::string package; // 假設這是已經讀取到的數據包,格式為 "5\r\n1 + 1"int len = My_protocol::ReadPackage(sock, inBuff, &package);if (len < 0)break;else if (len == 0)continue;// 2.移除報頭My_protocol::RemoveHeader(package, len);// 3.反序列化My_protocol::Request req;if (req.Deserialization(package) == false){logMessage(Warning, "Deserialization fail!");continue;}// 4.業務處理My_protocol::Response resp; // 業務處理完成后得到的響應對象_func(req, &resp);// 5.序列化std::string sendMsg;resp.Serialization(&sendMsg);cout << sendMsg << endl;// 6.添加報頭My_protocol::AddHeader(sendMsg);// 7.發送數據send(sock, sendMsg.c_str(), sendMsg.size(), 0);}}

至此服務器編寫完畢,接下來就是進行客戶端的編寫了.

4.5 客戶端

Client.hpp?客戶端頭文件

#pragma once#include "Sock.hpp"
#include "Protocol.hpp"
#include "Log.hpp"
#include "Err.hpp"#include <iostream>
#include <string>
#include <unistd.h>namespace My_Client
{class client{public:client(const std::string& ip, const uint16_t& port):_server_ip(ip),_server_port(port){}void Init(){_sock.Socket();}void Start(){int i = 5;while(i > 0){if(_sock.Connect(_server_ip, _server_port) != -1)break;logMessage(Warning, "Connect Server Fail! %d", i--);sleep(1);}if(i == 0){logMessage(Fatal, "Connect Server Fail!");exit(CONNECT_ERR);}// 執行讀寫函數ServiceIO();}void ServiceIO(){while(true){std::string str;std::cout << "Please Enter:> ";std::getline(std::cin, str);// 1.判斷是否需要退出if(str == "quit")break;// 2.分割輸入的字符串My_protocol::Request req;[&](){std::string ops = "+-*/%";int pos = 0;for(auto e : ops){pos = str.find(e);if(pos != std::string::npos)break;}req._x = Util::StrToInt(str.substr(0, pos));req._y = Util::StrToInt(str.substr(pos + 1));req._op = str[pos];}();// 3.序列化std::string sendMsg;req.Serialization(&sendMsg);// 4.添加報頭My_protocol::AddHeader(sendMsg);// 5.發送數據send(_sock.GetSock(), sendMsg.c_str(), sendMsg.size(), 0);// 6.獲取數據std::string inBuff;std::string package;int len = 0;while(true){len = My_protocol::ReadPackage(_sock.GetSock(), inBuff, &package);if(len < 0)exit(READ_ERR);else if(len > 0)break;}// 7.移除報頭My_protocol::RemoveHeader(package, len);// 8.反序列化My_protocol::Response resp;if(resp.Deserialization(package) == false){logMessage(Warning, "Deserialization fail!");continue;}// 9.獲取結果std::cout << "The Result: " << resp._result << " " << resp._code << endl;}}~client(){_sock.Close();}private:Sock _sock;std::string _server_ip;uint16_t _server_port;};
}

client.cc?客戶端源文件

#include "client.hpp"#include <iostream>
#include <memory>using namespace std;int main()
{unique_ptr<My_Client::client> tclt(new My_Client::client("127.0.0.1", 8888));tclt->Init();tclt->Start();return 0;
}

五 測試

六 使用庫

事實上,序列化與反序列化?這種工作輪不到我們來做,因為有更好更強的庫,比如?JsonXMLProtobuf?等

比如我們就可以使用?Json?來修改程序

首先需要安裝?json-cpp?庫,如果是?CentOS7?操作系統的可以直接使用下面這條命令安裝

yum install -y jsoncpp-devel

安裝完成后,可以引入頭文件?<jsoncpp/json/json.h>

然后就可以在?Protocol.hpp?頭文件中進行修改了,如果想保留原來自己實現的?序列化與反序列化?代碼,可以利用?條件編譯?進行區分

Protocol.hpp?協議相關頭文件

#pragma once
#include "Util.hpp"#include <jsoncpp/json/json.h>
#include <string>
#include <vector>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>namespace My_protocol
{
// 協議的分隔符
#define SEP " "
#define SEP_LEN strlen(SEP)
#define HEAD_SEP "\r\n"
#define HEAD_SEP_LEN strlen(HEAD_SEP)
#define BUFF_SIZE 1024
// #define USER 1// 添加報頭void AddHeader(std::string& str){// 先計算出長度size_t len = str.size();std::string strLen = Util::IntToStr(len);// 再進行拼接str = strLen + HEAD_SEP + str;}// 移除報頭void RemoveHeader(std::string& str, size_t len){// len 表示有效載荷的長度str = str.substr(str.size() - len);}// 讀取數據int ReadPackage(int sock, std::string& inBuff, std::string* package){// 也可以使用 read 函數char buff[BUFF_SIZE];int n = recv(sock, buff, sizeof(buff) - 1, 0);if(n < 0)return -1; // 表示什么都沒有讀到else if(n == 0)return 0; // 需要繼續讀取buff[n] = 0;inBuff += buff;// 判斷 inBuff 中是否存在完整的數據包(報頭\r\n有效載荷)int pos = inBuff.find(HEAD_SEP);if(pos == std::string::npos)return 0;std::string strLen = inBuff.substr(0, pos); // 有效載荷的長度int packLen = strLen.size() + HEAD_SEP_LEN + Util::StrToInt(strLen); // 這是 報頭+分隔符+有效載荷 的總長度if(inBuff.size() < packLen)return 0;*package = inBuff.substr(0, packLen); // 獲取 報頭+分隔符+有效載荷 ,也就是數據包inBuff.erase(0, packLen); // 從緩沖區中取走字符串return Util::StrToInt(strLen);}class Request{public:Request(int x = 0, int y = 0, char op = '+'): _x(x), _y(y), _op(op){}// 序列化bool Serialization(std::string *outStr){*outStr = ""; // 清空
#ifdef USERstd::string left = Util::IntToStr(_x);std::string right = Util::IntToStr(_y);*outStr = left + SEP + _op + SEP + right;
#else// 使用 JsonJson::Value root;root["x"] = _x;root["op"] = _op;root["y"] = _y;Json::FastWriter writer;*outStr = writer.write(root);
#endifstd::cout << "序列化完成: " << *outStr << std::endl << std::endl;return true;}// 反序列化bool Deserialization(const std::string &inStr){
#ifdef USERstd::vector<std::string> result;Util::StringSplit(inStr, SEP, &result);// 協議規定:只允許存在兩個操作數和一個運算符if(result.size() != 3)return false;// 規定:運算符只能為一個字符if(result[1].size() != 1)return false;_x = Util::StrToInt(result[0]);_y = Util::StrToInt(result[2]);_op = result[1][0];
#else// 使用JsonJson::Value root;Json::Reader reader;reader.parse(inStr, root);_x = root["x"].asInt();_op = root["op"].asInt();_y = root["y"].asInt();
#endifreturn true;}~Request(){}public:int _x;int _y;char _op;};class Response{public:Response(int result = 0, int code = 0):_result(result), _code(code){}// 序列化bool Serialization(std::string *outStr){*outStr = ""; // 清空
#ifdef USERstd::string left = Util::IntToStr(_result);std::string right = Util::IntToStr(_code);*outStr = left + SEP + right;
#else// 使用 JsonJson::Value root;root["_result"] = _result;root["_code"] = _code;Json::FastWriter writer;*outStr = writer.write(root);
#endifstd::cout << "序列化完成: " << *outStr << std::endl << std::endl;return true;}// 反序列化bool Deserialization(const std::string &inStr){
#ifdef USERstd::vector<std::string> result;Util::StringSplit(inStr, SEP, &result);if(result.size() != 2)return false;_result = Util::StrToInt(result[0]);_code = Util::StrToInt(result[1]);
#else// 使用JsonJson::Value root;Json::Reader reader;reader.parse(inStr, root);_result = root["_result"].asInt();_code = root["_code"].asInt();
#endifreturn true;}~Response(){}public:int _result; // 結果int _code;   // 錯誤碼};
}

注意:?因為現在使用了?Json?庫,所以編譯代碼時需要指明其動態庫

.PHONY:all
all:server clientserver:server.ccg++ -o $@ $^ -std=c++11 -lpthread -ljsoncppclient:client.ccg++ -o $@ $^ -std=c++11 -ljsoncpp.PHONY:clean
clean:rm -rf server  client

使用了?Json?庫之后,序列化?后的數據會更加直觀,當然也更易于使用

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/13090.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/13090.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/13090.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

抖店商品詳情API接口(店鋪|標題|主圖|價格|SKU屬性等)

抖店商品詳情API接口(店鋪|標題|主圖|價格|SKU屬性等) 抖店商品詳情API接口是指通過調用抖音開放平臺提供的接口&#xff0c;獲取抖店上商品的詳細信息的方法。 抖店開放平臺提供了一系列的接口&#xff0c;可以用于獲取商品的基本信息、價格、庫存、銷量、評價等各種信息。以…

UIKit之圖片瀏覽器

功能需求 實現一個圖片瀏覽器&#xff0c;點擊左右按鈕可以切換背景圖&#xff0c;且更新背景圖對應的索引頁和圖片描述內容。 分析&#xff1a; 實現一個UIView的子類即可&#xff0c;該子類包含多個按鈕。 實現步驟&#xff1a; 使用OC語言&#xff0c;故創建cocoa Touch類…

數據庫的存儲過程、函數與觸發器

使用下面的場景來引入 1.創建表 CREATE DATABASE staff; USE staff; CREATE TABLE employee(id INT NOT NULL AUTO_INCREMENT,userName VARCHAR(255),birthDate DATE,idCard VARCHAR(255),loginName VARCHAR(255),PASSWORD VARCHAR(255),mobile VARCHAR(255),email VARCHAR(2…

開源連鎖收銀系統哪個好

針對開源連鎖收銀系統的選擇&#xff0c;商淘云是一個備受關注的候選。商淘云以其功能豐富、易于定制和穩定性等優勢&#xff0c;吸引了眾多企業和開發者的關注。下面將從四個方面探討商淘云開源連鎖收銀系統的優勢&#xff1a; 首先&#xff0c;商淘云提供了豐富的功能模塊。作…

如何查看SNMP設備的OID

什么是OID和MIB OID OID 代表對象標識符。 OID 唯一地標識 MIB 層次結構中的托管對象。 這可以被描述為一棵樹&#xff0c;其級別由不同的組織分配。MIB MIB&#xff08;管理信息基&#xff09;提供數字化OID到可讀文本的映射。 使用MIB Browser掃描OID 我的設備是一臺UPS SN…

【Uniapp小程序】onShareAppMessage異步處理請求完后再分享

分享按鈕 <button type"primary" open-type"share">保存并分享 </button>修改onShareAppMessage saveImage為promise方法 async onShareAppMessage() {const saveRes await saveImage();if (saveRes.code 200) {return {title: "tit…

每日兩題 / 236. 二叉樹的最近公共祖先 124. 二叉樹中的最大路徑和(LeetCode熱題100)

236. 二叉樹的最近公共祖先 - 力扣&#xff08;LeetCode&#xff09; dfs統計根節點到p&#xff0c;q節點的路徑&#xff0c;兩條路徑中最后一個相同節點就是公共祖先 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* …

windows部署騰訊tmagic-editor02-Runtime

創建editor項目 將上一教程中的hello-world復制過來&#xff0c;改名hello-editor 創建runtime項目 和hello-editor同級 pnpm create vite刪除src/components/HelloWorld.vue 按鈕需要用的ts types依賴 pnpm add tmagic/schema tmagic/stage實現runtime 將hello-editor中…

【C語言】5.C語言函數(2)

文章目錄 7.嵌套調?和鏈式訪問7.1 嵌套調?7.2 鏈式訪問 8.函數的聲明和定義8.1 單個?件8.2 多個?件8.3 static 和 extern8.3.1 static 修飾局部變量8.3.2 static 修飾全局變量8.3.3 static 修飾函數 7.嵌套調?和鏈式訪問 7.1 嵌套調? 嵌套調用就是函數之間的互相調用。…

Docker安裝Mosquitto

在物聯網項目中&#xff0c;我們經常用到MQTT協議&#xff0c;用MQTT協議做交互就需要部署一個MQTT服務&#xff0c;而mosquitto是一個常用的MQTT應用服務&#xff0c; Mosquitto是一個實現了消息推送協議MQTT v3.1的開源消息代理軟件。MQTT&#xff08;Message Queuing Teleme…

python的幾個關于文本文件的demo腳本

部分來自WeTab AI PRO 1.在文末添加一行文字 def add_endline(filename, texts): # 文本末尾增加一行with open(filename, a) as file:file.write(f\n{texts})file.close() 當使用 open() 函數打開文件時&#xff0c;第二個參數指定了文件的打開模式。常見的文件打開模式包…

【LeetCode】每日一題 2024_5_14 完成所有任務需要的最少輪數(哈希)

文章目錄 LeetCode&#xff1f;啟動&#xff01;&#xff01;&#xff01;題目&#xff1a;完成所有任務需要的最少輪數題目描述代碼與解題思路 每天進步一點點 LeetCode&#xff1f;啟動&#xff01;&#xff01;&#xff01; 題目&#xff1a;完成所有任務需要的最少輪數 題…

拿到測試點如何跑

首先你要知道你測試點文件的位置,然后你要創建一個接收結果的文件,將你代碼中的std::cin替換成infile,std::cout替換成outfile即可 #include <fstream> int main() {// 打開輸入文件std::ifstream infile("C:\\Users\\Downloads\\P4779_1.in");// 打開輸出文件…

OpenCV 圖像退化與增強

退化 濾波 img_averagingcv2.blur(img2,(3,3)) #均值濾波 img_median cv2.medianBlur(img2,3) #中值濾波高斯模糊 result cv2.GaussianBlur(source, (11,11), 0)高斯噪聲 def add_noise_Guass(img, mean0, var0.01): # 添加高斯噪聲img np.array(img / 255, dtypefloat…

麒麟 V10 安裝docker2

1. 查看系統版本 2.安裝docker-ce 添加源 yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 安裝docker yum install docker-ce --allowerasing 重啟docker systemctl start docker 3.安裝nvidia-container-runtime 添…

el-tooltip 提示框樣式修改?

【element-plus el-tooltip官網地址&#xff1a; Tooltip 文字提示 | Element Plus】 <el-tooltippopper-class"Tooltip":content"content"placement"top"effect"light" ><span class"content">{{ content }}&l…

【TypeScript的JSX簡介以及使用方法】

TypeScript 是 JavaScript 的一個超集&#xff0c;它添加了靜態類型檢查和面向對象編程的特性。JSX 是一種 JavaScript 的語法擴展&#xff0c;主要用于 React 組件的聲明性渲染。TypeScript 完美地支持 JSX&#xff0c;并允許你為 React 組件和它們的 props 添加類型注解。 T…

C中Mysql的基本api接口

一、初始化參數返回值 二、鏈接服務器三、執行SQL語句注意事項 四、獲取結果集4.1mysql_affected_rows和mysql_num_rows4.2mysql_store_result與mysql_free_result注意事項注意事項整體的工作流程 4.3mysql_use_result&#xff08;&#xff09;4.4mysql_field_count&#xff08…

001 側邊欄 地址增刪改查 默認地址代碼沒完善

文章目錄 user_index.htmlmyaccount_style.cssmyaccount_scripts.jsaddress_edit.htmlReceiverAddressReceiverAddressControllerReceiverAddressServiceImplIReceiverAddressServiceRFshopAppApplicationServletInitializerpom.xmlReceiverAddressMapper.xmlReceiverAddressMa…

文件存儲解決方案-阿里云OSS

文章目錄 1.菜單分級顯示問題1.問題引出1.蘋果燈&#xff0c;放到節能燈下面也就是id大于1272.查看菜單&#xff0c;并沒有出現蘋果燈3.放到燈具下面id42&#xff0c;就可以顯示 2.問題分析和解決1.判斷可能出現問題的位置2.找到遞歸返回樹形菜單數據的位置3.這里出現問題的原因…