基于tcp实现自定义应用层协议

认识协议 

协议(Protocol) 是一种通信规则或标准,用于定义通信双方或多方之间如何交互和传输数据。在计算机网络和通信系统中,协议规定了通信实体之间信息交换的格式、顺序、定时以及有关同步等事宜的约定。简易来说协议就是通信双方所约定好的结构化字段。

 ​​​​​

 序列化与反序列化的认识

但在网络传输中由于不同主机和不同系统对于数据存储的大小端差异,所以在传输结构化字段的时候,并不能保证每个结构化的成员数据都能够准确的对应上。

所以一般会采用将结构化的字段内容进行序列化成一个字符串,然后再通过网络发送出去,接收端再将数据反序列化接收,也就是将各个序列化的结构字段数据提取出来。

自定义协议实现网络版本计算器 

在自定义协议时必须让客户端与服务器都能够看到同一份协议字段,这样才能在接收数据的时候按照规定的格式进行准确无误的接收。

对于序列化的字段格式采用空格作为分隔符。而反序列化的时候就可以通过空格分隔符将数据提取出来。

自定义协议(序列化与反序列化和报头)

对于我们实现的协议不仅仅有序列化结构体字段,其实还有包头的封装,因为tcp是有连接的,数据是流式传输的,所以传输过程并不是一发一收的形式,所以报头数据就可以分离每一次的收发,因为报头中有一个字段是存放序列化字段的长度。

#pragma once
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
using namespace std;// 模拟定协议const string sepa = " ";
const string line_break = "\n";// 添加报头数据和解报头
void Encode(string &mes)
{int len = mes.size();string ret = to_string(len) + line_break + mes + line_break;mes = ret;
}bool Decode(string &package, string &ret)
{//"len\n123 wuwu\n"// 先判断收到的数据是否完整int len = package.size();int pos = package.find(line_break); // 指向第一个换行符if (pos == string::npos)return false;int pos1 = package.find(line_break, pos + line_break.size()); // 指向第二个换行符if (pos1 == string::npos)return false;// 解包后的数据ret = package.substr(pos + line_break.size(), pos1 - pos - 1);// 去掉被读走的数据package = package.substr(pos1 + line_break.size());return true;
}class Request
{friend class Cal;public:Request() {}Request(int x, int y, char op): _x(x), _y(y), _oper(op){}void info(){cout << _x << ' ' << _oper << ' ' << _y << " = ?" << endl;}void Serialize(string &out) // 序列化{out = to_string(_x) + sepa + _oper + sepa + to_string(_y);}//"x + y"void Deserialize(const string s) // 反序列化{int begin = 0;int end = s.find(sepa, begin);_x = stoi(s.substr(begin, end - begin));begin = end + sepa.size(); // 加的1其实就是' '的长度end = s.find(sepa, begin);_oper = s.substr(begin, end - begin)[0];begin = end + sepa.size();_y = stoi(s.substr(begin));}private:int _x;int _y;char _oper;
};class Response
{
public:Response(){}Response(int re, string ret_info): _result(re), _ret_info(ret_info){}void Info(){cout << "result = " << _result << " (" << _ret_info << ')' << endl;}void Serialize(string &out) // 序列化{out = to_string(_result) + sepa + _ret_info;}//"_result _ret_info"void Deserialize(const string s) // 反序列化{int begin = 0;int end = s.find(sepa, begin);_result = stoi(s.substr(begin, end - begin));begin = end + sepa.size(); // 加的1其实就是分隔符的长度_ret_info = s.substr(begin);}private:int _result;      // 保存结果string _ret_info; // 结果信息
};

封装套接字

封装套接字就是实现代码分离,使得可读性更高,还有就是省的以后再写。

