应用层自定义协议以及序列化和反序列化

文章目录

  • 应用层自定义协议以及序列化和反序列化
    • 1、应用层自定义协议
      • 1.1、应用层
      • 1.2、协议
    • 2、序列化和反序列化
    • 3、TCP 为什么支持全双工
    • 4、jsoncpp基础
      • 4.1、序列化
      • 4.2、反序列化
    • 5、实现网络版计算器
    • 6、手写序列化和反序列化

img

应用层自定义协议以及序列化和反序列化

1、应用层自定义协议

1.1、应用层

应用层是计算机网络体系结构中的最高层,也是物联网三层结构中的最顶层,它直接面向用户和应用程序提供服务。


1.2、协议

协议就是通信双方约定好的结构化的数据!(比如结构体)

官方解释:应用层协议定义了交换的报文类型(如请求报文和响应报文)、报文类型的语法(如报文中的各个字段及其详细描述)、字段的语义(即包含在字段中信息的含义),以及进程何时、如何发送报文及对报文进行响应的规则。


2、序列化和反序列化

通信双方在发送和接收数据的时候,一般有两种方案:

方案一:

直接使用结构体发送,比如发送一个成员变量内容为1+1的结构体。

class request{
private:int _x;		// 左操作数int _y;		// 右操作数char _oper; // 操作符
};
  • 只需要填充相关的成员变量内容再发送即可。但是这个方案存在一个问题!
  • 就是互联网上的两台主机可能字节序不一样,比如发送端是大端机,接收方是小端机,那么接受数据就得处理大小端的问题。
  • 这个方案一般在底层的通信上使用。

方案二:

  • 使用定义的结构体来表示我们要交互的信息
  • 但是发送和接收数据的时候是使用字符串
  • 即发送的时候把结构体的内容转成字符串(序列化),接收的时候把字符串转换成结构体(反序列化)!

当然了,不管是方案一还是方案二或者其他方案,只要保证一段发送的数据,另一方能够对数据进行正确的解析,就可以。这种约定就是应用层协议!

下面我们使用方案二来理解协议,我们自定义协议,并且对于序列化和反序列化,有现成的方案–jsoncpp。我们后面还会自己实现序列化和反序列化!


3、TCP 为什么支持全双工

在前面博客代码中我们有谈到read/recv可能会出现bug,为什么?

因为对于TCP协议,接收数据的时候可能因为网络或者其他原因,可能读到的数据不完全或者过多,那么读取数据就会出错。

下面用一张图来解释TCP的全双工:

还有就是接收缓冲区填满的情况,有些数据会丢失(当然,OS会处理好,使用滑动窗口,后面博客会讲)

在上图我们可以看到:

  1. 在任何一台主机上,TCP有两个缓冲区(发送和接收缓冲区),所以在内核中,可以在发消息的同时接收消息,即全双工!

  2. 这就是为什么一个文件描述符(sockfd)就能实现读写的原因!(可能还有一个疑问就是为什么一个文件描述符能指向两个缓冲区?其实就是一个缓冲区,通过某种手段给它分成了两个部分,后面博客会讲)。

  3. 实际数据什么时候发,发多少,出错了怎么办,由TCP协议控制(后面会详细解释传输层TCP协议),所以TCP叫做传输控制协议。


4、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 中序列化和反序列化操作的详细介绍:

安装:

sudo apt-get install libjsoncpp-dev # Ubuntu
sudo yum install jsoncpp-devel # Centos

4.1、序列化

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

这里就介绍两个:Json::FastWriterJson::StyledWriter,用法相似。其中Json::FastWriter要更快,因为它不加额外的空格和换行符。

#include <iostream>
#include <fstream>
#include <string>
#include <jsoncpp/json/json.h>struct stu
{int id;std::string name;double grade;char c;void DebugPrint(){std::cout << id << " " << name << " " << grade << " " << c << std::endl;}
};int main()
{// 写Json::Value root;struct stu s = {1, "xp", 99.99, 'a'};// 序列化root["id"] = s.id;root["name"] = s.name;root["grade"] = s.grade;root["c"] = s.c;Json::FastWriter writer;// Json::StyledWriter writer; -- 一样使用,但速度更慢std::string str = writer.write(root);std::ofstream ofs("./text.txt");ofs << str;return 0;
}

命令行执行:g++ test.cc -ljsoncpp,也就是需要链接jsoncpp库,因为这不是内置的库。

运行结果:


4.2、反序列化

反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。Jsoncpp 提供:使用 Json::Reader方法进行反序列化,这里就介绍这个,因为简单。

#include <iostream>
#include <fstream>
#include <string>
#include <jsoncpp/json/json.h>struct stu
{int id;std::string name;double grade;char c;void DebugPrint(){std::cout << id << " " << name << " " << grade << " " << c << std::endl;}
};int main()
{// 读char buff[1024];std::ifstream ifs("./text.txt");if (!ifs.is_open()){return 1;}ifs.getline(buff, sizeof(buff) - 1);std::string res = buff;Json::Value root;struct stu s;Json::Reader reader;// 反序列化bool n = reader.parse(res, root);if (!n)return 2;s.id = root["id"].asInt();s.name = root["name"].asCString();s.grade = root["grade"].asDouble();s.c = root["c"].asInt(); // 没有asChars.DebugPrint();return 0;
}

运行结果:注意,文件内容是Json::FastWriter格式


5、实现网络版计算器

