当前位置: 首页 > news >正文

网络基础『 序列化与反序列化』

🔭个人主页: 北 海
🛜所属专栏: Linux学习之旅、神奇的网络世界
💻操作环境: CentOS 7.6 阿里云远程服务器

成就一亿技术人


文章目录

  • 🌤️前言
  • 🌦️正文
    • 1.协议的重要性
    • 2.什么是序列化与反序列化?
    • 3.实现相关程序
    • 4.封装socket相关操作
    • 5.服务器
    • 6.序列化与反序列
    • 7.工具类
    • 8.业务处理
    • 9.报头处理
    • 10.客户端
    • 11.测试
    • 12.使用库
  • 🌨️总结


🌤️前言

本文将介绍如何使用C++实现简单的服务器和客户端通信,并重点讲解序列化与反序列化的概念和实现。这篇文章将深入探究数据在网络传输中的转换过程,以及如何在C++中应用这些技术


🌦️正文

1.协议的重要性

假设张三在路上遇到了一位外国人 Jack,这位外国朋友急于寻找厕所,对张三进行了一波 英语 输出,这可难到了张三,因为他 英语 可谓是十分差劲,母语的差异导致双方无法正常交流,信息也无法传达,张三急中生智,打开了手机上的 同声传译功能,可以将信息转换为对方能听懂的语言,在工具的帮助之下外国友人最终知晓了厕所的位置

在这个故事中,张三和外国人 Jack 就是两台主机,同声传译 这个功能可以看做一种 协议(可以确保对端能理解自己传达的信息),协议 的出现解决了主机间的交流问题

对于网络来说,协议是双方通信的基石,如果没有协议,那么即使数据传输的再完美也无法使用,比如下面这个就是一个简单的 两正整数运算协议

  • 协议要求:发送的数据必须由两个操作数(正整数)和一个运算符组成,并且必须遵循 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;
};

无论是方案一还是方案二都存在问题,前者是对端接收到消息后无法解析,后者则是存在平台兼容问题(不同平台的结构体内存规则可能不同,会导致读取数据出错)

要想确保双方都能正确理解 协议,还需要进行 序列化与反序列化 处理


2.什么是序列化与反序列化?

序列化是指 将一个或多个需要传递的数据,按照一定的格式,拼接为一条数据,反序列化则是 将收到的数据按照格式解析

比如主机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;
};

以上就是一个简单的 序列化和反序列化 流程,简单来说就是 协议 定制后不能直接使用,需要配合 序列化与反序列化 这样的工具理解,接下来我们就基于 两正整数运算协议 编写一个简易版的网络计算器,重点在于 理解协议、序列化和反序列化


3.实现相关程序

我们接下来要编写的程序从实现功能来看是十分简单的:客户端给出两个正整数和一个运算符,服务器计算出结果后返回

整体框架为:客户端获取正整数与运算符 -> 将这些数据构建出 Request 对象 -> 序列化 -> 将结果(数据包)传递给服务器 -> 服务器进行反序列化 -> 获取数据 -> 根据数据进行运算 -> 将运算结果构建出 Response 对象 -> 序列化 -> 将结果(数据包)传递给客户端 -> 客户端反序列后获取最终结果


既然这是一个基于网络的简易版计算器,必然离不开网络相关接口,在编写 服务器客户端 的逻辑之前,需要先将 socket 接口进行封装,方面后续的使用


4.封装socket相关操作

关于 socket 的相关操作可以看看这两篇博客《网络编程『socket套接字 ‖ 简易UDP网络程序』》、《网络编程『简易TCP网络程序』》

注:当前实现的程序是基于 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 = "log/TcpServer.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 头文件后,服务器/客户端就可以专注于逻辑编写了


5.服务器

首先准备好 TcpServer.hpp 头文件,其中实现了服务器初始化、服务器启动、序列化与反序列化等功能

TcpServer.hpp 服务器头文件

#pragma once#include "Sock.hpp"#include <iostream>
#include <string>
#include <pthread.h>namespace CalcServer
{class TcpServer;// 线程所需要的信息类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 TcpServer{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:TcpServer(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);}}~TcpServer(){_listen_sock.Close();}private:Sock _listen_sock; // 监听套接字uint16_t _port;    // 服务器端口号};
}

上面这份代码我们之前在 《网络编程『简易TCP网络程序』》 中已经写过了,本文的重点在于实现 ServiceIO() 函数,现在可以先尝试编译并运行程序,看看代码是否有问题

CalcServer.cc 简易计算器服务器源文件

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

