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

【Linux】网络编程:初识协议,序列化与反序列化——基于json串实现,网络通信计算器中简单协议的实现、手写序列化与反序列化

目录

一、什么是协议?

二、为什么需要有协议呢?

三、协议的应用

四、序列化与反序列化的引入

什么是序列化和反序列化?

为什么需要序列化和反序列化?

五、序列化推荐格式之一:JSON介绍

六、网络版计算器编程逻辑


本文所涉及到的两种设计模式:

工厂模式:简单工厂模式、工厂方法模式、抽象工厂模式-CSDN博客

模板方法模式-CSDN博客

一、什么是协议?

在日常生活中,我们无时无刻不在遵守着“协议”。例如:当你点外卖时,你提前告知骑手要把外卖放在小区的5号外卖柜中。当外卖平台给你发送“订单已送达”信息后,你会前往5号外卖柜取出你的外卖。为什么不去6号外卖柜取走你的外卖呢?因为你和骑手已经事先约定好将外卖放在5号外卖柜中,这就是你和该名骑手间事先约定好的“协议”,双方必须同时遵守这一规则,这样才能保证资源处理的正确性。

协议(Protocol)在计算机科学和通信领域中,指的是一组规则或标准,用于规定数据如何在不同的系统、设备或进程之间进行交换和处理。协议定义了通信过程中的语法、语义、同步规则以及可能出现的错误处理方式。通过遵循相同的协议,不同的硬件、软件或网络系统能够相互理解和协作,从而实现信息的有效传输和处理。

二、为什么需要有协议呢?

无论是日常生活还是计算机系统、网络通信中,协议的存在都是至关重要的。它们为不同主体之间的交互提供了规则和标准,确保了沟通和操作的有效性、可靠性以及一致性。无论是人与人之间的交流,还是系统与系统之间的通信,都需要一套共同的语言或规则。协议就像是一种“共同的语言”,确保各方能够理解彼此,并按照预定的方式行动。

  • 日常生活例子:当你打电话订餐时,你和餐厅服务员之间遵循着一种“协议”——你告诉他你想吃什么,他确认并记录你的订单。如果每个人点餐时都使用不同的方式或语言,沟通就会混乱不堪,订单可能会出错。

  • 技术例子:在互联网中,设备和服务器之间通过HTTP协议进行通信。HTTP定义了请求和响应的格式,确保浏览器和服务器能够互相理解并交换信息。如果没有HTTP协议,浏览器可能无法正确解释服务器发送的内容。

三、协议的应用

在之前所学的TCP通信中,客户端与服务端之间的信息传输是面向字节流的。这就意味着无论是客户端还是服务端,我们都需要从套接字中读取完整的请求,并对该请求进行处理,做出相应的应答。但我们真的能保证每次所读取到的数据都是一个完整的请求吗?当客户端一直向服务端发送请求时,服务端一直在接收来自客户端的请求信息,我们如何保证每次从字节流中提取的一定是一个完整的请求呢?——协议!!!

我们事先约定好客户端的每个请求信息和服务端的应答信息遵守如下格式:

请求/应答报文:“len\r\nInformationStr\r\n

我们将上述字符串称之为报文,其中:

len:请求字符串的长度(字符串形式)。

"\r\n":分隔符字符串,用于分隔请求信息和标识一段完整报文的结尾。

InformationStr:请求/应答信息(字符串形式)。

当我们约定好请求与应答信息都要遵守上述格式后,那么对于以后客户端/服务端,当我们需要从字节流中提取到一个完整的报文信息时,我们需要做以下工作来判断当前字节流中是否包含至少一个完整的报文:

1、因为当第一次开始处理字节流时,字节流的起始端一定是包含len或len字符串的一部分。所以我们首先找到分隔符的位置,将len提取出来转化为整数。如果没找到则返回上层继续接收客户端传来的信息。

2、当我们得到信息的长度为len后,开始计算该条完整报文的理论长度。如果报文小于理论长度,返回上层继续接收信息。否则,则说明当前字节流中包含至少一条完整的报文。

3、依据已经得到的InformationStr的起始位置和InformationStr的长度len,将报文中的有效信息部分提取出来。并从现有字节流中删除此次提取的完整的报文(包括分隔符)。

经过上述操作,我们就完成了一次对报文的提取工作。

四、序列化与反序列化的引入

什么是序列化和反序列化?

序列化(Serialization)和反序列化(Deserialization)是计算机科学中用于将数据结构、对象状态或数据内容转换为可以存储或传输的格式的过程。具体来说:

  • 序列化:是将对象、数据结构或数据转换为字节流或某种特定格式的过程,以便于存储到文件、数据库或通过网络传输。序列化后的数据通常是连续的字节流,能够被传输或存储。

  • 反序列化:是将序列化后的字节流或格式化数据还原为原始对象或数据结构的过程,使得应用程序可以重新使用这些数据。

为什么需要序列化和反序列化?

