🌈前言
这篇文章给大家带来序列化和反序列化的学习!!!
🌸1、应用层
前言:之前所写的所有网络应用程序服务,比如:英文大小写转换、聊天室都是在应用层上进行编写的!!!
-
应用层:它是OSI模型中的第七层模块,用于提供基本的网络应用服务,主要功能是在网络上客户端与服务器之间的网络通信,并且服务器向客户端提供基础的应用服务,应用层就是来编写服务的
-
程序员写的一个个解决我们实际的问题;满足我们日常需求的网络程序,都是在应用层
-
我们前面在应用层所写的socket代码,都是调用下层的功能(比如使用传输层的TCP或UDP协议进行可靠或不可靠的方式进行数据传输)来辅助应用层来完成特定的定制化服务
-
应用层常用的协议有:HTTP、HTTPS、Telnet、DNS、FTP协议等等…
🌺2、重谈协议
前言:前面我们谈过协议其实是双方之间的约定,这次我们来进行深入的理解
- 协议是一种约定,那么我们在编写套接字代码时,读写数据都是按字符串的方式来发送和接收的
- 计算机与网络设备想要互相通信,双边就必须基于相同的方法
- 如何找到对方,使用什么语言进行通信;不同的硬件、OS之间的通信;都需要规则进行约束,这种规则就是协议!
-
如果我们要传输一些结构化数据该怎么办呢?
-
- 方案一:直接将结构体对象按字节进行发送,这样可行吗???
-
- 问题一:直接发送结构体对象,是不可取的,因为客户端和服务器的结构体数据内存对齐方式可能不同,比如客户端是windows运行的,服务器是部署在Linux上的,它们的内存对齐方式不同
-
- 问题二:客户端的版本号和服务器不同,比如新版本的结构体可能拓展了新的成员变量,如果我们直接发送结构体对象,也会出现异常问题~(数据丢失,数据错误问题)
-
序列化和反序列化
-
- 这时候,我们就需要定制化协议了,意思是客户端将结构体进行序列化,并且进行编码后发送请求给服务器
-
- 服务器拿到请求(报文)后进行解码且拿到有效载荷,并且进行反序列化拿到结构化数据!
-
- 序列化:将结构化数据转换成字符串
-
- 反序列化:将字符串转换成结构化数据
-
- 编码:结构化数据转换成字符串后,对字符串进行长度计算,添加到字符串的首部,并且添加特定的长度字段和每个有效载荷之间的分隔符 – 不考虑加密
-
- 解码:客户端拿到报文后,根据报文头部包含有效载荷长度的字段,通过分隔符找到长度字段来提取有效载荷 – 不考虑解密
-
- 序列化和反序列化是将数据从一种表示形式转换为另一种表示形式的过程,序列化和反序列化操作时,数据格式必须一致,否则会导致数据解析错误或者版本不兼容的问题!!!
🍁3、网络计算器
🍡3.1、定制协议
前言:因为网络计算器是需要传输结构化数据进行计算的,不是简简单单的英文大小写转换,需要定制协议
-
约定
-
- 客户端发送请求,构建一个请求类,里面包含序列化和反序列化接口
-
- 服务器响应请求,构建一个响应类,里面包含序列化和反序列化
-
- 编码和解码是双方都要进行使用,因为网络通信是全双工的,双方都能给对方发报文,所以都要编解码!
约定一
-
- 首先客户端构建请求类,序列化时,结构体的每个成员变量的分隔符为空格
-
- 对序列化后的数据进行编码,编码是将有效载荷(包含空格)的长度字段放到报文首部,并且添加特定的分隔符,长度字段和报文间的分隔符都使用"\r\n"
-
- 服务器对报文进行解码时,根据首部长度字段和分隔符提取有效载荷数据
-
- 提取到有效载荷后,将有效载荷反序列化为结构化数据
约定二
-
- 服务器拿到结构化数据后,通过它来构建响应类,后面又是序列化成字符串,然后进行编码发送报文
-
- 客户端拿到响应报文后,对其进行解码(按首部有效载荷长度和分割符提取有效载荷),随后将解码后的数据反序列化到响应类中,最后数据结果即可!
🍢3.2、样例代码
定制协议头文件 – CusPro.hpp
#pragma once
#include <iostream>
#include <string>
#define BUFFER 1024
#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF)
#define SPACE " "
#define SPACE_LEN 1
#define OPS_ALL "+-*/%"
// makefile控制使用jsoncpp还是定制序列化和反序列化
class Requst
{
public:
// 序列化 -- 结构化数据 -> 字符串
void Serialize(std::string &out)
{
// "LeftOprand_ op_ RightOprand_"
std::string x(std::to_string(LeftOprand_));
std::string y(std::to_string(RightOprand_));
out = x;
out += SPACE;
out += op_;
out += SPACE;
out += y;
}
// 反序列化 -- 字符串 -> 结构化数据 -> "100 + 200"
bool Deserialize(std::string &in)
{
// 找空格
size_t spaceOne = in.find(SPACE);
size_t spaceTwo = in.rfind(SPACE);
std::cout << in << std::endl;
if (spaceOne == std::string::npos)
return false;
if (spaceTwo == std::string::npos)
return false;
// 截取对应的成员变量,并且转换成对应的成员变量类型赋值给成员变量
std::string leftData(in.substr(0, spaceOne));
LeftOprand_ = std::stoi(leftData);
std::string Op(in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN)));
if (Op.size() == 1)
op_ = Op[0];
else
return false;
std::string rightData(in.substr(spaceTwo + SPACE_LEN));
RightOprand_ = std::stoi(rightData);
return true;
}
public:
int &GetLeftOprand() { return LeftOprand_; }
int &GetRightOprand() { return RightOprand_; }
char &GetOp() { return op_; }
private:
int LeftOprand_;
char op_;
int RightOprand_;
};
//-------------------------------------------------------------------------------------------------------
class Response
{
public:
// 序列化 -- 结构化数据 -> 字符串
void Serialize(std::string &out)
{
// "exitcode_ result_"
std::string exitcode(std::to_string(exitcode_));
std::string result(std::to_string(result_));
out = exitcode;
out += SPACE;
out += result;
}
// 反序列化 -- 字符串 -> 结构化数据
bool Deserialize(std::string &in)
{
// "exitcode_ result_"
int Space_Index = in.find(SPACE);
if (Space_Index == std::string::npos)
return false;
std::string exitcode(in.substr(0, Space_Index));
std::string result(in.substr(Space_Index + SPACE_LEN));
exitcode_ = std::stoi(exitcode);
result_ = std::stoi(result);
}
public:
int &GetResult() { return result_; }
int &GetExitcode() { return exitcode_; }
private:
int exitcode_ = 0; // 错误码,!0为异常 -- 除/模零错误...
int result_; // 计算结果
};
//-------------------------------------------------------------------------------------------------------
// 编码 -- 序列化后添加有效载荷的长度和分隔符
std::string enCoded(std::string &in, uint32_t len)
{
// "len\r\nxxx xxx xxx\r\n" -- "5\r\n1 + 1\r\n"
std::string package(std::to_string(len));
package += CRLF;
package += in;
package += CRLF;
return package;
}
// 解码 -- 根据编码的长度获取有效载荷 -- "9\r\n100 + 200\r\n"
std::string Decode(std::string &in, uint32_t *len)
{
// 1. 判断是否包含len
*len = 0;
size_t pos = in.find(CRLF);
if (pos == std::string::npos)
return "";
// 2. 提取长度
std::string inLenStr = in.substr(0, pos);
int inLen = std::atoi(inLenStr.c_str());
// 3. 判断有效数据长度是否符合
int DataLen = in.size() - (2 * CRLF_LEN) - pos;
if (DataLen < inLen)
return "";
// 4. 提取完整的数据
std::string package(in.substr(pos + CRLF_LEN, inLen));
*len = inLen;
// 5. 将当前报文从in中完整的移除(特殊情况,Server可能读多数据)
int removeLen = inLenStr.size() + (2 * CRLF_LEN) + package.size();
in.erase(0, removeLen);
return package;
}
//-------------------------------------------------------------------------------------------------------
// "1 + 1"; "1+ 1"; "1 +1" --> "1+1" -- 数据清洗(去除空格(SPACE))
void DataClean(std::string& outbuffer)
{
int strLen = outbuffer.size();
int j = 0;
for (int i = 0; i < strLen; ++i)
{
if (outbuffer[i] != ' ')
{
outbuffer[j] = outbuffer[i];
++j;
}
}
outbuffer = outbuffer.substr(0, j);
std::cout << "debug: " << outbuffer << std::endl;
}
// 构建请求类
bool makeRequst(const std::string &in, Requst &req)
{
// Client不同的输入格式: "123+1" -- "123-1"....
char strtmp[BUFFER];
snprintf(strtmp, BUFFER, "%s", in.c_str());
char *leftOperand = strtok(strtmp, OPS_ALL);
if (leftOperand == nullptr)
return false;
char *rightOperand = strtok(nullptr, OPS_ALL);
if (rightOperand == nullptr)
return false;
char Op = in[strlen(leftOperand)];
req.GetLeftOprand() = std::stoi(leftOperand);
req.GetOp() = Op;
req.GetRightOprand() = std::stoi(rightOperand);
return true;
}
Util.hpp – 工具头文件,用于保存重复的头文件
#pragma once
#include <iostream>
#include <thread>
#include <string>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
const int BUFFER_SIZE = 1024;
#define FORMAT_ERR 1
#define SER_SOCKET 2
#define BIND_ERR 3
#define LISTEN_ERR 4
#define ACCEPT_ERR 5
#define WRITE_ERR 6
#define CLISOCK_ERR 7
#define CONNECT_ERR 8
makefile
.PHONY:all
all:server client
server:TcpServer.cc
g++ -o $@ $^ -std=c++11 -pthread
client:TcpClient.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf server client
TcpServer.cc – 服务器
#include "Util.hpp"
#include "CusPro.hpp"
class ClientData
{
public:
ClientData()
{
}
private:
int ClientSockfd_;
std::string ClientIP_;
uint16_t ClientPort_;
};
class TcpServer
{
public:
TcpServer(uint16_t Port, std::string IP)
: sockfd_(-1), IP_(IP), Port_(Port)
{
}
void Init()
{
// 获取套接字资源
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
std::cout << "Get Socket error: " << strerror(errno) << std::endl;
exit(SER_SOCKET);
}
// 绑定网络信息
sockaddr_in ServerInDa;
socklen_t len = sizeof(ServerInDa);
memset(&ServerInDa, 0, len);
ServerInDa.sin_family = AF_INET;
ServerInDa.sin_port = htons(Port_);
ServerInDa.sin_addr.s_addr = (IP_.empty() ? htonl(INADDR_ANY) : inet_addr(IP_.c_str()));
if (bind(sockfd_, (const sockaddr *)&ServerInDa, len) < 0)
{
std::cout << "Bind error: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
// 设置监听套接字
if (listen(sockfd_, SOMAXCONN) < 0)
{
std::cout << "listen error: " << strerror(errno) << std::endl;
exit(LISTEN_ERR);
}
}
void Start()
{
int SerSockfd;
while (true)
{
// 获取用户连接
sockaddr_in ClientInDa;
socklen_t len = sizeof(ClientInDa);
memset(&ClientInDa, 0, len);
SerSockfd = accept(sockfd_, (sockaddr *)&ClientInDa, &len);
if (SerSockfd < 0)
{
std::cout << "accept error: " << strerror(errno) << std::endl;
exit(ACCEPT_ERR);
}
// 保存Client Network Data -- 网络序列->主机序列
std::string ClientIp = inet_ntoa(ClientInDa.sin_addr);
uint16_t ClientPort = ntohs(ClientInDa.sin_port);
std::cout << "用户[" << ClientIp << ":" << ClientPort
<< "]连接Server sucess!!!" << std::endl;
//----------------------------------------------------------------------------------------------
// 多线程版本
RunThread(ClientIp, ClientPort, SerSockfd);
}
}
void RunThread(std::string &ClientIp, uint16_t ClientPort, int SerSockfd)
{
// 定义C++标准库的线程类,因为线程只能调用全局和静态的函数,这里调用成员函数,所以这里必须要传this
std::thread Thread(&TcpServer::netCal, this, std::ref(ClientIp), ClientPort, SerSockfd);
Thread.detach();
}
//----------------------------------------------------------------------------------------------------
public:
// 网络计算器服务 -- 包含定制协议内容
void netCal(std::string &ClientIp, uint16_t ClientPort, int SerSockfd)
{
std::string inbuffer;
char buffer[BUFFER_SIZE];
// 因为TCP传输机制,可能会只传一部分,需要循环读取Data
while (true)
{
memset(buffer, 0, BUFFER_SIZE);
// 1. 读取数据
ssize_t s = read(SerSockfd, buffer, BUFFER_SIZE - 1);
if (s == 0)
{
std::cout << "Client write close!!!" << std::endl;
break;
}
else if (s < 0)
{
std::cout << "Server read error: " << strerror(errno) << std::endl;
break;
}
buffer[s] = '\0';
inbuffer += buffer;
// 2. 判断有效载荷是否完整 -- Client -> Data: "len\r\nxxxxxxxxxx\r\n"
Requst req;
uint32_t packagLen = 0;
std::string package = Decode(inbuffer, &packagLen);
if (packagLen == 0)
continue; // 提取完整有效载荷失败!!!
// 3. 构建响应类 -> 序列化 -> 编码 -> write Client
if (req.Deserialize(package))
{
Response resp = calculator(req);
std::string respackage;
resp.Serialize(respackage);
respackage = enCoded(respackage, respackage.size());
ssize_t w = write(SerSockfd, (void*)respackage.c_str(), respackage.size());
if (write < 0)
{
std::cout << "Server write to Client error -> " << strerror(errno) << std::endl;
break;
}
}
}
}
private:
Response calculator(Requst& req)
{
Response resp;
switch (req.GetOp())
{
case '+':
resp.GetResult() = req.GetLeftOprand() + req.GetRightOprand();
break;
case '-':
resp.GetResult() = req.GetLeftOprand() - req.GetRightOprand();
break;
case '*':
resp.GetResult() = req.GetLeftOprand() * req.GetRightOprand();
break;
case '/':
{
if (req.GetRightOprand() == 0)
{
resp.GetExitcode() = -1; // 除零错误
}
else
{
resp.GetResult() = req.GetLeftOprand() / req.GetRightOprand();
}
}
break;
case '%':
{
if (req.GetRightOprand() == 0)
{
resp.GetExitcode() = -2; // 模零错误
}
else
{
resp.GetResult() = req.GetLeftOprand() % req.GetRightOprand();
}
}
break;
default:
resp.GetExitcode() = -3; // 非法操作
std::cout << "Please enter the correct format: xx [+-*/%] xx" << std::endl;
break;
}
return resp;
}
//-----------------------------------------------------------------------------------
private:
int sockfd_;
std::string IP_;
uint16_t Port_;
};
int main(int argc, char *argv[])
{
// 命令行参数格式: [可执行程序] [IP地址(可以不传)] [port]
if (argc < 2)
{
std::cout << "Instruction format: [可执行程序] [IP地址(可以不传)] [port]" << std::endl;
exit(FORMAT_ERR);
}
std::string IP;
uint16_t Port;
if (argc > 2)
{
IP = argv[1];
Port = std::atoi(argv[2]);
}
else
{
Port = std::atoi(argv[1]);
}
// 启动服务器
TcpServer tps(Port, IP);
tps.Init();
tps.Start();
return 0;
}
TcpClient.cc – 客户端
#include "Util.hpp"
#include "CusPro.hpp"
class TcpClient
{
public:
TcpClient(std::string ServerIP, uint16_t Port)
: ServerIP_(ServerIP), Port_(Port)
{
}
~TcpClient()
{
if (sockfd_ > 2)
close(sockfd_);
}
void Init()
{
// 获取套接字资源
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
std::cout << "Get Socket error: " << strerror(errno) << std::endl;
exit(CLISOCK_ERR);
}
// 填充Server inetwork Data
sockaddr_in ServerInDa;
socklen_t len = sizeof(ServerInDa);
memset(&ServerInDa, 0, len);
ServerInDa.sin_family = AF_INET;
ServerInDa.sin_port = htons(Port_);
ServerInDa.sin_addr.s_addr = inet_addr(ServerIP_.c_str());
// 向指定Server发送连接请求 -- connect自动绑定网络信息(ip with port) to 内核
if (connect(sockfd_, (const sockaddr *)&ServerInDa, len) < 0)
{
std::cout << "Connect Server error: " << strerror(errno) << std::endl;
exit(CONNECT_ERR);
}
}
void Start()
{
std::string outbuffer;
char inbuffer[BUFFER_SIZE];
while (true)
{
// 发送请求
std::cout << "Client echo# ";
fflush(stdout);
std::getline(std::cin, outbuffer);
if (strcasecmp(outbuffer.c_str(), "quit") == 0)
{
std::cout << "Client exit!!!" << std::endl;
exit(0);
}
// -------------------------------------------------------------------------------------------
// 请求 -> 构建请求类 -> 序列化 -> 编码
DataClean(outbuffer); // 数据清洗 -- 去掉空格
Requst req;
if (!(makeRequst(outbuffer, req))) continue;
std::string package;
req.Serialize(package);
std::cout << package << std::endl;
package = enCoded(package, package.size());
std::cout << package << std::endl;
// -------------------------------------------------------------------------------------------
ssize_t Write = write(sockfd_, (void *)package.c_str(), package.size());
if (Write < 0)
{
std::cout << "Client write data error" << strerror(errno) << std::endl;
continue;
}
// 获取结果
ssize_t Read = read(sockfd_, inbuffer, sizeof(inbuffer) - 1);
if (Read > 0)
{
inbuffer[Read] = '\0';
// -------------------------------------------------------------------------------------------`
// 解码 -> 定义响应类 -> 将解码的str反序列化到响应类
std::string enPackage(inbuffer);
uint32_t len = 0;
enPackage = Decode(enPackage, &len);
if (len < 0)
continue;
Response resp;
resp.Deserialize(enPackage);
std::cout << "result: " << resp.GetResult() << ", exitcode: " << resp.GetExitcode() << std::endl;
// -------------------------------------------------------------------------------------------
// std::cout << "Client response result echo# " << inbuffer << std::endl;
}
else if (Read == 0)
{
std::cout << "Server read close, stop service!!!" << std::endl;
break;
}
else
{
std::cout << "Read result fail: " << strerror(errno) << std::endl;
continue;
}
memset(inbuffer, 0, BUFFER_SIZE);
}
}
private:
int sockfd_;
std::string ServerIP_;
uint16_t Port_;
};
int main(int argc, char *argv[])
{
// [可执行程序] [IP adress] [port]
if (argc < 3)
{
std::cout << "Instruction format: [可执行程序] [IP地址] [port]" << std::endl;
exit(FORMAT_ERR);
}
std::string IP = argv[1];
uint16_t port = std::atoi(argv[2]);
TcpClient tpc(IP, port);
tpc.Init();
tpc.Start();
return 0;
}
总结:
-
在进行序列化和反序列化操作时,必须约定号数据格式和版本号,并且确保发送方和接收方使用相同的格式和版本号进行数据传输!
-
在进行序列化和反序列化操作时,还需要注意内存对齐方式和字节序的问题,因为不同平台之间的字节序可能不一致,还要就是结构体对其在不同平台也可能不同!
-
序列化后的数据大小可能会比原始数据要大,需要注意大小的问题,避免数据过大而导致网络传输效率低下或存储空间不足的问题!