Makefile 自动编译脚本

.PHONY:all
all:CalcServer CalcClientCalcServer:CalcServer.ccg++ -o $@ $^ -std=c++11 -lpthreadCalcClient:CalcClient.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -rf CalcServer CalcClient

编译并运行程序,同时查看网络使用情况

netstat -nltp

此时就证明前面写的代码已经没有问题了,接下来是填充 ServiceIO() 函数


6.序列化与反序列

ServiceIO() 函数需要做这几件事

  • 读取数据
  • 反序列化
  • 业务处理
  • 序列化
  • 发送数据

除了 序列化和反序列化 外,其他步骤之前都已经见过了,所以我们先来看看如何实现 序列化与反序列化

ServiceIO() 函数 — 位于 TcpServer.hpp 头文件中的 TcpServer 类中

// 进行IO服务的函数
void ServiceIO(const int& sock, const std::string ip, const uint16_t& port)
{// 1.读取数据// 2.反序列化// 3.业务处理// 4.序列化// 5.发送数据
}

需要明白我们当前的 协议两正整数运算,分隔符为 (空格),客户端传给服务器两个操作数和一个运算符,服务器在计算完成后将结果返回,为了方便数据的读写,可以创建两个类:RequestResponse,类中的成员需要遵循协议要求,并在其中支持 序列化与反序列化

Protocol.hpp 协议处理相关头文件

#pragma once
#include <string>namespace my_protocol
{
// 协议的分隔符
#define SEP " "
#define 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){}// 反序列化bool Deserialization(const std::string &inStr){}~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){}// 反序列化bool Deserialization(const std::string &inStr){}~Response(){}public:int _result; // 结果int _code;   // 错误码};
}

接下来就是实现 Serialization()Deserialization() 这两个接口

  • Serialization()将类中的成员根据协议要求,拼接成一个字符串
  • Deserialization()将字符串根据格式进行拆解

Request 类 — 位于 Protocol.hpp 协议相关头文件中

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;
};

其中涉及 IntToStr()StringSplit()StrToInt() 等接口,等后面实现 Response 类时也需要使用,所以我们可以直接将其放入 Util 工具类中


7.工具类

工具类中包含了常用的工具函数

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 = str.find(sep, left);if(right == std::string::npos)break;result->push_back(str.substr(left, right - left));left = right + sep.size();}if(left < str.size())result->push_back(str.substr(left));}
};

接下来就可以顺便把 Response 中的 Serialization()Deserialization() 这两个接口给实现了,逻辑和 Request 类中的差不多(当然结果也要符合 协议 的规定,使用 (空格)进行分隔)

Response 类 — 位于 Protocol.hpp 协议相关头文件中

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() 函数 — 位于 TcpServer.hpp 头文件中的 TcpServer 类中

// 进行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; // 业务处理完成后得到的响应对象// 4.序列化std::string sendMsg;resp.Serialization(&sendMsg);// 5.发送数据}
}

至于业务处理函数如何实现,交给上层决定,也就是 CalcServer.cc


8.业务处理

TcpServer 中的业务处理函数由 CalcServer.cc 传递,规定业务处理函数的类型为 void(Request&, Response*)

Calculate() 函数 — 位于 CalcServer.cc

#include "TcpServer.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<CalcServer::TcpServer> tsvr(new CalcServer::TcpServer(Calculate));tsvr->Init();tsvr->Start();return 0;
}

既然 CalcServer 中传入了 Calculate() 函数对象,TcpServer 类中就得接收并使用,也就是业务处理

TcpServer.hpp 头文件