当我们在进行网络通信时,发送的信息可能不单纯是一个字符串,还可能是一个结构体/对象。而对于不同的主机而言,由于不同操作系统、编程语言和硬件架构对内存布局和数据类型的处理方式可能不同,直接将结构体以字节流的形式传输会面临以下几个问题:

  1. 字节序(Endianness)

            不同的硬件架构可能使用不同的字节序(大端序或小端序)来存储整数和浮点数。如果发送方和接收方的字节序不一致,直接传输字节流会导致数据解析错误。
  2. 内存对齐(Memory Alignment)

            不同平台和编译器可能有不同的内存对齐规则。例如,某些平台要求整数必须存储在4字节对齐的地址上,而另一些平台可能没有这样的要求。这会导致结构体在不同平台上占用不同大小的内存。
  3. 数据类型大小

            不同的操作系统和编译器对数据类型(如int、float、double等)的定义可能不同。例如,一个int在32位系统上占4字节,在16位系统上可能占2字节。直接传输结构体可能导致数据解析错误。
  4. 结构体成员顺序

            结构体的成员顺序在不同的平台上可能有所不同。例如,某些编译器可能会根据对齐规则重新排列结构体的成员,导致相同结构的成员在内存中的顺序不同。

这就意味着,我们无法将一个结构体以字节流的形式进行网络通信时的信息的传输,因为这可能面临着不可预知的错误,因此序列化与反序列化就成为了客户端与服务端之间正确通信的必要步骤。

五、序列化推荐格式之一:JSON介绍

JSON是一种轻量级的数据交换格式。它基于JavaScript编程语言的一个子集,但独立于语言,因此许多现代编程语言都提供了对JSON的支持。

JSON的定义与特点

  1. JSON是一种文本格式的结构化数据序列化方式,用于数据交换和存储。
  2. 它具有简洁、易读、易写的特点,同时也易于机器解析和生成。
  3. JSON是完全独立于语言的,这意味着它可以在不同的编程语言之间轻松传递数据。

JSON的数据类型

  1. JSON支持多种数据类型,包括字符串(string)、数值(number)、布尔值(boolean)、对象(object)、数组(array)以及null。
    • 字符串:由双引号包围的文本。
    • 数值:可以是整数或浮点数,不支持八进制和十六进制。
    • 布尔值:true或false。
    • 对象:由花括号{}包围,包含零个或多个“键值对”的集合。
    • 数组:由方括号[]包围,包含零个或多个值的有序集合。

解析与生成JSON

  1. 解析JSON:当接收到JSON格式的数据时,应用程序需要将其解析为内部可操作的数据结构(如对象、数组等)。大多数编程语言都提供了JSON解析库或内置功能来支持这一过程。
  2. 生成JSON:与解析相反,生成过程是将程序内部的数据结构转换为JSON格式的字符串,以便在网络传输或文件存储中使用。同样地,各种编程语言也提供了相应的库或方法来支持这一操作。

在C++中,引入了jsoncpp。jsoncpp 是一个用于处理 JSON 数据的 C++ 库。它提供了一种简单的方式来解析、生成、操作和查询 JSON 数据。其应用见如下文章,此处不再过多赘述。

Jsoncpp使用简介-CSDN博客

