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

深入了解linux网络—— 自定义协议(下)

前言

在上篇文章中,了解了TCP面向字节流和全双工;对socket进行了封装,简单实现了TcpServer;以及协议ProtocolRequestResponce

现在来基于这些内容,继续实现一个网络版计算器。

服务端

1. 序列化和反序列化

有了结构化数据,这里就需要序列化,将结构化数据转化为字符数据;也需要反序列化,将字符串数据转换为结构化数据。

这里我们可以自己实现序列化和反序列化操作,但是没必要;

这里就可以使用jsoncpp库来完成序列化和反序列化操作。

序列化:

对于序列化这里就直接使用StreamWriterBuilder,具体使用文章结尾详细介绍。

class Request
{
public:std::string Sequence(){Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::StreamWriterBuilder swb;std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());std::stringstream ss;sw->write(root, &ss);return ss.str();}
private:int _x;int _y;char _oper;
};

反序列化

有了序列化,可以有结构化数据转成字符串数据;当然有要有反序列化,将字符串数据转成结构化数据。

class Request
{
public:void Rsequence(std::string &massage){Json::Value root;Json::Reader reader;reader.parse(massage, root);_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();}
private:int _x;int _y;char _oper;
};

Request实现了序列化和反序列化,Responce也要实现序列化和反序列化方法。

class Responce
{
public:std::string Sequence(){Json::Value root;root["result"] = _result;root["code"] = _code;Json::StreamWriterBuilder swb;std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());std::stringstream ss;sw->write(root, &ss);return ss.str();}void Rsequence(std::string &massage){Json::Value root;Json::Reader reader;reader.parse(massage, root);_result = root["result"].asInt();_code = root["code"].asInt();}
private:int _result; // 结果int _code;   // 标识计算是否出错
};

2. 服务

还记得,上篇博客中实现的TcpServer,其中孙子进程要执行服务,这个服务是通过回调函数调用的。

而要执行什么服务呢?(简单来说,服务就是读取数据、处理数据和发送数据)

而如何读取、发送数据(序列化、反序列化以及保证读取到报文的完整性),也只有协议清楚。

所以,在协议字段protocol中就要实现一个方法GetRequest进行服务。

class protocol
{public:void GetRequest(std::shared_ptr<Socket> fd, InetAddr& peer){}
};

