💓博主CSDN主页:麻辣韭菜💓
⏩专栏分类:Linux初窥门径⏪
🚚代码仓库:Linux代码练习🚚
💻操作环境: CentOS 7.6 华为云远程服务器
🌹关注我🫵带你学习更多Linux知识
🔝
目录
🌃前言
网络通信的核心在于数据的传输和接收。为了确保数据能够在不同的系统和设备之间高效、准确地传递,我们需要遵循一定的规则和标准,这些规则和标准就是我们所说的“协议”。协议定义了数据如何被组织、传输和解释,它是网络通信的基石。
然而,在实际的应用场景中,我们经常会遇到标准协议无法满足特定需求的情况。这时,自定义协议就显得尤为重要。自定义协议允许开发者根据特定的应用需求来设计和实现通信规则,从而提供更加灵活和优化的解决方案。
在网络通信过程中,数据需要在不同的格式和表示之间转换,这一过程称为“序列化”和“反序列化”。序列化是将数据结构或对象状态转换为可存储或可传输的格式的过程;反序列化则是相反的过程,即将序列化后的数据重新转换回原始的数据结构或对象状态。这两个过程对于数据的传输和处理至关重要。
🎇正文
1.协议的重要性
如果我们在国外旅游,不精通外语,那么在国外旅行将会是非常痛苦的一件事。因为双方母语是两套系统,也就是各自有各自的协议。双方都能接受到对方输出信息,但是双方就是听不懂,所以我们需要针对双方母语,再定制一套协议,使双方的语言一一对应。现在同声传译就是一种协议。
通过上面的故事,那我们如何让两台计算机数据传输,使双方的数据各自都能看懂?
那肯定也是需要协议的。
就比如 我们要实现一个网络版本的计算机,客户端输入数字加运算符。服务器拿到数据,然后解析数据,对数据进行处理,得到的结果返回给客户端。
那么对于上面的数据传输就有两种方案。
方案一:
int x;
int y;
char op; //+ - * / %
我们把 x op y 拼接成字符串,发送给服务器。
方案二:
class Message
{
public:
int x;
int y;
char op;
};
结构体传递消息。
但是这里两种传递数据的方式都有问题,方案一对端的机器怎么能确定收到的是一个完整的报文 x op y?有可能是 x ?或者 x op? 再或者 x op y x?
方案二的问题是涉及内存对齐问题,不同OS 不同平台 内存对齐的规则都是不一样的。
那如何解决数据解析完整性?
要想双方都能正确的理解我们定制的协议,还需要序列化与反序列化
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,基于后面都是用的TCP/IP协议,我们直接把socket相关的接口做封装。
如果你不清楚socket相关操作请看网络编程 【Socket套接字、简易UDP、TCP网络程序】
3.1封装Socket.hpp
既然是网络,难免会有出错的情况,所以我们需要打印日志,看看出错的信息。直接把之前的Log.hpp拿过来用
Log.hpp 相关代码
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
printMethod = method;
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
printOneFile(filename, logtxt);
}
~Log()
{
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 格式:默认部分+自定义部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// printf("%s", logtxt); // 暂时打印
printLog(level, logtxt);
}
private:
int printMethod;
std::string path;
};
如果对上面的代码有疑问的读者,请看 Linux 进程间通信之命名管道 这里有对日志详细解释
#pragma once
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
Log lg;
const int backlog = 10;
enum
{
SocketErr = 2,
BindErr,
ListenErr,
};
class 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);
}
int opt = 1;
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
}
void Bind(uint16_t port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
socklen_t len = 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, len) < 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, uint16_t *clientport)
{
struct sockaddr_in peer;
//memset(&peer, 0, sizeof(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);
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &(peer.sin_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)));
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;
};
3.2 服务器编写
有了Socket.hpp我们就可以编写服务器了,创建TcpServer.hpp头文件
#pragma once
#include <iostream>
#include <string>
#include <thread>
#include <functional>
#include "Socket.hpp"
using func_t = std::function<std::string(std::string &packge)>;
class TcpServer;
class ThreadData
{
public:
ThreadData(int sock, std::string &ip, uint16_t port, TcpServer *tpsvr)
: _sock(sock), _ip(ip), _port(port), _tpsvr(tpsvr)
{
}
public:
int _sock;
std::string _ip;
uint16_t _port;
TcpServer *_tpsvr; // 回调指针
};
class TcpServer
{
public:
TcpServer(const uint16_t port = 8080, func_t callback = nullptr)
: _port(port), _callback(callback) {}
// 初始化
void Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
}
static void ServerIO(ThreadData *td)
{
// TODO
TcpServer *tpsvr = td->_tpsvr;
int sock = td->_sock;
std::string inbuffer_stream;
while (true)
{
char buffer[1024];
ssize_t n = read(sock, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
inbuffer_stream += buffer;
while (true)
{
std::string info = tpsvr->_callback(inbuffer_stream); // 业务处理
if (info.empty())
{
break;
}
lg(Debug, "respose:\n%s", info.c_str());
write(sock, info.c_str(), info.size());
}
}
else if (n == 0)
{
// Connection closed by client
break;
}
else
{
// Error
lg(Error, "Read error");
break;
}
}
}
// 启动服务器
void Start()
{
for (;;)
{
std::string clientip;
uint16_t clientport;
int sockfd = _listensock.Accept(&clientip, &clientport);
if (sockfd < 0)
{
continue;
}
lg(Info, "accept get a new link,sockfd: %d,clientip: %s,clientport: %d", sockfd, clientip.c_str(), clientport);
// 创建线程处理任务
ThreadData *td = new ThreadData(sockfd, clientip, clientport, this);
std::thread t(ServerIO, td);
// 线程分离
t.detach();
}
}
~TcpServer()
{
_listensock.Close();
}
private:
Sock _listensock;
uint16_t _port;
func_t _callback;
};
这里讲解一下SeverIO编写逻辑,
编写ServerIO函数需要明白几个流程
1. 客户端发来的数据我们需要读取
2. 反序列化(客户端发来的消息是经过序列化的)
3. 经过反序列化后,拿到数据做业务处理
4. 数据处理完毕,发送给客户端需要序列化
5. 发送数据
我们从客户端读来的数据,放到_callback进行回调处理(这里_callback涵盖了序列化与反序列化、数据的 加 减 乘 除 的业务处理) 关于_callback 是C++11 一个特性,叫做包装器详情请看C++ 11 【线程库】【包装器】_c++11线程库-CSDN博客
编写调用逻辑Main.cc
#include "TcpServer.hpp"
#include "ServerCal.hpp"
#include <memory>
int main()
{
ServerCal cal;
TcpServer *svr = new TcpServer(8080, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));
svr->Init();
svr->Start();
return 0;
}
在命令行 输入 netstat -nltp
3.3 编写序列化与反序列化
现在的问题是如何进行序列化与反序列化?
所以这时我们需要自己定制协议,说人话那就是利用一些特定的符号来进行分割,比如空格符号、@等特殊符号,下面我以空格符号为例来进行序列化与反序列化。
为了方便数据的读写,可以创建两个类:Request
和 Response
,类中的成员需要遵循协议要求,并在其中支持 序列化与反序列化
#pragma once
#include <iostream>
#include <string>
//定义分割符
const std::string blank_space_sep = " ";
const std::string protocol_sep = "\n";
class Request
{
public:
Request(const int data1, const int data2, const char op_)
: x(data1), y(data2), op(op_) {}
bool Serialization(std::string *out)
{
}
bool Deserialization(std::string &in)
{
}
public:
int x;
int y;
char op;
};
class Response
{
public:
Response(int re, int c)
: result(re), code(c) {}
bool Serialization(std::string *out)
{
}
bool Deserialization(std::string &in)
{
}
public:
int result;
int code;
};
现在的问题就转变成了编写序列化与反序列化函数了。
那我们直接开始编写先编写Request的序列化与反序列化函数。
bool Serialization(std::string *out)
{
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 Deserialization(std::string &in)
{
size_t left = in.find(blank_space_sep);
if (left == std::string::npos)
return false;
std::string part_x = in.substr(0, left);
size_t right = in.rfind(blank_space_sep);
if (right == std::string::npos)
return false;
std::string part_y = in.substr(right + 1);
if (left + 2 != right)
return false;
op = in[left + 1];
x = std::stoi(part_x);
y = std::stoi(part_y);
return true;
}
void DebugPrint()
{
std::cout << "新请求构建完成: " << x << op << y << "=?" << std::endl;
}
再编写Respose的序列化与反序列化函数
bool Serialization(std::string *out)
{
std::string s = std::to_string(result);
s += blank_space_sep;
s += std::to_string(code);
*out = s;
return true;
}
bool Deserialization(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 << "结果响应完成, result: " << result << ", code: " << code << std::endl;
}
但是光有序列化与反序列化还不行,为什么这么说?
因为我们不能确定读到是一个完整的报文,也就是说 “1 + 2” 这个报文长度是 5,如果后面还有一个报文?比如 “1 + 2 3 * 4” 这样的报文,所以我们还需要 对每个报文做分离,利用\n来做区分 也就是说在报文前面加报头,如果我们读报文有效载荷长度一直到\N 最后等于报头的长度,那说明我们读到是一个完整的报文。
在Protocal.hpp 中编写Encode函数
std::string Encode(std::string &content)
{
std::string package = std::to_string(content.size());
package += protocol_sep;
package += content;
package += protocol_sep;
return package;
}
增加了报头,但是双方都需要解析报文所以我们还需要Decode函数。
bool Decode(std::string &package, std::string *content)
{
std::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);
// package = len_str + content_str + 2
std::size_t total_len = len_str.size() + len + 2;
if(package.size() < total_len) return false;
*content = package.substr(pos+1, len);
// earse 移除报文 package.erase(0, total_len);
package.erase(0, total_len);
return true;
}
至此Protocol.hpp编写完毕。
3.4 编写回调函数相关业务处理
#pragma once
#include <iostream>
#include "Protocol.hpp"
enum
{
Div_Zero = 1,
Mod_Zero,
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._code = Div_Zero;
else
resp._result = req._x / req._y;
}
break;
case '%':
{
if (req._y == 0)
resp._code = Mod_Zero;
else
resp._result = req._x % req._y;
}
break;
default:
resp._code = Other_Oper;
break;
}
return resp;
}
// "len"\n"10 + 20"\n
std::string Calculator(std::string &package)
{
std::string content;
bool r = Decode(package, &content); // "len"\n"10 + 20"\n
if (!r)
return "";
// "10 + 20"
Request req;
r = req.Deserialization(content); // "10 + 20" ->x=10 op=+ y=20
if (!r)
return "";
content = ""; //
Response resp = CalculatorHelper(req); // result=30 code=0;
resp.Serialization(&content); // "30 0"
content = Encode(content); // "len"\n"30 0"
return content;
}
~ServerCal()
{
}
};
3.5 编写客户端
#include <iostream>
#include <string>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"
static 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]);
Sock sockfd;
sockfd.Socket();
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)
{
std::cout << "===============第" << cnt << "次测试....., " << "===============" << std::endl;
int x = rand() % 100 + 1;
usleep(1234);
int y = rand() % 100;
usleep(4321);
char oper = opers[rand()%opers.size()];
Request req(x, y, oper);
std::string package;
req.Serialization(&package);
package = Encode(package);
write(sockfd.Fd(), package.c_str(), package.size());
char buffer[128];
ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer)); // 我们也无法保证我们能读到一个完整的报文
if(n > 0)
{
buffer[n] = 0;
inbuffer_stream += buffer; // "len"\n"result code"\n
std::cout << inbuffer_stream << std::endl;
std::string content;
bool r = Decode(inbuffer_stream, &content); // "result code"
assert(r);
Response resp;
r = resp.Deserialization(content);
assert(r);
}
std::cout << "=================================================" << std::endl;
sleep(1);
cnt++;
}
sockfd.Close();
return 0;
}
4. 序列化与反序列化相关的库
我们前面手搓了那么多的代码,其实是有专门的用序列化与反序列化的库的。因为有更好更强的库,比如 Json
、XML
、Protobuf
等,而且大家手撕也容易错。
在JsonCpp库中,
Json::Value
是一个类,用于表示JSON值。它可以包含以下类型的数据:
- 布尔值(true/false)
- 数值(整数或浮点数)
- 字符串
- 数组(有序集合)
- 对象(无序集合)
#include <iostream>
#include <jsoncpp/json/json.h>
int main()
{
Json:: Value part1;
part1["haha"] = "haha";
part1["hehe"] = "hehe";
std::cout <<part1<<std::endl;
return 0;
}
在JsonCpp库中,Json::StyledWriter
是一个类,用于将Json::Value
对象格式化为风格化的JSON字符串。当你创建一个Json::StyledWriter
对象时,你可以使用它来将JSON值转换为一个格式化的字符串
Json::Value root;
root["x"] = 1;
root["y"] = 1;
root["op"] = '+';
root["dest"] = "this is a + oper";
root["test"] = part1;
Json::StyledWriter w;
//使用write方法将Json::Value对象转换为JSON字符串
std::string res = w.write(root);
std::cout << res << std::endl;
通过刚才代码演示 我们简单使用了Json库基本用法。下面我利用Json库来做序列化与反序列化
基于前面我们已经自己实现了序列化与反序列化,如果想保留原来自己实现的 序列化与反序列化 代码,可以利用 条件编译 进行区分
#pragma once
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
// 定义分割符
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)
{
std::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);
// package = len_str + content_str + 2
std::size_t total_len = len_str.size() + len + 2;
if (package.size() < total_len)
return false;
*content = package.substr(pos + 1, len);
// earse 移除报文 package.erase(0, total_len);
package.erase(0, total_len);
return true;
}
class Request
{
public:
Request(int data1, int data2, char op_)
: x(data1), y(data2), op(op_) {}
Request()
{
}
bool Serialization(std::string *out)
{
#ifdef MySelf
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;
#else
Json::Value root;
root["x"] = x;
root["y"] = y;
root["op"] = op;
Json::StyledWriter w;
*out = w.write(root);
return true;
#endif
}
bool Deserialization(std::string &in)
{
#ifdef MySelf
size_t left = in.find(blank_space_sep);
if (left == std::string::npos)
return false;
std::string part_x = in.substr(0, left);
size_t right = in.rfind(blank_space_sep);
if (right == std::string::npos)
return false;
std::string part_y = in.substr(right + 1);
if (left + 2 != right)
return false;
op = in[left + 1];
x = std::stoi(part_x);
y = std::stoi(part_y);
return true;
#else
Json::Value root;
Json::Reader r;
r.parse(in, root);
x = root["x"].asInt(); // 获取整型
y = root["y"].asInt();
op = root["op"].asInt();
return true;
#endif
}
void DebugPrint()
{
std::cout << "新请求构建完成: " << x << op << y << "=?" << std::endl;
}
public:
int x;
int y;
char op;
};
class Response
{
public:
Response(int re, int c)
: result(re), code(c) {}
Response() {}
bool Serialization(std::string *out)
{
#ifdef MySelf
std::string s = std::to_string(result);
s += blank_space_sep;
s += std::to_string(code);
*out = s;
return true;
#else
Json::Value root;
root["result"] = result;
root["code"] = code;
Json::StyledWriter w;
*out = w.write(root);
return true;
#endif
}
bool Deserialization(std::string &in)
{
#ifdef MySelf
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;
#else
Json::Value root;
Json::Reader r;
r.parse(in, root); // 将字符串in内容填充到 root中
result = root["result"].asInt();
code = root["code"].asInt();
return true;
#endif
}
void DebugPrint()
{
std::cout << "结果响应完成, result: " << result << ", code: " << code << std::endl;
}
public:
int result;
int code;
};
使用了 Json
库之后,序列化 后的数据会更加直观,当然也更易于使用
🌨️总结
在进行网络服务时,并不是我们想的那么简单,对数据传输要求非常高,能够让双方都能正确解析数据,协议起到了非常大作用, 虽然协议能够让双方都能看懂数据,但是数据里面的解析内容如果不是完整的,那么也没有意义,序列化与反序列化就如同我们语言中的标点符号,试问一大段文字一起看或者一小段文字拆开看,意思可能就变味了。