🎬 胖咕噜的稞达鸭 :个人主页
🔥 个人专栏 : 《数据结构》《C++初阶高阶》
《Linux系统学习》
《算法日记》
⛺️技术的杠杆,撬动整个世界!
完整代码请移步我的gitee:
网络计算器完整代码
protocol.hpp中解决序列反序列化问题,数据粘报
应用在网络计算器的项目中:
定义在协议中:protocol.hpp
cpp
std::string Serialize()//序列化
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::FastWriter writer;
std::string s = writer.write(root);
return s;
}
//{"x":10, "y" : 20}
bool Deserialize(std::string &in)//反序列化
{
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(in,root);//将收到in的数据反序列到root中
if(ok)
{
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();//作为一个整数
}
return ok;
}
cpp
std::string Serialize()//序列化
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::FastWriter writer;
std::string s = writer.write(root);
return s;
}
//{"x":10, "y" : 20}
bool Deserialize(std::string &in)//反序列化
{
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(in,root);//将收到in的数据反序列到root中
if(ok)
{
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asString();
}
return ok;
}

数据粘报问题---> 由程序员解决。
定义一个jsonstring串:数据长度+数据信息
bash
content_len jsonstring
//50\r\n{"x" : 10, "y" : 20, "oper" : '+'}\r\n
//50
//{"x" : 10, "y" : 20, "oper" : '+'}
完整的报文:一个jsonstring串+jsonstring串的长度,长度报头和有效载荷之间用特殊字符串区分开。
有jsonstring串的长度,我们就知道了Tcp面向字节流,发送的数据有多少个,我们应当接收多少个,不至于拿到不完整的报文。
如何具体解决:
以下面代码为例具体实现:
cpp
std::string Encode(const std::string jsonstr)//这一个函数用途:把一个jsonstring字符串分为包含长度和有效载荷的数据
{
std::string len = std::to_string(jsonstr.size());//计算数据报文有效载荷的长度
//50\r\n{"x" : 10, "y" : 20, "oper" : '+'}\r\n
/* std::string package = len + sep + jsonstr + sep; */
return len + sep + jsonstr + sep;//应用层封装报头
}
//50\r\n{"x" : 10, "y" : 20, "oper" : '+'}\r\n : package_len_str.size() + package_len_int + 2 * sep.size();
//用途:1.判断报文的完整性
// 2.如果包含至少一个完整请求,提取出来,方便处理下一个
//提取到的数据放到buffer中
bool Decode(std::string &buffer, std::string *package)
{
ssize_t pos = buffer.find(sep);//找到第一个分隔符
if(pos == std::string::npos)//报文中没有找到sep分隔符
{
return false;//不完整报文,让调用方继续从内核中取出数据
}
//走到这一步就说明:有长度+分隔符
std::string package_len_str = buffer.substr(0,pos);
int package_len_int = std::stoi(package_len_str);//计算长度
//buffer一定有长度,不一定有完整的报文
int target_len = package_len_str.size() + package_len_int + 2 * sep.size();//整个buffer的长度必须大于target_len
if(buffer.size() < target_len)
{
//buffer不完整
return false;
}
//走到这一步说明BUffer中一定至少有一个完整的报文
*package = buffer.substr(pos + sep.size(),package_len_int);//从buffer串中提取出来我们想要的jsonstring
buffer.erase(0,target_len);
return true;
}
获取请求:在协议中要实现:接收到信息
- 读取:在解析报文,获取到完整的
json请求(*out += buffer,socket.hpp中封装的含义,就是不断提取,直到完整且符合要求的字符串才开始执行下面的代码)。 - 拿到完整报文之后需要反序列化。
- 拿取完了而且也反序列化解析出来了,此时就需要执行业务。业务不属于协议,在外面定义一个
NetCal.hpp来实现我们的网络计算器的代码。
cpp
//获取请求
void GetRequest(std::shared_ptr<Socket> &sock,InetAddr &client)
{
//读取:需要通过套接字读取
//定义一个缓冲区
std::string inbuffer;
while(true)
{
int n = sock->Recv(&inbuffer);//用Recv读取
if(n > 0)
{
//1.解析报文,提取完整的json请求,不完整就让服务器继续读取
bool ret = Decode(inbuffer, &json_package);
if(!ret)//流式字符串中不存在一个完整的报文,不做处理,继续读报文:所以socket.hpp中封装的是*out += buffer
continue;
//2.此时走到这里,一定拿到了一个完整的报文,提取出来了一个完整的json串,而且也从队列中移除了
//50\r\n{"x" : 10, "y" : 20, "oper" : '+'}\r\n 此时拿到了字符串,对方发给我们的是一个请求字符串,我们要做反序列化
Request req;
bool ok = req.Deserialize(json_package);
if(!ok)
continue;//报文不符合要求,继续拿取
//3.此时我们拿到的是一个内部属性已经被设置了的req了
//通过req 得到 resp,此时要完成计算功能
//这一步的计算功能属于业务级别了,不属于协议的内容,所以就在外面实现一个函数
}
else if(n == 0)
{
LOG(LogLevel::INFO) << "client: " << client.StringAddr() << "Quit!";
break;
}
else
{
LOG(LogLevel::WARNING) << "client: " << client.StringAddr() << "recv error!";
break;
}
/* sock->Recv(&inbuffer);//用Recv读取
//此时要对读取到的数据进行处理,不然会越来越长
std::cout << "inbuffer:\n" << inbuffer << std::endl; */
}
sock->Close();
}
socket.hpp中封装实现数据读取和发送
当实现了序列化反序列化,将数据封装解包,此时就需要读取数据。读取的时候我们用套接字读取。所以在socket.hpp中封装。
cpp
virtual int Recv(std::string *out) = 0;
virtual int Send(const std::string out) = 0;
recv接口:接收信息

