如何定制自己的应用层协议?|面向字节流|字节流如何解决黏包问题?如何将字节流分成数据报?

前言

那么这里博主先安利一些干货满满的专栏了!

首先是博主的高质量博客的汇总,这个专栏里面的博客,都是博主最最用心写的一部分,干货满满,希望对大家有帮助。

高质量干货博客汇总https://blog.csdn.net/yu_cblog/category_12379430.html?spm=1001.2014.3001.5482

该项目GITHUB地址

网络计算器-序列化和反序列化-协议定制icon-default.png?t=N6B9https://github.com/Yufccode/BitCode/tree/main/Linux/%E4%BB%A3%E7%A0%81/0226%E7%BD%91%E7%BB%9C%E8%AE%A1%E7%AE%97%E5%99%A8-%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E5%8D%8F%E8%AE%AE%E5%AE%9A%E5%88%B6

什么是字节流和数据报

在计算机网络理论中,什么是字节流,什么是数据报,他们和TCP,UDP的关系是什么,字节流和数据报的关系是什么?

字节流是一种连续的、无边界的数据流。它将数据视为一个连续的字节序列,没有明确的分组或边界。在字节流中,数据按照发送的顺序进行传输,接收方按照相同的顺序接收数据。字节流是一种面向连接的传输方式,它提供可靠的、有序的、基于错误检测和纠正的数据传输。TCP(传输控制协议)是一个使用字节流传输的协议。

数据报是一种离散的、有边界的数据传输方式。它将数据划分为固定大小的数据包,每个数据包都有自己的头部信息(通常包含源地址、目标地址、序列号等),并独立发送。每个数据包在网络中独立传输,可能沿不同的路径到达接收方。数据报传输通常是无连接的,不保证可靠性和有序性。UDP(用户数据报协议)是一个使用数据报传输的协议。

关于它们和TCP、UDP的关系:

  • TCP使用字节流传输方式,通过TCP连接来提供可靠的、有序的、面向连接的数据传输。TCP使用序号和确认机制来确保数据的可靠性和有序性。
  • UDP使用数据报传输方式,提供了无连接、不可靠的数据传输。UDP适用于对实时性要求较高,但对数据可靠性和顺序性要求不高的应用场景。

字节流和数据报之间没有直接的关系,它们是不同的传输方式。TCP使用字节流传输方式,而UDP使用数据报传输方式。这两种传输方式适用于不同的网络应用场景,具体选择取决于应用的要求和设计。

粘包问题

粘包问题是在网络通信中常见的一个现象,指的是接收方无法准确地将字节流拆分为原始的数据报,导致数据解析错误。为了解决粘包问题,可以采取以下方法:

  • 使用固定长度分割,每个数据报长度固定,接收方按照固定长度提取数据。
  • 使用特定字符分割,定义一个特殊的字符或字符序列作为数据报之间的分隔符,接收方根据分隔符提取数据报。
  • 使用长度字段分割,数据报头部添加表示长度的字段,接收方读取长度字段并提取相应长度的字节作为数据报。

学习计算机网络应用层协议的原理是为了理解网络通信机制和实现自定义协议。定制协议原理的学习能帮助我们根据特殊字符将字节流分割为数据报,并通过反序列化解析出所需报文。这对于开发网络应用、数据交互和错误处理至关重要。

序列化和反序列化

序列化(Serialization)是将对象的状态转换为字节流的过程,以便将其存储在内存、文件或网络中,或者将其传输到其他远程系统。序列化将对象转换为字节序列的形式,使得可以在不同的平台、系统或编程语言之间进行数据交换和持久化存储。

反序列化(Deserialization)是将字节流转换回对象的状态的过程,即从序列化的字节流中恢复对象的属性和数据结构。反序列化将字节流重新转换为原始对象的形式,以便可以使用和操作这些对象。

序列化和反序列化通常在分布式系统、网络通信和持久化存储等场景中使用。通过序列化,可以将对象转换为字节流,在网络传输中发送或存储到磁盘上。然后,通过反序列化,可以将字节流重新还原为原始对象,以便进行处理、操作或者重新恢复对象的状态。

在使用HTTP协议的时候,使用过浏览器的时候,浏览器的后端,服务器的后端,会帮我们做好这些事情,但是今天,博主要带着大家来定制一个自己的协议,带着大家来理解上述的原理。