这里我们对socket的API进行封装成Socket.hpp文件。

方便TCP服务可以调用,UDP服务也可以调用(这里没实现UDP的socket 的 API具体细节,可以模仿TCP自行实现)。

定制协议封装了Protocol.hpp文件

其中序列化是采用的jsoncpp的方案,但是序列化完的数据不能直接发,直接发可能会出现粘报问题(一次收到多个数据分离不了,或者一次收到的数据不完全,需要识别,等待收到至少一条完整的数据到达),因此封装了两个函数解析字符串来解决这个问题:Encode添加报头长度和分隔符,Decode是相反的功能。

比如:{"x":_x,"y":_y,"oper":_oper}这样发送可以吗?不行,不一定一次到达的数据刚好是1条,可能是半条,也可能是2条,因此我们需要对发送的数据进行封装:"len\r\n{有效载荷}\r\n" – 其中len是有效载荷的长度。

  • Socket.hpp文件
#pragma once#include <string.h>
#include <memory>#include "Log.hpp"
#include "Comm.hpp"namespace socket_ns
{const static int gbacklog = 8;class Socket;using socket_sptr = std::shared_ptr<Socket>; // 定义智能指针,以便于后面多态// 使用// std::unique_ptr<Socket> listensocket = std::make_unique<TcpSocket>();// listensocket->BuildListenSocket();// socket_sptr retsock = listensocket->Accepter();// retsock->Recv();// retsock->Send();// std::unique_ptr<Socket> clientsocket = std::make_unique<TcpSocket>();// clientsocket->BuildClientSocket();// clientsocket->Send();// clientsocket->Recv();class Socket{public: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 ssize_t Recv(std::string *out) = 0;virtual ssize_t Send(std::string &in) = 0;// virtual void Other() = 0;public:void BuildListenSocket(InetAddr &addr){CreateSocketOrDie();BindSocketOrDie(addr);ListenSocketOrDie();}bool BuildClientSocket(InetAddr &addr){CreateSocketOrDie();return Connector(addr);}};class TcpSocket : public Socket{public:TcpSocket(int sockfd = -1) : _socktfd(sockfd){}virtual void CreateSocketOrDie() override{// 创建_socktfd = socket(AF_INET, SOCK_STREAM, 0); // 这个就是文件描述符if (_socktfd < 0){LOG(FATAL, "create sockfd error, error code : %d, error string : %s", errno, strerror(errno));exit(CREATE_ERROR);}LOG(INFO, "create sockfd success");}virtual void BindSocketOrDie(InetAddr &addr) override{struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(addr.Port());local.sin_addr.s_addr = INADDR_ANY;// 绑定int n = ::bind(_socktfd, CONV(&local), sizeof(local));if (n < 0){LOG(FATAL, "bind sockfd error, error code : %d, error string : %s", errno, strerror(errno));exit(BIND_ERROR);}LOG(INFO, "bind sockfd success");}virtual void ListenSocketOrDie() override{// 监听int ret = ::listen(_socktfd, gbacklog);if (ret < 0){LOG(FATAL, "listen error, error code : %d , error string : %s", errno, strerror(errno));exit(LISTEN_ERROR);}LOG(INFO, "listen success!");}virtual socket_sptr Accepter(InetAddr *addr) override{struct sockaddr_in peer;socklen_t len = sizeof(peer);// 获取新连接int newsockfd = accept(_socktfd, CONV(&peer), &len); // 建立连接成功,创建新文件描述符进行通信if (newsockfd < 0){LOG(WARNING, "accept error, error code : %d , error string : %s", errno, strerror(errno));return nullptr;}LOG(INFO, "accept success! new sockfd : %d", newsockfd);*addr = peer;socket_sptr sock = std::make_shared<TcpSocket>(newsockfd); // 创建新的文件描述符,传出去以便于后面的Recv和Sendreturn sock;}virtual bool Connector(InetAddr &addr) override{struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(addr.Port());local.sin_addr.s_addr = inet_addr(addr.Ip().c_str());// 发起连接int n = ::connect(_socktfd, CONV(&local), sizeof(local));if (n < 0){LOG(WARNING, "create connect error, error code : %d, error string : %s", errno, strerror(errno));return false;}LOG(INFO, "create connect success");return true;}virtual int SockFd() override{return _socktfd;}virtual ssize_t Recv(std::string *out) override{char buff[1024];ssize_t n = recv(_socktfd, buff, sizeof(buff) - 1, 0);if (n > 0){buff[n] = 0;*out += buff; // 方便当数据到来不是刚好1条数据的时候,进行合并后来的数据}return n;}virtual ssize_t Send(std::string &in) override{ssize_t n = send(_socktfd, in.c_str(), in.size(),0);return n;}private:int _socktfd; // 用同一个_socket};
}
  • Calculate.hpp文件
