【网络】自定义协议——序列化和反序列化
> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。> 目标:了解什么是序列化和分序列,并且自己能手撕网络版的计算器。
> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!
> 专栏选自:网络
> 望小伙伴们点赞👍收藏✨加关注哟💕💕
一、前言
前面我们已经学习了网络的基础知识,对网络的基本框架已有认识,算是初步认识到网络了,如果上期我们的学习网络是步入基础知识,那么这次学习的板块就是基础知识的实践,我们今天的板块是学习网络重要之一,学习完这个板块对虚幻的网络就不再迷茫!!!
二、主体
学习【网络】自定义协议——序列化和反序列化咱们按照下面的图解:
2.1 自定义协议(应用层)
概念:
网络协议:是通信计算机双方必须共同遵从的一组约定,为了使数据在网络上能够从源地址到目的地址,网络通信的参与方必须遵循相同的规则,因此我们将这套规则称为协议,协议最终需要通过计算机语言的方式表示出来。
应用层和传输层的概述:
- 应用层:位于网络协议的最高层,直接为用户提供服务。这一层的协议如HTTP、FTP、SMTP等,负责处理特定应用程序的数据传输和通信。
- 传输层:位于应用层之下,网络层之上。它提供端到端的通信服务,确保数据在网络中的可靠传输。传输层协议如TCP和UDP,管理数据包的顺序和流量控制。
应用层和传输层的区别:
- 目的:应用层的目的是为了支持各种应用程序的特定需求,而传输层则致力于提供通用的通信服务。
- 可靠性:应用层协议可以根据应用程序的需求提供可靠或不可靠的服务。而传输层协议,如TCP,保证数据包的顺序和完整性,提供可靠的端到端通信。
- 数据传输方式:应用层协议如HTTP、FTP等,通常基于TCP或UDP传输数据。而传输层协议则负责管理这些数据包的发送和接收。
- 服务类型:应用层协议服务于各种类型的应用,如网页浏览、电子邮件、文件传输等。而传输层协议主要为应用程序提供通用的数据传输服务。
应用层和传输层的关系:
应用层和传输层在网络通信中是紧密相关的。传输层提供了一种可靠的数据传输机制,确保数据在网络中的正确传输,而应用层协议定义了特定应用程序的数据格式和通信规则。两者之间的协同工作使得应用程序能够有效地进行数据交换和通信。
例如,当我们使用浏览器访问一个网页时,HTTP(应用层协议)定义了请求和响应的格式,而TCP(传输层协议)确保了数据的可靠传输。两者共同作用,使得我们能够顺利地浏览网页。
2.2 序列化和反序列化
2.2.1 结构化数据
概念说明:
如果需要传输的数据是字符串,那么我们可以直接将这个字符串发送到网络当中。但是如果我们传输的是一些结构化的数据,此时就不能直接将这些数据发送到网络当中。
如果服务器将这些结构化的数据单独一个一个发送到网络当中,那么服务器从网络当中获取这些数据时也只能一个一个获取,并且结构化的数据往往具有特定的格式和规范,例如数据库表格或者特定的数据模型。如果直接将这些数据发送到网络,服务端可能需要处理大量复杂的格式转换和数据清洗工作,而且还有可能会影响数据的正确性。所以客户端最好把这些结构化的数据打包后统一发送到网络当中,此时服务端每次从网络当中获取到的就是一个完整的请求数据。
常见的打包方式:
- 将结构化的数据组合成一个字符串:
客户端可以按照某种方式将这些结构化的数据组合成一个字符串,然后将该字符串发送到网络当中,当服务器接收到这个字符串时,以相同的方式将这个字符串进行解析。就可以从这个字符串中提取出这些结构化的数据。
- 定制结构体 + 序列化与反序列化:
客户端可以定制一个结构体,将需要交互的信息定义到这个结构体当中。客户端发送数据时先对数据进行序列化,服务端接收到数据后在对其进行反序列化,此时服务端就能得到客户端发送过来的结构体了。
2.2.2 序列化和反序列化
概念:
- 序列化: 将对象的状态信息转换为可以存储或传输的形式的过程。
- 反序列化: 将字节序列恢复为的过程。
补充说明:
OSI七层模型中表示层的作用就是,实现设备固有数据格式和网络标准数据格式的转换。其中设备固有的数据格式指的是数据在应用层上的格式,而网络标准数据格式则指的是序列化之后可以进行网络传输的数据格式。
序列化和反序列化的目的:
- 序列化的目的是方便网络数据的发送和接收,无论何种类型的数据,经过序列化后都变成了二进制序列,此时底层在进行网络传输时看到的都是二进制序列。
- 序列化后的二进制序列只有在网络传输时能够被底层识别,上层应用是无法识别序列化后的二进制数据的,因此需要将从网络中获取到的数据进行反序列化,将二进制序列的数据转换成应用层能够识别的数据格式。
2.3 网络版计算器
2.3.1 协议定制
请求数据和响应数据:(实现一个请求结构体和一个响应结构体)
- 请求数据结构体:包含两个操作数和一个操作符,操作数表示需要进行计算的两个数,操作符表示是要进行 +-*/ 中的哪一个操作。
- 响应数据结构体:需要包含一个计算结果和一个状态码,表示本次计算的结果和计算状态。
态码所表示的含义:
0
表示计算成功1
表示出现除0错误2
表示模0错误3
表示非法计算
因为协议定制好以后要被客户端和服务端同时遵守,所以需要将它写到一个头文件中,同时被客户端和服务端包含该头文件:
// 请求
class Request
{
public:Request(){}Request(int x, int y, char op):_x(x), _y(y), _op(op){}
public: int _x;int _y;char _op;
};// 响应
class Response
{
public:Response(){}Response(int result, int code):_result(result), _code(code){}
public:int _result;int _code;
};
同时,最好还是把协议中的分隔符给定义出来,方便后续统一使用或更改:
#define CRLF "\t" //分隔符
#define CRLF_LEN strlen(CRLF) //分隔符长度
#define SPACE " " //空格
#define SPACE_LEN strlen(SPACE) //空格长度#define OPS "+-*/%" //运算符
2.3.2 编码解码
编码的是往字符串的开头添加上长度和分隔符:
长度\t序列化字符串\t
解码就是将长度和分隔符去掉,只解析出序列化字符串:
序列化字符串
编码解码的整个过程在注释里面都写明了 为了方便请求和回应去使用,直接放到外头,不做类内封装:
//参数len为in的长度,是一个输出型参数。如果为0代表err
std::string decode(std::string& in,size_t*len)
{assert(len);//如果长度为0是错误的// 1.确认in的序列化字符串完整(分隔符)*len=0;size_t pos = in.find(CRLF);//查找\t第一次出现时的下标//查找不到,errif(pos == std::string::npos){return "";//返回空串}// 2.有分隔符,判断长度是否达标// 此时pos下标正好就是标识大小的字符长度std::string inLenStr = in.substr(0,pos);//从下标0开始一直截取到第一个\t之前//到这里我们要明白,我们这上面截取的是最开头的长度,也就是说,我们截取到的一定是个数字,这个是我们序列化字符的长度size_t inLen = atoi(inLenStr.c_str());//把截取的这个字符串转int,inLen就是序列化字符的长度//传入的字符串的长度 - 第一个\t前面的字符数 - 2个\tsize_t left = in.size() - inLenStr.size()- 2*CRLF_LEN;//原本预计的序列化字符串长度if(left<inLen){//真实的序列化字符串长度和预计的字符串长度进行比较return ""; //剩下的长度(序列化字符串的长度)没有达到标明的长度}// 3.走到此处,字符串完整,开始提取序列化字符串std::string ret = in.substr(pos+CRLF_LEN,inLen);//从pos+CRLF_LEN下标开始读取inLen个长度的字符串——即序列化字符串*len = inLen;// 4.因为in中可能还有其他的报文(下一条)// 所以需要把当前的报文从in中删除,方便下次decode,避免二次读取size_t rmLen = inLenStr.size() + ret.size() + 2*CRLF_LEN;//长度+2个\t+序列字符串的长度in.erase(0,rmLen);//移除从索引0开始长度为rmLen的字符串// 5.返回return ret;
}//编码不需要修改源字符串,所以const。参数len为in的长度
std::string encode(const std::string& in,size_t len)
{std::string ret = std::to_string(len);//将长度转为字符串添加在最前面,作为标识ret+=CRLF;ret+=in;ret+=CRLF;return ret;
}
2.3.3 request(请求)
2.3.3.1 构造
功能说明:
用户可能输入x+y,x+ y,x +y,x + y等等格式
这里还需要注意,用户的输入不一定是标准的X+Y,里面可能在不同位置里面会有空格。为了统一方便处理,在解析之前,最好先把用户输入内的空格给去掉!
代码呈现:
class Request
{
public:// 将用户的输入转成内部成员// 用户可能输入x+y,x+ y,x +y,x + y等等格式// 提前修改用户输入(主要还是去掉空格),提取出成员Request(std::string in,bool* status):_x(0),_y(0),_ops(' '){rmSpace(in);//删除空格// 这里使用c的字符串,因为有strtokchar buf[1024];// 打印n个字符,多的会被截断snprintf(buf,sizeof(buf),"%s",in.c_str());//将报文存到buf里面去,方便使用strtokchar* left = strtok(buf,OPS);//left变成从字符串in的最开始到第一个运算符的这段字符串——即左操作数if(!left){//找不到*status = false;return;}//right变成从第一个运算符开始,到第二个运算符中间的这段字符串——即右操作数char*right = strtok(nullptr,OPS);//在上次寻找的基础上继续寻找if(!right){//找不到*status = false;return;}// x+y, strtok会将+设置为\0char mid = in[strlen(left)];//截取出操作符//这是在原字符串里面取出来,buf里面的这个位置被改成\0了_x = atoi(left);_y = atoi(right);_ops = mid;*status=true;}public:int _x;int _y;char _ops;
};
2.3.3.2 序列化
功能说明:
解析出成员以后,我们要做的就是对成员进行序列化,将其按指定的位置摆成一个字符串。这里采用了输出型参数的方式来序列化字符串,也可以改成用返回值的方式来操作。
代码呈现:
// 序列化 (入参应该是空的)
void serialize(std::string& out)
{// x + yout.clear(); // 序列化的入参是空的out+= std::to_string(_x);out+= SPACE;out+= _ops;//操作符不能用tostring,会被转成asciiout+= SPACE;out+= std::to_string(_y);// 不用添加分隔符(这是encode要干的事情)
}
2.3.3.3 反序列化
说明:
在客户端和服务端都需要使用request,客户端进行序列化,服务端对接收到的结果利用request进行反序列化。request只关注于对请求的处理,而不处理服务器的返回值。
代码呈现:
// 反序列化
bool deserialize(const std::string &in)
{// x + y 需要取出x,y和操作符size_t space1 = in.find(SPACE); //第一个空格if(space1 == std::string::npos) //没找到{return false;}size_t space2 = in.rfind(SPACE); //第二个空格if(space2 == std::string::npos) //没找到{return false;}// 两个空格都存在,开始取数据std::string dataX = in.substr(0,space1);std::string dataY = in.substr(space2+SPACE_LEN);//默认取到结尾std::string op = in.substr(space1+SPACE_LEN,space2 -(space1+SPACE_LEN));if(op.size()!=1){return false;//操作符长度有问题}//没问题了,转内部成员_x = atoi(dataX.c_str());_y = atoi(dataY.c_str());_ops = op[0];return true;
}
2.3.4 response(响应)
2.3.4.1 构造
说明:
返回值的构造比较简单,因为是服务器处理结果之后的操作;这些成员变量都设置为了公有,方便后续修改。
代码呈现:
class Response//服务端必须回应
{Response(int code=0,int result=0):_exitCode(code),_result(result){}public:int _exitCode; //计算服务的退出码int _result; // 结果
};
2.3.4.2 序列化
功能说明:
解析出成员以后,我们要做的就是对成员进行序列化,将其按指定的位置摆成一个字符串。这里采用了输出型参数的方式来序列化字符串,也可以改成用返回值的方式来操作。
代码呈现:
class Response//服务端必须回应
{Response(int code=0,int result=0):_exitCode(code),_result(result){}// 入参是空的void serialize(std::string& out){// code retout.clear();out+= std::to_string(_exitCode);out+= SPACE;out+= std::to_string(_result);out+= CRLF;}public:int _exitCode; //计算服务的退出码int _result; // 结果
};
2.3.4.3 反序列化
说明:
响应的反序列化只需要处理一个空格,相对来说较为简单。
代码呈现:
class Response//服务端必须回应
{Response(int code=0,int result=0):_exitCode(code),_result(result){}//序列化void serialize(std::string& out){// code retout.clear();out+= std::to_string(_exitCode);out+= SPACE;out+= std::to_string(_result);out+= CRLF;}// 反序列化bool deserialize(const std::string &in){// 只有一个空格size_t space = in.find(SPACE);//寻找第一个空格的下标if(space == std::string::npos)//没找到{return false;}std::string dataCode = in.substr(0,space);std::string dataRes = in.substr(space+SPACE_LEN);_exitCode = atoi(dataCode.c_str());_result = atoi(dataRes.c_str());return true;}public:int _exitCode; //计算服务的退出码int _result; // 结果
};
2.3.5 完整的自定义协议代码
使用说明:
客户端发送的消息是使用Request来进行序列化和反序列化的:
- 客户端发消息时:客户端先序列化,再编码
- 服务端收消息时,服务端先解码,再反序列化
服务端发送的消息是使用Response来进行序列化和反序列化的:
- 服务端发消息时:客户端先序列化,再编码
- 客户端收消息时,服务端先解码,再反序列化
代码呈现:Serialization.hpp
#pragma
#define CRLF "\t" // 分隔符
#define CRLF_LEN strlen(CRLF) // 分隔符长度
#define SPACE " " // 空格
#define SPACE_LEN strlen(SPACE) // 空格长度
#define OPS "+-*/%" // 运算符#include <iostream>
#include <string>
#include <cstring>
#include<assert.h>//参数len为in的长度,是一个输出型参数。如果为0代表err
std::string decode(std::string& in,size_t*len)
{assert(len);//如果长度为0是错误的// 1.确认in的序列化字符串完整(分隔符)*len=0;size_t pos = in.find(CRLF);//查找\t第一次出现时的下标//查找不到,errif(pos == std::string::npos){return "";//返回空串}// 2.有分隔符,判断长度是否达标// 此时pos下标正好就是标识大小的字符长度std::string inLenStr = in.substr(0,pos);//从下标0开始一直截取到第一个\t之前//到这里我们要明白,我们这上面截取的是最开头的长度,也就是说,我们截取到的一定是个数字,这个是我们序列化字符的长度size_t inLen = atoi(inLenStr.c_str());//把截取的这个字符串转int,inLen就是序列化字符的长度//传入的字符串的长度 - 第一个\t前面的字符数 - 2个\tsize_t left = in.size() - inLenStr.size()- 2*CRLF_LEN;//原本预计的序列化字符串长度if(left<inLen){//真实的序列化字符串长度和预计的字符串长度进行比较return ""; //剩下的长度(序列化字符串的长度)没有达到标明的长度}// 3.走到此处,字符串完整,开始提取序列化字符串std::string ret = in.substr(pos+CRLF_LEN,inLen);//从pos+CRLF_LEN下标开始读取inLen个长度的字符串——即序列化字符串*len = inLen;// 4.因为in中可能还有其他的报文(下一条)// 所以需要把当前的报文从in中删除,方便下次decode,避免二次读取size_t rmLen = inLenStr.size() + ret.size() + 2*CRLF_LEN;//长度+2个\t+序列字符串的长度in.erase(0,rmLen);//移除从索引0开始长度为rmLen的字符串// 5.返回return ret;
}//编码不需要修改源字符串,所以const。参数len为in的长度
std::string encode(const std::string& in,size_t len)
{std::string ret = std::to_string(len);//将长度转为字符串添加在最前面,作为标识ret+=CRLF;ret+=in;ret+=CRLF;return ret;
}class Request//客户端使用的
{
public:// 将用户的输入转成内部成员// 用户可能输入x+y,x+ y,x +y,x + y等等格式// 提前修改用户输入(主要还是去掉空格),提取出成员Request(std::string in, bool *status): _x(0), _y(0), _ops(' '){rmSpace(in); // 删除空格// 这里使用c的字符串,因为有strtokchar buf[1024];// 打印n个字符,多的会被截断snprintf(buf, sizeof(buf), "%s", in.c_str()); // 将报文存到buf里面去,方便使用strtokchar *left = strtok(buf, OPS); // left变成从字符串in的最开始到第一个运算符的这段字符串——即左操作数if (!left){ // 找不到*status = false;return;}// right变成从第一个运算符开始,到第二个运算符中间的这段字符串——即右操作数char *right = strtok(nullptr, OPS); // 在上次寻找的基础上继续寻找if (!right){ // 找不到*status = false;return;}// x+y, strtok会将+设置为\0char mid = in[strlen(left)]; // 截取出操作符// 这是在原字符串里面取出来,buf里面的这个位置被改成\0了_x = atoi(left);_y = atoi(right);_ops = mid;*status = true;}// 删除输入中的空格void rmSpace(std::string &in){std::string tmp;for (auto e : in){if (e != ' '){tmp += e;}}in = tmp;}// 序列化 (入参应该是空的,会返回一个序列化字符串)void serialize(std::string &out)//这个是客户端在发送消息给服务端时使用的,在这之后要先编码,才能发送出去{// x + yout.clear(); // 序列化的入参是空的out += std::to_string(_x);out += SPACE;out += _ops; // 操作符不能用tostring,会被转成asciiout += SPACE;out += std::to_string(_y);// 不用添加分隔符(这是encode要干的事情)}//序列化之后应该要编码,去加个长度// 反序列化(解开bool deserialize(const std::string &in)//这个是服务端接收到客户端发来的消息后使用的,在这之前要先解码{// x + y 需要取出x,y和操作符size_t space1 = in.find(SPACE); // 第一个空格if (space1 == std::string::npos) // 没找到{return false;}size_t space2 = in.rfind(SPACE); // 第二个空格if (space2 == std::string::npos) // 没找到{return false;}// 两个空格都存在,开始取数据std::string dataX = in.substr(0, space1);std::string dataY = in.substr(space2 + SPACE_LEN); // 默认取到结尾std::string op = in.substr(space1 + SPACE_LEN, space2 - (space1 + SPACE_LEN));if (op.size() != 1){return false; // 操作符长度有问题}// 没问题了,转内部成员_x = atoi(dataX.c_str());_y = atoi(dataY.c_str());_ops = op[0];return true;}public:int _x;int _y;char _ops;
};class Response // 服务端必须回应
{Response(int code = 0, int result = 0): _exitCode(code), _result(result){}// 序列化void serialize(std::string &out)//这个是服务端发送消息给客户端使用的,使用之后要编码{// code retout.clear();out += std::to_string(_exitCode);out += SPACE;out += std::to_string(_result);out += CRLF;}// 反序列化bool deserialize(const std::string &in)//这个是客户端接收服务端消息后使用的,使用之前要先解码{// 只有一个空格size_t space = in.find(SPACE); // 寻找第一个空格的下标if (space == std::string::npos) // 没找到{return false;}std::string dataCode = in.substr(0, space);std::string dataRes = in.substr(space + SPACE_LEN);_exitCode = atoi(dataCode.c_str());_result = atoi(dataRes.c_str());return true;}public:int _exitCode; // 计算服务的退出码int _result; // 结果
};
2.3.6 客户端/服务端完整代码
CalculatorClient.cc:
#include "TcpClient.hpp"static void usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}enum
{LEFT,OPER,RIGHT
};Request ParseLine(const std::string &line)
{std::string left, right;char op;int status = LEFT;int i = 0;while(i < line.size()){switch (status){case LEFT:if (isdigit(line[i]))left.push_back(line[i++]);elsestatus = OPER;break;case OPER:op = line[i++];status = RIGHT;break;case RIGHT:if (isdigit(line[i]))right.push_back(line[i++]);break;}}Request req;req._x = std::stoi(left);req._y = std::stoi(right);req._op = op;return req;
}int main(int argc, char* argv[])
{if(argc != 3){usage(argv[0]);exit(USAGE_ERR);}string serverip = argv[1];uint16_t serverport = atoi(argv[2]);Sock sock;sock.Socket();int n = sock.Connect(serverip, serverport);if(n != 0) return 1;string buffer;while(true){cout << "Enter# ";string line;getline(cin, line);Request req = ParseLine(line);cout << "test:" << req._x << req._op << req._y << endl;// 序列化string sendString;req.Serialize(&sendString);// 添加报头sendString = AddHeader(sendString);// sendsend(sock.Fd(), sendString.c_str(), sendString.size(), 0);// 获取响应string package;int n = 0;START:n = ReadPackage(sock.Fd(), buffer, &package);if(n == 0)goto START;else if(n < 0)break;else{}// 去掉报头package = RemoveHeader(package, n);// 反序列化Response resp;resp.Deserialize(package);cout << "result: " << resp._result << "[code: " << resp._code << "]" << endl;}sock.Close();return 0;
}
CalculatorServer.cc:
#include "TcpServer.hpp"
#include <memory>
using namespace tcpserver_ns;Response calculate(const Request &req)
{// 走到这里,一定保证req是有具体数据的!Response resp(0, 0);switch (req._op){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;elseresp._result = req._x / req._y;break;case '%':if (req._y == 0)resp._code = 2;elseresp._result = req._x % req._y;break;default:resp._code = 3;break;}return resp;
}int main()
{uint16_t port = 8080;unique_ptr<TcpServer> tsvr(new TcpServer(calculate, port));tsvr->InitServer();tsvr->Start();return 0;
}
Err.hpp:
#pragma onceenum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR,CONNECT_ERR,SETSID_ERR,OPEN_ERR
};
Log.hpp:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdio>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <stdarg.h>
using namespace std;const string filename = "log/tcpserver.log";// 枚举日志等级
enum
{Debug = 0, // 调试信息Info, // 正常运行Warning, // 报警Error, // 正常错误Fatal, // 严重错误Uknown
};// 字符串形式获取日志等级
static string toLevelString(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 "Uknown";}
}// 获取日志产生时间
static string getTime()
{time_t curr = time(nullptr);struct tm* tmp = localtime(&curr);// 缓冲区char buffer[128];snprintf(buffer, sizeof buffer, "%d-%d-%d %d:%d:%d", tmp->tm_year + 1900, tmp->tm_mon + 1, tmp->tm_mday,tmp->tm_hour, tmp->tm_min, tmp->tm_sec);return buffer;
}// 日志格式: 日志等级 时间 pid 消息体 —— 日志函数void logMessage(int level, const char* format, ...)
{char logLeft[1024];string level_string = toLevelString(level);string curr_time = getTime();snprintf(logLeft, sizeof(logLeft), "[%s] [%s] [%d] ", level_string.c_str(), curr_time.c_str(), getpid());char logRight[1024];va_list p;va_start(p, format);vsnprintf(logRight, sizeof(logRight), format, p);va_end(p);// 打印日志printf("%s%s\n", logLeft, logRight);// 将日志保存到文件中// FILE* fp = fopen(filename.c_str(), "a");// if(!fp) return;// fprintf(fp, "%s%s\n", logLeft, logRight);// fflush(fp);// fclose(fp);
}
Protocol.hpp:
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include "Sock.hpp"
#include <jsoncpp/json/json.h>#include "Util.hpp"
// 给网络版本计算器定制协议// #define MYSELF 1// 给网络版本计算器定制协议
namespace protocol_ns
{#define SEP " "#define SEP_LEN strlen(SEP) // 注意不能写成 sizeof#define HEADER_SEP "\r\n"#define HEADER_SEP_LEN strlen("\r\n")// 添加报头后 —— "长度"\r\n""_x _op _y"\r\n string AddHeader(const string& str){std::cout << "AddHeader 之前:\n"<< str << std::endl;string s = to_string(str.size());s += HEADER_SEP;s += str;s += HEADER_SEP;std::cout << "AddHeader 之后:\n"<< s << std::endl;return s;}// 去除报头 "长度"\r\n""_x _op _y"\r\n —— _x _op _ystring RemoveHeader(const std::string &str, int len){std::cout << "RemoveHeader 之前:\n"<< str << std::endl;string res = str.substr(str.size() - HEADER_SEP_LEN - len, len);return res;std::cout << "RemoveHeader 之后:\n"<< res << std::endl;}int ReadPackage(int sock, string& inbuffer, string*package){std::cout << "ReadPackage inbuffer 之前:\n"<< inbuffer << std::endl;// 边读取char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer - 1), 0);if(s <= 0)return -1;buffer[s] = 0;inbuffer += buffer;std::cout << "ReadPackage inbuffer 之中:\n"<< inbuffer << std::endl;// 边读取边分析, "7"\r\n""10 + 20"\r\nauto pos = inbuffer.find(HEADER_SEP); // 查找 \r\nif(pos == string::npos)return 0;string lenStr = inbuffer.substr(0, pos); // 获取头部字符串int len = Util::toInt(lenStr);int targetPackageLen = lenStr.size() + len + 2 * HEADER_SEP_LEN; // 获取到的完整字符串的长度if(inbuffer.size() < targetPackageLen)return 0;*package = inbuffer.substr(0, targetPackageLen);inbuffer.erase(0, targetPackageLen); std::cout << "ReadPackage inbuffer 之后:\n"<< inbuffer << std::endl;return len; // 返回有效载荷的长度}// Request && Response都要提供序列化和反序列化功能——自己手写// 请求class Request{public:Request(){}Request(int x, int y, char op):_x(x), _y(y), _op(op){}// 协议的序列化 struct -> stringbool Serialize(string* outStr){*outStr = "";
#ifdef MYSELFstring x_string = to_string(_x);string y_string = to_string(_y);// 手动序列化*outStr = x_string + SEP + _op + SEP + y_string;std::cout << "Request Serialize:\n"<< *outStr << std::endl;
#elseJson::Value root; // Value 是一种万能对象, 可以接受任意的kv类型root["x"] = _x;root["y"] = _y;root["op"] = _op;// Json::FastWriter writer; // Writer 是用来序列化的, struct -> stringJson::StyledWriter writer;*outStr = writer.write(root);
#endifreturn true;}// 协议的反序列化 string -> structbool Deserialize(const string& inStr){
#ifdef MYSELF// 将inStr分隔到数组里面 -> [0]=>10 [1]=>+ [2]=>20vector<string> result;Util::StringSplit(inStr, SEP, &result);if(result.size() != 3)return false;if(result[1].size() != 1)return false;_x = Util::toInt(result[0]);_y = Util::toInt(result[2]);_op = result[1][0];
#elseJson::Value root;Json::Reader reader; // Reader是用来进行反序列化的reader.parse(inStr, root);_x = root["x"].asInt();_y = root["y"].asInt();_op = root["op"].asInt();
#endifreturn true;}~Request(){}public: int _x; // 操作数 _xint _y; // 操作数 _ychar _op;// 操作符 _op};// 响应class Response{public:Response(){}Response(int result, int code):_result(result), _code(code){}bool Serialize(string* outStr){*outStr = "";
#ifdef MYSELFstring res_string = to_string(_result);string code_string = to_string(_code);// 手动序列化*outStr = res_string + SEP + code_string;std::cout << "Response Serialize:\n"<< *outStr << std::endl;
#elseJson::Value root;root["result"] = _result;root["code"] = _code;// Json::FastWriter writer;Json::StyledWriter writer;*outStr = writer.write(root);
#endifreturn true;}bool Deserialize(const string& inStr){
#ifdef MYSELF// 将inStr分隔到数组里面 -> [0]=>10 [1]=>+ [2]=>20vector<string> result;Util::StringSplit(inStr, SEP, &result);if(result.size() != 2)return false;_result = Util::toInt(result[0]);_code = Util::toInt(result[1]);
#elseJson::Value root;Json::Reader reader;reader.parse(inStr, root);_result = root["result"].asInt();_code = root["code"].asInt();
#endifPrint();return true;}void Print(){std::cout << "_result: " << _result << std::endl;std::cout << "_code: " << _code << std::endl;}public:int _result;int _code;};
}
Sock.hpp:
#pragma once#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Err.hpp"using namespace std;
static const int gbacklog = 32;
static const int defaultfd = -1;class Sock
{
public:Sock():_sock(defaultfd){}Sock(int sock):_sock(sock){}// 创建套接字void Socket(){_sock = socket(AF_INET, SOCK_STREAM, 0);if(_sock < 0){logMessage(Fatal, "socket error, code: %d, errstring: %s", errno, strerror(errno));exit(SOCKET_ERR);}}// 绑定端口号 和 ip地址void Bind(const uint16_t& port){struct sockaddr_in local;local.sin_addr.s_addr = INADDR_ANY; // 绑定任意IP地址local.sin_port = htons(port); // 绑定端口号local.sin_family = AF_INET;if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0){logMessage(Fatal, "bind error, code: %d, errstring: %s", errno, strerror(errno));exit(BIND_ERR);}}// 监听void Listen(){if(listen(_sock, gbacklog) < 0){logMessage(Fatal, "listen error, code: %d, errstring: %s", errno, strerror(errno));exit(LISTEN_ERR);}}// 获取连接int Accept(string* clientip, uint16_t* clientport){struct sockaddr_in temp;socklen_t len = sizeof(temp);int sock = accept(_sock, (struct sockaddr*)&temp, &len);if(sock < 0){logMessage(Warning, "accept error, code: %d, errstring: %s", errno, strerror(errno));}else{*clientip = inet_ntoa(temp.sin_addr);*clientport = ntohs(temp.sin_port);}return sock;}// 客户端和服务器建立连接int Connect(const string& serverip, const uint16_t& serverport){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());return connect(_sock, (struct sockaddr*)&server, sizeof(server));}int Fd(){return _sock;}void Close(){if(_sock != defaultfd)close(_sock);}~Sock(){}private:int _sock;
};
TcpClient.hpp:
#pragma once#include "TcpClient.hpp"
#include "Sock.hpp"
#include "Protocol.hpp"#include <iostream>
#include <string>using namespace std;
using namespace protocol_ns;
TcpServer.hpp:
#pragma once
#include <iostream>
#include <functional>
#include <cstring>
#include <pthread.h>
#include "Sock.hpp"
#include "Protocol.hpp"using namespace std;namespace tcpserver_ns
{using namespace protocol_ns;using func_t = function<Response(const Request&)>;class TcpServer;class ThreadData{public:ThreadData(int sock, string ip, uint16_t port, TcpServer* tsvrp):_sock(sock), _ip(ip), _port(port), _tsvrp(tsvrp){}~ThreadData(){}public:int _sock;string _ip;uint16_t _port;TcpServer *_tsvrp;};class TcpServer{public:TcpServer(func_t func, uint16_t port):_func(func), _port(port){}// 初始化服务器void InitServer(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();logMessage(Info, "init server done, listensock: %d, errstring: %s", errno, strerror(errno));}// 运行服务器void Start(){while(true){string clientip;uint16_t clientport;int sock = _listensock.Accept(&clientip, &clientport);if(sock < 0) continue;logMessage(Debug, "get a new client, client info : [%s:%d]", clientip.c_str(), clientport);pthread_t tid; // 创建多线程ThreadData *td = new ThreadData(sock, clientip, clientport, this);pthread_create(&tid, nullptr, ThreadRoutine, td);}}static void* ThreadRoutine(void* args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);td->_tsvrp->ServiceIO(td->_sock, td->_ip, td->_port);logMessage(Debug, "thread quit, client quit ...");delete td;return nullptr;}// 服务器对客户端的数据进行IO处理void ServiceIO(int sock, const std::string &ip, const uint16_t &port){string inbuffer;while(true){// 保证自己读到一个完整的字符串报文 "7"\r\n""10 + 20"\r\nstring package;int n = ReadPackage(sock, inbuffer, &package);if(n == -1)break;else if(n == 0)continue;else{// 已经得到了一个"7"\r\n""10 + 20"\r\n// 1. 提取有效载荷package = RemoveHeader(package, n);// 2. 已经读到了一个完整的stringRequest req;req.Deserialize(package);// 3. 直接提取用户的请求数据Response resp = _func(req); // 业务逻辑// 4. 给用户返回响应——序列化string send_string;resp.Serialize(&send_string);// 5. 添加报头send_string = AddHeader(send_string);// 6. 发送send(sock, send_string.c_str(), send_string.size(), 0);}}close(sock);}~TcpServer(){}private:uint16_t _port; // 端口号Sock _listensock; // 监听套接字func_t _func;};
}
Util.hpp:
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>using namespace std;class Util
{
public:static bool StringSplit(const string&str, const string& sep, vector<string>* result){size_t start = 0;while(start < str.size()){auto pos = str.find(sep, start);if(pos == string::npos) break;result->push_back(str.substr(start, pos - start));start = pos + sep.size();}if(start < str.size())result->push_back(str.substr(start));return true;}static int toInt(const string& s){return atoi(s.c_str());}
};
makefile:
.PHONY:all
all:calserver calclientcalclient:CalculatorClient.ccg++ -o $@ $^ -std=c++11 -ljsoncppcalserver:CalculatorServer.ccg++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp.PHONY:clean
clean:rm -f calclient calserver
三、结束语
今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。
相关文章:

【网络】自定义协议——序列化和反序列化
> 作者:დ旧言~ > 座右铭:松树千年终是朽,槿花一日自为荣。 > 目标:了解什么是序列化和分序列,并且自己能手撕网络版的计算器。 > 毒鸡汤:有些事情,总是不明白,所以我不…...

Pytorch如何精准记录函数运行时间
0. 引言 参考Pytorch官方文档对CUDA的描述,GPU的运算是异步执行的。一般来说,异步计算的效果对于调用者来说是不可见的,因为 每个设备按照排队的顺序执行操作Pytorch对于CPU和GPU的同步,GPU间的同步是自动执行的,不需…...

使用 Java 实现邮件发送功能
引言 1. JavaMail API 简介 2. 环境准备 2.1 Maven 依赖 2.2 Gradle 依赖 3. 发送简单文本邮件 4. 发送 HTML 邮件 5. 发送带附件的邮件 6. 注意事项 引言 在现代应用开发中,邮件发送功能是非常常见的需求,例如用户注册验证、密码重置、订单确认…...

html第一个网页
创建你的第一个HTML网页是一个激动人心的步骤。以下是创建一个简单网页的基本步骤和代码示例: 基础结构:所有的HTML文档都应该包含以下基本结构。 <!DOCTYPE html> <html> <head><title>我的第一个网页</title> </he…...

前后端交互接口(三)
前后端交互接口(三) 前言 前两集我们先做了前后端交互接口的约定以及浅浅的阅读了一些proto代码。那么这一集我们就来看看一些重要的proto代码,之后把protobuffer给引入我们的项目当中! gateway.proto 我们来看一眼我们的网关…...

华为Mate70前瞻,鸿蒙NEXT正式版蓄势待发,国产系统迎来关键一战
Mate 70系列要来了 上个月,vivo、小米、OPPO、荣耀等众多智能手机制造商纷纷发布了他们的年度旗舰产品,手机行业内竞争异常激烈。 同时,华为首席执行官余承东在其个人微博上透露,Mate 70系列将标志着华为Mate系列手机达到前所未有…...

【安卓13 源码】Input子系统(4)- InputReader 数据处理
1. 多指触控协议 多指触控协议有 2 种: > A类: 处理无关联的接触: 用于直接发送原始数据; > B类: 处理跟踪识别类的接触: 通过事件slot发送相关联的独立接触更新。 B协议可以使用一个ID来标识触点&…...

Xserver v1.4.2发布,支持自动重载 nginx 配置
Xserver——优雅、强大的 php 集成开发环境 本次更新为大家带来了更好的用户体验。 🎉 下载依赖组件时,显示进度条,展示下载进度。 🎉 保存站点信息和手动修改 vhost 配置文件之后,自动重载 nginx 配置 🐞…...

Java反射原理及其性能优化
目录 JVM是如何实现反射的反射的性能开销体现在哪里如何优化反射性能开销 1. JVM是如何实现反射的? 反射是Java语言中的一种强大功能,它允许程序在运行时动态地获取类的信息以及操作对象。下面是一个简单的示例,演示了如何使用反射调用方法ÿ…...

RabbitMQ 管理平台(控制中心)的介绍
文章目录 一、RabbitMQ 管理平台整体介绍二、Overview 总览三、Connections 连接四、Channels 通道五、Exchanges 交换机六、Queues 队列查看队列详细信息查看队列的消息内容 七、Admin 用户给用户分配虚拟主机 一、RabbitMQ 管理平台整体介绍 RabbitMQ 管理平台内有六个模块&…...

【SQL】在 SQL Server 中创建数据源是 MySQL 数据表的视图
背景:Windows系统已安装了mysql5.7和sqlServer数据库,现在需要在sqlServer创建视图或者查询来自mysql的数据,视图的数据来源mysql数据库。下面进行实现在sqlserver实现获取mysql数据表数据构建视图。 1、打开 ODBC 数据源管理器,…...

现代Web开发:Next.js 深度解析与最佳实践
💓 博客主页:瑕疵的CSDN主页 📝 Gitee主页:瑕疵的gitee主页 ⏩ 文章专栏:《热点资讯》 现代Web开发:Next.js 深度解析与最佳实践 现代Web开发:Next.js 深度解析与最佳实践 现代Web开发…...

LeetCode题练习与总结:赎金信--383
一、题目描述 给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。 如果可以,返回 true ;否则返回 false 。 magazine 中的每个字符只能在 ransomNote 中使用一次。 示例 1࿱…...

eval: jdk1.8.0_431/jre/bin/java: Permission denied
当您在启动Tomcat或其他Java应用时遇到“Permission denied”错误,这通常表示当前用户没有执行指定Java可执行文件的权限。以下是解决这个问题的几种方法: 方法一:检查文件权限 查看文件权限: 使用ls -l命令查看Java可执行文件的…...

.Net IOC理解及代码实现
IOC理解 IoC(Inversion of Control):即控制反转,这是一种设计思想,指将对象的控制权交给IOC容器,由容器来实现对象的创建、管理,程序员只需要从容器获取想要的对象就可以了。DI(Dependency Injection),即依…...

履带机器人(一、STM32控制部分--标准库)
一、履带机器人整体逻辑框架 通过在PC端搭建上位机,使得在PC端可以给STM32发送控制指令并且接受STM32的状态信息。 通过RS485通信,使得STM32可以和电机进行通信,STM32发送启动、停止、转速、方向等指令,并接受电机返回的状态信息。 二、STM32逻辑框架 整体逻辑: 1、先…...

地理空间-Java实现航迹稀释
Java实现航迹点稀释算法(Douglas - Peucker算法)的示例代码,该算法可在保证航迹整体形状变化不大的情况下减少航迹点数量: import java.util.ArrayList; import java.util.List; class Point { double x; double y; public Point…...

qt QHttpMultiPart详解
1. 概述 QHttpMultiPart是Qt框架中用于处理HTTP多部分请求的类。它类似于RFC 2046中描述的MIME multipart消息,允许在单个HTTP请求中包含多个数据部分,如文件、文本等。这种多部分请求在上传文件或发送带有附件的邮件等场景中非常有用。QHttpMultiPart类…...

【测试】【Debug】vscode中同一个测试用例出现重复
这种是正常的情况 当下面又出现一个 类似python_test->文件夹名->test_good ->test_pad 同一个测试用例出现两次,名称都相同,显然是重复了。那么如何解决? 这种情况是因为在终端利用“pip install pytest”安装 之后,又…...

Mac上的免费压缩软件-FastZip使用体验实测
FastZip是Mac上的一款免费的压缩软件,分享一下我在日常使用中的体验 压缩格式支持7Z、Zip,解压支持7Z、ZIP、RAR、TAR、GZIP、BZIP2、XZ、LZIP、ACE、ISO、CAB、PAX、JAR、AR、CPIO等所有常见格式的解压 体验使用下来能满足我所有的压缩与解压的需求&a…...

Linux(CentOS)运行 jar 包
1、在本地终端运行,关闭终端,程序就会终止 java -jar tlias-0.0.1-SNAPSHOT.jar 发送请求,成功 关闭终端(程序也会终止) 发送请求,失败 2、在远程终端运行,关闭终端,程序就会终止 …...

基于YOLOv8 Web的安全帽佩戴识别检测系统的研究和设计,数据集+训练结果+Web源码
摘要 在工地,制造工厂,发电厂等地方,施工人佩戴安全帽能有效降低事故发生概率,在工业制造、发电等领域需要进行施工人员安全帽监测。目前大多数的 YOLO 模型还拘泥于公司、企业开发生产的具体产品中,大多数无编程基础…...

LabVIEW VISA通信常见问题
在工业自动化和测试测量等应用中,使用LabVIEW的VISA函数与设备进行通信时,若发送指令后未能接收数据,以下因素可能是原因: 设备未响应或响应延迟应用示例:例如,在控制测量仪器(如电压表…...

Node.js Stream(流)以及模块系统使用介绍 (基础介绍 五)
Stream(流) Stream 是 Node.js 中非常重要的一个模块,应用广泛。 Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出…...

嵌入式linux中设备树控制硬件的方法
大家好,今天主要给大家分享一下,如何使用linux系统下的设备树进行硬件控制方法。 第一:linux系统中设备树驱动LED原理 在linux系统中可以使用设备树向Linux内核传递相关的寄存器地址,linux驱动中使用OF函数从设备树中获取所需的属性值,然后使用获取到的属性值来初始化相关…...

定时器入门:Air780E定时器基础与进阶
今天我们学习的是Air780E定时器基础与进阶,让大家更深入的了解定时器。 一、定时器(timer)的概述 在Air780E模组搭载的LuatOS系统中,定时器(timer)是一项基础且关键的服务。它允许开发者在特定的时间点或周期性地执行代码段&…...

Java LeetCode练习
3216. 交换后字典序最小的字符串 package JavaExercise;public class Exercise {public static void main(String[] args) {String s "45320";Solution solution new Solution();System.out.println(solution.getSmallestString(s));} }class Solution {public St…...

go 集成go-redis 缓存操作
一、什么是Go Redis 这是一个流行的Go语言Redis客户端库,它提供了细化的API,对每个Redis命令的功能进行了封装,使得用户只需记住命令,具体的用法可以直接查看接口的声明,使用成本较低。go-redis对数据类型按照Redis底…...

python数据结构基础(3)
书接上文.要创建一个单链表类,首先是初始化方法: class singlelink:def __init__(self):self.head Noneself.tail Noneself.length0return 判断链表是否为空: def isempty(self):return self.length 0 向链表尾部添加节点: def add_node(self,item):if not isinstance(…...

java-智能识别车牌号_基于spring ai和开源国产大模型_qwen vl
用大模型做车牌号识别,最简单高效 在Java场景中,java识别车牌号的需求非常普遍。过去,我们主要依赖OCR等传统方法来实现java识别车牌号,但这些方法的效果往往不稳定。随着技术的发展,现在有了更先进的解决方案——大模…...