基于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.mzph.cn/news/843032.shtml

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

相关文章

【Linux】Linux下centos更换国内yum源

&#x1f331;博客主页&#xff1a;青竹雾色间 &#x1f331;系列专栏&#xff1a;Linux &#x1f618;博客制作不易欢迎各位&#x1f44d;点赞⭐收藏➕关注 目录 1. 备份旧的 YUM 源文件2. 下载国内的 YUM 源文件阿里云&#xff1a;网易&#xff1a; 3. 清理 YUM 缓存4. 更新…

scp问题:Permission denied, please try again.

我把scp归纳三种情况&#xff1a; 源端root——》目标端root 源端root——》目标端mysql&#xff08;任意&#xff09;用户 源端&#xff08;任意用户&#xff09;——》目标端root用户 在scp传输文件的时候需要指导目标端的用户密码&#xff0c;如root用户密码、mysql用户…

选择海外代理IP需要注意什么?

跨境电商近年来的兴起与发展&#xff0c;越来越多的跨境从业者从事该行业&#xff0c;但在从事跨境贸易中则需要海外IP代理来突破地域限制、提升访问速度和稳定性、防止账号关联以及保护隐私和安全。这些功能都有助于跨境电商企业在全球范围内拓展业务&#xff0c;提升竞争力&a…

一文搞定jdk8升级到jdk11

一、背景 为什么要升级JDK11 性能 JDK11的G1的GC性能高很多&#xff0c;对比JDK8无论是性能还是内存占比都有很大的提升&#xff0c;业内各项数据指标也都表明JDK11的G1在应对突发流量的下的效果惊人&#xff1b; 版本兼容 Spring Boot 2.7.x及以后的版本将不再支持Java 8作为…

【IC】partial good

假设单core良率80%&#xff0c;core pass 数量分布呈二项分布。 16个core全pass的概率为&#xff1a; 有n个core pass的概率为&#xff1a; 分布如下&#xff1a; 当np>5且nq>5时&#xff0c;二项分布近似服从正态分布

python fstring教程(f-string教程)(python3.6+格式化字符串方法)

文章目录 Python F-String 教程&#xff1a;深度探究与实用指南引言基础用法什么是F-String?表达式嵌入 格式化选项小数点精度宽度与对齐数字格式化 高级用法复杂表达式调用函数多行F-String嵌套格式化 总结 Python F-String 教程&#xff1a;深度探究与实用指南 引言 在Pyt…

中间件是什么?信创中间件有哪些牌子?哪家好用?

当今社会&#xff0c;中间件的重要性日益凸显&#xff0c;尤其是在信创背景下&#xff0c;选择适合的中间件产品对于推动企业数字化转型和升级具有重要意义。今天我们就来聊聊中间件是什么&#xff1f;信创中间件有哪些牌子&#xff1f;哪家好用&#xff1f;仅供参考哈&#xf…

山东大学软件学院项目实训-创新实训-基于大模型的旅游平台(二十二)- 微服务(2)

目录 4. Ribbon负载均衡 4.1 负载均衡流程 4.2 负载均衡策略 4.3 Ribbon饥饿加载 5. Nacos注册中心 5.1 服务注册到nacos 5.2 nacos服务分级存储模型 5.3 根据权重负载均衡 5.4 环境隔离--namespace 4. Ribbon负载均衡 4.1 负载均衡流程 4.2 负载均衡策略 默认实现是…

cesium绘制区域编辑

npm 安装也是可以的 #默认安装最新的 yarn add cesium#卸载插件 yarn remove cesium#安装指定版本的 yarn add cesium1.96.0#安装指定版本到测试环境 yarn add cesium1.96.0 -D yarn install turf/turf <template><div id"cesiumContainer"></div&…

大学理科生搜题软件?分享四个软件和公众号,来对比看看吧 #笔记#知识分享

在快节奏的大学生活中&#xff0c;合理利用这些日常学习工具&#xff0c;能够让你事半功倍&#xff0c;提高学习效率。 1.福昕翻译 可以一键翻译文档内容&#xff0c;并提供还原排版的译文&#xff0c;对经常看外文文献的朋友来说&#xff0c;绝对是福音 福昕翻译是一流专业…

设计模式基础——设计原则介绍

1.概述 ​ 对于面向对象软件系统的设计而言&#xff0c;如何同时提高一个软件系统的可维护性、可复用性、可拓展性是面向对象设计需要解决的核心问题之一。面向对象设计原则应运而生&#xff0c;这些原则你会在设计模式中找到它们的影子&#xff0c;也是设计模式的基础。往往判…

HTML大雪纷飞

目录 写在前面 HTML简介 完整代码 代码分析 运行结果 系列文章 写在后面 写在前面 小编又又又出现啦&#xff01;这次小编给大家带来大雪纷飞HTML版&#xff0c;不需要任何的环境&#xff0c;只要有一个浏览器&#xff0c;就可以随时随地下一场大雪哦&#xff01; HTM…

Diffusion Model, Stable Diffusion, Stable Diffusion XL 详解

文章目录 Diffusion Model生成模型DDPM概述向前扩散过程前向扩散的逐步过程前向扩散的整体过程 反向去噪过程网络结构训练和推理过程训练过程推理过程优化目标 详细数学推导数学基础向前扩散过程反向去噪过程 Stable Diffusion组成结构运行流程网络结构变分自编码器 (VAE)文本编…

富港银行 邀请码 兑换码 优惠码 分享

首次记得一定要扫码注册&#xff0c;扫码注册开户费50美金&#xff0c;每笔26美金手续费&#xff0c;目前能接收CBI银行资金的有&#xff1a;工行、交通、中行&#xff0c;请知悉 cbi帐户管理费&#xff1a;10美元/月&#xff0c;余额>500美元&#xff0c;1美元/月/&#x…

在vue中实现下载文件功能

实际操作为&#xff0c;在表格中 我们可以获取到文件的id&#xff0c;通过插槽就可以实现 <template #default"scope"><el-button type"text" click"handleDown(scope.row)"><span>下载</span></el-button> </…

数组基础-笔记

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

Python的selenium爬取

1.selenium 1.1.前言 使用python的requests模块还是存在很大的局限性&#xff0c;例如&#xff1a;只发一次请求&#xff1b;针对ajax动态加载的网页则无法获取数据等等问题。特此&#xff0c;本章节将通过selenium模拟浏览器来完成更高级的爬虫抓取任务。 1.2.什么是seleniu…

Redhat7.4部署MySQL-5.7.17搭建双主互为主从

一、准备工作 需要先准备已经搭建好的两台数据库&#xff0c;并且保证服务器之间网络是通的&#xff0c;3306端口可以相互访问。 二、修改两台数据库my.cnf 配置文件&#xff0c;将下列内容添加进去&#xff0c;放在 [mysqld] 下 我们暂定两台服务器为A服务和B服务&#xff…

【音视频基础概念】颜色与图像

文章目录 前言一、三原色不同三原色的概念三原色的作用 二、颜色空间颜色空间是什么颜色空间的作用常见颜色空间示例灰度图像是什么灰度图像的作用灰度图像的技术细节示例 总结 前言 在当今数字媒体时代&#xff0c;音视频技术在我们的日常生活中占据了重要位置。无论是观看电…

线代与图形学的暧昧二三事

A Swift and Brutal Introduction to Linear Algebra 计算机图形学依赖于线性代数、微积分、统计...物理方面涉及到光学&#xff08;波动光学&#xff1a;不再假设光是直线传播&#xff0c;作为一种光波与物体表面材质进行作用接触&#xff0c;如何生成不同的外观&#xff09;…