#pragma once#include <iostream>
#include <string>
#include "Protocol.hpp"using namespace protocol_ns;// 应用层
class Calculate
{
public:Calculate(){}std::unique_ptr<Response> Execute(const Request &req){std::unique_ptr<Response> resptr = std::make_unique<Response>();switch (req._oper){case '+':resptr->_result = req._x + req._y;resptr->_equation = std::to_string(req._x) + " " + req._oper + " " + std::to_string(req._y) + " = " + std::to_string(resptr->_result);break;case '-':resptr->_result = req._x - req._y;resptr->_equation = std::to_string(req._x) + " " + req._oper + " " + std::to_string(req._y) + " = " + std::to_string(resptr->_result);break;case '*':resptr->_result = req._x * req._y;resptr->_equation = std::to_string(req._x) + " " + req._oper + " " + std::to_string(req._y) + " = " + std::to_string(resptr->_result);break;case '/':{if (req._y == 0){resptr->_flag = 1;resptr->_equation = "除0错误";}else{resptr->_result = req._x / req._y;resptr->_equation = std::to_string(req._x) + " " + req._oper + " " + std::to_string(req._y) + " = " + std::to_string(resptr->_result);}break;}case '%':{if (req._y == 0){resptr->_flag = 2;resptr->_equation = "模0错误";}else{resptr->_result = req._x % req._y;resptr->_equation = std::to_string(req._x) + " " + req._oper + " " + std::to_string(req._y) + " = " + std::to_string(resptr->_result);}break;}default:resptr->_flag = 3;break;}return resptr;}~Calculate() {}private:
};
  • Comm.hpp文件
#pragma once
#include "InetAddr.hpp"enum errorcode
{CREATE_ERROR = 1,BIND_ERROR,LISTEN_ERROR,SEND_ERROR,RECV_ERROR,CONNECT_ERROR,FORK_ERROR,USAGE_ERROR
};#define CONV(ADDR) ((struct sockaddr *)ADDR)std::string CombineIpAndPort(InetAddr addr)
{return "[" + addr.Ip() + ":" + std::to_string(addr.Port()) + "] ";
}
  • InetAddr.hpp文件
#pragma once#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>class InetAddr
{void GetAddress(std::string *ip, uint16_t *port){// char *inet_ntoa(struct in_addr in);*ip = inet_ntoa(_addr.sin_addr);*port = ntohs(_addr.sin_port);}public:InetAddr(const struct sockaddr_in &addr) : _addr(addr){GetAddress(&_ip, &_port);}InetAddr(std::string ip, uint16_t port) : _ip(ip), _port(port){_addr.sin_family = AF_INET;_addr.sin_port = htons(_port);_addr.sin_addr.s_addr = inet_addr(_ip.c_str());}InetAddr() {}std::string Ip(){return _ip;}uint16_t Port(){return _port;}bool operator==(InetAddr &addr){return _ip == addr.Ip() && _port == addr.Port();}const struct sockaddr_in &GetAddr(){return _addr;}~InetAddr() {}private:struct sockaddr_in _addr;std::string _ip;uint16_t _port;
};
  • LockGuard.hpp文件
# pragma once#include <pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t *mutex) : _mutex(mutex){pthread_mutex_lock(_mutex); // 构造加锁}~LockGuard(){pthread_mutex_unlock(_mutex); // 析构解锁}private:pthread_mutex_t *_mutex;
};
  • Log.hpp文件
#pragma once#include <string>
#include <iostream>
#include <fstream>
#include <unistd.h>
#include <stdarg.h>
#include <sys/types.h>
#include "LockGuard.hpp"using namespace std;bool isSave = false; // 默认向显示器打印
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
#define FILEPATH "./log.txt"enum level
{DEBUG = 0,INFO,WARNING,ERROR,FATAL
};void SaveToFile(const string &message)
{ofstream out(FILEPATH, ios_base::app);if (!out.is_open())return;out << message;out.close();
}std::string LevelToString(int level)
{switch (level){case DEBUG:return "Debug";case INFO:return "Info";case WARNING:return "Warning";case ERROR:return "Error";case FATAL:return "Fatal";default:return "Unknow";}
}std::string GetTimeString()
{time_t curr_time = time(nullptr);struct tm *format_time = localtime(&curr_time);if (format_time == nullptr)return "None";char buff[1024];snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d",format_time->tm_year + 1900,format_time->tm_mon + 1,format_time->tm_mday,format_time->tm_hour,format_time->tm_min,format_time->tm_sec);return buff;
}void LogMessage(const std::string filename, int line, bool issave, int level, const char *format, ...)
{std::string levelstr = LevelToString(level);std::string timestr = GetTimeString();pid_t pid = getpid();char buff[1024];va_list arg;// int vsnprintf(char *str, size_t size, const char *format, va_list ap); // 使用可变参数va_start(arg, format);vsnprintf(buff, sizeof(buff), format, arg);va_end(arg);LockGuard lock(&mutex);std::string message = "[" + timestr + "]" + "[" + levelstr + "]" + "[pid:" + std::to_string(pid) + "]" + "[" + filename + "]" + "[" + std::to_string(line) + "] " + buff + '\n';if (issave == false)std::cout << message;elseSaveToFile(message);
}// 固定文件名和行数
#define LOG(level, format, ...)                                               \do                                                                        \{                                                                         \LogMessage(__FILE__, __LINE__, isSave, level, format, ##__VA_ARGS__); \} while (0)#define EnableScreen()  \do                  \{                   \isSave = false; \} while (0)#define EnableFile()   \do                 \{                  \isSave = true; \} while (0)void Test(int num, ...)
{va_list arg;va_start(arg, num);while (num--){int data = va_arg(arg, int);std::cout << data << " ";}std::cout << std::endl;va_end(arg);
}
  • Main.cc文件
