计算机网络 -- 序列化与反序列化

一 协议的重要性

  我们都知道,在进行网络通信的过程中,通信的双方可以是不同的设备,不同的平台,不同的平台,比如说,手机用户和电脑用户进行通信,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.mzph.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…

【LeetCode】每日一题 2024_5_14 完成所有任务需要的最少轮数(哈希)

文章目录 LeetCode&#xff1f;启动&#xff01;&#xff01;&#xff01;题目&#xff1a;完成所有任务需要的最少轮数题目描述代码与解题思路 每天进步一点点 LeetCode&#xff1f;启动&#xff01;&#xff01;&#xff01; 题目&#xff1a;完成所有任务需要的最少轮数 题…

麒麟 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…

文件存储解决方案-阿里云OSS

文章目录 1.菜单分级显示问题1.问题引出1.苹果灯&#xff0c;放到节能灯下面也就是id大于1272.查看菜单&#xff0c;并没有出现苹果灯3.放到灯具下面id42&#xff0c;就可以显示 2.问题分析和解决1.判断可能出现问题的位置2.找到递归返回树形菜单数据的位置3.这里出现问题的原因…

算法学习笔记(5.0)-基于比较的高效排序算法-归并排序

##时间复杂度O(nlogn) 目录 ##时间复杂度O(nlogn) ##递归实现归并排序 ##原理 ##图例 ##代码实现 ##非递归实现归并排序 ##释 #代码实现 ##递归实现归并排序 ##原理 是一种基于分治策略的基础排序算法。 1.划分阶段&#xff1a;通过不断递归地将数组从中点处分开&…

Java 开发 框架安全:Spring 命令执行漏洞.(CVE-2022-22965)

什么叫 Spring 框架. Spring 框架是一个用于构建企业级应用程序的开源框架。它提供了一种全面的编程和配置模型&#xff0c;可以简化应用程序的开发过程。Spring 框架的核心特性包括依赖注入&#xff08;Dependency Injection&#xff09;、面向切面编程&#xff08;Aspect-Or…

DeepSpeed

文章目录 一、关于 DeepSpeed1、DeepSpeed 是什么2、深度学习训练和推理的极致速度和规模3、DeepSpeed 的四大创新支柱1&#xff09;DeepSpeed 训练2&#xff09;DeepSpeed 推理3&#xff09;DeepSpeed 压缩4&#xff09;DeepSpeed4Science 4、DeepSpeed 软件套件DeepSpeed 库推…

React 第二十七章 Hook useCallback

useCallback 是 React 提供的一个 Hook 函数&#xff0c;用于优化性能。它的作用是返回一个记忆化的函数&#xff0c;当依赖发生变化时&#xff0c;才会重新创建并返回新的函数。 在 React 中&#xff0c;当一个组件重新渲染时&#xff0c;所有的函数都会被重新创建。这可能会…

DTC 2024回顾丨zData X 多元数据库一体机:开创多元数据库时代部署新范式

导语 在2024“数据技术嘉年华”上&#xff0c;云和恩墨数据库一体机产品总经理刘宇在“数据库极致特性”专题论坛发表了题为《打造多元数据库部署新范式&#xff0c;引领一体化资源池创新之路》的演讲。他深入分析了国产数据库面临的挑战&#xff0c;并详细介绍了云和恩墨如何利…

5.10.1 Pre-Trained Image Processing Transformer

研究了低级计算机视觉任务&#xff08;例如去噪、超分辨率和去雨&#xff09;并开发了一种新的预训练模型&#xff0c;即图像处理变压器&#xff08;IPT&#xff09;。利用著名的 ImageNet 基准来生成大量损坏的图像对。 IPT 模型是在这些具有多头和多尾的图像上进行训练的。此…