recv是专门用来做套接字的读取封装的。与read使用方式多了一个int flags。此处我们设置为0.
send接口:发送信息

cpp
int Send(const std::string &message)override{
return send(_sockfd,message.c_str(),message.size(),0);
//在套接字中写,写message.c_str(),写入message.size()个长度,标记位为0
}
独立封装一个文件实现计算器业务NetCal.hpp:+ - * / %
cpp
#pragma once
#include "Protocol.hpp"
#include <iostream>
class Cal
{
public:
Response Execute(Request &req)
{
Response resp(0,0);//code 表示成功
switch(req.Oper())
{
case '+':
resp.SetResult(req.X() + req.Y());
break;
case '-':
resp.SetResult(req.X() - req.Y());
break;
case '*':
resp.SetResult(req.X() * req.Y());
break;
case '/':
{
if(req.Y() == 0)
{
resp.SetCode(1);//发生了除零错误
}
else
{
resp.SetResult(req.X() / req.Y());
}
}
break;
case '%':
{
if(req.Y() == 0)
{
resp.SetCode(2);//发生了模零错误
}
else
{
resp.SetResult(req.X() % req.Y());
}
}
break;
default:
resp.SetCode(3);//非法错误
break;
}
return resp;
}
};
增加客户端TcpClient.cc
客户端代码的雏形:终端输入+套接字建立+链接+发送给服务端
cpp
class Socket内public:
virtual int Connect(const std::string &server_ip, uint16_t port ) = 0;//客户端用,发起连接的请求
void BuildTcpClientSocketMethod()//创建套接字
{
SocketOrDie();
}
Tcpsocket派生类:
virtual int Connect(const std::string &server_ip, uint16_t port )override //实现客户端连接服务端
{
InetAddr server(server_ip,port);
return ::connet(_sockfd,server.NetAddrPtr(),server.NetAddrLen());
}
cpp
include <iostream>
#include "Common.hpp"
#include "Socket.hpp"
#include <string>
#include <memory>
void Usage()
{
std::cerr << "Usage: " << proc << "server_ip server_port" << std::endl;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
//创建一个套接字
std::unique_ptr<Socket> client = std::make_unique<TcpSocket>();
client->BuildTcpClientSocketMethod();
/* client->Connect(server_ip,server_port);//发起建立链接的请求 */
if(client->Connect(server_ip,server_port) == 0)
{
//成功
client->Send()
}
}
从标准输入中获取数据
cpp
void GetDataFromStdin(int *x,int *y,char *oper)
{
std::cout << "Please Enter x:";
std::cin >> *x;
std::cout << "Please Enter y:";
std::cin >> *y;
std::cout << "Please Enter oper:";
std::cin >> *oper;
}
从标准输入中获取到数据,我们还想这个数据是可以直接使用的,所以最好将请求端的字符串实现到协议中:Protocol.hpp
cpp
//获取请求
void GetRequest(std::shared_ptr<Socket> &sock,InetAddr &client)
{
//读取:需要通过套接字读取
//定义一个缓冲区
std::string inbuffer;
while(true)
{
int n = sock->Recv(&inbuffer);//用Recv读取
if(n > 0)
{
std::cout << "..............request_buffer......" << std::endl;
std::cout << inbuffer << std::endl;
std::cout << "..................................." << std::endl;
std::string json_package;
//1.解析报文,提取完整的json请求,不完整就让服务器继续读取
bool ret = Decode(inbuffer, &json_package);
if(!ret)//流式字符串中不存在一个完整的报文,不做处理,继续读报文:所以socket.hpp中封装的是*out += buffer
continue;
//2.此时走到这里,一定拿到了一个完整的报文,提取出来了一个完整的json串,而且也从队列中移除了
//50\r\n{"x" : 10, "y" : 20, "oper" : '+'}\r\n 此时拿到了字符串,对方发给我们的是一个请求字符串,我们要做反序列化
Request req;
bool ok = req.Deserialize(json_package);
if(!ok)
continue;//报文不符合要求,继续拿取
//3.此时我们拿到的是一个内部属性已经被设置了的req了
//通过req 得到 resp,此时要完成计算功能
Response resp = _func(req);//计算完成之后有resp接收
//4.返回给客户端:序列化
std::string json_str = resp.Serialize();
//5.添加自定义长度
std::string send_str = Encode(json_str);//此时就是一个携带长度的报文了
//6.直接发送
sock->Send(send_str);
}
else if(n == 0)
{
LOG(LogLevel::INFO) << "client: " << client.StringAddr() << "Quit!";
break;
}
else
{
LOG(LogLevel::WARNING) << "client: " << client.StringAddr() << "recv error!";
break;
}
/* sock->Recv(&inbuffer);//用Recv读取
//此时要对读取到的数据进行处理,不然会越来越长
std::cout << "inbuffer:\n" << inbuffer << std::endl; */
}
}
总结
1. 基础封装模块(Socket.hpp)
核心接口及逻辑
- Socket基类纯虚接口 :定义统一通信规范,屏蔽底层差异
SocketOrDie():创建套接字(调用::socket),失败则日志打印并退出,确保套接字创建成功。BindOrDie(uint16_t port):绑定端口(调用::bind),关联本地地址与套接字,失败则日志+退出。ListenOrDie(int backlog):开启监听(调用::listen),设置等待连接队列大小,失败则日志+退出。Accept(InetAddr *client):接收客户端连接(调用::accept),获取客户端地址并返回新连接套接字的智能指针。Recv(std::string *out):纯虚接口,从套接字读取数据,将字节流存入out缓冲区(本质是内核缓冲区到用户内存的拷贝)。Send(const std::string out):纯虚接口,发送数据(调用::send),将out中的数据拷贝到内核发送缓冲区。Connect(const std::string &server_ip, uint16_t port):客户端专属接口,发起TCP连接(调用::connect),关联服务端IP和端口。
派生类(TcpSocket)实现逻辑
- 继承Socket基类,实现所有纯虚接口,内部维护
_sockfd(套接字文件描述符),统一管理连接生命周期。 - 提供
BuildTcpSocketMethod()(服务端)和BuildTcpClientSocketMethod()(客户端),封装套接字创建、绑定、监听/连接的流程,简化调用。
2. 协议模块(Protocol.hpp)
核心结构与接口
-
Request类(请求端):封装客户端计算请求数据
Serialize():将请求参数(_x、_y、_oper)序列化为JSON字符串(通过Json库的FastWriter),便于网络传输。Deserialize(std::string &in):将接收的JSON字符串反序列化为Request对象,提取x、y、oper赋值给成员变量,失败返回false。
-
Response类(响应端):封装服务端计算结果
Serialize():将计算结果(_x、_y、_oper、_result、_code)序列化为JSON字符串,用于回传客户端。Deserialize(std::string &in):将服务端返回的JSON字符串反序列化为Response对象,提取结果和状态码。
-
粘包解决方案接口
Encode(const std::string jsonstr):给JSON字符串添加长度报头和分隔符(格式:长度+sep+JSON串+sep),明确报文边界。Decode(std::string &buffer, std::string *package):解析缓冲区数据,通过分隔符提取长度报头,判断是否为完整报文;完整则提取JSON串,移除缓冲区中已处理部分,不完整则返回false。
3. 业务逻辑模块(NetCal.hpp)
核心接口及逻辑
Cal类的Execute(Request &req):实现计算器核心业务- 接收
Request对象,根据oper(+、-、*、/、%)执行对应运算。 - 处理异常情况:除零/模零错误设对应状态码(1/2),非法运算符设状态码(3),正常则设置计算结果。
- 返回
Response对象,包含原始参数、运算结果和状态码。
- 接收
4. 服务端模块(TcpServer.hpp)
核心接口及逻辑
ioservice_t回调函数:std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>- 实现业务解耦,TcpServer框架不关心具体业务,通过该回调将连接和客户端地址传递给业务逻辑(如
GetRequest)。
- 实现业务解耦,TcpServer框架不关心具体业务,通过该回调将连接和客户端地址传递给业务逻辑(如
GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client):服务端请求处理流程- 循环调用
sock->Recv(&inbuffer)读取数据,存入缓冲区。 - 调用
Decode解析缓冲区,提取完整JSON报文。 - 调用
Request::Deserialize将JSON串转为请求对象。 - 调用
Cal::Execute执行业务计算,获取Response对象。 - 序列化
Response并通过sock->Send回传客户端。 - 处理客户端断开(
n==0)或接收错误(n<0)的情况,打印日志并退出循环。
- 循环调用
5. 客户端模块(Tcpclient.cc)
核心接口及逻辑
BuildTcpClientSocketMethod():调用SocketOrDie创建客户端套接字。Connect(const std::string &server_ip, uint16_t port):连接服务端IP和端口,成功则进入数据交互流程。GetDataFromStdin(int *x, int *y, char *oper):从终端读取用户输入的运算数(x、y)和运算符(oper)。- 数据发送流程:读取输入后,通过
Request::Serialize生成JSON请求串,调用Encode添加报头,再通过Send发送给服务端;接收服务端响应后,反序列化并解析结果。
关键依赖与设计原则
- 序列化/反序列化 :依赖Json库(
Json::Value、FastWriter、Reader),实现结构化数据与字节流的转换,适配网络传输。 - 解耦设计 :通过回调函数(
ioservice_t)分离框架与业务,Socket封装屏蔽底层API差异,协议模块独立处理粘包和数据格式。 - TCP适配 :通过"长度报头+分隔符"解决TCP面向字节流的粘包问题,确保报文完整性;
Recv循环读取缓冲区,适配"多次写、一次读"或"一次写、多次读"的场景。