linux网络 | 序列化反序列化的概念 与 结合网络计算器深度理解
前言:进行套接字编程的时候, write和read的本质其实是用户层和tcp缓冲区之间进行交互。 交互的时候read方可能会出现数据读取不完整或者数据多读取的情况。解决这些问题使用的就是协议定制和序列化反序列化。 本节内容就是对序列化反序列化和协议定制进行讲解。
ps: 本节内容建议学习完套接字编程之后在进行学习。
目录
tcp内部传送数据的弊端
结构化协议与序列化反序列化
协议
编辑序列化反序列化
网络版本计算机服务端
Socket组件
服务端代码
序列化反序列化
计算机计算
mian
tcp内部传送数据的弊端
我们要思考一个问题, 就是我们在进行套接字编程的时候, tcp协议使用write和read。那么, 我们怎么保证我们写入的和读取的是一个完整的报文呢?
这个问题就类似于管道, 我们也说过,我们的读端和写端存在数据完整性的问题。 也类似于文件操作, 我们如何向文件中写入数据, 以及如何保证读取上来的数据是一些完整的数据。 所以, 我们如果直接使用套接字编程, 不添加任何协议, 代码就是有bug的。
下面这是一个用户通过tcp协议向远端发送数据,具体的工作流程就是用户向tcp协议的发送缓冲区使用write接口写入数据。 然后tcp的缓冲区里面有了这些数据, 就把这些数据发送到远端。
那么, 我们既要知道是tcp管理自己发送缓冲区的数据, 将里面的数据发送到远端。
我们也要知道,网络发送过程中有着各种各样的问题, 比如发多少,什么时候发送, 出错了怎么办, 数据丢包怎么办。因为tcp协议管理发送,所以这些问题都是tcp要解决的问题。
所以, 我们把tcp叫做传输控制协议, 这里重要的就是控制这两个字。比如发多少,什么时候发, 出错了怎么办等等。那么, 我们之前写代码是用read和write从远端读取和发送。但是本质上是将我们用户层要传输的数据拷贝到tcp协议的缓冲区里面。 然后发送的问题, 发送中的问题由我们的tcp协议全权负责。上层用户不用管。
那么, tcp是不是操作系统的一部分?我们之前谈网络分层的时候说过,我们说tcp和ip是在内核当中的,数据链路是在驱动层里面的,应用层才是属于用户的。
所以, tcp是操作系统的一部分,所以我们把数据交给tcp就是把数据交给操作系统。 那我们交给操作系统就可以放心了,因为操作系统一定比我们自己实现的更加快速, 安全。
上面是发送, 发送是没有问题的,操作系统帮我们做好了这一切, 但是读取就有问题了。
read是用户从tcp的接收缓冲区里面读取数据, 但是我们此时的接收缓冲区里面可能有对面发送过来的一组数据, 两组数据, 多组数据。 又或者把一份整体的数据分成了三份, 但是只发送了一份过来。又或者是已经发送了三四次了, 这些数据在接收缓冲区里面。 那么这个时候我们的read可以控制读取多少数据, 是全部读上来呢, 还是读取一半。又或者是读取一部分。
所以, 这个就需要我们在上层应用层定制协议来解决, 我们的读端到时候直接根据协议将这些读上来的数据进行解析。 就能获得完整的对方想要的数据了。
所以, 发送端是直接将是数据拷贝给tcp, tcp怎么发是由操作系统决定, 读端读上来的数据不确定, 那么就通过双方定制协议来定制专属的双方能看懂的数据。
结构化协议与序列化反序列化
协议
我们规定协议是一种结构化的表示。 就比如下面这种四则运算协议。
说是结构化的表示, 其实就是结构体。这个协议里面有数字,有运算符,有运算结构, 有运算的异常处理。但是, 只有这个结构化的数据是不够的, 因为我们平时两个主机进行通信,比如说我们的服务器一般都是使用linux,但是客户端使用的是windows。 而且两个主机的编译器不一样, 那么结构体在两台主机中占的大小就不一样, 因为结构体有内存对齐。 那么主机和主机之间读取数据的时候明明是同一个结构体, 一个主机内结构体是30个字节, 发过去了30个字节。规定好了,但是这个结构体在对面只占15字节。那么它只会读取15个字节, 那么就出问题了。
序列化反序列化
我们通过上面的内容知道只有结构化协议并不能解决问题。那么怎么办? 就必须使用序列化和反序列化。
定义结构体只是第一步, 就比如此时有一个结构体:
这个结构体假设就是我们微信通信的时候双方约定的结构化数据。 然后我们通信的时候,就是在用户层先将数据打包称为message结构体对象。然后再将message结构体序列化为一个字符串, 这个字符串由结构体里面的数据成员组成。就比如message结构体, 就是序列化成: string code = info + nickname + time; 然后把这个字符串发送给对面主机, 对面主机再根据字符串将其转化为对方的message结构体。对方拿到结构体之后,就可以打印数据。
图一
图二
综上, 上面的双方规定相同的结构化对象, 也就是结构体, 其实就是通信双方规定了一个协议。 而将这个message结构体对象里面的多个成员转化为一个字符串的过程就叫做序列化。 接收方拿到字符串后将一个字符串转化为协议对象内部多个成员变量的过程就叫做反序列化。
所以, 我们把双方规定结构体, 叫做协议的定制, 即图一。我们把协议对象与字符串相互转化,数据传输这一层叫做序列化反序列化, 即图二。
网络版本计算机服务端
接下来将实现一个网络版本的计算器的服务端。 利用了上面的序列化反序列化的知识点。主要是学习里面的序列化反序列化的思想:
Socket组件
首先我们自己先封装一套socket套接字的接口。 把它当作组件用。 这样以后我们socket编程的时候就直接复用就行了。 比较方便。如何实现这里不解释,这个不是本节内容的重点:
#include <iostream>
using namespace std;
#include <stdarg.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
// 如何获取时间——时间戳
#define SIZE 1024
#define Info 0
#define Debug 1
#define Waring 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printStyle = Screen;
path = "./log/";
}
~Log()
{
}
void printOneFile(const string &logname, const string &logtxt)
{
string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const string &logtxt)
{
string filename = LogFile;
filename += ".";
filename += levelToString(level); // "log.txt.Debug/Waring/Fatal"
printOneFile(filename, logtxt);
}
void printLog(int level, const string logtxt)
{
switch (printStyle)
{
case Screen:
cout << logtxt;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void Enable(int method)
{
printStyle = method;
}
string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Waring:
return "Warning";
case Fatal:
return "Fatal";
case Error:
return "Error";
default:
return "None";
}
}
// // 未来打印日志直接格式化输出
// void message(int level, const char *format, ...) // 日志的等级 //打印格式 //可变参数
// {
// // 获取时间
// // time函数, 单纯打印时间戳
// 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, ctime->tm_mon, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
// va_list s;
// va_start(s, format);
// char rightbuffer[SIZE * 2];
// vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
// va_end(s);
// char logtxt[SIZE * 2];
// snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
// // printf("%s", logtxt);
// printLog(level, logtxt);
// }
void operator()(int level, const char *format, ...)
{
// 获取时间
// time函数, 单纯打印时间戳
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, ctime->tm_mon, 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\n", leftbuffer, rightbuffer);
// printf("%s", logtxt);
printLog(level, logtxt);
}
private:
int printStyle; // 打印风格
string path;
};
Log lg;
我们可以给socket小组件添加日志信息, 所以里面再使用log这个组件。 下面才是socket套接字部分的代码:
#include<iostream>
#include"Log.hpp"
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<cstring>
enum
{
SockErr = 2,
BindErr,
ListenErr,
};
const int backlog = 10;
class Socket
{
public:
Socket(){}
~Socket(){}
public:
//初始化
void SocketInit()
{
sockfd = socket(AF_INET, SOCK_STREAM, 0); //创建套接字
if (sockfd < 0)
{
lg(Fatal, "socket error...");
exit(SockErr);
}
}
//绑定
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, (sockaddr*)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error");
exit(BindErr);
}
}
//监听
void Listen()
{
//设置监听状态:
if (listen(sockfd, backlog) < 0)
{
lg(Fatal, "listen error");
exit(ListenErr);
}
}
//接受链接, 这个给服务端用
int Accpet(string* clientip, uint16_t* clientport)
{
//创建套接字结构体
struct sockaddr_in client;
socklen_t len = sizeof(client);
//接受客户端套接字
int newfd = accept(sockfd, (sockaddr*)&client, &len);
if (newfd < 0)
{
lg(Waring, "accept error");
return -1;
}
//将套接字里面的数据带出去, 端口号和IP地址
char bufip[64];
memset(bufip, 0, sizeof(bufip));
inet_ntop(AF_INET, &client.sin_addr, bufip, sizeof(bufip));
*clientip = bufip;
*clientport = ntohs(client.sin_port);
return newfd;
}
//链接, 这个给客户端用
void Connect(string serverip, uint16_t serverport)
{
sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr =
}
//关闭句柄
void Close()
{
close(sockfd);
}
private:
int sockfd;
};
服务端代码
服务端代码的流程就是创建两个变量用来接收客户端的ip地址和端口号, 以便将处理后的结果返回给客户端。 然后就是创建一个字节流, 用来接收客户端的数据, 然后经过一系列处理。得到结果后返回给客户端。 这里的一系列处理返回结果就是序列化反序列化以及计算。 这些代码可以封装成一个接口。 让这个接口来完成。也就是里面的callback_。
#include<iostream>
#include"Log.hpp"
#include"socket.hpp"
#include"servercal.hpp"
#include<functional>
#define func_t function<string(string&)>
class TcpServer
{
public:
TcpServer(uint16_t port, func_t callback) : port_(port), callback_(callback)
{}
~TcpServer(){}
public:
void Init()
{
listensockfd_.SocketInit();
listensockfd_.Bind(port_);
listensockfd_.Listen();
lg(Info, "listen success");
}
void Start()
{
while (true)
{
//创建监听到的客户端的各种信息。
string clientip;
uint16_t clientport;
//创建字节流
string str_istream;
int sockfd = listensockfd_.Accpet(&clientip, &clientport);
if (sockfd < 0)
{
continue;
}
lg(Info, "accept success, sockfd:%d, clientip:%s", sockfd, clientip.c_str());
//提供服务
if (fork() == 0)
{
//子程序, 提供服务
listensockfd_.Close();
while (true)
{
//创建一个缓冲区
char buffer[128];
memset(buffer, 0, sizeof(buffer));
//接收客户端的信息
int n = read(sockfd, buffer, sizeof(buffer));
if (n <= 0) break;
else buffer[n] = 0;
//将缓冲区内容拷贝到字节流当中
str_istream += buffer;
//然后就对字符串进行处理,将处理后的结构返回给info
string info = callback_(str_istream);
if (info == "") continue;
//这里是日志信息debug
lg(Debug, "%s", info.c_str());
//将得到的结果返回给客户端
write(sockfd, info.c_str(), info.size());
}
}
exit(0);
}
}
private:
func_t callback_;
uint16_t port_;
Socket listensockfd_;
};
序列化反序列化
我们把请求和结果作为两个结构体。 request和response。 里面分别对请求就是类似:1 + 1, 2 + 2这种表达式进行序列化反序列化。 结果就是对result和code(错误码)进行序列化反序列化。
序列化的过程就是将结构体的成员变量变成字符串, 反序列化的过程就是将字符串变成结构体变量。
然后我们为了写入和读取, 又在字符串前面可以封装报头和解开报头。 报头就是字符串的长度。报头和字符串中间用\n隔开。 每一组数据之间用\n隔开。 这样我们每收到一组数据, 就可以利用报头解开数据。 然后用报头显示的大小和数据进行对比, 如果不对, 就说明不是一个完整的报文。 我们发送字符串之前也要算一下字符串长度, 然后添加报头。 这样就是双方的协议。
#pragma once
#include<iostream>
using namespace std;
#include<string>
#include<jsoncpp/json/json.h>
//封装报头
string Encode(string& content)
{
string package = to_string(content.size());
package += "\n";
package += content;
package += "\n";
return package;
}
//解开报头
bool Decode(string& package, string* content)
{
int pos = package.find('\n', 0); //查找第一个\n
if (pos == string::npos) return false;
string len_str = package.substr(0, pos);
int len = stoi(len_str.c_str());
ssize_t size = len_str.size() + len + 2;
if (package.size() < size) return false;
*content = package.substr(pos + 1, len);
package.erase(0, size);
return true;
}
class Request
{
public:
Request(){}
Request(int data1, int data2, char oper) : x_(data1), y_(data2), op_(oper){}
~Request(){}
bool Serialize(string* out)
{
string s;
s += to_string(x_);
s += " ";
s += op_;
s += " ";
s += to_string(y_);
*out = s;
return true;
}
bool DeSerialize(string in)
{
string s = in;
int left = s.find(' ', 0);
if (left == string::npos) return false;
string part_x = s.substr(0, left);
int right = s.find(' ', left + 1);
if (right == string::npos) return false;
char part_op = s[left + 1];
string part_y = s.substr(right + 1);
x_ = stoi(part_x);
y_ = stoi(part_y);
op_ = part_op;
return true;
}
int x_;
int y_;
char op_;
};
class Response
{
public:
Response(){}
Response(int result, int code) : result_(result), code_(code)
{}
~Response(){}
bool Serialize(string* out)
{
string s;
s += to_string(result_);
s += " ";
s += to_string(code_);
*out += to_string(sizeof(s));
*out += "\n";
*out += s;
return true;
}
bool DeSerialize(string in)
{
string s = in;
int pos = s.find(' ', 0);
if (pos == string::npos) return false;
string part_res = s.substr(0, pos);
string part_code = s.substr(pos + 1);
result_ = stoi(part_res);
code_ = stoi(part_code);
return true;
}
int result_;
int code_;
};
计算机计算
最后一部分就是计算器计算的内容。 就是拿到一个数据, 进行解包, 解包之后反序列化。 然后计算。 这个函数其实就是服务器代码里面的callback_。
#include"protocol.hpp"
class ServerCal
{
public:
Response CalcuHelper(Request req)
{
Response res;
switch (req.op_)
{
case '+':
res.result_ = req.x_ + req.y_;
break;
case '-':
res.result_ = req.x_ - req.y_;
break;
case '*':
res.result_ = req.x_ * req.y_;
break;
case '/':
if (req.y_ == 0)
{
res.code_ = 1;
}
else
{
res.result_ = req.x_ / req.y_;
}
break;
case '%':
if (req.y_ == 0)
{
res.code_ = 1;
}
else
{
res.result_ = req.x_ % req.y_;
}
break;
default:
break;
}
}
string Calculater(string& package) //接受一个报文
{
string content;
bool r = Decode(package, &content); //将报文解包,如果这个时候报文, 第一个数字是\n说明后面一定是一个 然后把数据放到新的字符串中
if (!r) return "";
cout << content << endl; //此时拿到的是完整的字符串, 这个字符串, 就是1 + 1, 没有\n
//
Request req; //重新创建一个变量
req.DeSerialize(content); //然后将变量反序列化
Response res = CalcuHelper(req);
content.clear();
req.Serialize(&content);
content = Encode(content);
return content;
}
};
mian
最后mian函数就直接启动服务就行了
void Usage(string proc)
{
cout << "Usage: " << proc << endl;
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = stoi(argv[1]);
ServerCal cal;
function<string(string&)> func = bind(&ServerCal::Calculater, &cal, std::placeholders::_1);
TcpServer* tsvp = new TcpServer(port, func);
tsvp->Init();
tsvp->Start();
return 0;
}
——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!