#pragma once
#include <iostream>
#include <cstdint>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <thread>
#include <functional>
#include <memory>
using namespace std;#define default_backlog 5// 设计模式:模版方法类
class my_socket // 抽象类
{
public:virtual void Creat_socket() = 0; // 纯虚函数,必须重写virtual void Bind(int port) = 0;virtual void Listen(int backlog) = 0;virtual my_socket *Accept(string &ip, uint16_t &port) = 0;virtual void Connect(string ip, uint16_t port) = 0;virtual int Get_sockfd() = 0;virtual void Close() = 0;virtual void Recv(string &ret, int len) = 0;public:void tcpserver_socket(uint16_t port, int backlog = default_backlog){Creat_socket();Bind(port);Listen(backlog);// 因为服务会返回的执行accept获取连接,所以选择分离}void tcpclient_socket(string ip, uint16_t port){Creat_socket();Connect(ip, port);}
};class tcp_socket : public my_socket // 继承并重写虚函数
{
public:tcp_socket(){}tcp_socket(int sockfd): _sockfd(sockfd){}virtual void Creat_socket(){_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){cerr << "创建套接字失败" << endl;exit(-1);}}virtual void Bind(int port){struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;int n = bind(_sockfd, (sockaddr *)&local, sizeof(local));if (n < 0){cerr << "绑定套接字失败" << endl;exit(-1);}}virtual void Listen(int backlog){int n = listen(_sockfd, backlog);if (n == -1){cerr << "监听套接字失败" << endl;exit(-1);}}virtual my_socket *Accept(string &ip, uint16_t &port){while (1){struct sockaddr_in client;socklen_t len = sizeof(client);int newsockfd = accept(_sockfd, (sockaddr *)&client, &len); // 监听套接字不关闭,可以用来接收多个客户端的连接if (newsockfd < 0){cerr << "获取连接失败" << endl;}port = ntohs(client.sin_port);char buffer[64];inet_ntop(AF_INET, &client.sin_addr, buffer, sizeof(buffer)); // 1.网络转本机 2.4字节ip转字符串ipip = buffer;if (newsockfd < 0){cerr << "接收套接字失败" << endl;}elsecout << "接收套接字成功" << endl;return new tcp_socket(newsockfd);}}virtual void Connect(string ip, uint16_t port){struct sockaddr_in server;server.sin_family = AF_INET;   // socket inet(ip) 协议家族,绑定网络通信的信息server.sin_port = htons(port); // 将主机端口号转成网络// server.sin_addr.s_addr = inet_addr(ip.c_str()); // 转成网络序列的四字节ipinet_pton(AF_INET, ip.c_str(), &server.sin_addr); // 转成网络序列的四字节ipint n = connect(_sockfd, (sockaddr *)&server, sizeof(server)); // 自动bindif (n != 0){cerr << "连接失败" << endl;exit(-1);}elsecout << "连接成功" << endl;}virtual int Get_sockfd(){return _sockfd;}virtual void Close(){if (_sockfd > 0)close(_sockfd);}virtual void Recv(string &ret, int len){char stream_buffer[len];int n = recv(_sockfd, stream_buffer, len - 1, 0);if (n > 0){stream_buffer[n] = 0;ret += stream_buffer; // ret在读取之前可能还有内容残留}else{exit(0);}}private:int _sockfd;
};

计算器代码

计算器类实现的功能就是服务于服务端的,将客户端发送的请求进行计算,并且同时将计算出的结果与返回信息都存到协议中的response类中,所以服务端就可以直接进行序列化,从而将数据发送给客户端。

#pragma once
#include "protocol.h"class Cal
{
public:Cal(Request req): _x(req._x), _y(req._y), _oper(req._oper){}Response cal(){switch (_oper){case '+':_result = _x + _y;break;case '-':_result = _x - _y;break;case '*':_result = _x * _y;break;case '/':{if (_y == 0)_retinfo = "除数为0,结果无意义";else_result = _x / _y;}break;case '%':{if (_y == 0)_retinfo = "模0,结果未定义";else_result = _x % _y;}break;default:_retinfo = "结果无效,未录入该运算符";break;}return {_result, _retinfo};}string Answer(){return "result = " + to_string(_result) + " ret_info = " + _retinfo;}private:int _x;int _y;char _oper;int _result;string _retinfo = "结果无误";
};

服务端代码

服务端就是负责接收客户端的信息并进行处理,然后将处理结果发送回去,为了满足多客户端的请求,服务端会采用创建线程的方式来进行与客户端对接,而服务端的主线程就负责实现accept,接受客户端发送的连接请求。

#pragma once
#include "socket.h"
#include "calculate.h"using func_t = function<void(my_socket *)>; // 执行任务的方法class tcp_server
{
public:tcp_server(uint16_t port, func_t f): _port(port), _sv(new tcp_socket()), _func(f){_sv->tcpserver_socket(port);}void thread_run(my_socket *socket) // 线程执行区域{_func(socket);socket->Close(); // 运行完毕就直接关闭accept返回的套接字描述符}void loop(){while (1){string client_ip;uint16_t client_port;my_socket *socket = _sv->Accept(client_ip, client_port); // 接收套接字cout << "获取新连接,ip= " << client_ip << " port= " << client_port << endl;// sleep(3);// _sv->Close();//监听套接字就是用来接收多个客户端的连接// 创建线程执行任务thread t(std::bind(&tcp_server::thread_run, this, placeholders::_1), socket);t.detach(); // 线程分离// t.join();}}~tcp_server(){delete _sv;}private:my_socket *_sv;uint16_t _port;func_t _func;
};
#include "tcp_server.h"
#include "protocol.h"void deal(my_socket *socket) // 存的套接字描述符就是accept返回值
{string buffer;while (1){// 1.数据读取socket->Recv(buffer, 100); // 将每一次序列化的数据都读进buffer里string msg;string total_info;// 2.解包装(将所有独读到的数据都解包,最后完成后一起再发送出去)while (Decode(buffer, msg)) // 此时buffer会存在残留数据{// 3.反序列化buffer,Request rq;rq.Deserialize(msg);// 4.数据读取完毕可以进行处理Cal c(rq);Response rsp = c.cal(); // 计算结果存到rsp里// 5.将处理结果返回给客户端(需要进行序列化和加包)string s;rsp.Serialize(s);Encode(s);total_info += s;}send(socket->Get_sockfd(), total_info.c_str(), total_info.size(), 0); // 任务发送给服务器}
}
int main(int argc, char *argv[])
{if (argc != 2){cout << "格式错误\n正确格式:" << argv[0] << " port"<< endl;}uint16_t port = atoi(argv[1]);// tcp_server tsv(port);unique_ptr<tcp_server> tsv(new tcp_server(port, deal));tsv->loop(); // accept客户端套接字
}

客户端代码

客户端也同样采用创建线程的方式来进行发送数据与接收数据,这其中一个线程专门发送数据,一个线程专门接收数据,这其中的好处就是不会受到干扰,如果都通过一个线程来完成的话就会导致数据必须是一发一收的方式,并不满足数据流式传输。

#include "socket.h"
#include "protocol.h"string ops = "+-*/%&|^";void thread_run(my_socket *clt)
{while (1){// 1.读取服务端处理后的信息string buffer;string msg;clt->Recv(buffer, 100); // 将每一次序列化的数据都读进buffer里// 2.解包装Decode(buffer, msg);// 3.反序列化msg,Response rsp;rsp.Deserialize(msg);rsp.Info();sleep(3);}
}
int main(int argc, char *argv[])
{srand((unsigned int)time(nullptr));if (argc != 3){cout << "格式错误\n正确格式:" << argv[0] << " ip"<< " port" << endl;}string ip = argv[1];uint16_t port = atoi(argv[2]);my_socket *clt = new tcp_socket();clt->tcpclient_socket(ip, port); // 连接服务端套接字//创建线程专门负责接收信息thread reciver(thread_run,clt);reciver.detach();while (1){int x = rand() % 100;int y = rand() % 100;char oper = ops[rand() % ops.size()];Request rq(x, y, oper);rq.info(); // 向客户端打印任务// 1.进行序列化并打包 发送数据string s;rq.Serialize(s);Encode(s);send(clt->Get_sockfd(), s.c_str(), s.size(), 0); // 任务发送给服务器sleep(1);}delete clt;
}

认识JSON

JSON是一种成熟的序列化反序列化方案。需要使用的话要安装JSON库

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
using namespace std;
// using namespace Json;int main()
{// Json::Value 万能类型Json::Value root;root["a"] = 10;root["b"] = 20;root["哈哈哈"] = "嘎嘎嘎";Json::Value tmp;tmp["who"] = "cr";tmp["age"] = 20;root["id"] = tmp;// Json::FastWriter writer;//行式风格序列化Json::StyledWriter writer;     // 样式风格序列化string s = writer.write(root); // 将root结构化字段进行序列化操作cout << s << endl;cout << "-------------------------------------" << endl;// 反序列化Json::Value rets;Json::Reader reader;bool ret = reader.parse(s, root); // 调用反序列化方法,将序列化的数据s反序列到root里if (ret)                          // 解析root{int a = root["a"].asInt();int b = root["b"].asInt();string st = root["哈哈哈"].asString();tmp = root["id"];cout << a << ' ' << b << ' ' << st << ' ' << tmp << endl;}
}

 ​

目录

认识协议 

 序列化与反序列化的认识

自定义协议实现网络版本计算器 

自定义协议(序列化与反序列化和报头)

封装套接字

计算器代码

服务端代码

客户端代码

认识JSON


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

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

相关文章

Linux shell编程学习笔记50:who命令

0 前言 2024年的网络安全检查又开始了&#xff0c;对于使用基于Linux的国产电脑&#xff0c;我们可以编写一个脚本来收集系统的有关信息。比如&#xff0c;我们可以使用who命令来收集当前已登陆系统的用户信息&#xff0c;当前运行级别等信息。 1. who命令 的功能、格式和选项…

初级爬虫的总结一

初级爬虫的总结一之百度网页爬虫 一、寻找正确的sugrec二、url拼接出问题&#xff0c;解决办法 我遇到的问题&#xff1a; 1、没有找对网页sugrec&#xff0c;导致connect-type没有找对&#xff0c;以及一些小问题 2、url拼接时候出现乱码 一、寻找正确的sugrec 1、打开百度网…

【讲解下Web前端三大主流的框架】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

node.js学习P3-P10

P3 npm package.json&#xff08;package解读npm工具换镜像源&#xff09; 一个package.json文件可以的作用 作为一个描述文件&#xff0c;描述了你的项目依赖哪些包 &#xff0c;用来干什么的允许我们使用“语义版本规则”&#xff0c;指明你项目依赖的版本让你的构建更好的…

Web安全:SQL注入之时间盲注原理+步骤+实战操作

「作者简介」&#xff1a;2022年北京冬奥会网络安全中国代表队&#xff0c;CSDN Top100&#xff0c;就职奇安信多年&#xff0c;以实战工作为基础对安全知识体系进行总结与归纳&#xff0c;著作适用于快速入门的 《网络安全自学教程》&#xff0c;内容涵盖系统安全、信息收集等…

系统安全扫描扫出了:可能存在 CSRF 攻击怎么办

公司的H5在软件安全测试中被检查出可能存在 CSRF 攻击&#xff0c;网上找了一堆解决方法&#xff0c;最后用这种方式解决了。 1、问题描述 CSRF 是 Cross Site Request Forgery的缩写(也缩写为也就是在用户会话下对某个 CGI 做一些 GET/POST 的事&#xff0c;RIVTSTCNNARGO一这…

esp8266的rtos和nonos区别

https://bbs.espressif.com/viewtopic.php?t75242#p100294 https://blog.csdn.net/ydogg/article/details/72598752

存储方式 - 前端学习

1. cookie是什么&#xff1f;你了解cookie吗&#xff1f; 在计算机领域中&#xff0c;特指一种由服务器发送到用户浏览器并保存在用户计算机上的小型文本文件。这个文件可以被服务器用来识别用户身份、跟踪用户活动、保存用户设置等。它通常由名称、值、域名、路径、过期时间等…

【pm2 - sdk 集成到程序中,典型用法】

pm2作为一款进程管理神器&#xff0c;除了命令行的启动方式外&#xff0c;其还对应有sdk&#xff0c;集成到程序中&#xff0c;我们可以连接到已有或创建pm2的守护进程&#xff0c;与其进行交互&#xff0c;动态&#xff0c;编程式地控制程序的启停等。以下为示例&#xff1a; …

酷开科技大屏营销,多元需求唤醒“客厅经济”

随着科技的发展和消费者习惯的变化&#xff0c;OTT大屏营销正逐渐成为客厅经济的新风向。OTT不仅改变了人们获取信息和娱乐的方式&#xff0c;也为品牌营销提供了新的机遇和挑战&#xff0c;OTT大屏营销已经成为客厅经济的重要组成部分。酷开科技通过其自主研发的智能电视操作系…

PHP框架 Laravel

现在因为公司需求&#xff0c;需要新开一个Laravel框架的项目&#xff0c;毫无疑问&#xff0c;我又被借调过去了&#xff0c;最近老是被借调&#xff0c;有点阴郁&#xff0c;不过反观来看&#xff0c;这也是好事&#xff0c;又可以复习和巩固一下自己的知识点&#xff0c;接下…

数组基础-笔记

数组是非常基础的数据结构&#xff0c;实现运用和理解是两回事 数组是存放在连续内存空间上的相同类型的数据的集合 可以方便的通过下表索引的方式获取到下标下对应的数据。 举一个字符数组的例子&#xff1a; 注意两点&#xff1a; 数组下标从0开始 数组内存空间的地址是连…

yarn dev启动项目时遇到的问题

用yarn dev启动项目的时候&#xff0c;遇到了如下问题&#xff1a; 这个时候&#xff0c;我们可以这样解决&#xff1a;用nvm list 看下已安装的node版本&#xff0c;用nvm use切换一下node版本&#xff0c;当然前提是你已经安装了nvm。

C++: 二叉搜索树及实现

目录 一、二叉搜索树的概念 二、二叉搜索树的操作 2.1插入 2.2删除 1.有左子树&#xff0c;无右子树 2.有右子树&#xff0c;无左子树 3.有左子树和右子树 三、二叉搜索树的实现 要点 前言&#xff1a;为了学习map和set&#xff0c;需要先学二叉搜索树作为铺垫。 一、…

[论文笔记]Chain-of-Thought Prompting Elicits Reasoning in Large Language Models

引言 今天带来思维链论文 Chain-of-Thought Prompting Elicits Reasoning in Large Language Models的笔记。 作者探索了如何通过生成一系列中间推理步骤的思维链&#xff0c;显著提升大型语言模型在进行复杂推理时的能力。 1 总体介绍 语言模型的规模扩大已被证明能够带来…

[数据集][目标检测]伤口检测数据集VOC+YOLO格式2760张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2760 标注数量(xml文件个数)&#xff1a;2760 标注数量(txt文件个数)&#xff1a;2760 标注…

课时138:变量进阶_变量实践_综合案例

2.1.3 综合案例 学习目标 这一节&#xff0c;我们从 免密认证、脚本实践、小结 三个方面来学习 免密认证 案例需求 A 以主机免密码认证 连接到 远程主机B我们要做主机间免密码认证需要做三个动作1、本机生成密钥对2、对端机器使用公钥文件认证3、验证手工演示 本地主机生成…

调整GIF图大小的方法是什么?分享4个

调整GIF图大小的方法是什么&#xff1f;在数字化时代&#xff0c;GIF以其独特的动图魅力&#xff0c;成为了网络交流中不可或缺的一部分。无论是社交媒体、博客文章还是工作汇报&#xff0c;一个恰到好处的GIF图往往能有效吸引观众的注意&#xff0c;传递信息&#xff0c;但过大…

YOLOv8+PyQt5面部表情检测系统完整资源集合(yolov8模型,从图像、视频和摄像头三种路径识别检测,包含登陆页面、注册页面和检测页面)

1.资源包含可视化的面部表情检测系统&#xff0c;基于最新的YOLOv8训练的面部表情检测模型&#xff0c;和基于PyQt5制作的可视化面部表情检测系统&#xff0c;包含登陆页面、注册页面和检测页面&#xff0c;该系统可自动检测和识别图片或视频当中出现的八类面部表情&#xff1a…

3D开发工具HOOPS在BIM系统中的应用

建筑信息模型是一种革命性的建筑设计、施工和管理方法。它通过创建和利用数字信息来优化建筑项目的设计、施工和运营过程。在这个过程中&#xff0c;3D开发工具HOOPS扮演着至关重要的角色&#xff0c;为BIM系统提供了强大的技术支持和丰富的功能。HOOPS中文网http://techsoft3d…