六、网络版计算器编程逻辑

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 <pthread.h>
#include <memory>#include "Log.hpp"
#include "InternetAddr.hpp"const static int gblcklog = 8;class Socket;                             // 先声明
using SockSPtr = std::shared_ptr<Socket>; // 智能指针类型别名// 模版方法模式
// 基类提供纯虚函数方法,子类需要根据需求设计方法的具体实现
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 *cliaddr) = 0;virtual bool Conntecor(const 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();}bool BuildClientSocket(const std::string &peerip, uint16_t peerport){CreateSocketOrDie();return Conntecor(peerip, peerport);}// void BuildUdpSocket()// {}
};class Tcp_Socket : public Socket
{
private:int _sockfd;public:Tcp_Socket(int sockfd = -1): _sockfd(sockfd){}~Tcp_Socket(){}void CreateSocketOrDie() override{// 1、 创建套接字_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(FATAL, "Sockfd Create False!\n");exit(-1);}LOG(INFO, "Sockfd Create Success!\n");}void CreateBindOrDie(uint16_t port) override{struct sockaddr_in local_addr;memset(&local_addr, 0, sizeof(local_addr));local_addr.sin_addr.s_addr = INADDR_ANY;local_addr.sin_port = htons(port);local_addr.sin_family = AF_INET;// 2、绑定本地ip地址和port端口号if (::bind(_sockfd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0){LOG(FATAL, "Listen Sockfd Bind False!\n");exit(-1);}LOG(INFO, "Listen Sockfd Bind Success!\n");}void CreateListenOrDie(int backlog) override{// 3、将套接字设置为【监听状态】, 以监听来自客户端的连接请求if (::listen(_sockfd, backlog) < 0){LOG(FATAL, "Listen Sockfd Listen False!\n");exit(-1);}LOG(INFO, "Listen Sockfd Listen Success!\n");}SockSPtr Accepter(InetAddr *client_addr) override{// 1、获取来自客户端的连接请求,并获得I/O专用套接字struct sockaddr_in from_client;socklen_t addr_len = sizeof(from_client);memset(&from_client, 0, addr_len);int _io_sockfd = accept(_sockfd, (struct sockaddr *)&from_client, &addr_len);if (_io_sockfd < 0){LOG(FATAL, "Server Sockfd Accept False!");return nullptr;}LOG(DEBUG, "Server Sockfd Accept Success!");*client_addr = from_client;return std::make_shared<Tcp_Socket>(_io_sockfd);}bool Conntecor(const std::string &peerip, uint16_t peerport) override{// 连接服务端struct sockaddr_in to_server;memset(&to_server, 0, sizeof(to_server));inet_pton(AF_INET, peerip.c_str(), &to_server.sin_addr);to_server.sin_family = AF_INET;to_server.sin_port = htons(peerport);if (connect(_sockfd, (struct sockaddr *)&to_server, sizeof(to_server)) < 0){return false;}return true;}int Sockfd() override{ return _sockfd;}void Close() override{if (_sockfd > 0){::close(_sockfd);}}ssize_t Recv(std::string *out) override{char inbuffer[4096];ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);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);}
};

 Protocol.hpp : 协议头文件,其中约定了报文格式、请求格式、应答格式、序列化与反序列化方法。

#pragma once
#include <memory>
#include <string>
#include <jsoncpp/json/json.h>
#include "Log.hpp"
#include <iostream>
#include <cstring>
// version1 报文:“len\r\njson串\r\n”
// version2 自定义序列串报文:"len\r\nx#operator#y\r\n"
// 分隔字符串
static const std::string separate_str = "\r\n";
static const std::string self_cut_str = "#";
// 网络计算器// 添加报头
std::string AddCode(const std::string &json_str)
{int len = json_str.size();std::string len_str = std::to_string(len);return len_str + separate_str + json_str + separate_str;
}// 传递进来的报文可能有以下形式。如果报文不完整,返回空串到上层,让上层继续接收来自客户端的信息。如果能提取到完整json串,在传入报文的基础上将json串拆分出来。
// 不能带const
// "le
// "len"
// "len"\r\n
// "len"\r\n"{json}"\r\n (]
// "len"\r\n"{j
// "len"\r\n"{json}"\r\n"len"\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
std::string RemoveCode(std::string &message) // 移除报头,返回提取到的json串
{auto _pos = message.find(separate_str); // 先找到分隔符的位置if (_pos == std::string::npos)          // 没找到则返回上层继续接收客户端传来的信息{return std::string();}std::string json_len_str = message.substr(0, _pos); // 提取出报文首部json串的长度int json_len = stoi(json_len_str);int _total = json_len + separate_str.size() * 2 + json_len_str.size(); // 计算报文理论长度if (message.size() < _total)                                           // 如果报文小于理论长度,返回上层继续接收信息{return std::string();}std::string ret_json_str = message.substr(_pos + separate_str.size(), json_len); // 提取报文中的json串message.erase(0, _total);                                                        // 在传入报文中删除已经提取的该段报文return ret_json_str;                                                             // 返回提取的json串
}// 客户端发起请求
class Request
{
public:Request(int x = -1, int y = -1, char oper = -1): _x(x), _y(y), _operator(oper){}// 序列化:结构体成员 -》 json串bool Serialize(std::string *out_jsonstr){
#ifdef FLAG*out_jsonstr = std::to_string(_x) + self_cut_str + _operator + self_cut_str + std::to_string(_y);return true;
#elseJson::Value root;root["x"] = _x;root["y"] = _y;root["operator"] = _operator;Json::FastWriter writer;*out_jsonstr = writer.write(root);return true;
#endif}// 反序列化:json串 -》 结构体成员bool DeSerialize(const std::string &in_jsonstr){#ifdef FLAGauto x_pos = in_jsonstr.find(self_cut_str);if(x_pos == std::string::npos) return false;auto y_pos = in_jsonstr.rfind(self_cut_str);if(y_pos == std::string::npos) return false;if(x_pos + 1 + self_cut_str.size() != y_pos) return false;_x = std::stoi(in_jsonstr.substr(0, x_pos));_y = std::stoi(in_jsonstr.substr(y_pos + self_cut_str.size()));_operator = in_jsonstr[y_pos - 1];return true;
#elseJson::Value root;Json::Reader reader;bool res = reader.parse(in_jsonstr, root);_x = root["x"].asInt();_y = root["y"].asInt();_operator = root["operator"].asInt();return res;
#endif}int X(){return _x;}int Y(){return _y;}char Oper(){return _operator;}void SetValue(int x, int y, char oper){_x = x;_y = y;_operator = oper;}private:int _x; // 左操作数int _y; // 右操作数char _operator; // 运算符
};// 服务端返回应答
class Response
{
public:Response(int result = -1, int exit_code = 0, std::string exit_info = "Success"): _result(result), _exit_code(exit_code), _exit_info(exit_info){}// 序列化:结构体成员 -》 json串bool Serialize(std::string *out_jsonstr){
#ifdef FLAG*out_jsonstr = std::to_string(_result) + self_cut_str + std::to_string(_exit_code) + self_cut_str + _exit_info;return true;
#elseJson::Value root;root["result"] = _result;root["exit_code"] = _exit_code;root["exit_info"] = _exit_info;Json::FastWriter writer;*out_jsonstr = writer.write(root);return true;
#endif}// version2 自定义序列串报文:"len\r\nresult#exit_code#info\r\n"// 反序列化:json串 -》 结构体成员bool DeSerialize(const std::string &in_jsonstr){
#ifdef FLAGauto left = in_jsonstr.find(self_cut_str);if(left == std::string::npos) return false;auto right = in_jsonstr.rfind(self_cut_str);if(right == std::string::npos) return false;if(left + 1 + self_cut_str.size() != right) return false;_result = std::stoi(in_jsonstr.substr(0, left));_exit_code = in_jsonstr[right - 1] -'\0';_exit_info = in_jsonstr.substr(right + self_cut_str.size());return true;
#elseJson::Value root;Json::Reader reader;bool res = reader.parse(in_jsonstr, root);_result = root["result"].asInt();_exit_code = root["exit_code"].asInt();_exit_info = root["exit_info"].asString();return res;
#endif}void PrintResult(){std::cout << "result: " << _result << ", code: " << _exit_code << ", desc: " << _exit_info << std::endl;}public:int _result; // 计算结果int _exit_code; // 退出码std::string _exit_info; // 退出码对应的退出信息
};// 工厂模式,提供构造对象实例的函数方法
class FactoryForRequestAndReponse
{
public:static std::shared_ptr<Request> BulidRequestObject(){return std::make_shared<Request>();}static std::shared_ptr<Response> BulidResponseObject(){return std::make_shared<Response>();}
};

NetCol.hpp : 计算器头文件,其中提供了将请求对象转化为应答对象的方法。

#pragma once#include "Protocol.hpp"
#include <memory>class NetCal
{
public:std::shared_ptr<Response> Calculator(std::shared_ptr<Request> req){auto resp = FactoryForRequestAndReponse::BulidResponseObject();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->_exit_code = 1;resp->_exit_info = "div zero";}else{resp->_result = req->X() / req->Y();}}break;case '%':{if (req->Y() == 0){resp->_exit_code = 2;resp->_exit_info = "mod zero";}else{resp->_result = req->X() % req->Y();}}break;default:{resp->_exit_code = 3;resp->_exit_info = "illegal operation";}break;}return resp;}
};

 Service.hpp : I/O服务类,其中包含了服务端进行网络通信的方法。作为服务端类的参数构造服务器对象,目的是实现服务器自身启动功能与通信功能方法的解耦。通过传递不同的参数,可以使服务器具有对通信信息不同的处理能力。