本项目:实现一个网络版本计算器利用自己定制的协议

Github地址

网络计算器-序列化和反序列化-协议定制icon-default.png?t=N6B9https://github.com/Yufccode/BitCode/tree/main/Linux/%E4%BB%A3%E7%A0%81/0226%E7%BD%91%E7%BB%9C%E8%AE%A1%E7%AE%97%E5%99%A8-%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E5%8D%8F%E8%AE%AE%E5%AE%9A%E5%88%B6

底层服务器准备

底层使用TCP连接,在博主实现的代码中,博主简单实用了一下多线程,写了一个多线程的版本,这里博主就不展示底层服务器的代码了,小伙伴可以直接看博主github上的那个就行。

制定序列化反序列化规则和报文规则

规定一个运算包括三个字段:

x,y,op 

x代表第一个操作数

y代表第二个操作数

op代表运算符

1+2 即 x=1,y=2,op='+'

规定,客户端输入格式如下所示:

 序列化后为:

Request报文格式

length\r\n__x __op __y\r\n

即第一个字段是报文长度,紧接着两个特殊字符\r\n 然后是x,y和op,中间用空格分开,最后再加上一个\r\n。

这个就是我们的报文! 

步骤如下:

当用户输入到客户端之后,客户端首先会将三个字段用结构体Request存起来,然后调用序列化的接口,把结构体序列化成一个Request字符串,就是上面我们展示的报文的格式。

当服务端收到这个Request报文之后,再调用反序列化的接口得到三个字段,得到一个结Request构体。

然后通过这个Request结构体的内容,计算得到结果后,形成Respone结构体,对Respone进行序列化,发送给客户端,客户端再把Respone反序列化,得到最后的结果。

Respones报文格式

length\r\n__x __op __y\r\n

Protocol.hpp

