目录
[2, 序列化和反序列化](#2, 序列化和反序列化)
一,问题引入:
当我们使用TCP协议来进行网络通信时,因为TCP通信的特点:
面向字节流,没有数据边界。
所以,当我们在读取数据时就会遇到一个问题:在使用TCP通信时到底怎样才算将一段数据读取完成呢?
二,协议
为了解决上面的问题,在网络中便引入了协议。比较著名的协议就有HTTP协议,TCP协议等等。
这些协议通俗的讲其实就是一个约定,收发数据两方的约定。两方约定好如何发数据,如何收数据。
三,自定义协议
虽然在平时使用网络通信时并不需要我们来写协议。但是,我们还是有自己定制协议的能力的。 所以,为了更好的了解协议这个东西。我们可以自己尝试来写一个协议,然后让双方互相通信。
1,协议
现在,我们就来实现一个网络版的计算器。但是我们要做如下约定:数据要以如下格式发送:
len: 代表内容content的长度
\n:代表一个分割符
content:代表数据的正文内容
\n:代表一个分割符
2, 序列化和反序列化
在QQ接收消息时,我们不仅仅会收到信息,我们还会收到头像和昵称。这些消息就是一个结构化的消息 。这些消息经过打包后会形成一段报文(一个整体),打包的过程就是序列化的过程 。在将这段报文解开的过程就是一个反序列化的过程。序列化和反序列化的过程中要使用的就是协议。
四,网络版本的计算器
1,协议的定制
在写这个计算器时首先确定的便是协议的定制。协议定制如下:
cpp
#include<iostream>
#include"log.hpp"
const std::string blank_sep = " ";
const std::string protocol_sep = "\n";
std::string Encode(std::string& content)//加密:"len\ncontent\n"
{
std::string package = std::to_string(content.size());
package += protocol_sep;
package+=content;
package += protocol_sep;
return package;//返回打包后的数据
}
bool Decode(std::string &package,std::string* content) // 解密一段打包后数据:"len\ncontent\n"
{
//先找第一个protocol_sep
int pos = package.find(protocol_sep);
if(pos == std::string::npos)
{
// lg(Debug,"Decode err1");
return false;
}
//找到了第一个protocol_sep,找到这段数据的长度
std::string len_str = package.substr(0, pos);
//std::cout << len_str << std::endl;
int len = std::atoi(len_str.c_str());
int total_len = len_str.size()+2+len;
if(package.size()<total_len)
{
// lg(Debug, "Decode err2");
return false;
}
*content = package.substr(pos+1,len);
package.erase(0,total_len);//解码后将package消掉
return true;
}
//定制协议
class Request//
{
public:
Request()
:x(0),y(0),op('+')
{}
Request(int data1,int data2,char oper)
:x(data1),y(data2),op(oper)
{}
//序列化:将结构化的数据变成字符串(x op y),并带出到外面
void Serialize(std::string* content)
{
std::string str = std::to_string(x);
str += blank_sep;
str += op;
str+= blank_sep;
str += std::to_string(y);
*content = str;
}
bool Deserialize(std::string &content) // 将字符串转化为结构化的数据
{
int pos = content.find(blank_sep);
if(pos == std::string :: npos)
{
//lg(Debug,"Request Deserilization err1");
return false;
}
x = std::stoi(content.substr(0, pos));
op = content[pos + 1];
pos = content.rfind(blank_sep);
if (pos == std::string ::npos)
{
// lg(Debug, "Request Deserilization err2");
return false;
}
y = std::stoi(content.substr(pos+1));
return true;
}
public:
int x;
int y;
char op;
};
class Response
{
public:
Response()
:result(0),code(0)
{}
Response(int res,int c)
: result(res), code(c)
{}
void Serialize(std::string* content)//序列化:str:"result code"
{
std::string str;
str += std::to_string(result);
str += blank_sep;
str+=std::to_string(code);
*content = str;
}
bool Deserialize(std::string &content)
{
int pos = content.find(blank_sep);
if(pos == std::string::npos)
{
//lg(Debug, "Response Deserilization err");
return false;
}
result = std::stoi(content.substr(0, pos));
code = std::stoi(content.substr(pos + 1));
return false;
}
void Debugprint()
{
std::cout <<"result: "<< result << " "<<"code: " << code << std::endl;
}
public:
int result;//结果
int code;//0表示结果正确,!0表示结果错误
};
在这段代码中,我定义了两个类:
Request:代表一个请求。这个类里面有两个方法,代表着序列化和反序列化方法。 类里面有三个成员:x y op,代表着左右操作数和操作符。
Response:代表一个响应。这个类里面有两个方法,代表着序列化和反序列化方法。 类里面有三个成员:result code,代表着结果和结果码(显示结果可不可信)。
在这段代码中还有两个公共的方法:
Encode:对序列化后的内容进行加码,变成如下形式:
Decode:对加码后的内容进行解码,变成一个简单的反序列化的代码。
2,计算逻辑
在制定好协议以后,便可以开始根据这些协议来对客户发来的数据进行处理了。但是如何处理呢?因为我们这里的处理逻辑就是一个计算。所以,写出计算逻辑如下:
cpp
#pragma once
#include "Protocol.hpp"
class Calculator // 计算逻辑
{
public:
//开始计算
Response CalHelper(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.result = req.x / req.y;
else
resp.code = 3;
break;
case '%':
if (req.y != 0)
resp.result = req.x % req.y;
else
resp.code = 3;
break;
default:
resp.code = 4;
break;
}
return resp;
}
// 对加包的数据进行计算,并且要返回序列化并打包的结果
std::string Calculate(std::string &package)//要保证我的数据是一个package
{
std::string content;
bool r = Decode(package, &content);//将package解包并且带出内容
if(!r)
return "";
Request req;
r = req.Deserialize(content);//反序列化后得到了一个结果,然后去计算
if(!r)
return "";
Response resp;
resp = CalHelper(req);//对req进行计算
content = "";//序列化并打包
resp.Serialize(&content);
//std::string package;
content = Encode(content);
return content;//返回打包后的结果
}
};
Calculate:对服务端接收到的消息进行处理。处理过程便是先对数据进行解包,然后再对数据进行反序列化。
CalHelper:对解包并且反序列化后的数据进行计算。并将结果存于Response对象中返回。
3,服务端
在写完如上代码后,我们的服务端便可以开始处理数据了。现在就让我们来搭建一个基于TCP协议的服务端。代码如下:
cpp
#pragma once
#include "log.hpp"
#include"Protocol.hpp"
#include"Socket.hpp"
#include<signal.h>
#include"Calculator.hpp"
Calculator Cal;//定义一个计算器
class CalServer
{
public:
CalServer(uint16_t port) : port_(port)
{
}
bool Init()
{
listensock_.Sock();
listensock_.Bind(port_);
listensock_.Listen();
lg(Info, "init server .... done");
return true;
}
void Start()
{
signal(SIGCHLD, SIG_IGN);//父进程忽略子进程的信号
while (true)
{
std::string clientip;
int clientport;
int sockfd = listensock_.Accept(&clientip, &clientport);
if (sockfd < 0)
continue;
lg(Info, "accept a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);
// 提供服务
if (fork() == 0)
{
listensock_.Close();
std::string inbuffer_stream;
// 数据计算
while (true)//为什么是两个循环?因为我要保证读取到的数据拼接到inbuffer_stream中时是一个完整的报文。
{
char buffer[1280];
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
inbuffer_stream += buffer;
lg(Debug, "debug:\n%s", inbuffer_stream.c_str());
while (true)
{
std::string info = Cal.Calculate(inbuffer_stream);
if (info.empty())
break;
lg(Debug, "debug, response:\n%s", info.c_str());
lg(Debug, "debug:\n%s", inbuffer_stream.c_str());
write(sockfd, info.c_str(), info.size());
}
}
else if (n == 0)
break;
else
break;
}
exit(0);
}
close(sockfd);
}
}
~CalServer()
{
}
private:
uint16_t port_;
Socket listensock_;
};
这里的listensock_ 对象是一个Socket类对象。这个Socket类定义如下:
cpp
#pragma once
#include<iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include<cstring>
#include<arpa/inet.h>
#include<unistd.h>
//定义一些变量
#define blog 10
#define defaultport 8080
class Socket
{
public:
//构造函数
Socket()
: sockfd_(0)
{}
public:
//创建套接字
bool Sock()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
if (sockfd_ < 0)
{
std::cerr << "创建套接字失败" << std::endl;
return false;
}
return true; // 将创建好的套接字返回
}
//bind,服务端只要绑定端口号
bool Bind(int16_t port = defaultport)
{
sockaddr_in server_addr;
memset(&server_addr, 0, sizeof (server_addr));//清空数据
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = INADDR_ANY;
int r1 = bind(sockfd_,(sockaddr*)&server_addr,sizeof server_addr);
if(r1<0)
{
std::cerr << "bind err" << std::endl;
return false;
}
return true;
}
//监听
bool Listen()
{
int r2 = listen(sockfd_, blog);
if(r2<0)
{
std::cerr << "listen err" << std::endl;
return 0;
}
return true;
}
//接收
int Accept(std::string* ip,int* port)
{
sockaddr_in cli_addr;
socklen_t len = sizeof(cli_addr);
int sockfd = accept(sockfd_, (sockaddr *)&cli_addr, &len);
if(sockfd<0)
{
std::cerr << "accept err" << std::endl;
return -1;
}
char buff[64]={0};
inet_ntop(AF_INET, &cli_addr, buff, sizeof(buff));
*ip = buff;
*port = ntohs(cli_addr.sin_port);
return sockfd;
}
//连接
bool Connect(std::string& ip,int16_t port)
{
sockaddr_in addr_;
addr_.sin_family = AF_INET;
addr_.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(addr_.sin_addr));
int r = connect(sockfd_, (sockaddr *)&addr_, sizeof (addr_));
if(r<0)
{
std::cerr << "connect err" << std::endl;
return false;
}
return true;
}
//关闭
void Close()
{
close(sockfd_);
}
public:
//成员
int sockfd_;
};
CalServer内的函数作用:
**Init:**创建套接字,bind套接字,监听套接字。
**Start:**循环接收客户端套接字,并且创建子进程对客户端的Request进行服务。在服务过程中父进程要对子进程的信号进行忽略,所以在创建子进程之前加上 signal(SIGCHLD, SIG_IGN)对子进程信号进行忽略。
4,客户端
客户端的创建代码比较简单,代码如下:
cpp
#include "Socket.hpp"
#include "Protocol.hpp"
#include "log.hpp"
class CalClient
{
public:
void Init(std::string ip, int port)
{
Sock.Sock(); // 创建套接字
Sock.Connect(ip, port);
srand(time(0));
lg(Info, "Connect sucess");
}
void Start()
{
// 创建100以内的数据
const std::string opers = "+-*/%";
const int len = opers.size();
int cnt = 10;
while (cnt--)
{
std::cout <<"------------"<< "第" << cnt << "次测试"
<< "------------" << std::endl;
int data1 = rand() % 100;
int data2 = rand() % 100;
char op = opers[rand() % len];
// 建立需求
Request req(data1, data2, op);
// 序列化
std::string content;
req.Serialize(&content);
// encode
content = Encode(content);
// 送数据到Server
std::cout << "请求构建完成:" << req.x << req.op << req.y << "="
<< "?" << std::endl;
write(Sock.sockfd_, content.c_str(), content.size());
// 接收数据
char buff[1280];
read(Sock.sockfd_, buff, sizeof(buff));
// 解码
std::string package = buff;
content = "";
Decode(package, &content);
// 反序列化
Response resp;
resp.Deserialize(content);
resp.Debugprint();
std::cout << "结果相应完成"
<< "-----------------" << std::endl;
sleep(1);
}
}
private:
Socket Sock;
};
**Init:**创建套接字 向客户端建立连接,生成随机数种子。
**Start:**随机数的方式构建请求,将请求序列化和加码后使用write发送给服务端,然后再使用read将服务端发送回来的结果读取显示。
5,main函数
在是实现完如上代码后,我们可以来实现两个main函数来调用一下如上代码。
cpp
#include "CalServer.hpp"
void usage(std::string proc)
{
std::cout << proc << "port[1024+]" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc!=2)
{
usage(argv[0]);
}
int port = std::stoi(argv[1]);
CalServer *sev = new CalServer(port);
sev->Init();
sev->Start();
return 0;
}
cpp
#include"CalClient.hpp"
void usage(std::string proc)
{
std::cout << proc << "port[1024+]" << std::endl;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
usage(argv[0]);
}
std::string serip = argv[1];
int serport = std::stoi(argv[2]);
CalClient *cli = new CalClient;
cli->Init(serip, serport);
cli->Start();
}
调用以后结果如下:
