当前位置: 首页 > news >正文

【计算机网络-应用层】基于C++与JSON的自定义协议实现(序列化、反序列化)——构建网络版计算器

📚 博主的专栏

🐧 Linux   |   🖥️ C++   |   📊 数据结构  | 💡C++ 算法 | 🅒 C 语言  | 🌐 计算机网络

上篇文章:TCP(传输控制协议)套接字编程,多线程远程执行命令编程

下篇文章:HTTP协议

目录

应用层

再谈 "协议"

序列化 和 反序列化​编辑

重新理解 read、 write、 recv、 send 和 tcp 为什么支持全双工

开始实现

Socket.hpp

TcpServer.hpp

Service.hpp:

Protocol.hpp 

简单讲解Jsoncpp

特性:

安装

序列化

1.  使用 Json::FastWriter:

示例:

 允许我们做嵌套:

数组追加:

反序列化:

1.   使用 Json ::Reader: 

示例:

继续定制协议:Protocol.hpp 

NetCal.hpp

调用函数cpp:ServerMain.cc

客户端:ClientMain.cc

我们的整个代码一共分成了三层:会话、表示(是我们本文章的重点、协议沟通)、和应用层(业务逻辑)


摘要:本文深入探讨应用层协议的设计与实践,以构建一个网络版计算器为例,详细解析如何通过自定义协议实现客户端与服务端的高效通信。文章重点讲解协议的核心作用,通过序列化(JSONcpp库)与反序列化技术将结构化数据转化为字符串传输,并设计报文头(如“len\r\n{json}\r\n”)确保数据完整性。结合Socket编程,演示TCP全双工通信的实现,以及如何通过多线程处理并发请求。代码层面分层设计会话层(TcpServer)、表示层(协议解析)和应用层(业务逻辑),最终实现支持加减乘除运算的网络服务。本文通过完整项目实践,帮助读者理解协议定制、数据传输控制及跨平台通信的关键技术。

应用层

我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层

再谈 "协议"

协议是一种 "约定". socket api 的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些 "结构化的数据" 怎么办呢?

其实, 协议就是双方约定好的结构化的数据

也就是在之后,我们将实现一个复杂的网络版计算器:实现这样的效果,这两个结构体,就是协议

但是我们非常不推荐直接以结构体的方式传给客户端或者服务端,以结构体的方式来进行双方通信,因为不具有跨平台性, 可扩展性非常不好。

例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端。

约定方案一:

• 客户端发送一个形如"1+1"的字符串;

• 这个字符串中有两个操作数, 都是整形;

• 两个数字之间会有一个字符是运算符, 运算符只能是 + ;

• 数字和运算符之间没有空格;

• ...

约定方案二:

• 定义结构体来表示我们需要交互的信息;

• 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;

• 这个过程叫做 "序列化" 和 "反序列化"

序列化 和 反序列化

无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据,

在另一端能够正确的进行解析, 就是 ok 的. 这种约定, 就是 应用层协议

但是, 为了让我们深刻理解协议, 我们打算自定义实现一下协议的过程。

我们采用方案 2, 我们也要体现协议定制的细节

• 我们要引入序列化和反序列化, 只不过我们直接采用现成的方案 -- jsoncpp库

• 我们要对 socket 进行字节流的读取处理

重新理解 read、 write、 recv、 send 和 tcp 为什么支持全双工

一个fd代表一个连接、一个连接,有两个缓冲区。

结论:

1. read、 write、 recv、 send本质都是拷贝函数 

2. 发数据的本质,是从发送方的发送缓冲区把数据通过协议栈和网络拷贝给接受方的缓冲区

3. tcp支持全双工通信的原因:接口不同。

注意:(传输层很网络层属于OS)、数据什么时候发、数据发多少,出错怎么办,OS管理(定期刷新)由TCP决定。

4. tcp叫做传输控制的原因(OS)

5. 生产者消费者模型

6. 为什么IO函数会阻塞:本质就是在维护同步关系

所以:

• 在任何一台主机上, TCP 连接既有发送缓冲区, 又有接受缓冲区, 所以, 在内核中,可以在发消息的同时, 也可以收消息, 即全双工

• 这就是为什么一个 tcp sockfd 读写都是它的原因

• 实际数据什么时候发, 发多少, 出错了怎么办, 由 TCP 控制, 所以 TCP 叫做传输控制协议

面向字节流:

客户端发的,不一定全部是服务端收的

上篇博客所遗留的问题:

recv为什么是不对的:

如果我给服务器发送的字符串是ls -a -l,但os只发了一个 ls -。(偶发性)

因此如何保证操作系统发的是一个完整的请求?

使用"序列化" 和 "反序列化"制定协议,并且分割完整的报文

开始实现

代码结构

Makefile

Protocol.hpp ---->协议

Socket.hpp    --->套接字

Calculate.hpp --->

TcpServer.hpp ---->服务端

Daemon.hpp

TcpClientMain.cc

TcpServerMain.cc --->服务端调用

简单起见, 可以直接采用自定义线程

直接 client<<->>server 通信, 这样可以省去编写没有干货的代码

我先对上一篇博客中最后完成的代码Tcp Echo Server 进行重构

Socket.hpp

封装了套接字相关方法:套接字创建、绑定、监听、连接

#pragma once
#include <iostream>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <memory>
// #include "Command.hpp"#include "Log.hpp"
#include "InetAddr.hpp"namespace sock_ns
{class Socket;using namespace log_ns;using SockSPtr = std::shared_ptr<Socket>;const static int gblcklog = 8;enum{SOCKET_ERROR = 1,BIND_ERROR,LISTEN_ERROR};// 模版方法类模式:class Socket{public:virtual void CreateSocketOrDie() = 0;virtual void CreateBindOrDie(uint16_t port) = 0;virtual void CreateListenOrDie(int backlog = gblcklog) = 0;virtual SockSPtr Accepter(InetAddr *clientaddr) = 0;virtual bool Connector(std::string peerip, uint16_t peerport) = 0;virtual int Sockfd() = 0;virtual void Close() = 0;virtual ssize_t Recv(std::string *out) = 0;virtual ssize_t Send(const std::string &in) = 0;public:// 创建监听套接字void BuildListenSocket(uint16_t port){CreateSocketOrDie();CreateBindOrDie(port);CreateListenOrDie();}// 创建客户端套接字void BuildClientSocket(std::string peerip, uint16_t peerport){CreateSocketOrDie();Connector(peerip, peerport);}// void BuildUdpSocket()// {}};class TcpSocket : public Socket{public:TcpSocket(){}TcpSocket(int sockfd) : _sockfd(sockfd){}// 创建套接字void CreateSocketOrDie() override{// 1.创建socket_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(FATAL, "socker create error\n");exit(SOCKET_ERROR);}LOG(INFO, "socket create success, sockfd: %d\n", _sockfd);}// 绑定、本地socket信息、端口和IPvoid CreateBindOrDie(uint16_t port) override{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;// 2.绑定sockfd 和 socket addrif (::bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0){LOG(FATAL, "bind error \n");exit(BIND_ERROR);}LOG(INFO, "bind success \n");}// 监听void CreateListenOrDie(int backlog) override{// 让套接字设置为listen状态if (::listen(_sockfd, gblcklog)){LOG(FATAL, "listen error \n");exit(LISTEN_ERROR);}LOG(INFO, "listen success \n");}SockSPtr Accepter(InetAddr *clientaddr) override{struct sockaddr_in client;socklen_t len = sizeof(client);// 4.获取新链接// 从监听套接字获取新的套接字、获取客户端信息int sockfd = ::accept(_sockfd, (struct sockaddr *)&client, &len);// 获取连接失败、继续获取if (sockfd < 0){LOG(WARNING, "accept error\n");return nullptr;}*clientaddr = InetAddr(client);// 获客成功,提供服务return std::make_shared<TcpSocket>(sockfd);}// 链接客户端服务器// peerip远端ip、peerport远端portbool Connector(std::string peerip, uint16_t peerport) override{ // 2.不需要显示的bind,但是一定要有自己的IP和port,所以需要隐式的bind,OS会自动bind sockfd,用自己的IP和随机算口号// 什么时候进行自动bind,if the connection or binding succeedsstruct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(peerport);// server.sin_addr.s_addr =// 进程序列转为网络序列::inet_pton(AF_INET, peerip.c_str(), &server.sin_addr);int n = ::connect(_sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){// 连接失败std::cerr << "connect socket error" << std::endl;return false;}return true;}int Sockfd(){return _sockfd;}void Close(){if (_sockfd > 0){::close(_sockfd);}}// 还需调整ssize_t Recv(std::string *out) override{// 缺点:inbuffer不是动态的,每次只能读取这么多,或者说,明明只发了5个字节,但是却还是要读取1023个字节char inbuffer[4096];// n是实际读取的字节数                                   // 当做字符串ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0); //(留一个\n的位置)if (n > 0){inbuffer[n] = 0;*out = inbuffer;}return n;}ssize_t Send(const std::string &in) override{return ::send(_sockfd, in.c_str(), in.size(), 0);}~TcpSocket(){}private:int _sockfd; // 可以是listen套接字、也可以是普通套接字};
}

