鸿蒙网络编程系列36-固定包头可变包体解决TCP粘包问题

1. TCP数据传输粘包简介

在本系列的第6篇文章《鸿蒙网络编程系列6-TCP数据粘包表现及原因分析》中,我们演示了TCP数据粘包的表现,如图所示:

随后解释了粘包背后的可能原因,并给出了解决TCP传输粘包问题的两种思路,第一种是指定数据包结束标志,在本系列第35篇《鸿蒙网络编程系列35-通过数据包结束标志解决TCP粘包问题》中给出了具体的实现,第二种是通过固定包头指定包的长度,本文将通过一个示例演示这种思路的实现。

2. 固定包头可变包体解决TCP粘包问题演示

本示例运行后的界面如图所示:

和上一篇文章类似,输入服务端的地址,这里可以使用本系列第25篇文章《鸿蒙网络编程系列25-TCP回声服务器的实现》中创建的TCP回声服务器,也可以使用其他类似的回声服务器;然后输入服务器端口,最后单击"测试"按钮循环发送0到99的数字字符串到服务端,服务端会回传收到的信息,本示例在收到服务器信息后在日志区域输出,如图所示:

从中可以看出,这次也彻底解决了数据粘包问题,收到的信息和发送时保持一致。

3. 固定包头可变包体解决TCP粘包问题示例编写

下面详细介绍创建该示例的步骤。
步骤1:创建Empty Ability项目。

步骤2:在module.json5配置文件加上对权限的声明:

"requestPermissions": [{"name": "ohos.permission.INTERNET"}]

这里添加了访问互联网的权限。

步骤3:在Index.ets文件里添加如下的代码:

import { socket } from '@kit.NetworkKit';
import { Decimal, util, buffer } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';@Entry
@Component
struct Index {@State title: string = '固定包头可变包体演示示例';//服务端端口号@State port: number = 9990//服务端IP地址@State serverIp: string = ""//操作日志@State msgHistory: string = ''//最大缓存长度maxBufSize: number = 1024 * 8//接收数据缓冲区receivedDataBuf: buffer.Buffer = buffer.alloc(this.maxBufSize)//缓冲区已使用长度receivedDataLen: number = 0//日志显示区域的滚动容器scroller: Scroller = new Scroller()build() {Row() {Column() {Text(this.title).fontSize(14).fontWeight(FontWeight.Bold).width('100%').textAlign(TextAlign.Center).padding(10)Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {Text("服务端地址:").fontSize(14).width(90)TextInput({ text: this.serverIp }).onChange((value) => {this.serverIp = value}).height(40).width(80).fontSize(14).flexGrow(1)Text(":").fontSize(14)TextInput({ text: this.port.toString() }).onChange((value) => {this.port = parseInt(value)}).height(40).width(70).fontSize(14)Button("测试").onClick(() => {this.test()}).height(40).width(60).fontSize(14)}.width('100%').padding(10)Scroll(this.scroller) {Text(this.msgHistory).textAlign(TextAlign.Start).padding(10).width('100%').backgroundColor(0xeeeeee)}.align(Alignment.Top).backgroundColor(0xeeeeee).height(300).flexGrow(1).scrollable(ScrollDirection.Vertical).scrollBar(BarState.On).scrollBarWidth(20)}.width('100%').justifyContent(FlexAlign.Start).height('100%')}.height('100%')}//测试async test() {//服务端地址let serverAddress: socket.NetAddress = { address: this.serverIp, port: this.port, family: 1 }//执行TCP通讯的对象let tcpSocket: socket.TCPSocket = socket.constructTCPSocketInstance()//收到消息时的处理tcpSocket.on("message", (value: socket.SocketMessageInfo) => {this.receiveMsgFromServer(value)})await tcpSocket.connect({ address: serverAddress }).then(() => {this.msgHistory += "连接成功\r\n";}).catch((e: BusinessError) => {this.msgHistory += `连接失败 ${e.message} \r\n`;})//循环发送0到99的数字字符串到服务端for (let i = 0; i < 100; i++) {let msg = i.toString()await this.sendMsg2Server(tcpSocket, msg)let sleepTime = Decimal.random().toNumber() + 0.5//休眠sleepTime时间,大概0.5毫秒到1.5毫秒await sleep(sleepTime)}}//发送数据到服务端async sendMsg2Server(tcpSocket: socket.TCPSocket, msg: string) {let textEncoder = new util.TextEncoder();let encodeValue = textEncoder.encodeInto(msg)let sendBuf = buffer.alloc(2 + encodeValue.byteLength)//写入固定包头中的长度信息sendBuf.writeUInt16LE(encodeValue.byteLength)//写入可变包体信息sendBuf.write(msg, 2)await tcpSocket.send({ data: sendBuf.buffer })}//读取服务端发送过来的数据receiveMsgFromServer(value: socket.SocketMessageInfo) {//把接收到的数据复制到缓冲区有效数据尾部let copyCount = buffer.from(value.message).copy(this.receivedDataBuf, this.receivedDataLen)this.receivedDataLen += copyCount//至少写入了3个字节才需要解析if (this.receivedDataLen < 3) {return;}//当前数据包长度let packLen = this.receivedDataBuf.readUInt16LE()let textDecoder = util.TextDecoder.create("utf-8");//当前数据包长度加上固定包体的2字节,如果小于等于缓冲区已使用长度,就可以解析while ((packLen + 2) <= this.receivedDataLen) {//把可变包体中的数据转换为字符串let msgArray = new Uint8Array(this.receivedDataBuf.subarray(2, packLen + 2).buffer);let msg = textDecoder.decodeToString(msgArray)//剩余的未解析数据let leaveBufData = this.receivedDataBuf.subarray(packLen + 2, this.receivedDataLen)//剩余的未解析数据移动到缓冲区头部for (let pos = 0; pos < leaveBufData.length; pos++) {this.receivedDataBuf.writeUInt8(leaveBufData.readUInt8(pos), pos)}//重新设置缓冲区已使用长度this.receivedDataLen = leaveBufData.length//输出接收的数据到日志this.msgHistory += "S:" + msg + "\r\n"//至少写入了3个字节才需要解析,否则跳出循环if (this.receivedDataLen < 3) {break;}//开始查找下一个固定包头中的可变包体长度packLen = this.receivedDataBuf.readUInt16LE()}this.scroller.scrollEdge(Edge.Bottom)}
}//休眠指定的毫秒数
function sleep(time: number): Promise<void> {return new Promise((resolve) => setTimeout(resolve, time));
}

