【Linux】自定义协议与序列化和反序列化
一、自定义协议
1.1 自定义报文格式
在前面的博客中,我们可以知道在TCP协议中,面向的是字节流;而UDP协议中面向的是数据报。因此,在手写简单的TCP和UDP服务器中,所使用的是接收函数和发送函数不同。因此,在TCP协议中,我们需要分清楚一个完整的报文,并将其分离出来,因此,我们应该如何进行分离出一个完整的报文呢??
如果一个报文中什么标志也没有,那么必然是不可能将一个完整的报文分离出来。因此,我们可以重新定义一下报文格式:为了简单起见,我们采用LV格式,在后面的学习中,当我们学习了TCP报文和UDP报文后,就会知道报文 = 报头 + 有效载荷。报头中存放有效载荷的长度,我们定义的一个简单的报文格式如图所示:
在上一篇博客中,我们也简单的学习如何使用Json来进行序列化和反序列化的操作。我们可以在协议层中定义出来将请求和响应的报文进行序列化和反序列化操作,然后将数据通过传输层进行传输。在根据具体的报文的格式进行分离出一个完整的报文。
1.2 协议类(表示层)
在协议类中,我们创建了请求类和响应类,并且创建出添加报头函数和分解完整报文函数,最后在定义出一个工厂用来创建请求和响应。
1.2.1 请求类
class Request
{
public:
// 我们自定义协议
// 报文 = 包头 + 有效载荷
// LV格式 固定字段长——后续字符串的长度 正文内容\n\t
// 对报文进行分析
// "len\r\n"_x_op_y\r\n"
Request()
{
}
Request(int x, int y, char oper)
: _x(x), _y(y), _oper(oper)
{
}
bool Serialize(std::string *out) // 序列化
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
bool Deserialize(std::string &in) // 反序列化
{
Json::Value root;
Json::Reader reader;
bool ret = reader.parse(in, root);
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
return ret;
}
~Request()
{
}
public:
int _x;
int _y;
char _oper; // "+-*/%"
};
1.2.2 响应类
class Response
{
public:
Response()
{
}
Response(int result, int code)
: _result(result), _code(code)
{
}
bool Serialize(std::string *out) // 序列化
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
bool Deserialize(std::string &in) // 反序列化
{
Json::Reader reader;
Json::Value root;
bool ret = reader.parse(in, root);
_result = root["result"].asInt();
_code = root["code"].asInt();
return ret;
}
~Response()
{
}
public:
int _result;
int _code;
};
1.2.3 添加报文函数
在将创建出来的请求或者响应进行序列化后,将其进行添加报头方式,将其构造成我们所需要的报文格式,以便在之后进行分离出完整报文的操作。
const std::string SEP = "\n\t";
// 进行拼装报文
std::string Encode(const std::string &json_str)
{
int json_str_len = json_str.size();
std::string proto_str = std::to_string(json_str_len);
proto_str += SEP;
proto_str += json_str;
proto_str += SEP;
return proto_str;
}
1.2.4 分解完整报文函数
解决粘包问题,我们根据报文格式,我们可以得出:在第一次遇见“\n\t”的时候,其前面的字符串会有两种情况:要么是空串,要么是报头。当我们解析到报头后,计算出一个完整的报文的总长度,然后个根据总长度将完整报文解析出来。
const std::string SEP = "\n\t";
// 处理粘包问题
std::string Decode(std::string &inbuffer) // const 不能修改
{
// 先找出SEP,然后截取出来len的长度,最后将完整报文截取出来
auto pos = inbuffer.find(SEP);
if (pos == std::string::npos)
return "";
std::string len_str = inbuffer.substr(0, pos);
if (len_str.empty())
return "";
int len = std::stoi(len_str);
int total = len + len_str.size() + SEP.size() * 2;
if (inbuffer.size() < total)
return "";
std::string package = inbuffer.substr(pos + SEP.size(), len);
inbuffer.erase(0, total); // yichu
return package;
}
1.2.5 工厂类
在这里采用简单工厂模式,我们可以将请求类和响应类设置为私有类,不想外部暴漏;只将工厂类向外暴漏,方便用户进行注册请求和响应。
// 简单工厂模式
class Factory
{
public:
Factory()
{
srand(time(nullptr) ^ getpid());
}
// 生产数据 利用随机值将报文填充
std::shared_ptr<Request> BuildRequest()
{
opers = "+-*/%&^";
int x = rand() % 10;
int y = rand() % 5;
char oper = opers[rand() % 8];
std::shared_ptr<Request> req = std::make_shared<Request>(x, y, oper);
return req;
}
std::shared_ptr<Response> BuildResponse()
{
return std::make_shared<Response>();
}
~Factory() {}
private:
std::string opers;
};
二、复习一下TCP服务器的写法
2.1 回忆一下Socket类
在上一节课中,我们将Socket进行封装。我们将TCP套接字和UDP套接字的方法进行封装成一个类中,来回忆一下他们的方法总共有哪些?创建套接字,绑定套接字,监听套接字,TCP接收数据,TCP连接,返回套接字,接收数据,发送数据。
// 在抽象类中,我们可以定义出一些虚函数,供TCPSocket和UDPSocket方便构造各自的函数
virtual void CreateSocketOrDie() = 0; // 创建套接字
virtual void BindSocketOrDie(InetAddr &addr) = 0; // 绑定套接字
virtual void ListenSocketOrDie() = 0; // 监听套接字
virtual socket_sptr Accepter(InetAddr *addr) = 0; // 接受数据
virtual bool Connector(InetAddr &addr) = 0; // 连接
virtual int Sockfd() = 0; // 返回套接字
virtual int Recv(std::string *out) = 0; // 接收数据
virtual int Send(const std::string &in) = 0; // 发送数据
2.2 TcpServer类
在这个类中,我们可以通过2.1中的TcpSocket类进行创建,我们可以创建出智能指针,通过智能指针来进行管理这个变量,调用里面的函数。在执行函数中,我们可以利用Acceptor函数创建出新的套接字,以便于进行与客户端通信。在这个函数中,我们可以使用多线程来进行。
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"
#include "Socket.hpp"
#include <memory>
using namespace socket_ns;
using io_service_t = std::function<void(socket_sptr, InetAddr)>;
class TcpServer;
const static int backlog = 16; // 连接队列
class ThreadDate
{
public:
ThreadDate(socket_sptr sockfd, InetAddr addr, TcpServer *s) : _sockfd(sockfd), _addr(addr), self(s) {}
InetAddr _addr;
socket_sptr _sockfd;
TcpServer *self;
};
class TcpServer
{
public:
TcpServer(int port, io_service_t service)
: _local("0", port), _isrunning(false), _listensock(std::make_unique<TcpSocket>()), _service(service)
{
_listensock->BuildListenSocket(_local); // 创建套接字,绑定套接字,监听套接字
}
~TcpServer()
{
}
static void *HandlerSock(void *args)
{
pthread_detach(pthread_self());
ThreadDate *td = static_cast<ThreadDate *>(args);
td->self->_service(td->_sockfd, td->_addr); // 这个地址就是客户端的地址
::close(td->_sockfd->Sockfd());
delete td;
return nullptr;
}
void Loop()
{
// 4. 不能直接收数据,先获取连接
// accept在通信之前先获取客户端的地址, 返回值是文件描述符
// 这个文件描述符是什么?? 每建立一个链接就会有一个套接字, 用于IO操作
_isrunning = true;
while (_isrunning)
{
// 利用accept函数进行创建出新的套接字
InetAddr peeraddr;
socket_sptr s = _listensock->Accepter(&peeraddr);
if (s == nullptr)
continue;
// version 2 : 采用多线程
pthread_t t;
ThreadDate *td = new ThreadDate(s, peeraddr, this);
pthread_create(&t, nullptr, HandlerSock, td);
}
_isrunning = false;
}
private:
InetAddr _local;
std::unique_ptr<Socket> _listensock;
bool _isrunning;
io_service_t _service;
};
三、业务服务类(应用层)
为了使业务服务与通信服务进一步解耦合,我们可以将业务服务单独封装成一个类,使得整体布局更加具有层次性。
#pragma once
#include <iostream>
#include "Protocol.hpp"
using namespace protocol_ns;
// 解决计算层
class Calculate
{
public:
Calculate()
{
}
// 处理函数
Response Excute(const Request &req)
{
Response rsp(0, 0);
switch (req._oper)
{
case '+':
rsp._result = req._x + req._y;
break;
case '-':
rsp._result = req._x - req._y;
break;
case '*':
rsp._result = req._x * req._y;
break;
case '/':
{
if (req._y == 0)
{
rsp._code = 1;
}
else
{
rsp._result = req._x / req._y;
}
}
break;
case '%':
{
if (req._y == 0)
{
rsp._code = 2;
}
else
{
rsp._result = req._x % req._y;
}
}
break;
default:
rsp._code = 3;
break;
}
return rsp;
}
~Calculate()
{
}
private:
};
四、创建客户端(会话层)
由于之前的套接字封装,我们可以简单地通过智能指针创建出TcpSocket,通过智能指针可以直接创建出客户端套接字并连接服务器成功。我们之后就可以进行通信。
客户端与服务器通信的步骤:创建一个请求;将请求进行序列化;将请求添加报头;发送报文;读取应答;判断应答是否是一个完整的报文;将报文反序列化,最后拿到了结构化的应答。
void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(2);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
InetAddr serveraddr(serverip, serverport);
std::unique_ptr<Socket> cli = std::make_unique<TcpSocket>();
bool res = cli->BuildClientSocket(serveraddr);
std::string buffer;
while (res)
{
Factory factory;
// 1. 创建一个请求
auto req = factory.BuildRequest();
// 2. 将请求进行序列化
std::string request;
req->Serialize(&request);
// 3. 添加报文长度
request = Encode(request);
// 4. 发送报文
cli->Send(request);
// 5. 读取应答
int n = cli->Recv(&buffer);
if (n < 0)
{
break; // 出错了
}
buffer = Encode(buffer);
// 6. 判断应答是否是一个完整的报文
auto resp = factory.BuildResponse();
resp->Deserialize(buffer);
// 7. 拿到了结构化的应答
std::cout << resp->_result << "[" << resp->_code << "]" << std::endl;
}
return 0;
}
五、总结
将OSI七层模式与TCP/IP分层模式进行对比,我们会发现TCP/IP分层模型将会话层、表示层与应用层合并为一层。因为应用层必须要在用户层完成,用户决定了与谁构建连接,用户决定了采用什么样的报文格式以及协议,用户决定了采用什么服务。