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

文章目录

  • 应用层自定义协议以及序列化和反序列化
    • 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.rhkb.cn/news/384081.html

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

相关文章

Anconda 快速常用命令简洁版

目的&#xff1a;简单清楚的使用基本的conda 命令 可能需求 查看项目中的虚拟环境及依赖是否满足需求操作新环境来满足项目或者论文的实现 Anconda 常用命令 conda 查看基础命令1. 进入Anaconda 环境2. 查看版本3.查看有哪些虚拟环境4.激活虚拟环境5. 进入虚拟环境查看6. 退出…

基于STM32瑞士军刀--【FreeRTOS开发】学习笔记(二)|| 堆 / 栈

堆和栈 1. 堆 堆就是空闲的一块内存&#xff0c;可以通过malloc申请一小块内存&#xff0c;用完之后使用再free释放回去。管理堆需要用到链表操作。 比如需要分配100字节&#xff0c;实际所占108字节&#xff0c;因为为了方便后期的free&#xff0c;这一小块需要有个头部记录…

IDEA缓存和索引

IDEA缓存和索引 —2020年06月10日 IntelliJ IDEA首次加载项目的时候。都会创建索引&#xff0c;而创建索引的时间根项目的文件多少成正比。 IntelliJ IDEA的缓存和索引主要是用来加快文件查询&#xff0c;从而加快各种查找、代码提示等操作的速度。 某些特殊情况下&#xf…

Go语言入门

目录 前言——碎碎念 环境安装 配置环境变量 变量的定义 数据类型 数字型 字符与字符串 数据类型转换 运算符 算术运算符 关系运算符 逻辑运算符 位运算符&#xff08;二进制&#xff09; 赋值运算符 其他运算符&#xff08;指针&#xff09; 键盘的输入与输出…

使用 Python创建照片文件复制和压缩工具

在这篇博客中&#xff0c;我们将探索如何使用 wxPython 创建一个 GUI 工具&#xff0c;用于选择文件夹中的照片文件、显示预览、选择并复制文件到指定目录&#xff0c;以及将选中的照片压缩到一个 ZIP 文件中。这个工具不仅功能强大&#xff0c;而且提供了用户友好的界面。 C:\…

3.多租户调研1

https://gitee.com/xiaoqiangBUG/hello-ruoyi-cloud.git 1.mybatis plus 的插件 TenantLineInnerInterceptor 是 MyBatis Plus 框架中的一个拦截器&#xff0c;它用于实现多租户系统的数据隔离。在多租户应用中&#xff0c;不同的租户应该只能访问到自己的数据&#xff0c;而…

时序分解 | Matlab基于CEEMDAN-CPO-VMD的CEEMDAN结合冠豪猪优化算法(CPO)优化VMD二次分解

时序分解 | Matlab基于CEEMDAN-CPO-VMD的CEEMDAN结合冠豪猪优化算法&#xff08;CPO&#xff09;优化VMD二次分解 目录 时序分解 | Matlab基于CEEMDAN-CPO-VMD的CEEMDAN结合冠豪猪优化算法&#xff08;CPO&#xff09;优化VMD二次分解效果一览基本介绍程序设计参考资料 效果一览…

数据结构经典测试题4

1. #include <stdio.h> int main() { char *str[3] {"stra", "strb", "strc"}; char *p str[0]; int i 0; while(i < 3) { printf("%s ",p); i; } return 0; }上述代码运行结果是什么&#xff1f; A: stra strb strc B: s…

RFID温度标签: 冷链管理迈向智能化、精准化的新时代。

在现代商业和社会发展中&#xff0c;冷链物流扮演着至关重要的角色。它不仅涉及食品、药品等敏感物资的安全运输&#xff0c;更是保障公众健康与福祉的重要环节。随着人们对生鲜冷链需求的日益增长&#xff0c;冷链物流行业也迎来了以物联网技术为主导发展新阶段。我国冷链物流…

嵌入式中什么是三次握手

在开始前刚好我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「嵌入式的资料从专业入门到高级教程」&#xff0c;点个关注在评论区回复“666”之后私信回复“666”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; 在网络数据传输中&#xf…