#ifndef __Yufc_Protocol_For_Cal
#define __Yufc_Protocol_For_Cal#include <iostream>
#include <string>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"/* 协议的本质 --> 约定! */#define RECV_MAX_SIZE 1024/* 请求协议是必须自带序列化功能的 一个结构体是很难发送的 需要序列化成string */
/* 上次0222定制的协议是不完善的!我们需要定制报文*/// 协议
/* length\r\n__x __op __y\r\n*/
/* length这些就叫做协议报头 */namespace yufc_ns_protocol
{
#define MYSELF true
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP)class Request{public:std::string Serialize(){
#if MYSELFstd::string str;str = std::to_string(__x);str += SPACE;str += __op; // BUGstr += SPACE;str += std::to_string(__y);return str;
#else/* 使用别人的序列化方案 */#include <jsoncpp/json/json.h>Json::Value root;root["x"] = __x;root["y"] = __y;root["op"] = __op;Json::FastWriter writer;return writer.write(root);
#endif}bool Deserialize(const std::string &str){
#if MYSELFstd::size_t left = str.find(SPACE);if (left == std::string::npos){return false; // 反序列化失败}std::size_t right = str.rfind(SPACE);if (right == std::string::npos){return false; // 反序列化失败}__x = atoi(str.substr(0, left).c_str()); // 拿到__x的字符串之后直接 atoi 就行__y = atoi(str.substr(right + SPACE_LEN).c_str());if (left + SPACE_LEN > str.size())return false;__op = str[left + SPACE_LEN];return true;
#else#include <jsoncpp/json/json.h>Json::Value root;Json::Reader reader;reader.parse(str, root);__x = root["x"].asInt();__y = root["y"].anInt();__op = root["op"].asInt();return true;
#endif}public:Request() {}Request(int x, int y, char op) : __x(x), __y(y), __op(op) {}~Request() {}public:int __x;int __y;char __op; // '+' ...};class Response{public:/* "code result" */std::string Serialize(){
#if MYSELFstd::string s;s = std::to_string(__code);s += SPACE;s += std::to_string(__result);return s;
#else#include <jsoncpp/json/json.h>Json::Value root;root["code"] = __code;root["result"] = __result;Json::FastWriter writer;return writer.write(root);
#endif}bool Deserialize(const std::string &s){
#if MYSELF// std::cerr << "s: " << s << std::endl;std::size_t pos = s.find(SPACE);if (pos == std::string::npos)return false;__code = atoi(s.substr(0, pos).c_str());__result = atoi(s.substr(pos + SPACE_LEN).c_str());return true;
#else   Json::Value root;Json::Reader reader;reader.parse(s, root);__code = root["code"].asInt();__result = root["result"].asInt();return true;
#endif}public:Response() {}Response(int result, int code) : __result(result), __code(code) {}~Response() {}public:int __result; // 计算结果int __code;   // 计算结果的状态码/* 0表示计算结果可信任 */};// 临时方案bool Recv(int sock, std::string *out){char buffer[RECV_MAX_SIZE];memset(buffer, 0, sizeof buffer);ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;*out += buffer;}else if (s == 0){logMessage(NORMAL, "client quit");return false;}else{std::cerr << "recv error" << std::endl;return false;}return true;}// 临时方案void Send(int sock, const std::string sendStr){send(sock, sendStr.c_str(), sendStr.size(), 0);}// "length\r\n__x __op __y\r\n"std::string Decode(std::string &buffer){std::size_t pos = buffer.find(SEP);if (pos == std::string::npos) // 没有找到 返回空串return "";int size = atoi(buffer.substr(0, pos).c_str());int surplus = buffer.size() - pos - SEP_LEN - SEP_LEN; // 如果这个surplus大于报文长度// 则说明 这个报文长度是可以保证我们整体解析出一个合法字符串的if (surplus >= size){// 至少具有一个合法报文,可以动手提取了buffer.erase(0, pos + SEP_LEN); // 此时"__x __op __y\r\n"std::string s = buffer.substr(0, size);buffer.erase(0, size + SEP_LEN);// 此时我们就把合法的部分截取出来了 就是sreturn s;}else{return "";}}std::string Encode(std::string &s){// 添加报头std::string length = std::to_string(s.size());std::string new_package = length;new_package += SEP;new_package += s;new_package += SEP;return new_package;}}#endif

设置服务器进程为守护进程

 

CalServer.cc

#include "Protocol.hpp"
#include "TcpServer.hpp"
#include <memory>
#include <signal.h>
#include "Daemon.hpp"static void Usage(const std::string &proc)
{std::cout << "\nUsage: " << proc << " port\n"<< std::endl;
}void debug(int sock)
{std::cout << "service for debug, sock: " << sock << std::endl;
}static yufc_ns_protocol::Response calHelper(const yufc_ns_protocol::Request &req)
{yufc_ns_protocol::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 (0 == req.__y)resp.__code = 1;elseresp.__result = req.__x / req.__y;break;case '%':if (0 == req.__y)resp.__code = 2;elseresp.__result = req.__x % req.__y;break;default:resp.__code = 3;break;}return resp;
}void Calculator(int sock)
{/* 此处我们就要去定制协议 */std::string inbuffer;while (true){// 1. 读取数据bool res = yufc_ns_protocol::Recv(sock, &inbuffer); // 在这里我们读到了一个请求// 2. 协议解析 -- 需要保证得到一个完整的报文if (!res)break;                                                // 读取失败std::string package = yufc_ns_protocol::Decode(inbuffer); // 这里Decode保证返回的是一个完整的字节流,是正确的,是可以序列化反序列化的!// 如果这个package是空 表示Decode没有给我们返回完整报文if (package.empty())continue;// 3. 保证该报文是一个完整的报文yufc_ns_protocol::Request req;// 4. 反序列化req.Deserialize(package);// 5. 业务逻辑yufc_ns_protocol::Response resp = calHelper(req);// 6. 序列化std::string respString = resp.Serialize(); // 序列化// 7. 在发送之前,添加报头// "length\r\ncode result\r\n"respString = yufc_ns_protocol::Encode(respString);// 8. 发送(暂时这样写,高级IO的时候,我们再来谈发送的逻辑)yufc_ns_protocol::Send(sock, respString); // 发送}
}int main(int argc, char **argv)
{if (argc != 2){Usage(argv[0]);exit(1);}signal(SIGPIPE, SIG_IGN);/*一般经验: server 在编写的时候都要有严谨的判断逻辑一般的服务器,都是要忽略SIGPIPE信号的!防止运行中出现非法写入的问题*/std::unique_ptr<yufc_tcpServer::TcpServer> server(new yufc_tcpServer::TcpServer(atoi(argv[1])));server->BindService(Calculator);MyDaemon();server->Start();return 0;
}

运行现象

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

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

相关文章

怎么把pytorch从CPU版本替换成GPU版本

使用pip命令pip uninstall torch就可以卸载当前的torch版本。&#xff08;不是cpu版本一般也没有必要重装吧&#xff1f;&#xff09; 接着找到官网https://pytorch.org/get-started/locally/ 在里面选择 根据你自己的需要选择最新的&#xff08;我现在是11.8&#xff09;或者没…

机器学习之随机森林(Random forest)

1 什么是随机森林 随机森林是一种监督式算法&#xff0c;使用由众多决策树组成的一种集成学习方法&#xff0c;输出是对问题最佳答案的共识。随机森林可用于分类或回归&#xff0c;是一种主流的集成学习算法。 1.1 随机森林算法原理 随机森林中有许多的分类树。我们要将一个输…

怎么使用Netty解码自定义通信协议

网络协议的基本要素 一个完备的网络协议需要具备哪些基本要素 魔数&#xff1a;魔数是通信双方协商的一个暗号&#xff0c;通常采用固定的几个字节表示。魔数的作用是防止任何人随便向服务器的端口上发送数据。协议版本号&#xff1a;随着业务需求的变化&#xff0c;协议可能…

OpenCV(图像处理)-图片搜索

图片搜索 1.知识介绍2.实现流程2.1 计算特征点与描述子2.2 描述子的匹配2.3 求出单应性矩阵并画出轮廓2.4 将特征点标出 此篇博客作者仍在探索阶段&#xff0c;还有一些模糊的概念没有弄懂&#xff0c;请读者自行分辨。 1.知识介绍 Opencv进行图片搜索需要的知识有&#xff1…

Leetcode-每日一题【147.对链表进行插入排序】

题目 给定单个链表的头 head &#xff0c;使用 插入排序 对链表进行排序&#xff0c;并返回 排序后链表的头 。 插入排序 算法的步骤: 插入排序是迭代的&#xff0c;每次只移动一个元素&#xff0c;直到所有元素可以形成一个有序的输出列表。 每次迭代中&#xff0c;插入排序…

青岛大学_王卓老师【数据结构与算法】Week05_09_顺序栈的操作3_学习笔记

本文是个人学习笔记&#xff0c;素材来自青岛大学王卓老师的教学视频。 一方面用于学习记录与分享&#xff0c; 另一方面是想让更多的人看到这么好的《数据结构与算法》的学习视频。 如有侵权&#xff0c;请留言作删文处理。 课程视频链接&#xff1a; 数据结构与算法基础…

ELK搭建

ELK介绍&#xff1a; ELK是一组开源工具的缩写&#xff0c;它由Elasticsearch、Logstash和Kibana三个组件组成&#xff0c;用于处理、分析和可视化大量日志数据。 入门级ELK搭建&#xff08;无Docker环境&#xff09; 安装前准备 1.获取安装包 https://artifacts.elastic…

Objective-C 父元素和子元素的点击事件

场景&#xff1a; &#xff08;需求1&#xff09;pageA一开始是【默认模式】&#xff0c;点击父元素view&#xff08;包括【搜索】文字&#xff09;&#xff0c;进入【搜索模式】&#xff1b; &#xff08;需求2&#xff09;在pageA中&#xff0c;点击【取消】文字时&#xff…

如何快速制作一个奶茶店小程序商城

如果你是一个奶茶店的老板&#xff0c;你可能会考虑开设一个小程序商城来增加销售渠道和提升品牌形象。那么&#xff0c;如何快速制作一个奶茶店小程序商城呢&#xff1f;下面我们将介绍一个简单的步骤供你参考。 首先&#xff0c;你需要登录乔拓云平台进入商城后台管理页面。在…

用ChatGPT解析Wireshark抓取的数据包样例

用Wireshark抓取的数据包&#xff0c;常用于网络故障排查、分析和应用程序通信协议开发。其抓取的分组数据结果为底层数据&#xff0c;看起来比较困难&#xff0c;现在通过chatGPT大模型&#xff0c;可以将原始抓包信息数据提交给AI进行解析&#xff0c;本文即是进行尝试的样例…

互联网行业真的不行了吗?

文章目录 前言一、起因二、互联网真的完了吗&#xff1f;三、是不是要转行&#xff1f;四、十年磨一剑五、统一回复 前言 英雄算法联盟 - 七月集训 已经开始 16 天&#xff0c;八月算法集训 将于 08月01日 正式开始&#xff0c;目前已经提前开始报名&#xff0c;报名方式参见&a…

OpenCv之滤波器

目录 一、卷积 二、方盒滤波与均值滤波 三、高斯滤波 四、中值滤波 五、双边滤波 一、卷积 图像卷积就是卷积核在图像上按行华东遍历像素时不断的相乘求和的过程 相关知识点: 步长:就是卷积核在图像上移动的步幅.(为充分扫描图片&#xff0c;步长一般为1)padding:指在图片…

matlab学习指南(1):matlab初步入门详细介绍

&#x1f305;*&#x1f539;** φ(゜▽゜*)♪ **&#x1f539;*&#x1f305; 欢迎来到馒头侠的博客&#xff0c;该类目主要讲数学建模的知识&#xff0c;大家一起学习&#xff0c;联系最后的横幅&#xff01; 喜欢的朋友可以关注下&#xff0c;私信下次更新不迷路&#xff0…

sqlite3交叉编译

1、交叉编译sqllite3可以先从官网下载最新最新的源码进行编译。sqlite3下载sqlite3有两种版本的源代码&#xff0c;sqlite-amalgamation-3420000.zip这种是将所有的操作放到sqlite3中进行使用的。虽然官方推荐使用这种方法。但是对于嵌入式移植还是使用sqlite-autoconf-3420000…

探索基于300W-LP的3D人脸关键点检测

目录 前言一、&#xff13;D 关键点可视化二、使用步骤1.300W-LP转为YOLO数据格式2.修改数据入口3.开始训练 总结 前言 300WLP数据集提供来丰富的人脸线索&#xff0c;包括&#xff12;D或&#xff13;D的关键点信息&#xff0c;Head Angle和&#xff13;DMM的参数等&#xff…

OpenCV中的RGB与YUV转换

1 基本概念 YUV 颜色空间从模拟电视时代开始就被广泛应用于彩色图像的转换与处理。其基于一个 3x3 的矩阵&#xff0c;通过线性变换将 RGB 像素转换为一个亮度&#xff08;Luma&#xff09;分量 Y 以及两个色度&#xff08;Chroma&#xff09;分量 U 和 V。由于模拟电视存在着多…

K8s 为什么要弃用 Docker

K8s 为什么要弃用 Docker 最近在学习容器技术的过程中&#xff0c;看到有关于Kubernetes“弃用 Docker”的事情&#xff0c;担心现在学 Docker 是否还有价值&#xff0c;是否现在就应该切换到 containerd 或者是其他 runtime。 随着深入了解&#xff0c;这些疑虑的确是有些道理…

git 工具使用--分支管理

git 工具使用–分支管理 文章目录 git 工具使用--分支管理理解分支创建分支切换分支合并分支删除分支合并冲突分支管理策略分支策略bug分支删除临时分支总结 理解分支 分支管理是Git的杀手级功能之一。分支&#xff1a;就是科幻中的平行宇宙&#xff0c;当你正在电脑面前学习C…

设计模式-单例模式

面向对象语言讲究的是万物皆对象。通常流程是先定义可实例化类&#xff0c;然后再通过各种不同的方式创建对象&#xff0c;因此类一般可以实例化出多个对象。但是实际项目开发时&#xff0c;我们还是希望保证项目运行时有且仅包含一个实例对象。这个需求场景的出发点包括但不限…

变压器试验介质损耗

试验目的 介质损耗因数 tanδ (% ) 是判断变压器绝缘状态的一种较有效的手段, 主要用来检 查变压器整体受潮、 油质劣化及严重的局部缺陷等, 但不一定能发现变压器局部受潮 等集中性局部缺陷。 试验设备 异频介质损耗测试仪 厂家&#xff1a; 湖北众拓高试 试验接线 (1) 介…