TcpServer.hpp

#pragma once
#include"Socket.hpp"
#include "InetAddr.hpp"
#include <functional>
//设置函数对象using namespace sock_ns;
static const int gport = 8888;using service_io_t = std::function<void(SockSPtr, const InetAddr &)>;class TcpServer
{public:TcpServer(service_io_t service,  uint16_t port = gport) : _port(port), _listensock(std::make_shared<TcpSocket>()), _isrunning(false), _service(service) {_listensock->BuildListenSocket(_port);}class ThreadData{public:SockSPtr _sockfd;TcpServer *_self;InetAddr _addr;//给InetAddr再添加一个无参构造public:ThreadData(SockSPtr sockfd, TcpServer *self, const InetAddr &addr) : _sockfd(sockfd), _self(self), _addr(addr) {}};void Loop(){// signal(SIGCHLD, SIG_IGN);_isrunning = true;while (_isrunning){InetAddr client;SockSPtr newsock = _listensock->Accepter(&client);if(newsock == nullptr){continue;}LOG(INFO, "get a new link, client info : %s, sockfd is: %d\n", client.AddrStr().c_str(), newsock->Sockfd());// version2-----多线程版本 --- 不能关闭fd,也不需要pthread_t tid;ThreadData* td = new ThreadData(newsock, this, client);pthread_create(&tid, nullptr, Execute, td);}_isrunning = false;}static void *Execute(void *args){pthread_detach(pthread_self());ThreadData* td = static_cast<ThreadData *>(args);//直接回调我们定义的函数对象td->_self->_service(td->_sockfd, td->_addr);//处理完了,直接关闭::close(td->_sockfd->Close());  delete td;return nullptr;}~TcpServer(){}private:uint16_t _port;
//   int _listensockfd;
//   定义套接字对象SockSPtr _listensock;bool _isrunning;service_io_t _service;
};

Service.hpp:

将来服务器要求的是这种类型:

using service_io_t = std::function<void(SockSPtr, const InetAddr &)>;
#pragma once
#include <iostream>
#include "InetAddr.hpp"
#include "Socket.hpp"
#include "Log.hpp"
using namespace log_ns;
using namespace sock_ns;
// 主要是进行io服务
// using service_io_t = std::function<void(SockSPtr, const InetAddr &)>;
class IOService
{
public:IOService(){}void IOExecute(SockSPtr sock, InetAddr &addr){// 长服务while (true){std::string message;ssize_t n = sock->Recv(&message);if (n > 0){LOG(INFO, "get message from client %s, message: %s\n", addr.AddrStr().c_str(), message.c_str());std::string hello = "hello";sock->Send(hello);}else if (n == 0){LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());break;}else{LOG(ERROR, "recv error: %s\n", addr.AddrStr().c_str());break;}}}~IOService(){}
};

现在我们可以开始制定协议;

Protocol.hpp 

对于将字符串转成结构化字段、和将结构化字段转成字符串,序列化和反序列化:

1.我们可以自己写:

规定:“x opr y”;做一个传的字符串的格式,根据分隔符来提取。

2.使用现成的库来完成

xml 、JSON、protobuf

我们选择JSON(C++、JAVA、Python都使用的是JSON)

而在C++中就必须使用JSON库(jsoncpp)

简单讲解Jsoncpp

Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。 它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。 Jsoncpp 是开源的, 广泛用于各种需要处理 JSON 数据的 C++ 项目中。

特性:

1. 简单易用: Jsoncpp 提供了直观的 API, 使得处理 JSON 数据变得简单。

2. 高性能: Jsoncpp 的性能经过优化, 能够高效地处理大量 JSON 数据。

3. 全面支持: 支持 JSON 标准中的所有数据类型, 包括对象、 数组、 字符串、 数字、 布尔值和 null。

4. 错误处理: 在解析 JSON 数据时, Jsoncpp 提供了详细的错误信息和位置, 方便开发者调试。当使用 Jsoncpp 库进行 JSON 的序列化和反序列化时, 确实存在不同的做法和工具类可供选择。 以下是对 Jsoncpp 中序列化和反序列化操作的详细介绍:

安装

ubuntu:

sudo apt-get install libjsoncpp-dev

centos:

sudo yum install jsoncpp-devel

序列化

序列化指的是将数据结构或对象转换为一种格式, 以便在网络上传输或存储到文件中。 Jsoncpp 提供了多种方式进行序列化:

1.  使用 Json::FastWriter:

优点 :比 StyledWriter 更快 ,因为它不添加额外的空格和换行符。 

示例:

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
// 结构化转字符串、字符串转结构化---->序列化和反序列化
// 请求要有序列化的接口、客户端要发请求给服务器, 也要有反序列化,因为服务器也需要读到请求,然后我们做反序列化
class Request
{
public: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;// FastWriter是一个类Json::FastWriter writer;std::string s = writer.write(root);// std::cout << s << std::endl;*out = s;return true;}// 服务端用反序列,别人会给我传进来一个字符串,我需要将这个字符串转成结构化字段(内部属性的值void Deserialize(const std::string &in){}~Request(){}private:int _x;int _y;char _oper; //+ 、— 、/、% //x oper y
};int main()
{Request req(111, 222, '-');std::string s;req.Serialize(&s);std::cout << s << std::endl;return 0;
}

运行结果:

注意在执行命令时,要添加上第三方库jsoncpp:

g++ -o test test.cc -ljsoncpp
./test 
{"oper":45,"x":111,"y":222}

最后转换成了这种格式的字符串,就叫做JSON串,序列化的结果是json自定义的,但是我们也可以修改一些属性自定义。

StyleWriter

        Json::StyledWriter writer;

运行结果:

{"oper" : 45,"x" : 111,"y" : 222
}

 允许我们做嵌套:

    bool Serialize(std::string *out){Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::Value sub;sub["name"] = "张三";sub["age"] = 12;//Json::value中可以套对象,最后是可以呈树状呈现root["sub"] = sub;// FastWriter是一个类// Json::FastWriter writer;Json::StyledWriter writer;std::string s = writer.write(root);// std::cout << s << std::endl;*out = s;return true;}
{"oper" : 45,"sub" : {"age" : 12,"name" : "\u5f20\u4e09"},"x" : 111,"y" : 222
}

数组追加:

    bool Serialize(std::string *out){Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::Value sub;sub["name"] = "张三";sub["age"] = 12;//Json::value中可以套对象,最后是可以呈树状呈现root["sub"] = sub;Json::Value array(Json::arrayValue);array.append(1);array.append(2);array.append(5);root["array"] = array;// FastWriter是一个类// Json::FastWriter writer;Json::StyledWriter writer;std::string s = writer.write(root);// std::cout << s << std::endl;*out = s;return true;}

运行结果:

{"array" : [ 1, 2, 5 ],"oper" : 45,"sub" : {"age" : 12,"name" : "\u5f20\u4e09"},"x" : 111,"y" : 222
}

反序列化:

反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。 Jsoncpp 提供了以下方法进行反序列化:

1.   使用 Json ::Reader: 

     优点 :提供详细的错误信息和位置,方便调试。

示例:

#pragma once
#include <iostream>
#include <jsoncpp/json/json.h>
#include <string>
#include<memory>
// 设计一下协议的报头,和报文的完整格式
// "len"\r\n"{json}"\r\n ---完整的报文 len代表有效载荷的长度// 结构化转字符串、字符串转结构化---->序列化和反序列化
// 请求要有序列化的接口、客户端要发请求给服务器, 也要有反序列化,因为服务器也需要读到请求,然后我们做反序列化static const std::string sep = "\r\n";
//添加报头
std::string Encode(const std::string &jsonstr)
{//将jsonstr编程我需要的这个:"len"\r\n"{json}"\r\nint len = jsonstr.size();std::string lenstr = std::to_string(len);return lenstr+sep+jsonstr+sep;
}// 不能带const、因为后面还要修改erase、为了处理下一次的字符串
std::string Decode(std::string &packagestream)
{auto pos = packagestream.find(sep);//无法提取到一个完整的报文,返回空串if(pos == std::string::npos) return std::string();//前闭后开:std::string lenstr = packagestream.substr(0, pos);int len = std::stoi(lenstr);// len代表有效载荷的长度int total = lenstr.size() + len + 2*sep.size();if(packagestream.size() < total) return std::string();//提取json字符串std::string jsonstr = packagestream.substr(pos+sep.size(), len);//下一次处理下一个字符串packagestream.erase(total);return jsonstr;
}class Request
{
public: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::StyledWriter writer;Json::FastWriter writer;std::string s = writer.write(root);// std::cout << s << std::endl;*out = s;return true;}// 服务端用反序列,别人会给我传进来一个字符串,我需要将这个字符串转成结构化字段(内部属性的值bool Deserialize(const std::string &in){Json::Value root;Json::Reader reader;// 传进来的字符串转换成发序列化成一个json对象bool res = reader.parse(in, root);// std::cout <_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}void Print(){std::cout << _x << std::endl;std::cout << _y << std::endl;std::cout << _oper << std::endl;}int X(){return _x;}int Y(){return _y;}    char Oper(){return _oper;}~Request(){}private:int _x;int _y;char _oper; //+ 、— 、/、% //x oper y
};// 最终相当于是服务器给客户端响应,序列化成字符串发过去,而服务器还会收需求,就可以反序列化成结构化字段
class Response
{
public:Response():_result(0), _code(0), _desc("success"){}// 客户端用序列----->目的是把结构化字段转化成一个字符串,发出去bool Serialize(std::string *out){Json::Value root;root["result"] = _result;root["code"] = _code;root["desc"] = _desc;// Json::StyledWriter writer;Json::FastWriter writer;std::string s = writer.write(root);// std::cout << s << std::endl;*out = s;return true;}// 服务端用反序列,别人会给我传进来一个字符串,我需要将这个字符串转成结构化字段(内部属性的值bool Deserialize(const std::string &in){Json::Value root;Json::Reader reader;// 传进来的字符串转换成发序列化成一个json对象bool res = reader.parse(in, root);if(!res) return false;_result = root["result"].asInt();_code = root["code"].asInt();_desc = root["desc"].asString();return true;}void Print(){std::cout << _result << std::endl;std::cout << _code << std::endl;std::cout << _desc << std::endl;}int Result(){return _result;}int Code(){return _code;}    std::string Desc(){return _desc;}~Response(){}public:int _result;//状态码int _code; // 0:success//描述std::string _desc;
};//工厂模式:能够直接帮我们去进行对象的创建
class Factory
{public:static std::shared_ptr<Request> BuildRequestDefault(){return std::make_shared<Request>();}static std::shared_ptr<Response> BuildResponseDefault(){return std::make_shared<Response>();}
};

运行结果:

111
222
-

如何保证服务器读到的是一个完整的请求

继续定制协议:Protocol.hpp 

设计一下协议的报头,和报文的完整格式

"len"\r\n"{json}"\r\n ---完整的报文 len代表有效载荷的长度

\r\n:是为了区分len 和 json 串

\r\n:暂时没有其他作用,便于打印,debug

我们收到的字符串有可能是:

可能出现异常情况
"le
"len"
"len"\r\n"
"len"\r\n"{j
"len"\r\n"{json}
"len"\r\n"{json}"\r\n----->正常情况
"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n
"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r

字符串不完整,就不要,要读到一个完整的,有多的就不要

协议处理类:

#pragma once
#include <iostream>
#include <jsoncpp/json/json.h>
#include <string>// 设计一下协议的报头,和报文的完整格式
// "len"\r\n"{json}"\r\n ---完整的报文 len代表有效载荷的长度// 结构化转字符串、字符串转结构化---->序列化和反序列化
// 请求要有序列化的接口、客户端要发请求给服务器, 也要有反序列化,因为服务器也需要读到请求,然后我们做反序列化static const std::string sep = "\r\n";
//添加报头
std::string Encode(const std::string &jsonstr)
{//将jsonstr编程我需要的这个:"len"\r\n"{json}"\r\nint len = jsonstr.size();std::string lenstr = std::to_string(len);return lenstr+sep+jsonstr+sep;
}// 不能带const,最后要删除掉我们读取到的报文
std::string Decode(std::string &packagestream)
{auto pos = packagestream.find(sep);//无法提取到一个完整的报文,返回空串if(pos == std::string::npos) return std::string();//前闭后开:std::string lenstr = packagestream.substr(0, pos);int len = std::stoi(lenstr);// len代表有效载荷的长度int total = lenstr.size() + len + 2*sep.size();if(packagestream.size() < total) return std::string();//提取json字符串std::string jsonstr = packagestream.substr(pos+sep.size(), len);//下一次处理下一个字符串packagestream.erase(0, total);return jsonstr;
}class Request
{
public: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::StyledWriter writer;Json::FastWriter writer;std::string s = writer.write(root);// std::cout << s << std::endl;*out = s;return true;}// 服务端用反序列,别人会给我传进来一个字符串,我需要将这个字符串转成结构化字段(内部属性的值bool Deserialize(const std::string &in){Json::Value root;Json::Reader reader;// 传进来的字符串转换成发序列化成一个json对象bool res = reader.parse(in, root);// std::cout <_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}void Print(){std::cout << _x << std::endl;std::cout << _y << std::endl;std::cout << _oper << std::endl;}~Request(){}private:int _x;int _y;char _oper; //+ 、— 、/、% //x oper y
};// 最终相当于是服务器给客户端响应,序列化成字符串发过去,而服务器还会收需求,就可以反序列化成结构化字段
class Response
{
public:Response():_result(0), _code(0), _desc("success"){}// 客户端用序列----->目的是把结构化字段转化成一个字符串,发出去bool Serialize(std::string *out){Json::Value root;root["result"] = _result;root["code"] = _code;root["desc"] = _desc;// Json::StyledWriter writer;Json::FastWriter writer;std::string s = writer.write(root);// std::cout << s << std::endl;*out = s;return true;}// 服务端用反序列,别人会给我传进来一个字符串,我需要将这个字符串转成结构化字段(内部属性的值bool Deserialize(const std::string &in){Json::Value root;Json::Reader reader;// 传进来的字符串转换成发序列化成一个json对象bool res = reader.parse(in, root);if(!res) return false;_result = root["result"].asInt();_code = root["code"].asInt();_desc = root["desc"].asString();return true;}void Print(){std::cout << _result << std::endl;std::cout << _code << std::endl;std::cout << _desc << std::endl;}int X(){return _x;}int Y(){return _y;}    char Oper(){return _oper;}~Response(){}public:int _result;//状态码int _code; // 0:success//描述std::string _desc;
};

有了协议处理类,我们再返回更新IOService类:

能保证我们读取到的是一个完整的报文吗?不能!

原来我们的读取Recv,是将读取的数据每一次进行覆盖,但是我们这里想要保留每一次读取到的内容,因此改动:*out += inbuffer;将每一次读取到的内容都拼接到原内容后:

        ssize_t Recv(std::string *out) override{// 缺点:inbuffer不是动态的,每次只能读取这么多,或者说,明明只发了5个字节,但是却还是要读取1023个字节char inbuffer[4096];// n是实际读取的字节数                                   // 当做字符串ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0); //(留一个\n的位置)if (n > 0){inbuffer[n] = 0;*out += inbuffer;}return n;}

Service.hpp中IOExecute修改:

    void IOExecute(SockSPtr sock, InetAddr &addr){std::string packagestreamqueue;// 长服务while (true){ssize_t n = sock->Recv(&packagestreamqueue);if (n <= 0){LOG(INFO, "client %s quit or recv error\n", addr.AddrStr().c_str());break;}//能保证我们读取到的是一个完整的报文吗?不能!std::string package = Decode(packagestreamqueue);if(package.empty()) continue;//现在能保证我们读取到的是一个完整的jsonstr//反序列化做处理}}

为了反序列做处理:

我们再在协议Protocol.hpp中添加一个外部类:

//工厂模式:能够直接帮我们去进行对象的创建
class Factory
{public:static std::shared_ptr<Request> BuildRequestDefault(){return std::make_shared<Request>();}
};

我们再在Service.hpp中定义一个函数对象

作用:

定义一个函数对象:返回值是std::shared_ptr<Response>、参数是:std::shared_ptr<Request>
专门做任务处理:将一个结构化的请求,变成一个结构化的响应

using process_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request>)>;

因此我们也需要在IOServer类当中添加相应的私有成员自定义函数对象变量:

private:process_t _process;

并且在构造函数当中初始化:

    IOService(process_t process): _process(process){}

最后我们一共经过:7个步骤

1.读取、2.报文解析、3.反序列化处理、4.业务处理、5.序列化应答、6.添加len长度报头、7.相应回去

    void IOExecute(SockSPtr sock, InetAddr &addr){std::string packagestreamqueue;// 长服务while (true){// 1.负责读取ssize_t n = sock->Recv(&packagestreamqueue);if (n <= 0){LOG(INFO, "client %s quit or recv error\n", addr.AddrStr().c_str());break;}//能保证我们读取到的是一个完整的报文吗?不能!// 2.报文解析,提取报文和有效载荷std::string package = Decode(packagestreamqueue);if(package.empty()) continue;//现在能保证我们读取到的是一个完整的jsonstr//反序列化做处理auto req = Factory::BuildRequestDefault();// 3.做反序列化req->Deserialize(package);// 4.业务处理//做业务处理,我想通过一个请求得到应答auto resp = _process(req);// 5.构建序列化应答//给别人响应回去std::string respjson;//先序列化resp->Serialize(&respjson);// 6.添加len长度报头//携带报文的应答集成串respjson = Encode(respjson);//7.响应回去//再发出去sock->Send(respjson);}}

NetCal.hpp

因为我们要做的是一个网络版本的计算器:

因此还需要处理收到的报文:

因此我们还需要构建Reponce对象:因此在工厂Factory类中再更新:

//工厂模式:能够直接帮我们去进行对象的创建
class Factory
{public:static std::shared_ptr<Request> BuildRequestDefault(){return std::make_shared<Request>();}static std::shared_ptr<Response> BuildResponseDefault(){return std::make_shared<Response>();}
};

NetCal.hpp

#pragma once
#include"Protocol.hpp"
#include<memory>class NetCal
{
public:NetCal(){}~NetCal(){}//将外部穿过来的请求,处理结果后,转回响应std::shared_ptr<Response> Calculator(std::shared_ptr<Request> req){auto resp = Factory::BuildResponseDefault();switch (req->Oper()){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->_code = 1;resp->_desc = "div zero";}else{resp->_result = req->X() / req->Y();}break;case '%':if(req->Y() == 0) {resp->_code = 2;resp->_desc = "mod zero";}else{resp->_result = req->X() % req->Y();}break;default:{resp->_code = 3;resp->_desc = "illegal operation";}break;}return resp;}
};

调用函数cpp:ServerMain.cc

#include "TcpServer.hpp"
#include "Service.hpp"
#include "NetCal.hpp"// ./tcpserver 8888
int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);// 我们的软件代码,我们手动划分成了三层NetCal cal;IOService service(std::bind(&NetCal::Calculator, &cal, std::placeholders::_1));//构建TCP服务器std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::bind(&IOService::IOExecute, &service, std::placeholders::_1, std::placeholders::_2), port);tsvr->Loop();return 0;
}

验证:

pupu@pupu-ubuntu:~/computer-network/class_50/3.cal_server$ ./calserver 8888
[INFO][4267][Socket.hpp][83][2025-04-28 22:52:56] socket create success, sockfd: 3
[INFO][4267][Socket.hpp][101][2025-04-28 22:52:56] bind success 
[INFO][4267][Socket.hpp][112][2025-04-28 22:52:56] listen success 

客户端:ClientMain.cc

#include <iostream>
#include <ctime>
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"using namespace socket_ns;int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);SockSPtr sock = std::make_shared<TcpSocket>();if (!sock->BuildClientSocket(serverip, serverport)){std::cerr << "connect error" << std::endl;exit(1);}//此时已经有套接字了srand(time(nullptr) ^ getpid());const std::string opers = "+-*/%&^!";int cnt = 3;std::string packagestreamqueue;while (true){// 构建数据int x = rand() % 10;usleep(x * 1000);int y = rand() % 10;usleep(x * y * 100);char oper = opers[y % opers.size()];//获取操作符// 构建请求auto req = Factory::BuildRequestDefault();req->SetValue(x, y, oper);// 1. 序列化std::string reqstr;req->Serialize(&reqstr);// 2. 添加长度报头字段reqstr = Encode(reqstr);std::cout << "####################################" << std::endl;std::cout << "request string: \n" <<  reqstr << std::endl;// 3. 发送数据sock->Send(reqstr);//需要保证我们获得的报文是完整的while (true){// 4. 读取应答,responsessize_t n = sock->Recv(&packagestreamqueue);if (n <= 0){break;}// 我们能保证我们读到的是一个完整的报文吗?不能!// 5. 报文解析,提取报头和有效载荷std::string package = Decode(packagestreamqueue);if (package.empty())continue;std::cout << "package: \n" << package << std::endl;// 6. 反序列化auto resp = Factory::BuildResponseDefault();//反序列化---->得到一个结构化数据resp->Deserialize(package);// 7. 打印结果resp->Print();break;}sleep(1);// break;}sock->Close();return 0;
}

运行结果:

我们的整个代码一共分成了三层:会话、表示(是我们本文章的重点、协议沟通)、和应用层(业务逻辑)

#include "TcpServer.hpp" //会话层

#include "Service.hpp" //表示层

#include "NetCal.hpp" //应用层

我们之前讲过TCP/IP五层(或四层)模型:我们一般做应用层、会话层和表示层

会话层:TcpServer做的就是会话层

表示层:设备通信的两个端,以固定的数据格式把请求反序列化、应答序列化发回去,以固定的格式,在本主机内上下层转化以及客户端服务器之间通信。

应用层:NetCal就是争对特定应用层,所定的协议Request、Require。

结语:

       随着这篇博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。    

         在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。

        你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容。

相关文章:

  • 修改或禁用Cursor的全局搜索默认快捷键
  • 【Java面试笔记:应用】36.谈谈MySQL支持的事务隔离级别,以及悲观锁和乐观锁的原理和应用场景?
  • 【云备份】热点管理模块
  • 终端与环境变量
  • [一文解决大模型微调+部署+RAG] LLamaFactory微调模型后使用Ollama + RAGFlow在Windows本地部署
  • Linux用户管理命令和用户组管理命令
  • 【文献阅读】全球干旱地区植被突变的普遍性和驱动因素
  • PowerBI企业运营分析——多维度日期指标分析
  • MCP协议的使用分享
  • 数据赋能(212)——质量管理——统一性原则
  • 第7章 【Python数据类型大爆炸】Python 基础语法和数据类型特性的实例
  • 时间交织(TIADC)的失配误差校正处理(以4片1GSPS采样率的12bitADC交织为例讲解)
  • Sentinel学习
  • 《AI大模型应知应会100篇》第46篇:大模型推理优化技术:量化、剪枝与蒸馏
  • Qwen3小模型实测:从4B到30B,到底哪个能用MCP和Obsidian顺畅对话?
  • 数据结构:顺序栈的完整实现与应用
  • shell(7)
  • More Effective C++学习笔记
  • 高中数学联赛模拟试题精选学数学系列第3套几何题
  • 影刀RPA中新增自己的自定义指令
  • 国铁集团:5月4日全国铁路预计发送旅客2040万人次
  • 熬夜又不想伤肝?方法只有一个
  • 洪纬读《制造三文鱼》丨毒素缠身的水生鸡
  • 2025五一档首日电影票房破亿
  • 不准打小孩:童年逆境经历视角下的生育友好社会
  • 解放日报:上海深化改革开放,系统集成创新局