在该方法中,要实现服务,具体流程:

  • 读取数据,保证读取到报文的完整性(去协议报头
  • 反序列化
  • 处理业务
  • 结果序列化
  • 加协议报头
  • 返回结果(将结果发送回去)

这里,要实现服务,其中肯定就会包含获取请求,处理请求,发送回复等操作;

所以在Socket中就要实现RecvSend方法用来获取请求和发送回复。

//Socket
class Socket
{
public:virtual int Recv(std::string &massage) = 0;virtual int Send(const std::string &massage) = 0;
};
//TcpSocket
class TcpSocket : public Socket
{
public:int Recv(std::string &massage) override{char buff[1024];int n = recv(_sockfd, buff, sizeof(buff), 0);if (n > 0){buff[n] = '\0';massage += buff;}return n;}int Send(const std::string &massage) override{int n = send(_sockfd, massage.c_str(), massage.size(), 0);return n;}
private:int _sockfd;
};

这里在Recv获取请求时,通过参数massage将读取到的信息传给上层。

而使用+=操作,因为recv可能读取到的报文不完整,将多次读取的报文拼接,再提取完整报文处理。

协议报头

协议要保证读取到报文的完整性,就要存在协议报头(特殊标识);

这里就简单以有效载荷长度 + \r\n作为起始标识、\r\n作为结束标识。

假设序列化后的字符串是:"{\"x\":\"10\",\"y\":20,\"oper\":\"+\"}"

添加完协议报头就是:"28\r\n{\"x\":\"10\",\"y\":20,\"oper\":\"+\"}\r\n"

所以,在protocol类中,就要存在去协议报头Decode和添加协议报头Encode的方法。

1. 添加协议报头

给一个字符串数据添加协议报头还是非常容易的,做字符串拼接就OK了。

    std::string Encode(const std::string &str){int str_len = str.size();return std::to_string(str_len) + sep + str + sep;}

2. 删除协议报头

要删除协议报头,要注意:传递进来的可能并不是一个完整的报文

这里就要进行判断,具体操作

  • 判断传递进来的字符串str中,是否存在sep特殊标识。(如果不存在,当前一定不存在完整的报文)
  • 如果存在特殊标识,该特殊字符前面就是有效字符串的长度,只需要字符串的长度和一个完整报文长度作比较,就可以知道是否存在完整报文了。
  • 如果存在完整报文,就通过参数提取出来,并在字符串str中删除该报文。
    bool Decode(std::string &massage, std::string *ComPacket){auto pos = massage.find(sep);if (pos == std::string::npos)return false;std::string head = massage.substr(0, pos);int len = std::stoi(head.c_str());        // 有效载荷长度int sep_len = sep.size();                 // 特殊标识长度int packet_len = pos + len + sep_len * 2; // 完整报文长度if (massage.size() < packet_len)return false;*ComPacket = massage.substr(pos + packet_len, len); // 提取完整报文massage.erase(0, packet_len);                       // 在信息中删除该完整报文}

获取请求

有了序列化和反序列化、Socket套接字读取、添加协议报头和删除协议报头(保证读取到报文的完整性

现在就要实现GetResquest获取请求:

  • 读取数据,保证读取到报文的完整性去协议报头

    调用Socket读取信息,然后调用Decode去除完整报头,保证读取到报文的完整性。

  • 反序列化

    读到完整保证之后,就要对报文进行反序列化成结构化数据Request

  • 处理业务

    处理业务,这里就交由上层提供,参数是Request、返回值是Responce类型。

  • 结果序列化

    提供回调函数,获取结构化的结果Responce,这里就要对结构化数据进行序列化。

  • 加协议报头

    对于序列化的数据,在发送之前要加协议报头,加完协议报头才能发送。

  • 返回结果(将结果发送回去)

using task_t = std::function<Responce(Request &req)>;
class protocol
{
public:protocol(task_t task) : _task(task) {}void GetRequest(std::shared_ptr<Socket> fd, InetAddr &peer){std::string buff; // 读取缓冲区while (true){std::string packet; // 完整报文int n = fd->Recv(buff);if (n > 0){// 1. 判断报文的完整性while (Decode(buff, &packet)){// 存在完整报文Request req;// 2. 反序列化req.Rsequence(packet);// 3. 处理业务// ... 由上层提供Responce res = _task(req);// 4. 将结果序列化std::string json_res = res.Sequence();// 5. 加协议报头std::string send_res = Encode(json_res);// 6. 将信息发送回去fd->Send(send_res);}}else if (n == 0){LOG(Level::INFO) << "opponent exit";exit(0);}else{LOG(Level::FATAL) << "recv error";exit(1);}// 不存在完整报文就继续读取}}
private:task_t _task;
};

3. NetCal

上述代码已经实现了服务端GetResquest获取请求方法,其中对于数据的处理业务交由上层处理,而这里就要实现一个NetCal类,其中实现业务处理方法;

而处理业务也非常简单,这里就实现简单的计算器,进行简单的计算即可。

这里处理业务要获取Request中的_x_y_oper成员,在计算完成后要创建Responce对象,对其成员变量进行赋值。

这里在ResquestResponce就要实现对应的SetGet方法

class Request
{
public:Request() {}Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper) {}int GetX() { return _x; }int GetY() { return _y; }char GetOper() { return _oper; }
private:int _x;int _y;char _oper;
};class Responce
{
public:Responce() {}Responce(int result, int code) : _result(result), _code(code) {}void SetResult(int result) { _result = result; }void SetCode(int code) { _code = code; }
private:int _result; // 结果int _code;   // 标识计算是否出错
};

而对于NetCal的处理业务,只需进行简单的计算即可。

结果错误:在计算过程中可能出现/0%0等错误。

这里_code0表示结果可信、1表示/0错误、2表示%0错误、3表示未知错误。

class NetCal
{
public:Responce Handler(Request &res){Responce ret_res;int x = res.GetX();int y = res.GetY();char oper = res.GetOper();switch (oper){case '+':ret_res.SetResult(x + y);ret_res.SetCode(0);break;case '-':ret_res.SetResult(x - y);ret_res.SetCode(0);break;case '*':ret_res.SetResult(x * y);ret_res.SetCode(0);break;case '/':if (y == 0){ret_res.SetCode(1);break;}ret_res.SetResult(x / y);ret_res.SetCode(0);break;case '%':if (y == 0){ret_res.SetCode(2);break;}ret_res.SetResult(x % y);ret_res.SetCode(0);break;default:ret_res.SetCode(3);break;}}
};

4. 上层调用

有了上述代码,在上层就可以调用起来了。

tcpserver.cc

  • 首先创建NetCal对象
  • 然后创建Protocol对象,处理业务的方法在NetCal中。
  • 创建TcpServer对象,获取连接成功后要执行的服务方法在Protocol中。

所以,就可以通过上层传递对应的方法,来实现不同模块()之间的调用。

#include "netcal.hpp"
#include "protocol.hpp"
#include "tcpserver.hpp"int main(int agrc, char *argv[])
{if (agrc != 2){std::cout << "err usage : " << argv[0] << " port" << std::endl;exit(1);}int port = std::stoi(argv[1]);NetCal cal;std::unique_ptr<Protocol> pro = std::make_unique<Protocol>([&cal](Request &res) -> Responce{return cal.Handler(res); });std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, [&pro](std::shared_ptr<Socket> fd, InetAddr &client){ pro->GetRequest(fd, client);});tsvr->Start();return 0;
}

客户端

上述实现了服务端的相关代码,现在来完善客户端代码:

1. 创建套接字

首先客户端要创建客户端通信使用的TcpSocket,在Socket中就要实现一个CreateTcpClientSocket方法,来创建一个客户端套接字。

class Socket
{void CreateTcpClientSocket(){SocketOrDie();}
};

2. 发送连接

对于客户端,创建完套接字之后,就可以发送连接请求,连接成功时自动绑定端口号。

//Socket
class Socket
{
public:virtual void ConnectOrDie(InetAddr &addr) = 0;void CreateTcpClientSocket(){SocketOrDie();}
};
class TcpSocket : public Socket
{
public:void ConnectOrDie(InetAddr &addr) override{int n = connect(_sockfd, addr.GetInetAddr(), addr.GetLen());if (n < 0){LOG(Level::FATAL) << "connect error";exit(SOCKET_ERR);}LOG(Level::DEBUG) << "connect success, sockfd : " << _sockfd;}
private:int _sockfd;
};
//tcpclient
int main(int agrc, char *argv[])
{if (agrc != 3){std::cout << "err usage : " << argv[0] << " server_ip server_port" << std::endl;exit(1);}std::string ip = argv[1];int port = std::stoi(argv[2]);InetAddr server(ip, port);// 创建套接字std::unique_ptr<Socket> sock = std::make_unique<TcpSocket>();sock->CreateTcpClientSocket();// 发送连接请求sock->ConnectOrDie(server);return 0;
}

3. 发送请求

做完了上述创建套接字、发送连接请求操作;接下来就可以获取用户输入,构建结构化数据、序列化、加协议报头,发送请求等等操作了。

获取用户输入:

这里就实现的简单一些,直接从键盘中获取xyoper

构建结构化数据:

获取完xyoper,直接构建Request结构化数据即可。

序列化:

对于构建完的结构化数据,还需要对其进行序列化操作。

添加协议报头:

序列化数据还不能直接发送,还需要添加协议报头(遵守协议)。

发送数据:

添加完协议报头,就可以直接发送该消息了;这里调用TcpSocketSend方法即可。

这里就直接在协议Protocol中实现一个BuildComData,由xyoper构造出可以直接发送的消息。(结构化、序列化、添加协议报头)

//Protocol
class Protocol
{
public:std::string BuildComData(int x,int y, char oper){Request res(x,y,oper);std::string json_res = res.Sequence();return Encode(json_res);}
private:task_t _task;
};
//TcpClient
int main(int agrc, char *argv[])
{if (agrc != 3){std::cout << "err usage : " << argv[0] << " server_ip server_port" << std::endl;exit(1);}std::string ip = argv[1];int port = std::stoi(argv[2]);InetAddr server(ip, port);// 创建套接字std::shared_ptr<Socket> sock = std::make_shared<TcpSocket>();sock->CreateTcpClientSocket();// 发送连接请求sock->ConnectOrDie(server);// 创建协议对象std::unique_ptr<Protocol> pro = std::make_unique<Protocol>();while (true){// 1. 获取数据int x, y;char oper;std::cout << "Please enter x :";std::cin >> x;std::cout << "Please enter y :";std::cin >> y;std::cout << "Please enter oper :";std::cin >> oper;// 2. 构建请求 -> 结构化数据 -> 序列化数据 -> 添加协议报头的数据std::string send_massage = pro->BuildComData(x, y, oper);// 3. 发送请求sock->Send(send_massage);}return 0;
}

4. 获取回应

客户端发送完请求之后,就要获取服务端发送来的回应。

这里获取回应,就是读取消息报文;

对于客户端,获取回应:

  • 保证读取到报文的完整性;
  • 将报文去协议报头、反序列化;
  • 获取到结构化数据,这里返回上层,交由上层去处理(也可以通过回调函数,处理该回应信息)。
class Responce
{
public:bool GetResponce(std::shared_ptr<Socket> fd, std::string &buff, Responce *res){// 读取int n = fd->Recv(buff);if (n > 0){std::string packet;if(Decode(buff,&packet)){//反序列化res->Rsequence(packet);return true;}return false;}else if (n == 0){LOG(Level::INFO) << "opponent exit";exit(0);}else{LOG(Level::FATAL) << "recv error";exit(1);}}
}

这里在Protocol中实现GetResponce方法,在TcpClient中就可以直接调用,通过参数获取结构化数据res

获取完之后,这里就简单实现一个方法Show,输出结果。

//Responce
class Responce
{
public:void Show(){std::cout << "result : " << _result << "[ " << _code << " ]" << std::endl;}
private:int _result; // 结果int _code;   // 标识计算是否出错
};
int main(int agrc, char *argv[])
{if (agrc != 3){std::cout << "err usage : " << argv[0] << " server_ip server_port" << std::endl;exit(1);}std::string ip = argv[1];int port = std::stoi(argv[2]);InetAddr server(ip, port);// 创建套接字std::shared_ptr<Socket> sock = std::make_shared<TcpSocket>();sock->CreateTcpClientSocket();// 发送连接请求sock->ConnectOrDie(server);// 创建协议对象std::unique_ptr<Protocol> pro = std::make_unique<Protocol>();std::string buffer; // 接受缓冲区while (true){// 1. 获取数据int x, y;char oper;std::cout << "Please enter x :";std::cin >> x;std::cout << "Please enter y :";std::cin >> y;std::cout << "Please enter oper :";std::cin >> oper;// 2. 构建请求 -> 结构化数据 -> 序列化数据 -> 添加协议报头的数据std::string send_massage = pro->BuildComData(x, y, oper);// 3. 发送请求sock->Send(send_massage);Responce res;if (pro->GetResponce(sock, buffer, &res))res.Show();}return 0;
}

测试

到这里简易版的网络版本计算器就完成了,现在进行简单测试:

1. 建立连接

在这里插入图片描述

2. 发送/获取请求

在这里插入图片描述
本篇文章到这里就结束了,感谢支持
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

http://www.dtcms.com/a/524838.html

相关文章:

  • 金麦建站官网成都视频剪辑培训
  • 【C++闯关笔记】详解多态
  • 数据库技术指南(二):MySQL CURD 与高级查询实战
  • 用mvc做网站报告做做做网站
  • 设置一个自定义名称的密钥,用于 git 仓库上下传使用
  • MAC Flood与ARP Flood攻击区别详解
  • 高兼容与超低延迟:互联网直播点播平台EasyDSS直播服务如何成为直播点播应用的“技术底座”?
  • MongoDB 集群优化实战指南
  • wordpress网站速度检测医院做网站需要多少钱
  • iOS 26 查看电池容量与健康状态 多工具组合的工程实践
  • 机器学习(10)L1 与 L2 正则化详解
  • 保险网站建设平台与别人相比自己网站建设优势
  • vscode中好用的插件
  • PCB过电流能力
  • 【数据库】KingbaseES数据库:首个多院区异构多活容灾架构,浙人医创新开新篇
  • 嵌入式软件算法之PID闭环控制原理
  • 性价比高seo网站优化免费下载模板的网站有哪些
  • 无棣网站制作襄樊网站制作公司
  • AI服务器工作之电源测试
  • 《Muduo网络库:实现Acceptor类》
  • 第十三篇《TCP的可靠性:三次握手与四次挥手全解析》
  • SSE 流式响应实战:如何在 JavaScript 中处理 DeepSeek 流式 API
  • 在线阅读网站开发教程品牌建设促进会是什么工作
  • 一站式服务门户网站充值支付宝收款怎么做
  • 网站建设超速云免费小程序源码php
  • 如何裁剪u-boot,保留其必要功能,使体积尽可能小
  • 借助智能 GitHub Copilot 副驾驶 的 Agent Mode 升级 Java 项目
  • 广州市网站建设 乾图信息科技在哪里建网站
  • Flutter---自定义日期选择对话框
  • 怎么代码放到网站上网站建设需要的公司