#pragma once#include "Sock.hpp"
#include "Protocol.hpp"#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>namespace CalcServer
{using func_t = std::function<void(my_protocol::Request&, my_protocol::Response*)>;class TcpServer{const static uint16_t default_port = 8888;private:// ...// 进行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.业务处理my_protocol::Response resp; // 业务处理完成后得到的响应对象_func(req, &resp);// 4.序列化std::string sendMsg;resp.Serialization(&sendMsg);cout << sendMsg << endl;// 5.发送数据}}public:// ...private:// ...func_t _func;      // 上层传入的业务处理函数};
}

这就做好业务处理了,ServiceIO() 函数已经完成了 50% 的工作,接下来的重点是如何读取和发送数据?

TCP 协议是面向字节流的,这也就意味着数据在传输过程中可能会因为网络问题,分为多次传输,这也就意味着我们可能无法将其一次性读取完毕,需要制定一个策略,来确保数据全部递达


9.报头处理

如何确认自己已经读取完了所以数据?答案是提前知道目标数据的长度,边读取边判断

数据在发送时,是需要在前面添加 长度 这个信息的,通常将其称为 报头,而待读取的数据称为 有效载荷报头有效载荷 的关系类似于快递单与包裹的关系,前者是后者成功递达的保障

最简单的 报头 内容就是 有效载荷 的长度

问题来了,如何区分 报头有效载荷 呢?

  • 当前可以确定的是,我们的报头中只包含了长度这个信息
  • 可以通过添加特殊字符,如 \r\n 的方式进行区分
  • 后续无论有效载荷变成什么内容,都不影响我们通过报头进行读取

报头处理属于协议的一部分

所以在正式读写数据前,需要解决 报头 的问题(收到数据后移除报头,发送数据前添加报头)

ServiceIO() 函数 — 位于 TcpServer.hpp 头文件中的 TcpServer 类中

// 进行IO服务的函数
void ServiceIO(const int& sock, const std::string ip, const uint16_t& port)
{while(true){// 1.读取数据std::string package; // 假设这是已经读取到的数据包,格式为 "5\r\n1 + 1"// 2.移除报头// 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.添加报头// 7.发送数据}
}

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);
}

此时对于 ServiceIO() 函数来说,核心函数都已经准备好了,只差拼装了

ServiceIO() 函数 — 位于 TcpServer.hpp 头文件中的 TcpServer 类中

// 进行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);}
}

至此服务器编写完毕,接下来就是进行客户端的编写了


10.客户端

编写客户端的 TcpCilent.hpp 头文件

TcpClient.hpp 客户端头文件

#pragma once#include "Sock.hpp"
#include "Protocol.hpp"
#include "Log.hpp"
#include "Err.hpp"#include <iostream>
#include <string>
#include <unistd.h>namespace CalcClient
{class TcpClient{public:TcpClient(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;}}~TcpClient(){_sock.Close();}private:Sock _sock;std::string _server_ip;uint16_t _server_port;};
}

注意: 客户端也需要边读取边判断,确保读取内容的完整性

下面是 CalcClient.cc 的代码

CalcClient.cc 客户端源文件

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

11.测试

编译并运行代码

可以在代码中添加一定的输出语句,感受 序列化和反序列化 的过程


12.使用库

事实上,序列化与反序列化 这种工作轮不到我们来做,因为有更好更强的库,比如 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 库,所以编译代码时需要指明其动态库

Makefile 自动编译脚本

.PHONY:all
all:CalcServer CalcClientCalcServer:CalcServer.ccg++ -o $@ $^ -std=c++11 -lpthread -ljsoncppCalcClient:CalcClient.ccg++ -o $@ $^ -std=c++11 -ljsoncpp.PHONY:clean
clean:rm -rf CalcServer CalcClient

使用了 Json 库之后,序列化 后的数据会更加直观,当然也更易于使用


🌨️总结

编写网络服务需要注意以下几点

  1. 确定协议
  2. 如何进行序列化和反序列化
  3. 业务处理

星辰大海

相关文章推荐

网络编程『简易TCP网络程序』

网络编程『socket套接字 ‖ 简易UDP网络程序』

网络基础『发展 ‖ 协议 ‖ 传输 ‖ 地址』

相关文章:

网络基础『 序列化与反序列化』

&#x1f52d;个人主页&#xff1a; 北 海 &#x1f6dc;所属专栏&#xff1a; Linux学习之旅、神奇的网络世界 &#x1f4bb;操作环境&#xff1a; CentOS 7.6 阿里云远程服务器 文章目录 &#x1f324;️前言&#x1f326;️正文1.协议的重要性2.什么是序列化与反序列化&…...

腾讯云和阿里云4核8G云服务器多少钱一年和1个月费用对比

4核8G云服务器多少钱一年&#xff1f;阿里云ECS服务器u1价格955.58元一年&#xff0c;腾讯云轻量4核8G12M带宽价格是646元15个月&#xff0c;阿腾云atengyun.com整理4核8G云服务器价格表&#xff0c;包括一年费用和1个月收费明细&#xff1a; 云服务器4核8G配置收费价格 阿里…...

Git误操作补救错失:恢复误删的本地分支、将某个提交从一个分支复制到另一个分支

一、恢复误删的本地分支 作为一枚强迫症&#xff0c;没用的分支总是喜欢及时删删删删掉删掉统统删掉&#xff0c;结果今天发现有些分支还是应该保留。 比如&#xff0c;①前段时间切了个分支用来专门做图表&#xff0c;但因为需求还没有最终确定&#xff0c;已经上线了测试服而…...

