可以做mc圖片的網(wǎng)站外鏈推廣
目錄
再談 "協(xié)議"
結(jié)構(gòu)化數(shù)據(jù)的傳輸
序列化和反序列化
網(wǎng)絡(luò)版計(jì)算器?
封裝套接字操作?
服務(wù)端代碼
服務(wù)進(jìn)程執(zhí)行例程
啟動(dòng)網(wǎng)絡(luò)版服務(wù)端
協(xié)議定制
客戶端代碼
代碼測(cè)試
使用JSON進(jìn)行序列化與反序列化
我們程序員寫的一個(gè)個(gè)解決我們實(shí)際問(wèn)題,滿足我們?nèi)粘P枨蟮木W(wǎng)絡(luò)程序,都是在應(yīng)用層。
再談 "協(xié)議"
我們之前講過(guò):協(xié)議是一種 "約定"。網(wǎng)絡(luò)協(xié)議是通信計(jì)算機(jī)雙方必須共同遵從的一組約定,只有通信計(jì)算機(jī)雙方都遵守相同的協(xié)議,計(jì)算機(jī)之間才能互相通信交流。
結(jié)構(gòu)化數(shù)據(jù)的傳輸
socket api的接口,在讀寫數(shù)據(jù)時(shí),都是按 "字符串" 的方式來(lái)發(fā)送接收的。
- 如果需要傳輸?shù)臄?shù)據(jù)是一個(gè)字符串,那么直接將這一個(gè)字符串發(fā)送到網(wǎng)絡(luò)當(dāng)中,此時(shí)對(duì)端也能從網(wǎng)絡(luò)當(dāng)中獲取到這個(gè)字符串。
- 但如果需要傳輸?shù)氖且恍┙Y(jié)構(gòu)化的數(shù)據(jù),此時(shí)就不能將這些數(shù)據(jù)一個(gè)個(gè)發(fā)送到網(wǎng)絡(luò)當(dāng)中。?
那么如果我們要傳輸一些"結(jié)構(gòu)化的數(shù)據(jù)" 怎么辦呢?
例如,我們需要實(shí)現(xiàn)一個(gè)服務(wù)器版的加法器。我們需要客戶端把要計(jì)算的兩個(gè)加數(shù)發(fā)過(guò)去,然后由服務(wù)器進(jìn)行計(jì)算,最后再把結(jié)果返回給客戶端。那么客戶端每次給服務(wù)端發(fā)送的請(qǐng)求數(shù)據(jù)當(dāng)中,就需要包括左操作數(shù)、右操作數(shù)以及對(duì)應(yīng)需要進(jìn)行的操作,此時(shí)客戶端要發(fā)送的就不是一個(gè)簡(jiǎn)單的字符串,而是一組結(jié)構(gòu)化的數(shù)據(jù)。
當(dāng)客戶端選擇將結(jié)構(gòu)化的數(shù)據(jù)逐一發(fā)送到網(wǎng)絡(luò)中,服務(wù)端接收數(shù)據(jù)的過(guò)程也會(huì)相應(yīng)地碎片化。每次從網(wǎng)絡(luò)中接收到一部分?jǐn)?shù)據(jù),服務(wù)端都需要對(duì)這些離散的信息進(jìn)行整理,嘗試將它們重新組合成原始的結(jié)構(gòu)化數(shù)據(jù)。這個(gè)過(guò)程既復(fù)雜又容易出錯(cuò),因?yàn)閿?shù)據(jù)可能在傳輸過(guò)程中出現(xiàn)丟失或順序混亂的情況。
因此,為了簡(jiǎn)化數(shù)據(jù)傳輸和處理的流程,客戶端通常會(huì)采取“打包”策略。打包意味著將多個(gè)相關(guān)的數(shù)據(jù)元素組合成一個(gè)整體,然后再進(jìn)行傳輸。這樣,服務(wù)端每次從網(wǎng)絡(luò)中接收到的都是一個(gè)完整的數(shù)據(jù)包,其中包含了所有必要的信息。
客戶端常見的“打包”方式主要有兩種:
約定方案一:
- 客戶端發(fā)送一個(gè)形如"1+1"的字符串;
- 這個(gè)字符串中有兩個(gè)操作數(shù), 都是整形;
- 兩個(gè)數(shù)字之間會(huì)有一個(gè)字符是運(yùn)算符, 運(yùn)算符只能是 + ;
- 數(shù)字和運(yùn)算符之間沒(méi)有空格;
客戶端能夠?qū)⒔Y(jié)構(gòu)化的數(shù)據(jù)編排成一個(gè)字符串格式,并通過(guò)網(wǎng)絡(luò)將其發(fā)送出去。當(dāng)服務(wù)端從網(wǎng)絡(luò)接收到這個(gè)字符串時(shí),它會(huì)采用與客戶端相同的解析方法,從而從這個(gè)字符串中提取出原始的結(jié)構(gòu)化數(shù)據(jù)。這樣的通信方式確保了數(shù)據(jù)的完整性和準(zhǔn)確性在客戶端和服務(wù)端之間的傳輸。
約定方案二:
- 定義結(jié)構(gòu)體來(lái)表示我們需要交互的信息;
- 發(fā)送數(shù)據(jù)時(shí)將這個(gè)結(jié)構(gòu)體按照一個(gè)規(guī)則轉(zhuǎn)換成字符串,接收到數(shù)據(jù)的時(shí)候再按照相同的規(guī)則把字符串轉(zhuǎn)化回結(jié)構(gòu)體;
- 這個(gè)過(guò)程叫做 "序列化" 和?“反序列化”
客戶端可以設(shè)計(jì)一個(gè)特定的結(jié)構(gòu)體,將需要交互的信息定義到這個(gè)結(jié)構(gòu)體當(dāng)中。在發(fā)送數(shù)據(jù)前,客戶端會(huì)利用序列化技術(shù)將這個(gè)數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換成一種統(tǒng)一的、可傳輸?shù)淖址蜃止?jié)流格式。當(dāng)服務(wù)端接收到這些數(shù)據(jù)后,它會(huì)利用反序列化技術(shù)將這個(gè)字符串或字節(jié)流還原成原始的數(shù)據(jù)結(jié)構(gòu)。通過(guò)這種方式,服務(wù)端可以輕松地提取出客戶端發(fā)送的信息,并進(jìn)行相應(yīng)的處理。這種序列化和反序列化的過(guò)程確保了數(shù)據(jù)在不同系統(tǒng)間的兼容性和可交換性。?
序列化和反序列化
- 序列化是將對(duì)象的狀態(tài)信息轉(zhuǎn)換為可以存儲(chǔ)或傳輸?shù)男问?#xff08;字節(jié)序列)的過(guò)程。
- 反序列化是把字節(jié)序列恢復(fù)為對(duì)象的過(guò)程。
OSI七層模型中的表示層的主要任務(wù)是將設(shè)備內(nèi)部特有的數(shù)據(jù)格式,即應(yīng)用層上的數(shù)據(jù)格式,轉(zhuǎn)換為符合網(wǎng)絡(luò)傳輸標(biāo)準(zhǔn)的格式。這種網(wǎng)絡(luò)標(biāo)準(zhǔn)數(shù)據(jù)格式通常是通過(guò)序列化過(guò)程得到的,使得數(shù)據(jù)能夠以一致和可理解的方式在網(wǎng)絡(luò)中進(jìn)行傳輸。
網(wǎng)絡(luò)版計(jì)算器?
封裝套接字操作?
由于服務(wù)端和客戶端都需要?jiǎng)?chuàng)建套接字,以及使用套接字完成一些固定的操作,因此我們實(shí)現(xiàn)一個(gè)簡(jiǎn)單的TCP套接字(socket)類的實(shí)現(xiàn),它封裝了套接字的基本操作:包括創(chuàng)建、綁定、監(jiān)聽、接受連接和連接。這樣服務(wù)端和客戶端都可以直接調(diào)用這些函數(shù)。封裝套接字操作可以使服務(wù)端和客戶端代碼更整潔、可重用,并減少重復(fù)代碼。
Socket.hpp
#pragma once#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"enum{SocketErr = 2,BindErr,ListenErr,AcceptErr
};#define backlog 10class Sock
{
public:Sock(){}~Sock(){}
public:void Socket(){sockfd_ = socket(AF_INET,SOCK_STREAM,0);if(sockfd_ < 0){lg(Fatal,"socker error, %s:%d",strerror(errno),errno);exit(SocketErr);} }void Bind(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(sockfd_,(struct sockaddr *)&local,sizeof(local)) < 0){lg(Fatal,"bind error, %s: %d", strerror(errno), errno);exit(BindErr);}}void Listen(){if(listen(sockfd_ , backlog) < 0){lg(Fatal,"listen error, %s: %d", strerror(errno), errno);exit(ListenErr);}}int Accept(std::string* clientip, std::uint16_t* clientport){struct sockaddr_in peer;socklen_t len = sizeof(peer);int newfd = accept(sockfd_,(struct sockaddr*)&peer,&len);if(newfd < 0){lg(Warning,"accept error, %s: %d", strerror(errno), errno);exit(AcceptErr);}char ipstr[64];inet_ntop(AF_INET,&(peer.sin_addr.s_addr),ipstr,sizeof(ipstr));*clientip = ipstr;*clientport = ntohs(peer.sin_port);return newfd;}bool Connect(const std::string &ip, const uint16_t &port){struct sockaddr_in peer;memset(&peer,0,sizeof(peer));peer.sin_family = AF_INET;peer.sin_port = htons(port);inet_pton(AF_INET,ip.c_str(),&(peer.sin_addr.s_addr));int n = connect(sockfd_,(struct sockaddr*)&peer,sizeof(peer));if(n == -1){std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;return false;}return true;}void Close(){close(sockfd_);}int FD(){return sockfd_;}private:int sockfd_;
};
服務(wù)端代碼
首先,我們需要初始化服務(wù)器,這包括三個(gè)關(guān)鍵步驟:
- 使用socket函數(shù)來(lái)創(chuàng)建一個(gè)新的套接字。
- 接著,通過(guò)bind函數(shù),我們將這個(gè)套接字綁定到一個(gè)特定的端口號(hào)上,這樣客戶端就可以通過(guò)這個(gè)端口與服務(wù)器建立連接。
- 然后,通過(guò)調(diào)用listen函數(shù),我們將套接字設(shè)置為監(jiān)聽狀態(tài),等待客戶端的連接請(qǐng)求。
服務(wù)器初始化完成后,就可以啟動(dòng)它了。啟動(dòng)后,服務(wù)器的主要任務(wù)是不斷地調(diào)用accept函數(shù),從監(jiān)聽套接字中接收新的連接請(qǐng)求。每當(dāng)成功接受到一個(gè)新連接時(shí),服務(wù)器會(huì)創(chuàng)建一個(gè)新的進(jìn)程。這個(gè)新進(jìn)程將負(fù)責(zé)為該客戶端提供計(jì)算服務(wù),確保每個(gè)客戶端都能得到及時(shí)且獨(dú)立的響應(yīng)。?
TcpServer.hpp?
#pragma once#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <functional>
#include "Socket.hpp"//這允許我們?yōu)?TCP 服務(wù)器提供一個(gè)自定義的回調(diào)函數(shù),該函數(shù)處理從客戶端接收到的數(shù)據(jù)。
using func_t = std::function<std::string(std::string &package)>; //std::function對(duì)象,該對(duì)象接受一個(gè) std::string 引用作為參數(shù)并返回一個(gè) std::stringclass TcpServer
{
public:TcpServer(uint16_t port,func_t callback):port_(port),callback_(callback){}//初始化tcp服務(wù)器bool InitServer(){listensock_.Socket();listensock_.Bind(port_);listensock_.Listen();lg(Info,"init server .... done");return true;}//啟動(dòng)服務(wù)器void Start(){ signal(SIGCHLD, SIG_IGN);//忽略了 SIGCHLD 和 SIGPIPE 信號(hào),當(dāng)子進(jìn)程終止或管道寫入失敗時(shí),服務(wù)器不會(huì)接收到這些信號(hào)。signal(SIGPIPE, SIG_IGN);//無(wú)限循環(huán),在該循環(huán)中,它嘗試接受新的客戶端連接。對(duì)于每個(gè)新的連接,它創(chuàng)建一個(gè)子進(jìn)程來(lái)處理該連接。//并使用之前提供的回調(diào)函數(shù)來(lái)處理這些數(shù)據(jù)。如果回調(diào)函數(shù)返回一個(gè)非空字符串,那么該字符串將被發(fā)送回客戶端。while (true){std::string clientip;uint16_t clientport;int sockfd = listensock_.Accept(&clientip,&clientport);if(sockfd < 0)continue;// 返回繼續(xù)監(jiān)聽 lg(Info,"accept a new link, sockfd: %d, clientip: %s, clientport: %d",sockfd, clientip, clientport);// 提供服務(wù)if(fork() == 0)//{listensock_.Close();//在子進(jìn)程中,服務(wù)器不再需要監(jiān)聽套接字,調(diào)用 listensock_.Close(); 關(guān)閉監(jiān)聽套接字,釋放相關(guān)的系統(tǒng)資源。 std::string inbuffer_stream;//數(shù)據(jù)計(jì)算while (true){char buffer[1280];ssize_t n = read(sockfd,buffer,sizeof(buffer));if(n > 0) {buffer[n] = 0;inbuffer_stream += buffer;while(true){std::string info = callback_(inbuffer_stream);if(info.empty())break;write(sockfd, info.c_str(), info.size());}}else if(n == 0)break;elsebreak;} exit(0);//}close(sockfd);} }~TcpServer(){}
private:uint16_t port_;Sock listensock_;func_t callback_;
};
說(shuō)明一下:?
- 當(dāng)前服務(wù)器采用的是多進(jìn)程的方案,對(duì)于每個(gè)新的連接,創(chuàng)建一個(gè)子進(jìn)程來(lái)處理該連接。
-
提供的回調(diào)函數(shù)來(lái)處理客戶端發(fā)送過(guò)來(lái)的數(shù)據(jù)。如果回調(diào)函數(shù)返回一個(gè)非空字符串,那么該字符串將被發(fā)送回客戶端。
服務(wù)進(jìn)程執(zhí)行例程
當(dāng)服務(wù)端調(diào)用accept函數(shù)獲取到新連接并創(chuàng)建新進(jìn)程后,該線程就需要為該客戶端提供計(jì)算服務(wù),此時(shí)該進(jìn)程需要先讀取客戶端發(fā)來(lái)的計(jì)算請(qǐng)求,然后進(jìn)行對(duì)應(yīng)的計(jì)算操作,如果客戶端發(fā)來(lái)的計(jì)算請(qǐng)求存在除0、模0、非法運(yùn)算等問(wèn)題,就將響應(yīng)結(jié)構(gòu)體當(dāng)中的狀態(tài)字段對(duì)應(yīng)設(shè)置為1、2、3即可。
ServerCal.hpp?
#pragma once
#include <iostream>
#include "Protocol.hpp"enum
{DivZero = 1,ModZero,Other_Oper
};class ServerCal
{
public:ServerCal(){}Response CalculatorHelper(const Request &req){Response resp(0, 0);switch (req.op){case '+':resp.result = req.x + req.y;break;case '-':resp.result = req.x - req.y;break;case '*':resp.result = req.x * req.y;break;case '/':{if (req.y != 0)resp.result = req.x / req.y;elseresp.code = DivZero;}break;case '%':{if (req.y != 0)resp.result = req.x % req.y;elseresp.code = ModZero;}break;default:resp.code = Other_Oper;break;}return resp;}// "len"\n"10 + 20"\nstd::string Calculator(std::string &package){std::string content;bool r = Decode(package, &content); // "len"\n"10 + 20"\nif (!r)return "";// "10 + 20"Request req;r = req.Deserialize(content); // 反序列化完req中的變量就拿到值了if (!r)return "";content = ""; //清空Response resp = CalculatorHelper(req); // result=30 code=0;// 計(jì)算完進(jìn)行序列化resp.Serialize(&content);content = Encode(content);return content;}~ServerCal(){}
};
啟動(dòng)網(wǎng)絡(luò)版服務(wù)端
ServerCal.cpp
前面我們?cè)赥cpServer.hpp封裝了服務(wù)器初始化和啟動(dòng)服務(wù)器函數(shù)的類,以及ServerCal類實(shí)現(xiàn)網(wǎng)絡(luò)版計(jì)算器的類執(zhí)行例程。下面我們實(shí)現(xiàn)一個(gè)ServerCal.cpp來(lái)啟動(dòng)網(wǎng)絡(luò)版服務(wù)器,只有要調(diào)用前面兩個(gè)類實(shí)現(xiàn)的接口即可。
- 從命令行參數(shù)獲取端口號(hào)
- 創(chuàng)建ServerCal實(shí)例
- 綁定ServerCal的Calculator方法
- 創(chuàng)建TcpServer實(shí)例,并將綁定的Calculator方法和端口號(hào)作為參數(shù)傳遞給它。
- 調(diào)用InitServer方法初始化服務(wù)器
- 最后調(diào)用Start方法啟動(dòng)服務(wù)器。
#include "TcpServer.hpp"
#include "ServerCal.hpp"void Usage(const std::string &proc)
{std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);ServerCal cal;//std::bind是C++標(biāo)準(zhǔn)庫(kù)中的一個(gè)函數(shù)模板,它可以將一個(gè)可調(diào)用對(duì)象(如函數(shù)、lambda函數(shù)或成員函數(shù)指針)與其參數(shù)綁定,生成一個(gè)新的可調(diào)用對(duì)象。//&ServerCal::Calculator是ServerCal類中的Calculator成員函數(shù)的指針。//&cal是ServerCal類的一個(gè)實(shí)例的地址,該實(shí)例用于調(diào)用Calculator成員函數(shù)。//std::placeholders::_1是一個(gè)占位符,它表示bind生成的新可調(diào)用對(duì)象接受的第一個(gè)參數(shù)將傳遞給Calculator成員函數(shù)作為它的第一個(gè)參數(shù)。TcpServer *tsvp = new TcpServer(port,std::bind(&ServerCal::Calculator,&cal,std::placeholders::_1));tsvp->InitServer();tsvp->Start();return 0;
}
協(xié)議定制
為了實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)版的計(jì)算器,確保通信雙方遵循共同的規(guī)則和約定是至關(guān)重要的。這就需要我們制定一套簡(jiǎn)明的協(xié)議。數(shù)據(jù)的交互通常涉及請(qǐng)求數(shù)據(jù)和響應(yīng)數(shù)據(jù),因此需要分別定義兩者的結(jié)構(gòu)。在實(shí)現(xiàn)層面,C++允許通過(guò)類來(lái)組織代碼和數(shù)據(jù),但同樣也可以使用更簡(jiǎn)單的結(jié)構(gòu)體來(lái)定義數(shù)據(jù)結(jié)構(gòu)??紤]到簡(jiǎn)潔性和直接性,這里我們選擇使用結(jié)構(gòu)體來(lái)定義請(qǐng)求和響應(yīng)的數(shù)據(jù)格式。
因此我們需要設(shè)計(jì)一個(gè)請(qǐng)求結(jié)構(gòu)體,用于封裝從客戶端發(fā)送到服務(wù)器的計(jì)算請(qǐng)求信息,以及一個(gè)響應(yīng)結(jié)構(gòu)體,用于封裝服務(wù)器處理完請(qǐng)求后返回給客戶端的結(jié)果。通過(guò)這種方式,我們可以確保通信雙方按照預(yù)定的格式發(fā)送和接收數(shù)據(jù),從而實(shí)現(xiàn)網(wǎng)絡(luò)計(jì)算器的功能。
- 請(qǐng)求結(jié)構(gòu)體中需要包括兩個(gè)操作數(shù),以及對(duì)應(yīng)需要進(jìn)行的操作。
- 響應(yīng)結(jié)構(gòu)體中需要包括一個(gè)計(jì)算結(jié)果,除此之外,響應(yīng)結(jié)構(gòu)體中還需要包括一個(gè)狀態(tài)字段,表示本次計(jì)算的狀態(tài),因?yàn)榭蛻舳税l(fā)來(lái)的計(jì)算請(qǐng)求可能是無(wú)意義的。
- 請(qǐng)求結(jié)構(gòu)體和響應(yīng)結(jié)構(gòu)體當(dāng)中都封裝了序列化函數(shù)和反序列化函數(shù)。
- 我們?cè)陬愅庠O(shè)置了編碼函數(shù)和解碼函數(shù)
#pragma once#include <iostream>
#include <string>const std::string blank_space_sep = " ";
const std::string protocol_sep = "\n";std::string Encode(std::string &content)
{std::string package = std::to_string(content.size());package += protocol_sep;package += content;package += protocol_sep;return package;
}// "len"\n"x op y"\nXXXXXX
// "protocolnumber"\n"len"\n"x op y"\nXXXXXX
bool Decode(std::string &package, std::string *content)
{size_t pos = package.find(protocol_sep);if(pos == std::string::npos) return false;std::string len_str = package.substr(0,pos);std::size_t len = std::stoi(len_str);std::size_t total_len = len_str.size() + len + 2;if(package.size() < total_len) return false;//傳入的序列化字符串沒(méi)有達(dá)到報(bào)頭提供的字符串長(zhǎng)度*content = package.substr(pos+1,len);package.erase(0,total_len);//return true;
}class Request
{
public:Request(int data1,int data2,char oper):x(data1),y(data2),op(oper){}Request()//{}
public:bool Serialize(std::string *out){// 構(gòu)建報(bào)文的有效載荷// struct => string, "x op y"std::string s = std::to_string(x);s += blank_space_sep;s += op;s += blank_space_sep;s += std::to_string(y);*out = s;return true;}bool Deserialize(const std::string &in)// "x op y"{std::size_t left = in.find(blank_space_sep);if(left == std::string::npos)return false;std::string part_x = in.substr(0,left);std::size_t right = in.rfind(blank_space_sep);if(right == std::string::npos)return false;std::string part_y = in.substr(right);if(left+2 != right)return false;op = in[left + 1];x = std::stoi(part_x);x = std::stoi(part_y);return true;}void DebugPrint(){std::cout << "新請(qǐng)求構(gòu)建完成: " << x << op << y << "=?" << std::endl;}public://x op yint x;int y;char op;//+ - * / %
};class Response
{
public:Response(int res,int c):result(res),code(c){}Response(){}
public:bool Serialize(std::string *out){// "result code"// 構(gòu)建報(bào)文的有效載荷std::string s = std::to_string(result);s += blank_space_sep;s += std::to_string(code);*out = s;return true;}bool Deserialize(const std::string &in){std::size_t pos = in.find(blank_space_sep);if(pos == std::string::npos) return false;std::string part_left = in.substr(0,pos);std::string part_right = in.substr(pos+1);result = std::stoi(part_left);code = std::stoi(part_right);return true;}void DebugPrint(){std::cout << "結(jié)果響應(yīng)完成, result: " << result << ", code: "<< code << std::endl;}public:int result;int code;//0表示結(jié)果是可信的;否則!0具體是幾,表明對(duì)應(yīng)的錯(cuò)誤原因
};
請(qǐng)求結(jié)構(gòu)體
- 序列化函數(shù)用于構(gòu)建報(bào)文的有效載荷,將 `Request` 對(duì)象轉(zhuǎn)換為一個(gè)字符串。它首先將 `x` 和 `y` 轉(zhuǎn)換為字符串,并使用空格和操作符 `op` 將它們連接在一起。例如,如果 `x` 是 5,`y` 是 3,并且 `op` 是 `+`,則生成的字符串將是 `"5 + 3"`
- 反序列化函數(shù)嘗試從一個(gè)字符串中恢復(fù)一個(gè) `Request` 對(duì)象。它首先查找空格和操作符來(lái)分隔 `x`、`op` 和 `y`。然后,它將這些部分轉(zhuǎn)換回它們的原始類型,并檢查字符串的格式是否正確。如果一切正常,它將更新 `Request` 對(duì)象的 `x`、`y` 和 `op`。
響應(yīng)結(jié)構(gòu)體
- Serialize 函數(shù)接受一個(gè)指向 std::string 的指針 out,并將 result 和 code 成員變量的值轉(zhuǎn)換為字符串,然后用空格(blank_space_sep)分隔它們,并將結(jié)果字符串存儲(chǔ)在 out 所指向的位置。函數(shù)總是返回 true,表示序列化操作總是成功的。
- Deserialize 函數(shù)接受一個(gè)常量字符串引用 in,并嘗試從中解析出 result 和 code 的值。它首先查找空格的位置,然后提取空格前后的兩個(gè)子字符串,并將它們分別轉(zhuǎn)換為整數(shù)來(lái)更新 result 和 code 的值。如果字符串中沒(méi)有找到空格,函數(shù)返回 false,否則返回 true。
編碼函數(shù) Encode:
- 函數(shù)接受一個(gè)字符串 content,并返回一個(gè)編碼后的字符串 package。
- 首先,將 `content` 的大小(長(zhǎng)度)轉(zhuǎn)換為字符串并添加到 `package`。 ?
- 然后,添加一個(gè)換行符。 ?
- 接著,添加原始的 `content`。 ?
- 最后,再添加一個(gè)換行符。這樣,編碼后的字符串格式是:`"length\ncontent\n"`。
解碼函數(shù) Decode:
- 這個(gè)函數(shù)嘗試從給定的 package 字符串中解碼出 content。它首先查找換行符來(lái)確定 content 的長(zhǎng)度,并檢查 package 是否包含足夠的數(shù)據(jù)。如果成功,它會(huì)提取 content 并從 package 中刪除已解碼的部分。
注意:
- 編碼函數(shù)和解碼函數(shù)是多個(gè)結(jié)構(gòu)體或類都可能需要的共同操作,因此將它們放在類外作為獨(dú)立的函數(shù)。這種做法不僅增強(qiáng)了代碼的可重用性,還方便了協(xié)議的編碼和解碼邏輯的更換。通過(guò)將編碼和解碼邏輯與具體的數(shù)據(jù)結(jié)構(gòu)分離,我們可以在不修改數(shù)據(jù)結(jié)構(gòu)定義的情況下更換編碼和解碼的實(shí)現(xiàn),從而實(shí)現(xiàn)了更好的模塊化和可擴(kuò)展性。
規(guī)定狀態(tài)字段對(duì)應(yīng)的含義:
- 狀態(tài)字段為0,表示計(jì)算成功。
- 狀態(tài)字段為1,表示出現(xiàn)除0錯(cuò)誤。
- 狀態(tài)字段為2,表示出現(xiàn)模0錯(cuò)誤。
- 狀態(tài)字段為3,表示非法計(jì)算。
此時(shí)我們就完成了協(xié)議的設(shè)計(jì),但需要注意,只有當(dāng)響應(yīng)結(jié)構(gòu)體當(dāng)中的狀態(tài)字段為0時(shí),計(jì)算結(jié)果才是有意義的,否則計(jì)算結(jié)果無(wú)意義。?
客戶端代碼
客戶端首先也需要進(jìn)行初始化:
- 調(diào)用socket函數(shù),創(chuàng)建套接字。
客戶端初始化完畢后需要調(diào)用connect函數(shù)連接服務(wù)端,當(dāng)連接服務(wù)端成功后,客戶端就可以向服務(wù)端發(fā)起計(jì)算請(qǐng)求了。這里可以讓用戶輸入兩個(gè)操作數(shù)和一個(gè)操作符構(gòu)建一個(gè)計(jì)算請(qǐng)求,然后將該請(qǐng)求發(fā)送給服務(wù)端。而當(dāng)服務(wù)端處理完該計(jì)算請(qǐng)求后,會(huì)對(duì)客戶端進(jìn)行響應(yīng),因此客戶端發(fā)送完請(qǐng)求后還需要讀取服務(wù)端發(fā)來(lái)的響應(yīng)數(shù)據(jù)。
客戶端在向服務(wù)端發(fā)送或接收數(shù)據(jù)時(shí),可以使用write或read函數(shù)進(jìn)行發(fā)送或接收(也可以使用send或recv函數(shù)對(duì)應(yīng)進(jìn)行發(fā)送或接收。)
- 連接服務(wù)器
由于我們前面封裝了TCP套接字(socket)類的實(shí)現(xiàn),這里我們之間調(diào)用我們封裝的接口即可,下面是客戶端代碼:
客戶端代碼:
#include <iostream>
#include <string>
#include <time.h>
#include <assert.h>
#include "Protocol.hpp"
#include"Socket.hpp"void Usage(const std::string &proc)
{std::cout << "\nUsage: " << proc << " serverip serverport\n" << std::endl;
}// ./clientcal ip port
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);//創(chuàng)建套接字Sock sockfd;sockfd.Socket();//鏈接服務(wù)器bool r = sockfd.Connect(serverip,serverport);if(!r) return 1;srand(time(nullptr)^getpid());int cnt = 1;const std::string opers = "+-*/%=-=&^";std::string inbuffer_stream;while(cnt <= 10){//準(zhǔn)備數(shù)據(jù)std::cout << "===============第" << cnt << "次測(cè)試....., " << "===============" << std::endl;int x = rand() % 100 + 1;usleep(1000);int y = rand() % 100 + 1;usleep(1000);char op = opers[rand() % opers.size()];Request req(x,y,op);req.DebugPrint();//客戶端發(fā)送請(qǐng)求std::string packge;req.Serialize(&packge);packge = Encode(packge);write(sockfd.FD(),packge.c_str(),packge.size());//接受請(qǐng)求響應(yīng)char buffer[128];ssize_t n = read(sockfd.FD(),buffer,sizeof(buffer));// 我們也無(wú)法保證我們能讀到一個(gè)完整的報(bào)文if(n > 0){buffer[n] = 0;inbuffer_stream += buffer;// "len"\n"result code"\nstd::cout << inbuffer_stream << std::endl;std::string content;bool r = Decode(inbuffer_stream,&content);// "result code"assert(r);Response resp;r = resp.Deserialize(content);assert(r);resp.DebugPrint();}std::cout << "=================================================" << std::endl;sleep(1);cnt++;}sockfd.Close();return 0;
}
代碼測(cè)試
運(yùn)行服務(wù)端后再讓客戶端連接服務(wù)端,此時(shí)服務(wù)端就會(huì)對(duì)客戶端發(fā)來(lái)的計(jì)算請(qǐng)求進(jìn)行處理,并會(huì)將計(jì)算后的結(jié)果響應(yīng)給客戶端。
我們看到如果客戶端要進(jìn)行除0、模0、非法運(yùn)算,在服務(wù)端識(shí)別后就會(huì)按照約定對(duì)應(yīng)將響應(yīng)數(shù)據(jù)的狀態(tài)碼設(shè)置為1、2、3,此時(shí)響應(yīng)狀態(tài)碼為非零,因此在客戶端打印出來(lái)的計(jì)算結(jié)果就是沒(méi)有意義的。
此時(shí)我們就以這樣一種方式約定出了一套應(yīng)用層的簡(jiǎn)單的網(wǎng)絡(luò)計(jì)算器,這就叫做協(xié)議。?
使用JSON進(jìn)行序列化與反序列化
上面我們進(jìn)行序列化和反序列化是自己進(jìn)行協(xié)議定制,其實(shí)我們也可以用JSON或者Protobuf進(jìn)行數(shù)據(jù)的序列化和反序列化操作。
JSON (JavaScript Object Notation) 和 Protobuf (Protocol Buffers) 都是數(shù)據(jù)序列化格式,但它們?cè)谠O(shè)計(jì)目標(biāo)、性能、使用場(chǎng)景等方面有所不同。
下面我們主要來(lái)介紹一下使用JSON進(jìn)行序列化和反序列化操作:
JSON
- 設(shè)計(jì)目標(biāo):JSON 主要用于人類可讀性和易于編寫。它是基于 JavaScript 的子集,但不僅限于 JavaScript 使用。
- 性能:JSON 的解析和序列化速度相對(duì)較慢,尤其是對(duì)于大型數(shù)據(jù)結(jié)構(gòu)。
- 使用場(chǎng)景:JSON 廣泛用于 API 通信、配置文件、Web 存儲(chǔ)等場(chǎng)景,因?yàn)樗子陂喿x和編寫,并且跨語(yǔ)言、跨平臺(tái)。
- 在使用 JsonCpp 之前,你需要確保已經(jīng)安裝了這個(gè)庫(kù)。
sudo yum install -y jsoncpp-devel
- 安裝完成后,項(xiàng)目中加入頭文件#include <jsoncpp/json/json.h>
- 編譯命令后面加上-ljsoncpp
下面是一個(gè)簡(jiǎn)單的示例,展示了如何使用 JsonCpp 來(lái)解析和生成 JSON 數(shù)據(jù):
#include <iostream>
#include <jsoncpp/json/json.h>
#include <unistd.h>int main() { // 創(chuàng)建一個(gè) JSON 對(duì)象 Json::Value root; // 將用于存儲(chǔ) JSON 數(shù)據(jù)的根對(duì)象 root["x"] = 40; root["y"] = 30; root["op"] = '+'; root["desc"] = "this is a + oper"; // 序列化:將 JSON 對(duì)象轉(zhuǎn)換為字符串 Json::FastWriter writer; //Json::StyledWriter writer; //StyledWriter比Fastwriter多加了\n,可讀性比較好std::string jsonString = writer.write(root); // 輸出 JSON 字符串 std::cout << "JSON string: " << jsonString << std::endl; sleep(3);// 反序列化:從字符串解析 JSON Json::Value v; Json::Reader Reader; Reader.parse(jsonString,v);// 訪問(wèn) JSON 對(duì)象中的值 int x = v["x"].asInt(); int y = v["y"].asInt(); char op = v["op"].asInt(); std::string desc = v["desc"].asString(); // 輸出解析后的值 std::cout << x <<std::endl; std::cout << y <<std::endl; std::cout << op <<std::endl; std::cout << desc <<std::endl; return 0;
}
運(yùn)行結(jié)果:
有了上面對(duì)JSON基本使用的理解后,下面我們?cè)诰W(wǎng)絡(luò)版計(jì)算器的協(xié)議定制的代碼中增加JSON方式的序列化與反序列化:
我們根據(jù)是否定義了MySelf宏,來(lái)選擇使用兩種序列化方式:
#pragma once#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>// #define MySelf 1const std::string blank_space_sep = " ";
const std::string protocol_sep = "\n";std::string Encode(std::string &content)
{std::string package = std::to_string(content.size());package += protocol_sep;package += content;package += protocol_sep;return package;
}// "len"\n"x op y"\nXXXXXX
// "protocolnumber"\n"len"\n"x op y"\nXXXXXX
bool Decode(std::string &package, std::string *content)
{size_t pos = package.find(protocol_sep);if(pos == std::string::npos) return false;std::string len_str = package.substr(0,pos);std::size_t len = std::stoi(len_str);std::size_t total_len = len_str.size() + len + 2;if(package.size() < total_len) return false;//傳入的序列化字符串沒(méi)有達(dá)到報(bào)頭提供的字符串長(zhǎng)度*content = package.substr(pos+1,len);package.erase(0,total_len);//return true;
}// json, protobuf
class Request
{
public:Request(int data1,int data2,char oper):x(data1),y(data2),op(oper){}Request()//{}
public:bool Serialize(std::string *out){
#ifdef MySelf// 構(gòu)建報(bào)文的有效載荷// struct => string, "x op y"std::string s = std::to_string(x);s += blank_space_sep;s += op;s += blank_space_sep;s += std::to_string(y);*out = s;return true;
#elseJson::Value root;root["x"] = x;root["y"] = y;root["op"] = op;// Json::FastWriter w;Json::StyledWriter w;*out = w.write(root);return true;
#endif}bool Deserialize(const std::string &in)// "x op y"{
#ifdef MySelfstd::size_t left = in.find(blank_space_sep);if(left == std::string::npos)return false;std::string part_x = in.substr(0,left);std::size_t right = in.rfind(blank_space_sep);if(right == std::string::npos)return false;std::string part_y = in.substr(right);if(left+2 != right)return false;op = in[left + 1];x = std::stoi(part_x);y = std::stoi(part_y);return true;
#elseJson::Value root;Json::Reader r;r.parse(in,root);x = root["x"].asInt();y = root["y"].asInt();char op = root["op"].asInt();return true;
#endif}void DebugPrint(){std::cout << "新請(qǐng)求構(gòu)建完成: " << x << op << y << "=?" << std::endl;}public://x op yint x;int y;char op;//+ - * / %
};class Response
{
public:Response(int res,int c):result(res),code(c){}Response(){}
public:bool Serialize(std::string *out){
#ifdef MySelf// "result code"// 構(gòu)建報(bào)文的有效載荷std::string s = std::to_string(result);s += blank_space_sep;s += std::to_string(code);*out = s;return true;
#elseJson::Value root;root["result"] = result;root["code"] = code;// Json::FastWriter w;Json::StyledWriter w;*out = w.write(root);return true;
#endif}bool Deserialize(const std::string &in){
#ifdef MySelfstd::size_t pos = in.find(blank_space_sep);if(pos == std::string::npos) return false;std::string part_left = in.substr(0,pos);std::string part_right = in.substr(pos+1);result = std::stoi(part_left);code = std::stoi(part_right);return true;
#elseJson::Value root;Json::Reader r;r.parse(in,root);int result = root["result"].asInt();int code = root["code"].asInt();
#endif}void DebugPrint(){std::cout << "結(jié)果響應(yīng)完成, result: " << result << ", code: "<< code << std::endl;}public:int result;int code;//0表示結(jié)果是可信的;否則!0具體是幾,表明對(duì)應(yīng)的錯(cuò)誤原因
};
makefile文件:
編譯時(shí)要加上-ljsoncpp選項(xiàng),我們也可以在makefile文件中進(jìn)行宏定義Myself
.PHONY:all
all:servercal clientcal# Flag= -DMySelf=1
Flag= #-DMySelf=1
Lib=-ljsoncppservercal:ServerCal.cppg++ -o $@ $^ -std=c++11 $(Lib) $(Flag)
clientcal:ClientCal.cppg++ -o $@ $^ -std=c++11 $(Lib) $(Flag).PHONY:clean
clean:rm -f servercal clientcal
代碼測(cè)試:
以上我們就成功用Json實(shí)現(xiàn)了數(shù)據(jù)序列化和反序列化。