#include <iostream>
#include <memory>
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Calculate.hpp"using namespace protocol_ns;using cal_t = std::function<std::unique_ptr<Response>(const Request &req)>;void Usage()
{// printf("./udp_server serverip serverport\n");printf("Usage : ./udp_server serverport\n"); // ip 已经设置为0
}class Service
{
public:Service(cal_t cb) : _cb(cb) {}void AddService(socket_sptr sockfd, InetAddr client){// TCP是字节流(可以使用write和read接口),UDP是数据报std::string clientaddr = CombineIpAndPort(client);std::string recvmessage;while (true){sleep(5);Request req; // 注意多线程问题,不能放在里面while// 1.接收数据int n = sockfd->Recv(&recvmessage);std::cout << "server recv:" << recvmessage << std::endl;if (n <= 0){LOG(INFO, "client %s quit", clientaddr.c_str());break;}// 2.分析数据,确定完整报文std::string package;while (true){package = Decode(recvmessage); // 可能为空if (package.empty())break;cout << "after Decode recvmessage : " << recvmessage << std::endl;// 完整的一条有效数据std::cout << "server Decode:" << package << std::endl;// 3.反序列化req.DeSerialize(package); // 把_x,_y,_oper赋值// 4.业务处理std::unique_ptr<Response> resptr = _cb(req);// 5.序列化std::string sendmessage;resptr->Serialize(&sendmessage);std::cout << "server Serialize:" << sendmessage << std::endl;// 6.加上报头数据封装sendmessage = Encode(sendmessage);std::cout << "server Encode:" << sendmessage << std::endl;// 7.发送数据int n = sockfd->Send(sendmessage);}}}~Service() {}private:cal_t _cb;
};int main(int argc, char *argv[])
{// if (argc != 3)if (argc != 2){Usage();exit(USAGE_ERROR);}uint16_t serverport = std::stoi(argv[1]);// __nochdir = 1:在当前工作目录执行// __nochdir = 0:在根目录/工作目录执行// __noclose = 1:不进行重定向// __noclose = 0:进行重定向 /dev/null// int daemon(int __nochdir, int __noclose)// if(fork > 0) exit(0);// setsid();// 先创建子进程,再父进程退出,因为组长不能直接调用setsid();变成守护进程// daemon(0, 0);// 执行下面的代码不是当前进程,而是当前进程的子进程//EnableFile();Calculate cal; // 应用层cal_t servercal = std::bind(&Calculate::Execute, &cal, placeholders::_1);Service sev(servercal);service_t service = std::bind(&Service::AddService, &sev, placeholders::_1, placeholders::_2); // 表示层std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(serverport, service);            //  会话层tsvr->Start();return 0;
}
  • Makefile文件
.PHONY:all
all:tcp_client tcp_servertcp_client:TcpClient.ccg++ -o $@ $^ -std=c++14 -lpthread -ljsoncpp
tcp_server:Main.ccg++ -o $@ $^ -std=c++14 -lpthread -ljsoncpp.PHONY:clean
clean:rm -f tcp_server tcp_client
  • Protocol.hpp文件
#pragma once#include <string>
#include <jsoncpp/json/json.h>
#include <iostream>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>// 表示层
namespace protocol_ns
{const std::string SEP = "\r\n";const std::string CAL_SEP = " ";// 对发送数据进行封装// "len\r\n{有效载荷}\r\n" -- 其中len是有效载荷的长度std::string Encode(const std::string &inbuff){int inbuff_len = inbuff.size();std::string newstr = std::to_string(inbuff_len);newstr += SEP;newstr += inbuff;newstr += SEP;return newstr;}// 解析字符串std::string Decode(std::string &outbuff){int pos = outbuff.find(SEP);if (pos == std::string::npos){// 没找到分隔符return std::string(); // 返回空串,等待接收到完整数据}// 找到分隔符std::string len_str = outbuff.substr(0, pos);if (len_str.empty())return std::string(); // 返回空串,等待接收到完整数据int data_len = std::stoi(len_str);// 判断长度是否符合要求int total_len = pos + SEP.size() * 2 + data_len; // 包装好的一条数据的长度if (outbuff.size() < total_len){return std::string(); // 小于包装好的一条数据的长度,返回空串,等待接收到完整数据}// 大于等于包装好的一条数据的长度std::string message = outbuff.substr(pos + SEP.size(), data_len); // 有效数据outbuff.erase(0, total_len);                                      // 数据长度减少包装好的一条数据的长度,从前面开始移除return message;}class Request{public:Request() {}Request(int x, int y, char oper): _x(x),_y(y),_oper(oper){}// 序列化 -- 转化为字符串发送// {"x":_x,"y":_y,"oper":_oper}// 这样发送可以吗?不行,不一定一次到达的数据刚好是1条,可能是半条,也可能是2条,因此我们需要对发送的数据进行封装:// "len\r\n{有效载荷}\r\n" -- 其中len是有效载荷的长度void Serialize(std::string *out) // 要带出来{Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::FastWriter writer;std::string str = writer.write(root);*out = str;}// 反序列化 -- 解析bool DeSerialize(const std::string &in){Json::Value root;Json::Reader reader;if (!reader.parse(in, root))return false;_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}~Request() {}public:int _x;int _y;char _oper; // +-*/% 如果不是这些操作法那就是非法的};class Response{public:Response() {}// 序列化 -- 转化为字符串发送void Serialize(std::string *out) // 要带出来{Json::Value root;root["result"] = _result;root["flag"] = _flag;root["equation"] = _equation;Json::FastWriter writer;std::string str = writer.write(root);*out = str;}// 反序列化 -- 解析bool DeSerialize(const std::string &in){Json::Value root;Json::Reader reader;if (!reader.parse(in, root))return false;_result = root["result"].asInt();_flag = root["flag"].asInt();_equation = root["equation"].asString();return true;}~Response() {}public:int _result = 0;int _flag = 0;                         // 0表示操作符正确,1表示除0错误,2表示取模0错误,3表示操作符错误string _equation = "操作符不符合要求"; // 等式};const std::string opers = "+-*/%&^";class CalFactory{public:CalFactory(){srand(time(nullptr) ^ getpid() ^ 2);}void Product(Request &req){req._x = rand() & 5 + 1;usleep(req._x * 20);req._y = rand() % 10 + 5;// req._y = 0; // 测试usleep(req._x * req._y + 20);req._oper = opers[(rand() % opers.size())];}~CalFactory() {}private:};
}
  • TcpClient.cc文件
#include <iostream>
#include <string>
#include <strings.h>
#include <unistd.h>#include "Comm.hpp"
#include "Socket.hpp"
#include "Protocol.hpp"using namespace socket_ns;
using namespace protocol_ns;enum class Status
{NEW,CONNECTED,CONNECTING,DISCONNECTED,CLOSE
};const int defaultsockfd = -1;
const int retryinterval = 1; // 重连间隔时间
const int retryamxtimes = 5; // 重连最大次数class Connection
{
public:Connection(std::string serverip, uint16_t serverport): _sockfdptr(std::make_unique<TcpSocket>()),_serverip(serverip),_serverport(serverport),_status(Status::NEW),_retry_interval(retryinterval),_retry_max_times(retryamxtimes){}Status ConnectStatus(){return _status;}void Connect(){InetAddr server(_serverip, _serverport);bool ret = _sockfdptr->BuildClientSocket(server);if (!ret){DisConnect(); //_status = Status::DISCONNECTED;return;}std::cout << "connect success" << std::endl;_status = Status::CONNECTED; // 已连接}void Process(){while (true){// std::cout << "Please Enter#  ";// std::string sendmessage;// 不是{"": ,}类型// std::getline(std::cin, sendmessage);sleep(1);Request req;CalFactory cal;// 1.对需要发送的数据进行序列化std::string sendmessage;// 1.1.这里一下构建5个请求并放在一起// for (int i = 0; i < 5; ++i)// {//     cal.Product(req);//     std::string sub_sendstr;//     req.Serialize(&sub_sendstr);//     std::cout << "client Serialize:" << sub_sendstr << std::endl;//     // 2.对序列化后的数据进行加报头等打包//     sub_sendstr = Encode(sub_sendstr);//     std::cout << "client Encode:" << sub_sendstr << std::endl;//     sendmessage += sub_sendstr;// }cal.Product(req);req.Serialize(&sendmessage);std::cout << "client Serialize:" << sendmessage << std::endl;// 2.对序列化后的数据进行加报头等打包sendmessage = Encode(sendmessage);std::cout << "client Encode:" << sendmessage << std::endl;// std::cout << "sendmessage : " << sendmessage << std::endl;// 3.发送数据int n = _sockfdptr->Send(sendmessage);if (n < 0){_status = Status::CLOSE; // 发送不成功就退出LOG(FATAL, "send error, errno : %d ,error string : %s", errno, strerror(errno));break;}// 发送成功std::string recvmessage;// 4.接收数据int m = _sockfdptr->Recv(&recvmessage);if (m <= 0){_status = Status::DISCONNECTED; // 接收不成功就重连std::cerr << "recv error" << std::endl;break;}// 接收成功// 5.分析数据,确定完整报文std::string package;while (true){package = Decode(recvmessage); // 可能为空if (package.empty())break;// 完整的一条有效数据Response resp;// 6.反序列化resp.DeSerialize(package); // 把_result,_flag赋值// 7.处理返回数据std::cout << "Server Echo$ " << "result : " << resp._result << " , flag :" << resp._flag << " --- equation : " << resp._equation << std::endl;}}}void ReConnect(){_status = Status::CONNECTING;int cnt = 1;while (true){Connect();if (_status == Status::CONNECTED){break;}std::cout << "正在重连,重连次数 : " << cnt++ << std::endl;if (cnt > _retry_max_times){_status = Status::CLOSE; // 重连失败std::cout << "重连失败,请检查网络.." << std::endl;break;}sleep(_retry_interval);}}void DisConnect(){if (_sockfdptr->SockFd() > defaultsockfd){close(_sockfdptr->SockFd());}}private:std::unique_ptr<Socket> _sockfdptr;std::string _serverip;uint16_t _serverport;Status _status;int _retry_interval;int _retry_max_times;
};class TcpClient
{
public:TcpClient(std::string serverip, uint16_t serverport) : _connect(serverip, serverport){}void Execute(){while (true){switch (_connect.ConnectStatus()){case Status::NEW:_connect.Connect();break;case Status::CONNECTED:_connect.Process();break;case Status::DISCONNECTED:_connect.ReConnect();break;case Status::CLOSE:_connect.DisConnect();return; // 断开连接了,重连不管用了default:break;}}}private:Connection _connect;
};void Usage()
{std::cout << "Please use format : ./tcp_client serverip serverport" << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage();exit(USAGE_ERROR);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);TcpClient tcpclient(serverip, serverport);tcpclient.Execute();return 0;
}
  • TcpServer.hpp文件
#pragma once#include <sys/types.h> /* See NOTES */
#include <sys/wait.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>#include <error.h>
#include <string.h>
#include <pthread.h>
#include <functional>#include "Log.hpp"
#include "InetAddr.hpp"
#include "Comm.hpp"
#include "Socket.hpp"using namespace socket_ns;using service_t = std::function<void(socket_sptr sockfd, InetAddr client)>;// 会话层
// 声明
class TcpServer;class ThreadData
{
public:ThreadData(socket_sptr sockfd, InetAddr addr, TcpServer *self): _sockfd(sockfd), _addr(addr), _self(self) {}~ThreadData() = default;public:socket_sptr _sockfd;InetAddr _addr;TcpServer *_self;
};class TcpServer
{
public:TcpServer(uint16_t port, service_t service): _localaddr("0", port),_listensock(std::make_unique<TcpSocket>()),_service(service),_isrunning(false){_listensock->BuildListenSocket(_localaddr);}static void *HandlerService(void *args){pthread_detach(pthread_self()); // 分离线程ThreadData *td = static_cast<ThreadData *>(args);td->_self->_service(td->_sockfd, td->_addr);::close(td->_sockfd->SockFd()); // 服务结束,关闭文件描述符,避免文件描述符泄漏delete td;return nullptr;}void Start(){_isrunning = true;while (_isrunning){InetAddr peerAddr;socket_sptr normalsock = _listensock->Accepter(&peerAddr);// v2 -- 多线程pthread_t tid;ThreadData *td = new ThreadData(normalsock, peerAddr, this); // 传指针pthread_create(&tid, nullptr, HandlerService, td);           // 这里创建线程后,线程去做执行任务,主线程继续向下执行 , 并且线程不能关闭sockf,线程和进程共享文件描述符表}_isrunning = false;}~TcpServer(){}private:service_t _service;InetAddr _localaddr;std::unique_ptr<Socket> _listensock;bool _isrunning;
};
  • 运行结果:


6、手写序列化和反序列化

这里我们只需要修改Protocol.hpp文件

#pragma once#include <string>
#include <jsoncpp/json/json.h>
#include <iostream>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>// #define SELF 1; // SELF=1就用自定义的序列化和反序列化,否则用默认的// 表示层
namespace protocol_ns
{const std::string SEP = "\r\n";const std::string CAL_SEP = " ";// 对发送数据进行封装// "len\r\n{有效载荷}\r\n" -- 其中len是有效载荷的长度std::string Encode(const std::string &inbuff){int inbuff_len = inbuff.size();std::string newstr = std::to_string(inbuff_len);newstr += SEP;newstr += inbuff;newstr += SEP;return newstr;}// 解析字符串std::string Decode(std::string &outbuff){int pos = outbuff.find(SEP);if (pos == std::string::npos){// 没找到分隔符return std::string(); // 返回空串,等待接收到完整数据}// 找到分隔符std::string len_str = outbuff.substr(0, pos);if (len_str.empty())return std::string(); // 返回空串,等待接收到完整数据int data_len = std::stoi(len_str);// 判断长度是否符合要求int total_len = pos + SEP.size() * 2 + data_len; // 包装好的一条数据的长度if (outbuff.size() < total_len){return std::string(); // 小于包装好的一条数据的长度,返回空串,等待接收到完整数据}// 大于等于包装好的一条数据的长度std::string message = outbuff.substr(pos + SEP.size(), data_len); // 有效数据outbuff.erase(0, total_len);                                      // 数据长度减少包装好的一条数据的长度,从前面开始移除return message;}class Request{public:Request() {}Request(int x, int y, char oper): _x(x),_y(y),_oper(oper){}// 序列化 -- 转化为字符串发送// {"x":_x,"y":_y,"oper":_oper}// 这样发送可以吗?不行,不一定一次到达的数据刚好是1条,可能是半条,也可能是2条,因此我们需要对发送的数据进行封装:// "len\r\n{有效载荷}\r\n" -- 其中len是有效载荷的长度void Serialize(std::string *out) // 要带出来{
#ifdef SELF// "len\r\nx op y\r\n" -- 自定义序列化和反序列化std::string data_x = std::to_string(_x);std::string data_y = std::to_string(_y);*out = data_x + CAL_SEP + _oper + CAL_SEP + data_y;
#elseJson::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::FastWriter writer;std::string str = writer.write(root);*out = str;
#endif}// 反序列化 -- 解析bool DeSerialize(const std::string &in){
#ifdef SELFauto left_blank_pos = in.find(CAL_SEP);if (left_blank_pos == std::string::npos)return false;std::string x_str = in.substr(0, left_blank_pos);if (x_str.empty())return false;auto right_blank_pos = in.rfind(CAL_SEP);if (right_blank_pos == std::string::npos)return false;std::string y_str = in.substr(right_blank_pos + 1);if (y_str.empty())return false;if (left_blank_pos + 1 + CAL_SEP.size() != right_blank_pos)return false;_x = std::stoi(x_str);_y = std::stoi(y_str);_oper = in[right_blank_pos - 1];return true;#elseJson::Value root;Json::Reader reader;if (!reader.parse(in, root))return false;_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;
#endif}~Request() {}public:int _x;int _y;char _oper; // +-*/% 如果不是这些操作法那就是非法的};class Response{public:Response() {}// 序列化 -- 转化为字符串发送void Serialize(std::string *out) // 要带出来{
#ifdef SELF// "len\r\nresult flag equation\r\n"std::string data_res = std::to_string(_result);std::string data_flag = std::to_string(_flag);*out = data_res + CAL_SEP + data_flag + CAL_SEP + _equation;
#elseJson::Value root;root["result"] = _result;root["flag"] = _flag;root["equation"] = _equation;Json::FastWriter writer;std::string str = writer.write(root);*out = str;
#endif}// 反序列化 -- 解析bool DeSerialize(const std::string &in){
#ifdef SELF// "result flag equation"auto left_blank_pos = in.find(CAL_SEP);if (left_blank_pos == std::string::npos)return false;std::string res_str = in.substr(0, left_blank_pos);if (res_str.empty())return false;auto second_blank_pos = in.find(CAL_SEP, left_blank_pos + 1);if (second_blank_pos == std::string::npos)return false;std::string equation = in.substr(second_blank_pos + 1);if (equation.empty())return false;if (left_blank_pos + 1 + CAL_SEP.size() != second_blank_pos)return false;_result = std::stoi(res_str);_flag = in[second_blank_pos - 1] - '0';_equation = equation;return true;
#elseJson::Value root;Json::Reader reader;if (!reader.parse(in, root))return false;_result = root["result"].asInt();_flag = root["flag"].asInt();_equation = root["equation"].asString();return true;
#endif}~Response() {}public:int _result = 0;int _flag = 0;                         // 0表示操作符正确,1表示除0错误,2表示取模0错误,3表示操作符错误string _equation = "操作符不符合要求"; // 等式};const std::string opers = "+-*/%&^";class CalFactory{public:CalFactory(){srand(time(nullptr) ^ getpid() ^ 2);}void Product(Request &req){req._x = rand() & 5 + 1;usleep(req._x * 20);req._y = rand() % 10 + 5;// req._y = 0; // 测试usleep(req._x * req._y + 20);req._oper = opers[(rand() % opers.size())];}~CalFactory() {}private:};
}

可以看到这个文件我们增加了#ifdef SELF #else #endif预处理指令。

作用如下:

#ifdef SELF  
// 如果定义了宏 SELF,则编译这部分代码  
#else  
// 如果没有定义宏 SELF,则编译这部分代码  
#endif

当然了,在文件开头我们使用了#define SELF 1,也可以不使用,直接在makefile文件使用LDFLAG=-DSELF=1 # 触发SELF=1

.PHONY:all
all:tcp_client tcp_serverLDFLAG=-DSELF=1 # 触发SELF=1tcp_client:TcpClient.ccg++ -o $@ $^ $(LDFLAG) -std=c++14 -lpthread -ljsoncpp
tcp_server:Main.ccg++ -o $@ $^ $(LDFLAG) -std=c++14 -lpthread -ljsoncpp.PHONY:clean
clean:rm -f tcp_server tcp_client

运行结果:


OKOK,应用层自定义协议以及序列化和反序列化就到这里,如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。

Xpccccc的github主页

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/50578.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

爬取贴吧的标题和链接

免责声明 感谢您学习本爬虫学习Demo。在使用本Demo之前&#xff0c;请仔细阅读以下免责声明&#xff1a; 学习和研究目的&#xff1a;本爬虫Demo仅供学习和研究使用。用户不得将其用于任何商业用途或其他未经授权的行为。合法性&#xff1a;用户在使用本Demo时&#xff0c;应确…

智能算法驱动的爬虫平台:解锁网络数据的无限潜力

摘要 在信息爆炸的时代&#xff0c;网络数据如同深海宝藏&#xff0c;等待着有识之士发掘其无尽价值。本文将探索智能算法驱动的爬虫平台如何成为解锁这一宝库的关键&#xff0c;不仅剖析其技术优势&#xff0c;还通过实例展示它如何助力企业与开发者高效、稳定地采集数据&…

C语言 ——— 数组指针的定义 数组指针的使用

目录 前言 数组指针的定义 数组指针的使用 前言 之前有编写过关于 指针数组 的相关知识 C语言 ——— 指针数组 & 指针数组模拟二维整型数组-CSDN博客 指针数组 顾名思义就是 存放指针的数组 那什么是数组指针呢&#xff1f; 数组指针的定义 何为数组指针&#xf…

【QT】UDP

目录 核心API 示例&#xff1a;回显服务器 服务器端编写&#xff1a; 第一步&#xff1a;创建出socket对象 第二步&#xff1a; 连接信号槽 第三步&#xff1a;绑定端口号 第四步&#xff1a;编写信号槽所绑定方法 第五步&#xff1a;编写第四步中处理请求的方法 客户端…

JAVA开发工具IDEA如何连接操作数据库

一、下载驱动 下载地址&#xff1a;【免费】mysql-connector-j-8.2.0.jar资源-CSDN文库 二、导入驱动 鼠标右击下载到IDEA中的jar包&#xff0c;选择Add as Library选项 如图就导入成功 三、加载驱动 Class.forName("com.mysql.cj.jdbc.Driver"); 四、驱动管理…

【C++】——红黑树(手撕红黑树,彻底弄懂红黑树)

目录 前言 一 红黑树简介 二 为什么需要红黑树 三 红黑树的特性 四 红黑树的操作 4.1 变色操作 4.2 旋转操作 4.3 插入操作 4.4 红黑树插入代码实现 4.5 红黑树的删除 五 红黑树迭代器实现 总结 前言 我们之前都学过ALV树&#xff0c;AVL树的本质就是一颗平…

计算机实验室排课查询小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;学生管理&#xff0c;教师管理&#xff0c;实验室信息管理&#xff0c;实验室预约管理&#xff0c;取消预约管理&#xff0c;实验课程管理&#xff0c;实验报告管理&#xff0c;报修信息管理&#xff0…

Linux的yum源安装MySQL5.7

linux的yum源安装MySQL5.7 一、MySQL 1、简介 MySQL 是一种流行的关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;由瑞典公司 MySQL AB 开发&#xff0c;后来被 Oracle Corporation 收购。它是一个开源软件&#xff0c;提供了高效、稳定和可靠的数据管理解决…

Spring AI (三) 提示词对象Prompt

3.提示词对象Prompt 3.1.Prompt Prompt类的作用是创建结构化提示词, 实现了ModelRequest<List<Message>>接口 Prompt(String contents)&#xff1a;创建一个包含指定内容的Prompt对象。 Prompt(String contents, ChatOptions modelOptions)&#xff1a;创建一个…

某数据泄露防护(DLP)系统NoticeAjax接口SQL注入漏洞复现 [附POC]

文章目录 某数据泄露防护(DLP)系统NoticeAjax接口SQL注入漏洞复现 [附POC]0x01 前言0x02 漏洞描述0x03 影响版本0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.构造POC3.复现某数据泄露防护(DLP)系统NoticeAjax接口SQL注入漏洞复现 [附POC] 0x01 前言 免责声明:请勿利用文章内…

DolphinDB Web 端权限管理:可视化操作指南

在现代数据库管理中&#xff0c;高效和直观的权限管理对于用户的数据安全是至关重要的。过去 DolphinDB 用户需要依赖系统脚本来管理用户和权限&#xff0c;这对于缺乏技术背景的管理员来说既复杂又容易出错。 为了提升用户体验和操作效率&#xff0c;DolphinDB 目前在 Web 上…

数据库设计三范式

文章目录 数据库设计三范式第一范式第二范式第三范式一对一怎么设计主键共享外键唯一 一对多怎么设计多对多怎么设计 数据库设计三范式 数据库表设计的原则。教你怎么设计数据库表有效&#xff0c;并且节省空间。 如果客户有速度要求极致&#xff0c;可以不用。根据客户需求来 …

MySQL数据库练习(5)

1.建库建表 # 使用数据库 use mydb16_trigger;# 表格goods create table goods( gid char(8) primary key, name varchar(10), price decimal(8,2), num int);# 表格orders create table orders( oid int primary key auto_increment, gid char(10) not null, name varchar(10…

scrapy 爬取旅游景点相关数据(一)

第一节 Scrapy 练习爬取穷游旅游景点 配套视频可以前往B站&#xff1a;https://www.bilibili.com/video/BV1Vx4y147wQ/?vd_source4c338cd1b04806ba681778966b6fbd65 本项目为scrapy 练手项目&#xff0c;爬取的是穷游旅游景点列表数据 0 系统的环境 现在网上可以找到很多scr…

Pytorch使用教学6-张量的分割与合并

在使用PyTorch时&#xff0c;对张量的分割与合并是不可避免的操作&#xff0c;本节就带大家深刻理解张量的分割与合并。 在开始之前&#xff0c;我们先对张量的维度进行深入理解&#xff1a; t2 torch.zeros((3, 4)) # tensor([[0., 0., 0., 0.], # [0., 0., 0., 0.…

C语言边界互通传送迷宫

目录 注意事项开头程序程序的流程图程序输入与输出的效果结尾 注意事项 程序里有关字符’\033’的输出都关于Sunshine-Linux的其中一篇博客——《printf函数高级用法设置打印字体颜色和背景色等》 开头 大家好&#xff0c;我叫这是我58。今天&#xff0c;我们来看一下我用C语…

现代Java开发:使用jjwt实现JWT认证

前言 jjwt 库 是一个流行的 Java 库&#xff0c;用于创建和解析 JWT。我在学习spring security 的过程中看到了很多关于jwt的教程&#xff0c;其中最流行的就是使用jjwt实现jwt认证&#xff0c;但是教程之中依然使用的旧版的jjwt库&#xff0c;许多的类与方法已经标记弃用或者…

InsCode GPU服务器快速使用

文章目录 1. 背景介绍2. 环境配置 1. 背景介绍 InsCode服务器地址&#xff1a;https://inscode.csdn.net/workbench?tabcomputed。 2. 环境配置 新建环境后&#xff0c;按照如下步骤快速配置&#xff0c;以便后续执行深度学习模型训练。 数据 openlane 环境依赖 Copy Mini…

数据结构与算法--顺序表(Java)

&#x1f4dd;个人主页&#x1f339;&#xff1a;誓则盟约 ⏩收录专栏⏪&#xff1a;Java SE &#x1f921;往期回顾&#x1f921;&#xff1a;Java SE--基本数据类型&#xff08;详细讲解&#xff09; &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; 什么…

【Python面试题收录】Python编程基础练习题②(数据类型+文件操作+时间操作)

本文所有代码打包在Gitee仓库中https://gitee.com/wx114/Python-Interview-Questions 一、数据类型 第一题 编写一个函数&#xff0c;实现&#xff1a;先去除左右空白符&#xff0c;自动检测输入的数据类型&#xff0c;如果是整数就转换成二进制形式并返回出结果&#xff1b…