MySQL系列-分析SQL性能

查找慢SQL MySQL 慢查询日志是用来记录 MySQL 在执行命令中&#xff0c;响应时间超过预设阈值的 SQL 语句。 开启慢查询 # 开启慢查询日志功能 SET GLOBAL slow_query_log ON; # 慢查询日志存放位置 SET GLOBAL slow_query_log_file /var/lib/mysql/ranking-list-slow.log…...

Java 学习和实践笔记(34):对象的转型(casting)

对象的转型&#xff08;casting)有两种&#xff0c;一种是向上转型&#xff0c;一种是向下转型。 向上转型&#xff1a;父类引用指向子类对象。这属于自动类型转换&#xff0c;编译器会自动完成。 上一节的多态中&#xff0c;形参为父类Animal, 但是调用时实参为子类对象Dog&…...

【Qt】不透明指针(Opaque Pointer)在Qt源码中的应用

目录 什么是不透明指针&#xff08;Opaque Pointer&#xff09;不透明指针在Qt代码中的应用Qt中与不透明指针相关的一些宏 什么是不透明指针&#xff08;Opaque Pointer&#xff09; GeeksforGeeks中给的定义如下&#xff1a; An opaque pointer is a pointer that points to …...

【Python】牛客网—软件开发-Python专项练习

专栏文章索引&#xff1a;Python 1.&#xff08;单选&#xff09;下面哪个是Python中不可变的数据结构&#xff1f; A.set B.list C.tuple D.dict 可变数据类型&#xff1a;列表list[ ]、字典dict{ }、集合set{ }(能查询&#xff0c;也可更改)数据发生改…...

HBase分布式数据库的原理和架构

一、HBase简介 HBase是是一个高性能、高可靠性、面向列的分布式数据库&#xff0c;它是为了在廉价的硬件集群上存储大规模数据而设计的。HBase利用Hadoop HDFS作为其文件存储系统&#xff0c;且Hbase是基于Zookeeper的。 二、HBase架构 *图片引用 Hbase采用Master/Slave架构…...

c#类属性与字段例说说

在C#中&#xff0c;类属性&#xff08;Properties&#xff09;和字段&#xff08;Fields&#xff09;是两种用于存储和访问数据的机制。 属性是一种特殊的方法&#xff0c;通过使用get和set访问器来定义&#xff0c;用于读取和写入类的私有字段。属性可以提供对字段的封装&…...

Linux Centos系统 磁盘分区和文件系统管理 (深入理解)

CSDN 成就一亿技术人&#xff01; 作者主页&#xff1a;点击&#xff01; Linux专栏&#xff1a;点击&#xff01; CSDN 成就一亿技术人&#xff01; 前言———— 磁盘 在Linux系统中&#xff0c;磁盘是一种用于存储数据的物理设备&#xff0c;可以是传统的硬盘驱动器&am…...

华为配置ISP选路实现报文按运营商转发

CLI举例&#xff1a;配置ISP选路实现报文按运营商转发 介绍通过配置ISP选路实现报文按运营商转发的配置举例。 组网需求 如图1所示&#xff0c;FW作为安全网关部署在网络出口&#xff0c;企业分别从ISP1和ISP2租用一条链路。 企业希望访问Server 1的报文从ISP1链路转发&#…...

软件测试APP完整测试作业流程(附流程图),公司级软件测试流程化办公

目录 1. 概述 2. 软件测试流程 3. 软件测试周期人员活动图 4. 总结 1. 概述 1.1 目的 有效的保证软件质量&#xff1b; 有效的制定不同测试类型&#xff08;软件系统测试、音频主观性测试、Field Trial、专项测试、自动化测试、性 能测试、用户体验测试&#xff09;的软件…...

搭建交换机模拟环境及SSH连接,华为NSP软件入门使用教程

搭建交换机模拟环境及SSH连接&#xff0c;华为NSP软件入门使用教程 如果你是通过搜索搜到了这篇文章&#xff0c;那么一定是工作或者学习中需要用交换机&#xff0c;但是又没物理机测试学习&#xff0c;所以需要搭建本地的虚拟环境学习。 这篇文章是我进行交换机命令入门学习写…...

mineadmin 快速安装部署(docker环境)