#pragma once
#include "NetCol.hpp"
#include "Protocol.hpp"
#include "Socket.hpp"
#include <functional>// 服务端需要的任务函数的类型
// using Tcp_Server_FuncType = std::function<void(SockSPtr, InetAddr)>; // 注:InetAddr 为类,该对象中包含目标端的IP地址、端口号// IO_Service 类用于处理服务端对客户端信息的接收和应答工作, 内部由外层提供 将 请求 转变为 应答 的功能函数
using Process_Request_and_Return_Response_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request>)>;class IO_Service
{
private:Process_Request_and_Return_Response_t _process;public:IO_Service(Process_Request_and_Return_Response_t process): _process(process){}void IOExecute(SockSPtr io_socksptr, InetAddr from_client_inetaddr){std::string message;while (true){// 1、接收来自客户端的请求报文if(io_socksptr->Recv(&message) < 0){LOG(FATAL, "Recv fail!");}// 2、将接收到的请求进行解码操作,获取json串std::string json_str = RemoveCode(message);// 如果没有提取到完整的json串,说明服务端当前接收到的报文不完整,继续进行recv操作if(json_str.empty()){continue;}std::cout << json_str << std::endl; // 3、将json串反序列化为请求std::shared_ptr<Request> request = std::make_shared<Request>();if(!request->DeSerialize(json_str)){LOG(FATAL, "request->DeSerialize fail!");}// 4、处理请求,返回应答结果;构建应答,接收应答结果std::shared_ptr<Response> response = _process(request);// 5、将应答进行序列化std::string return_json_str;if(!response->Serialize(&return_json_str)){LOG(FATAL, "response->Serialize fail!");}// 将已经序列化的应答做成完整报文,发送给客户端// 1、对应答序列化后的json串加码return_json_str = AddCode(return_json_str);// 2、将报文发送给客户端if(io_socksptr->Send(return_json_str) < 0){LOG(FATAL, "Send fail!");}}}
};

TCP_Server.hpp:服务端类

#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string>
#include <iostream>
#include <arpa/inet.h>
#include <cstring>
#include <signal.h>
#include <sys/wait.h>
#include "ThreadPool.hpp"
#include <functional>
#include "Socket.hpp"static uint16_t gport = 8888;static const int MAX_LEN = 5;
static const int BUFFER_SIZE = 256;using Tcp_Server_FuncType = std::function<void(SockSPtr, InetAddr)>; // 注:InetAddr 为类,该对象中包含目标端的IP地址、端口号class Tcp_Server
{
private:SockSPtr _listen_sockfd; // 监听套接字,使用listen函数设置为监听态,负责监听来自客户端的连接请求bool _is_running;uint16_t _port;Tcp_Server_FuncType _tcp_service; // 服务端需要执行的任务对象
public:// 构造函数,提供端口号Tcp_Server(Tcp_Server_FuncType tcp_service, uint16_t port = gport): _port(port), _listen_sockfd(std::make_shared<Tcp_Socket>()), _is_running(false), _tcp_service(tcp_service){}void InitServer(){// 1、 创建监听套接字// 2、绑定本地ip地址和port端口号// 3、将套接字设置为【监听状态】, 以监听来自客户端的连接请求_listen_sockfd->BuildListenSocket(_port);}// 运行服务端void Loop(){// 多线程版本_is_running = true;while(_is_running){// 1、获取来自客户端的连接请求,并获得I/O专用套接字InetAddr from_client;SockSPtr _io_sockfd = _listen_sockfd->Accepter(&from_client);pthread_t tid = 0;ThreadData * thread_data = new ThreadData(_io_sockfd, this, from_client);//  线程需要执行类中的Service函数,同时主线程不能对该线程进行等待回收,所以需要该线程进行线程分离,让线程退出后自动由系统回收if(pthread_create(&tid, nullptr, ThreadRoute, thread_data) < 0){LOG(FATAL, "Thread Create False!");exit(-1);}}}// 为什么要单独设置一个线程数据类呢?// 如果在Tcp_Server类中设置一个静态方法,该方法无法访问类中的非静态成员。当然,将Tcp_Server类对象本身的指针作为线程函数的参数传递给线程执行函数也是可以的// 但是,服务器类对象中包含的成员变量和方法或许会非常多,而线程执行函数仅需执行IO工作和对信息的处理工作,并不需要这些数据。所以我们单独设计一个内部类,// 在该类中添加所需的成员变量即可class ThreadData{public:SockSPtr _io_sockfd; // 进行io通信的套接字描述符Tcp_Server* _self; // Tcp_Server类指针,用于调取该类中的函数方法InetAddr _net_addr; // ip + portpublic:ThreadData(SockSPtr io_sockfd, Tcp_Server* self, InetAddr net_addr): _io_sockfd(io_sockfd), _self(self), _net_addr(net_addr){}};// static void* ThreadRoute(void* thread_data){// 1、将该线程设置为分离态,该线程运行结束后系统自动回收资源pthread_detach(pthread_self());// 2、运行任务函数ThreadData* thread_self_data = static_cast<ThreadData*>(thread_data);thread_self_data->_self->_tcp_service(thread_self_data->_io_sockfd, thread_self_data->_net_addr);close(thread_self_data->_io_sockfd->Sockfd());delete thread_self_data;return nullptr;}
};

TCP_Server_main.cc:服务端运行逻辑 

#include "Tcp_Server_New.hpp"
#include "Service.hpp"
#include "NetCol.hpp"
//在命令行需自主输入绑定的端口号
int main(int argc, char* argv[])
{if(argc < 2){std::cout << "未输入端口号..." << std::endl;exit(-1);}uint16_t port = std::stoi(argv[1]);NetCal net_calculator;// 为服务类绑定 请求-》应答 方法IO_Service service(std::bind(&NetCal::Calculator, &net_calculator, std::placeholders::_1));// 绑定命令类中的命令处理方法,作为服务端的执行函数构造服务端Tcp_Server server(std::bind(&IO_Service::IOExecute, &service, std::placeholders::_1, std::placeholders::_2), port);server.InitServer(); // 初始化服务端server.Loop(); // 启动服务端return 0;
}

TCP_Client.hpp:客户端类

#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string>
#include <iostream>
#include <arpa/inet.h>
#include <cstring>
#include <signal.h>
#include <sys/wait.h>
#include "Log.hpp"
#include "Socket.hpp"
#include "Protocol.hpp"static const int BUFFER_SIZE = 256;
const std::string opers = "+-*/%&^!";
class Tcp_Client
{
private:SockSPtr _sockfd;std::string _to_server_ip;uint16_t _to_server_port;bool _is_running;public:Tcp_Client(const std::string &ip, const uint16_t port): _to_server_ip(ip), _to_server_port(port), _sockfd(std::make_shared<Tcp_Socket>()){}~Tcp_Client(){_sockfd->Close();}void InitClient(){// 客户端无需手动绑定bind,在使用connect函数连接时,自动绑定IP地址和端口号// 1、创建套接字,连接服务端if (!_sockfd->BuildClientSocket(_to_server_ip, _to_server_port)){LOG(FATAL, "BuildClientSocket fail!!!");}}// 启动客户端void Start(){_is_running = true;while (_is_running){// // 1、输入需要计算的数字和操作符// int x, y;// char oper;// std::cout << "Please Enter X # ";// std::cin >> x;// std::cout << "Please Enter Operator # ";// std::cin >> oper;// std::cout << "Please Enter Y # ";// std::cin >> y;// 构建数据int x = rand() % 10;usleep(x * 1000);int y = rand() % 10;usleep(x * y * 100);char oper = opers[y % opers.size()];// 2、根据输入信息创建请求std::shared_ptr<Request> resquest = std::make_shared<Request>(x, y, oper);// 3、将请求序列化,获得序列化后的json串std::string json_str;if (!resquest->Serialize(&json_str)){LOG(FATAL, "request->DeSerialize fail!");}std::cout << std::endl << json_str;// 4、为序列化后的json串加码,生成报文json_str = AddCode(json_str);// 5、将报文发送给服务端if (_sockfd->Send(json_str) < 0){LOG(FATAL, "Client Write To Server False!\n");exit(-1);}LOG(INFO, "Client Write To Server Success!\n");// 保证读取至少一条完整的报文while (true){// 6、接受来自服务端的应答std::string from_client_message;if (_sockfd->Recv(&from_client_message) < 0){LOG(FATAL, "Client Write To Server False!\n");break;}// 7、对报文解码获取json串std::string from_client_json_str = RemoveCode(from_client_message);if (from_client_json_str.empty()){continue;}// 8、根据json串构建应答std::shared_ptr<Response> response = std::make_shared<Response>();response->DeSerialize(from_client_json_str);// 9、打印应答结果response->PrintResult();break;}sleep(1);}_is_running = false;}
};

TCP_Client_main.cc:客户端运行逻辑 

#include "Tcp_Client.hpp"//在命令行需自主输入目标客户端的IP地址和需要绑定的端口号
int main(int argc, char* argv[])
{if(argc < 3){std::cout << "命令行参数过少..." << std::endl;exit(-1);}// 构造服务端Tcp_Client client(argv[1], std::stoi(argv[2]));client.InitClient(); // 构造客户端client.Start(); // 启动客户端return 0;
}

通过上述代码我们可以理解:协议实际上就是通信双方必须遵守提前约定好通信格式、信息的处理方式、错误检测与纠正机制、数据传输的顺序等方面的细节。

例如:在HTTP协议中,客户端和服务器之间的通信就是通过提前约定好的格式进行的。客户端发送一个HTTP请求,服务器根据请求的内容返回相应的HTTP响应。HTTP协议规定了请求和响应的格式、状态码的含义、头部的字段等等,这就是一种协议。

通过协议,通信双方可以在不明确各自内部实现细节的情况下,依然能够有效地进行数据交换和信息处理,确保通信的准确性和可靠性。

相关文章:

【Linux】网络编程:初识协议,序列化与反序列化——基于json串实现,网络通信计算器中简单协议的实现、手写序列化与反序列化

目录 一、什么是协议&#xff1f; 二、为什么需要有协议呢&#xff1f; 三、协议的应用 四、序列化与反序列化的引入 什么是序列化和反序列化&#xff1f; 为什么需要序列化和反序列化&#xff1f; 五、序列化推荐格式之一&#xff1a;JSON介绍 六、网络版计算器编程逻…...

Educational Codeforces Round 171 (Rated for Div. 2)(A~D) 题解

Problem - A - Codeforces--PerpendicularSegments 思路:正方形对角线最长,并且相互垂直.直接输出即可. int x,y,k; void solve(){ //Acin>>x>>y>>k;int resmin(x,y);cout<<"0 0"<<" "<<res<<" &q…...

【教程】Git 标准工作流

目录 前言建仓&#xff0c;拉仓&#xff0c;关联仓库修改代码更新本地仓库&#xff0c;并解决冲突提交代码&#xff0c;合入代码其他常用 Git 工作流删除本地仓库和远程仓库中的文件日志打印commit 相关 前言 Git 是日常开发中常用的版本控制工具&#xff0c;配合代码托管仓库…...

Nico,从零开始干掉Appium,移动端自动化测试框架实现

开头先让我碎碎念一波~去年差不多时间发布了一篇《 UiAutomator Nico&#xff0c;一个基于纯 adb 命令实现的安卓自动化测试框》&#xff08;https://testerhome.com/topics/37042&#xff09;&#xff0c; 由于种种原因 (详见此篇帖子) 当时选择了用纯 adb 命令来实现安卓自动…...

PHP合成图片,生成海报图,poster-editor使用说明

之前写过一篇使用Grafika插件生成海报图的文章&#xff0c;但是当我再次使用时&#xff0c;却发生了错误&#xff0c;回看Grafika文档&#xff0c;发现很久没更新了&#xff0c;不兼容新版的GD&#xff0c;所以改用了intervention/image插件来生成海报图。 但是后来需要对海报…...

微信小程序 - 数组 push / unshift 追加后数组返回内容为数字(数组添加后打印结果为 Number 数值类型)

前言 假设一个空数组,通过 push 方法追加了一个项,控制台打印的结果竟然是 Number 数值。 例如,以下微信小程序代码: // 源数组 var arr = [] // 追加数据 var tem = arr.push(数据)...

1、DevEco Studio 鸿蒙仓颉应用创建

1. 仓颉鸿蒙应用简介 因为仓颉是静态编译型语言&#xff0c;使用仓颉开发的应用执行效率更高。而且主打全场景&#xff0c;后续可并入仓颉生态&#xff0c;其和ArkTS都是基于ArkUI进行开发&#xff0c;最大的区别是typescript和仓颉语法间的差异。 2. 应用创建 前置条件&…...

从头开始学PHP之面向对象

首先介绍下最近情况&#xff0c;因为最近入职了且通勤距离较远&#xff0c;导致精力不够了&#xff0c;而且我发现&#xff0c;人一旦上了班&#xff0c;下班之后就不想再进行任何脑力劳动了&#xff08;对大部分牛马来说&#xff0c;精英除外&#xff09;。 话不多说进入今天的…...

C++ | Leetcode C++题解之第519题随机翻转矩阵

题目&#xff1a; 题解&#xff1a; class Solution { public:Solution(int m, int n) {this->m m;this->n n;this->total m * n;srand(time(nullptr));}vector<int> flip() {int x rand() % total;vector<int> ans;total--; // 查找位置 x 对应的…...

vrrp和mstp区别

思路 vrrp是用来虚拟网关&#xff0c;噢&#xff0c;是虚拟一条虚拟网关 优先级&#xff0c;priority越大越优先&#xff0c;优先级相同&#xff0c;哪个的路由器的vrrp先起来&#xff0c;谁就是主 mstp是快速生成树协议&#xff0c;防止环路用的 优先级越小越优先 华为命令…...

前端页面整屏滚动fullpage.js简单使用

官网CSS,JS地址 fullPage.js/dist/fullpage.min.js at master alvarotrigo/fullPage.js GitHub fullPage.js/dist/fullpage.min.css at master alvarotrigo/fullPage.js GitHub <!DOCTYPE html> <html lang"en"><head><meta charset"…...

JQuery基本介绍和使用方法

JQuery基本介绍和使用方法 W3C 标准给我们提供了⼀系列的函数, 让我们可以操作: ⽹⻚内容⽹⻚结构⽹⻚样式 但是原⽣的JavaScript提供的API操作DOM元素时, 代码⽐较繁琐, 冗⻓. 我们可以使⽤JQuery来操作⻚⾯对象. jQuery是⼀个快速、简洁且功能丰富的JavaScript框架, 于20…...

【案例】旗帜飘动

开发平台&#xff1a;Unity 6.0 开发工具&#xff1a;Shader Graph 参考视频&#xff1a;Unity Shader Graph 旗帜飘动特效   一、效果图 二、Shader Graph 路线图 三、案例分析 核心思路&#xff1a;顶点偏移计算 与 顶点偏移忽略 3.1 纹理偏移 视觉上让旗帜保持动态飘动&a…...

大模型思维链推理的综述:进展、前沿和未来

转自公众号AIRoobt A Survey of Chain of Thought Reasoning: Advances, Frontiers and Future 思维链推理的综述&#xff1a;进展、前沿和未来 摘要&#xff1a;思维链推理&#xff0c;作为人类智能的基本认知过程&#xff0c;在人工智能和自然语言处理领域引起了极大的关注…...

项目一:使用 Spring + SpringMVC + Mybatis + lombok 实现网络五子棋

一&#xff1a;系统展示: 二&#xff1a;约定前后端接口 2.1 登陆 登陆请求&#xff1a; GET /login HTTP/1.1 Content-Type: application/x-www-form-urlencodedusernamezhangsan&password123登陆响应&#xff1a; 正常对象&#xff1a;正常对象会在数据库中存储&…...

openEuler 系统中 Samba 文件共享服务器管理(windows、linux文件共享操作方法)

一、Samba 简介 Samba 是在 Linux 和 Unix 系统上实现 SMB/CIFS 协议的一个免费软件&#xff0c;使得这些系统可以与 Windows 系统进行文件和打印机共享。通过 Samba&#xff0c;可以将 openEuler 系统配置为文件服务器&#xff0c;让 Windows、Linux 和其他支持 SMB/CIFS 协议…...

使用 Elasticsearch 进行语义搜索

Elasticsearch 是一款功能强大的开源搜索引擎&#xff0c;可用于全文搜索、分析和数据可视化。传统上&#xff0c;Elasticsearch 以其执行基于关键字/词汇的搜索的能力而闻名&#xff0c;其中文档基于精确或部分关键字匹配进行匹配。然而&#xff0c;Elasticsearch 已经发展到支…...

软考:中间件

中间件 中间件是一类位于操作系统软件与用户应用软件之间的计算机软件&#xff0c;它包括一组服务&#xff0c;以便于运行在一台或多台机器上的多个软件通过网络进行交互。 中间件的主要功能包括通信支持和应用支持。 通信支持为应用软件提供平台化的运行环境&#xff0c;屏蔽…...

银行家算法(Banker’s Algorithm)

银行家算法&#xff08;Banker’s Algorithm&#xff09;是计算机操作系统中一种避免死锁发生的著名算法。该算法由艾兹格迪杰斯特拉&#xff08;Edsger Dijkstra&#xff09;在1965年为T.H.E系统设计&#xff0c;其核心理念基于银行借贷系统的分配策略&#xff0c;以确保系统的…...

用魔数严谨的判别文件类型:杜绝上传风险

在文件处理和管理中&#xff0c;确定文件的类型是一个常见的需求。虽然文件扩展名可以提供一些信息&#xff0c;但并不总是可靠的。魔数&#xff08;Magic Numbers&#xff09;是一种更为准确的方法&#xff0c;通过检查文件开头的特定字节序列来识别文件类型。本文将介绍如何在…...

【MacOS实操】如何基于SSH连接远程linux服务器

MacOS上远程连接linux服务器&#xff0c;可以使用ssh命令pem秘钥文件连接。 一、准备pem秘钥文件 如果已经有pem文件&#xff0c;则跳过这一步。如果手上有ppk文件&#xff0c;那么需要先转换为pem文件。 macOS 的默认 SSH 客户端不支持 PPK 格式&#xff0c;你需要将 PPK 文…...

EXPLAIN 针对性优化 SQL 查询

在数据库管理和应用中&#xff0c;高效的 SQL 查询是确保系统性能的关键。随着数据量的不断增长和业务需求的日益复杂&#xff0c;优化 SQL 查询变得尤为重要。而 EXPLAIN 命令是一种强大的工具&#xff0c;可以帮助我们深入了解 SQL 查询的执行计划&#xff0c;从而进行有针对…...

MR30分布式IO:石化行业的智能化革新

在浩瀚的工业领域中&#xff0c;石化行业如同一座巨大的化工厂&#xff0c;将自然界的原始资源转化为人们日常生活中不可或缺的各种产品。然而&#xff0c;随着生产规模的扩大和工艺复杂度的提升&#xff0c;石化行业面临着前所未有的挑战&#xff1a;如何在保证生产效率的同时…...

linux图形化X窗口

【linux图形化协议框架】 X、X11系统&#xff1a;X协议&#xff0c;X服务器&#xff0c;窗口管理器&#xff0c;X客户端&#xff08;客户端库Xcb&#xff0c;Xlib库等&#xff09;&#xff0c;输入、绘制 Wayland系统&#xff1a;Wayland 协议&#xff0c;合成器、客户端&#…...

练习LabVIEW第三十五题

学习目标&#xff1a; 刚学了LabVIEW&#xff0c;在网上找了些题&#xff0c;练习一下LabVIEW&#xff0c;有不对不好不足的地方欢迎指正&#xff01; 第三十五题&#xff1a; 使用labview模拟一个3-8译码器 开始编写&#xff1a; 用LabVIEW做3-8译码器首先要知道它是个啥…...

Decision Tree Regressor (决策树) --- 论文实战

一、前言 在《机器学习论文复现实战---linear regression》中通过Pearson 相关性分析,去除了2个高相关性特征 "PN" 和 "AN" ,数据维度变为890*25。(数据集地址) 这里我们不做前期处理,直接就将数据放入 DecisionTreeRegressor 模型中进行训练了。 二…...

三层交换技术,eNSP实验讲解

三层交换技术&#xff0c;eNSP实验讲解 一、简要介绍1、概念2、工作原理3、优点4、应用场景5、与路由器的区别 二、eNSP仿真实验1、步骤一&#xff1a;创建连接&#xff0c;明确参数。2、步骤二&#xff1a;设置PC1和PC2参数3、步骤三&#xff1a;配置交换机&#xff0c;通过命…...

单链表OJ题(3):合并两个有序链表、链表分割、链表的回文结构

目录 一、合并两个有序链表 二、链表分割 三、链表的回文结构 u解题的总体思路&#xff1a; 合并两个有序链表&#xff1a;首先创建新链表的头节点&#xff08;哨兵位&#xff1a;本质上是占位子&#xff09;&#xff0c;为了减少一些判断情况&#xff0c;简化操作。然后我们…...

研究了100个小绿书十万加之后,我们发现2024小绿书独家秘籍就是:在于“先抄后超,持续出摊,量大管饱”!

小绿书作为今年最大的红利&#xff0c;很多人已经吃到了螃蟹。看——&#xff1a; 今天我们总结了100个10万爆款&#xff0c;我们发现要在这个平台上脱颖而出&#xff0c;找到属于自己的方法尤为重要。在这里分享一个主题——小绿书的秘诀就是“先抄后超&#xff0c;持续出摊”…...

Java 中 HashMap集合使用

目录 一. HashMap概述 二. HashMap特点 三. HashMap构造方法 四. HashMap的常用方法 五. 使用注意事项 六. 代码示例 一. HashMap概述 HashMap 是 Java 中的一个非常重要的类&#xff0c;它实现了 Map 接口&#xff0c;用于存储键值对&#xff08;key-value pairs&#…...