步骤4:编译运行,可以使用模拟器或者真机。

步骤5:按照本文第2部分“数据包结束标志解决TCP粘包问题演示”操作即可。

4. 代码分析

本示例的关键点在于构造数据包的格式,具体数据包的格式是这样的,前两个字节为固定的包长度,使用小端的16位无符号整数表示,后面是包内容。以发送数据包为例,代码如下所示:

  async sendMsg2Server(tcpSocket: socket.TCPSocket, msg: string) {let textEncoder = new util.TextEncoder();let encodeValue = textEncoder.encodeInto(msg)let sendBuf = buffer.alloc(2 + encodeValue.byteLength)//写入固定包头中的长度信息sendBuf.writeUInt16LE(encodeValue.byteLength)//写入可变包体信息sendBuf.write(msg, 2)await tcpSocket.send({ data: sendBuf.buffer })}

这里首先把要发送的内容编码为Uint8Array类型,然后为缓冲区分配长度,长度为内容编码后的长度加上2,随后把内容长度作为无符号数写入缓冲区,然后把发送的内容也写入缓冲区,最后使用TCP客户端发送缓冲区到服务端。

接收时,首先把所有收到的数据都复制到接收缓冲区中,然后从缓冲区头部取两个字节作为数据包内容长度,然后判断接收缓冲区中已接收的数据是不是大于等于数据包内容长度加2,如果是,说明接收到了完整的数据包,就可以从中提取内容了,提取完毕把剩下的缓冲区数据移动到缓冲区头部,继续下一次循环,从缓冲区中提取完整数据包的数据,知道已接收的缓冲区小于数据包长度加2为止。相关代码位于方法receiveMsgFromServer中,源码包含了详细的注释,这里就不再赘述了。

(本文作者原创,除非明确授权禁止转载)

本文源码地址:
https://gitee.com/zl3624/harmonyos_network_samples/tree/master/code/tcp/PacketHeadWithLen

本系列源码地址:
https://gitee.com/zl3624/harmonyos_network_samples

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

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

相关文章

网络通信与并发编程(六)线程、进程池与线程池

线程、进程池与线程池 文章目录 线程、进程池与线程池一、线程二、线程的相关操作2.1创建线程的两种方式2.2线程的其他操作2.3死锁现象和递归锁2.4条件2.5定时器2.6 队列与堆栈 三、进程池与线程池 一、线程 线程是指cpu上实际执行计算的单位&#xff0c;而进程是将计算所需资…

潮畔汽车文化营地开营啦!全民测试场启动典礼圆满成功

在这个充满活力与创新的时代&#xff0c;汽车已不仅仅是代步工具&#xff0c;它承载着人们对速度、自由、科技与梦想的追求&#xff0c;成为了一种生活方式的象征。随着汽车文化的日益丰富和多元化&#xff0c;如何更好地连接汽车制造商、消费者以及广大汽车爱好者&#xff0c;…

群控系统服务端开发模式-应用开发-业务架构逻辑开发准备工作

安装与仓库已经调整完毕&#xff0c;现在开发业务架构逻辑&#xff0c;其次再开发功能逻辑。业务架构逻辑开发与功能逻辑开发不是一回事&#xff0c;一定要明白。业务架构指的是做某一件事或是某一种类型的事的逻辑&#xff0c;在互联网web应用中通常指一套系统的外在逻辑&…

Vue学习笔记(四)

事件处理 我们可以使用 v-on 指令 (通常缩写为 符号) 来监听 DOM 事件&#xff0c;并在触发事件时执行一些 JavaScript。用法为 v-on:click"methodName" 或使用快捷方式 click"methodName" 事件处理器的值可以是&#xff1a; 内联事件处理器&#xff1…

Jetpack架构组件_LiveData组件

1.LiveData初识 LiveData:ViewModel管理要展示的数据&#xff08;VM层类似于原MVP中的P层&#xff09;&#xff0c;处理业务逻辑&#xff0c;比如调用服务器的登陆接口业务。通过LiveData观察者模式&#xff0c;只要数据的值发生了改变&#xff0c;就会自动通知VIEW层&#xf…

基于Python大数据的王者荣耀战队数据分析及可视化系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

IDEA开发工具使用技巧积累

一、IDEA 工具设置默认使用maven的settings.xml文件 第一步&#xff1a;打开idea工具&#xff0c;选中 File ——> New Projects Setup ——> Settings for New Projects 第二步&#xff1a;先设置下自动构建项目这个选项 第三步&#xff1a;选中 Build Tools ——>…

windows下pycharm社区版2024下载与安装(包含新建第一个工程)

windows下pycharm社区版2024下载与安装 下载pycharm pycharm官网 安装pycharm 1.进入官网 pycharm官网 下载 点击Download–>右侧Other versions 下载对应的社区版&#xff08;如下图&#xff09;&#xff1a;下载网址 2.点击运行下载好的安装包 点击下一步 3.更改pychar…

2020款Macbook Pro A2251无法充电无法开机定位及修复

问题背景 up主有一台2020年的Macbook Pro&#xff0c;带Touch Bar&#xff0c;16G512G&#xff0c;四核I5&#xff0c;型号A2251 应该是一周没充电了&#xff0c;之前还用的好好的&#xff0c;后来有一天出差想带上 打开没电&#xff0c;手头上有个小米的66W快充头&#xff0c…

C#的自定义Tip窗体 - 开源研究系列文章

上次编写了自定义的提示和对话框窗体&#xff0c;这次记录的是自定义的Tip窗体&#xff0c;用于显示提示操作。有时间没编程了&#xff0c;这次就当进行了记录。 1、 项目目录&#xff1b; 2、 源码介绍&#xff1b; 1) 实现&#xff1b; 2) 应用&#xff1b; 3、 运行界面&…

Leetcode刷题笔记12

HJ1 字符串最后一个单词的长度 字符串最后一个单词的长度_牛客题霸_牛客网 这里可以使用rfind()&#xff0c;rfind()函数从字符串的末尾向前查找第一个空格的位置。这个空格将是最后一个单词和前面的单词的分隔符。首先使用getline读取字符串&#xff0c;然后用rfind找到最后一…

现在设备普遍切换成TYPE-C适配器后,一拖三数据线接口变革探析

随着科技的飞速发展&#xff0c;电子设备的接口标准也在不断地更新换代。近年来&#xff0c;TYPE-C接口凭借其高速传输、正反可插等显著优势&#xff0c;逐渐成为了众多电子设备的主流接口。从智能手机到平板电脑&#xff0c;从笔记本电脑到移动电源&#xff0c;TYPE-C接口的应…

Java-图书管理系统

我的个人主页 欢迎来到我的Java图书管理系统&#xff0c;接下来让我们一同探索如何书写图书管理系统吧&#xff01; 1管理端和用户端 2建立相关的三个包&#xff08;book、operation、user&#xff09; 3建立程序入口Main类 4程序运行 1.首先图书馆管理系统分为管理员端和…

TPLCM柔性屏自动化贴合应用

在当前的显示屏制造领域&#xff0c;TP&LCM贴合技术是推动产品升级和满足市场需求的关键环节。随着技术的不断进步&#xff0c;全贴合技术因其卓越的显示效果和用户体验&#xff0c;逐渐成为中高端产品的标配。然而&#xff0c;这一技术的高精度要求和复杂工艺也带来了诸多…

物联网数据采集网关详细介绍-天拓四方

一、物联网数据采集网关的概述 物联网数据采集网关&#xff0c;简称数据采集网关&#xff0c;是物联网系统中的重要组成部分&#xff0c;位于物联网设备和云端平台之间。其主要职责是实现数据的采集、汇聚、转换、传输等功能&#xff0c;确保来自不同物联网设备的数据能够统一…

学习笔记——动态路由——OSPF(距离矢量协议)OSPF路由类型

OSPF路由类型 在OSPF中&#xff0c;路由类型指的是不同种类的路由&#xff0c;用于描述网络中不同的路由信息及其传输方式。 1、Intra Area路由(区域内路由) Intra Area路由(区域内路由/本地路由/内部路由)是OSPF协议中的一种路由类型&#xff0c;用于描述在同一个OSPF区域内…

小白直接冲!一区蛇群优化算法+双向深度学习+注意力机制!SO-BiTCN-BiGRU-Attention多输入单输出回归预测

小白直接冲&#xff01;一区蛇群优化算法双向深度学习注意力机制&#xff01;SO-BiTCN-BiGRU-Attention多输入单输出回归预测 目录 小白直接冲&#xff01;一区蛇群优化算法双向深度学习注意力机制&#xff01;SO-BiTCN-BiGRU-Attention多输入单输出回归预测预测效果基本介绍程…

Linux相关概念和易错知识点(16)(Shell原理、进程属性和环境变量表的联系)

Shell原理及其模拟实现 在认识进程exec系列函数、命令行参数列表、环境变量之后&#xff0c;我们可以尝试理解一下Shell的原理&#xff0c;将各方知识串联起来&#xff0c;让Shell跑起来才能真正理解这些概念。我会以模拟Shell执行的原理模拟一个Shell。途中配上相关讲解。 1…

Mybatis-03.入门-配置SQL提示

一.配置SQL提示 目前的Springboot框架在mybatis程序中编写sql语句并没有给到任何的提示信息&#xff0c;这对于开发者而言是很不友好的。因此我们需要配置SQL提示。 配置SQL提示 这样再去写SQL语句就会有提示了。 但是会发现指定表名时并没有给出提示。这是因为&#xff1a…

用kali入侵 DarkHole_2测试

进入kali系统调出root交互式界面 netdiscover -r 000.000.000.000/24 -------局域网探测IP工具 nmap 设备端口扫描 发现两个攻击点一个是80端口的Http 一个是22端口的ssh 发现有许多GIT文件 可能会出现git源码泄露 使用githack URL 命令还原git源文件 打开面板控制命令行 输入…