前提条件&#xff1a;已安装docker 一、下载dnmp环境包 github地址&#xff1a;https://github.com/tomorrow-sky/dnmp gitee地址&#xff1a; https://gitee.com/chenjianchuan/dnmp 二、看一下dnmp包目录结构 三、打开docker-compose.yml 文件&#xff0c;将不需要…...

【力扣刷题练习】93. 复原 IP 地址

题目描述&#xff1a; 有效 IP 地址 正好由四个整数&#xff08;每个整数位于 0 到 255 之间组成&#xff0c;且不能含有前导 0&#xff09;&#xff0c;整数之间用 ‘.’ 分隔。 例如&#xff1a;“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址&#xff0c;但是 “0.011…...

linux查看文件内容cat,less,vi,vim

学习记录 目录 catlessvi vim cat 输出 FILE 文件的全部内容 $ cat [OPTION] FILE示例 输出 file.txt 的全部内容 $ cat file.txt查看 file1.txt 与 file2.txt 连接后的内容 $ cat file1.txt file2.txt为什么名字叫 cat&#xff1f; 当然和猫咪没有关系。 cat 这里是 co…...

【恒源智享云】conda虚拟环境的操作指令

conda虚拟环境的操作指令 由于虚拟环境经常会用到&#xff0c;但是我总忘记&#xff0c;所以写个博客&#xff0c;留作自用。 在恒源智享云上&#xff0c;可以直接在终端界面输入指令&#xff0c;例如&#xff1a; 查看已经存在的虚拟环境列表 conda env list查看当前虚拟…...

Flask python 开发篇:项目布局

一、背景简介 Flask应用程序可以像单个文件一样简单。就像上一篇简单实现一个接口一样&#xff0c;所有的东西都在一个python文件内&#xff1b; 然而&#xff0c;当项目越来越大的时候&#xff0c;把所有代码放在单个文件中就有点不堪重负了。 Python 项目使用 包 来管理代码…...

docker 部署prometheus+grafana

首先进行部署docker 配置阿里云依赖&#xff1a; curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo # 配置centos 7的镜像源 yum install -y yum-utils device-mapper-persistent-data lvm2 # 安装一些后期或需要的的一下依…...

RNN实战

本主要是利用RNN做多分类任务&#xff0c;在熟悉RNN训练的过程中&#xff0c;我们可以理解 1&#xff09;超参数 batch_size和pad_size对训练过程的影响。 2&#xff09;文本处理过程中是如何将文本的文字表示转化为向量表示 3&#xff09;RNN梯度消失和序列长度的关系 4&#…...

测试微信模版消息推送

进入“开发接口管理”--“公众平台测试账号”&#xff0c;无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息&#xff1a; 关注测试号&#xff1a;扫二维码关注测试号。 发送模版消息&#xff1a; import requests da…...

Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误

HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误&#xff0c;它们的含义、原因和解决方法都有显著区别。以下是详细对比&#xff1a; 1. HTTP 406 (Not Acceptable) 含义&#xff1a; 客户端请求的内容类型与服务器支持的内容类型不匹…...

SciencePlots——绘制论文中的图片

文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了&#xff1a;一行…...

MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例

一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...

基于当前项目通过npm包形式暴露公共组件

1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹&#xff0c;并新增内容 3.创建package文件夹...

【项目实战】通过多模态+LangGraph实现PPT生成助手

PPT自动生成系统 基于LangGraph的PPT自动生成系统&#xff0c;可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析&#xff1a;自动解析Markdown文档结构PPT模板分析&#xff1a;分析PPT模板的布局和风格智能布局决策&#xff1a;匹配内容与合适的PPT布局自动…...

DBAPI如何优雅的获取单条数据

API如何优雅的获取单条数据 案例一 对于查询类API&#xff0c;查询的是单条数据&#xff0c;比如根据主键ID查询用户信息&#xff0c;sql如下&#xff1a; select id, name, age from user where id #{id}API默认返回的数据格式是多条的&#xff0c;如下&#xff1a; {&qu…...

解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错

出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上&#xff0c;所以报错&#xff0c;到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本&#xff0c;cu、torch、cp 的版本一定要对…...

优选算法第十二讲:队列 + 宽搜 优先级队列

优选算法第十二讲&#xff1a;队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...

视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)

前言&#xff1a; 最近在做行为检测相关的模型&#xff0c;用的是时空图卷积网络&#xff08;STGCN&#xff09;&#xff0c;但原有kinetic-400数据集数据质量较低&#xff0c;需要进行细粒度的标注&#xff0c;同时粗略搜了下已有开源工具基本都集中于图像分割这块&#xff0c…...