基站光伏直流叠光能效管理方案

安科瑞 华楠 基站现状和趋势 5G基站是专门提供5G网络服务的公用移动通信基站。5G基站主要用于提供5G空口协议功能&#xff0c;支持与用户设备、核心网之间的通信。按照逻辑功能划分&#xff0c;5G基站可分为5G基带单元与5G射频单元&#xff0c;二者之间可通过CPRI或eCPRI接口…

Python3网络爬虫开发实战(3)网页数据的解析提取

文章目录 一、XPath1. 选取节点2. 查找某个特定的节点或者包含某个指定的值的节点3. XPath 运算符4. 节点轴5. 利用 lxml 使用 XPath 二、CSS三、Beautiful Soup1. 信息提取2. 嵌套选择3. 关联选择4. 方法选择器5. css 选择器 四、PyQuery1. 初始化2. css 选择器3. 信息提取4. …

【游戏制作】使用Python创建一个完整的2048游戏项目

目录 项目运行展示 项目概述 项目目标 项目结构 安装依赖 代码实现 1. 导入库 2. 创建 Game2048 类 3. 设置UI界面 4. 加载二维码图片 5. 创建菜单 6. 游戏逻辑和功能 7. 运行应用 总结 创建一个完整的2048游戏项目 项目运行展示 项目概述 在这个项目中&#xff…

TypeScript核心

常用操作方式 1、类型推断 ts会根据变量存放的初始值来进行变量类型限定。 如上&#xff1a;开始str是字符串&#xff0c;则此变量以后就只能存字符串值。 开发中的意义&#xff1a;变量分配字符串值&#xff0c;后期可能会书写一些字符串功能、方法等相关的操作&#xff0c;如…

在Linux中,部署及优化Tomcat

tomcat概述 自 2017 年 11月编程语言排行榜 Java 占比 13%,高居榜首&#xff0c;Tomcat 也一度成为 Java开发人员的首选。其开源、占用系统资源少、跨平台等特性深受广大程序员喜爱。本章主要学习如何部署 Tomcat 服务&#xff0c;根据生产环境实现多个虚拟主机的配置&#xf…

GPT-4引领:AI新浪潮的转折点

OneFlow编译 **翻译&#xff5c;贾川、杨婷、徐佳渝 编辑&#xff5c;王金许** 一朝成名天下知。ChatGPT/GPT-4相关的新闻接二连三刷屏朋友圈&#xff0c;如今&#xff0c;这些模型背后的公司OpenAI的知名度不亚于任何科技巨头。 不过&#xff0c;就在ChatGPT问世前&#x…

ISP 代理提供商:互联网安全的关键参与者

简介&#xff1a;互联网安全的演变态势 互联网改变了我们互动、工作和开展业务的方式&#xff0c;但也带来了与安全性和可访问性相关的重大挑战。在这个数字时代&#xff0c;互联网服务提供商 (ISP) 代理提供商在解决这些问题方面发挥着关键作用。他们提供的基本服务不仅可以增…

pytest使用

主要技术内容 1.pytest设计 接口测试 框架设想 common—公共的东西封装 1.request请求 2.Session 3.断言 4.Log 5.全局变量 6.shell命令 ❖ config---配置文件及读取 ❖ Log— ❖ payload—请求参数—*.yaml及读取 ❖ testcases—conftest.py; testcase1.py…….可…

Can we Deploy Web Application in Azure OpenAI of Production Level

题意&#xff1a;我们可以在Azure OpenAI中部署生产级别的Web应用程序吗 问题背景&#xff1a; I have created azure ai search service and used Text split skillset and made index. I also deployed a web Application but have a question that If I want to create to …

【React】JSX 实现列表渲染

文章目录 一、基础语法1. 使用 map() 方法2. key 属性的使用 二、常见错误和注意事项1. 忘记使用 key 属性2. key 属性的选择 三、列表渲染的高级用法1. 渲染嵌套列表2. 条件渲染列表项3. 动态生成组件 四、最佳实践 在 React 开发中&#xff0c;列表渲染是一个非常常见的需求。…