写点什么

鸿蒙网络编程系列 35- 通过数据包结束标志解决 TCP 粘包问题

作者:长弓三石
  • 2024-10-24
    广东
  • 本文字数:3549 字

    阅读完需:约 12 分钟

1. TCP 数据传输粘包简介

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



随后解释了粘包背后的可能原因,并给出了解决 TCP 传输粘包问题的两种思路,其中一种就是指定数据包结束标志,本节将通过一个示例演示这种思路的实现。

2. 数据包结束标志解决 TCP 粘包问题演示

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



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



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

3. 数据包结束标志解决 TCP 粘包问题示例编写

下面详细介绍创建该示例的步骤。


步骤 1:创建 Empty Ability 项目。


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


"requestPermissions": [      {        "name": "ohos.permission.INTERNET"      }    ]
复制代码


这里添加了读取和设置 Wifi 配置的权限。


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


import { socket } from '@kit.NetworkKit';import { Decimal, util, buffer } from '@kit.ArkTS';import { BusinessError } from '@kit.BasicServicesKit';
@Entry@Componentstruct Index { @State title: string = '数据包结束标志演示示例'; //服务端端口号 @State port: number = 9990 //服务端IP地址 @State serverIp: string = "" //操作日志 @State msgHistory: string = '' //数据包结束标志 packetEndFlag: string = "\r\n" //最大缓存长度 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 senderMsg = msg + this.packetEndFlag await tcpSocket.send({ data: senderMsg }) }
//读取服务端发送过来的数据 receiveMsgFromServer(value: socket.SocketMessageInfo) { //把接收到的数据复制到缓冲区有效数据尾部 let copyCount = buffer.from(value.message).copy(this.receivedDataBuf, this.receivedDataLen) //缓冲区已使用长度加上本次接收的数据长度 this.receivedDataLen += copyCount //缓冲区数据中数据包结束标志的位置 let endFlagPos = this.receivedDataBuf.subarray(0, this.receivedDataLen).indexOf(this.packetEndFlag) let textDecoder = util.TextDecoder.create("utf-8"); while (endFlagPos > -1) { //把数据包结束标志前面的数据转换为字符串 let msgArray = new Uint8Array(this.receivedDataBuf.subarray(0, endFlagPos).buffer); let msg = textDecoder.decodeToString(msgArray) //剩余的未解析数据 let leaveBufData = this.receivedDataBuf.subarray(endFlagPos + 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" //开始查找下一个数据包结束标志 endFlagPos = this.receivedDataBuf.subarray(0, this.receivedDataLen).indexOf(this.packetEndFlag) } this.scroller.scrollEdge(Edge.Bottom) }}
//休眠指定的毫秒数function sleep(time: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, time));}
复制代码


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


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

4. 代码分析

本示例的关键点在于设置数据包的结束标志,虽然 TCP 数据本身是没有边界的,但是通过人为添加额外的数据包分界标志,就可以把发送者的意图通过分界标志表示出来。为简单起见,本示例把结束标志设置为回车换行符,读者也可以使用其他的标志,需要注意的是,在发送信息的时候,信息本身不能包含结束标志符号,针对本示例而言,发送信息本身不能包含回车换行符序列,否则会出现意想不到的结果。


发送时添加结束标志的代码如下所示:


    //发送的原始数据后加上数据包结束标志    let senderMsg = msg + this.packetEndFlag    await tcpSocket.send({ data: senderMsg })
复制代码


接收时,首选把所有收到的数据都复制到接收缓冲区中,然后遍历缓冲区查找结束标志,找到后就把结束标志以前的部分作为完整数据包提取出来使用,剩余的部分复制到缓冲区首部进行下一次遍历,直到找不到结束标志为止,相关代码位于方法 receiveMsgFromServer 中,源码包含了详细的注释,这里就不再赘述了。


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


本文源码地址:


https://gitee.com/zl3624/harmonyos_network_samples/tree/master/code/tcp/PacketEndFlag


本系列源码地址:


https://gitee.com/zl3624/harmonyos_network_samples


发布于: 刚刚阅读数: 5
用户头像

长弓三石

关注

还未添加个人签名 2024-10-16 加入

二十多年软件开发经验的软件架构师,华为HDE、华为云HCDE、仓颉语言CLD、CCS,著有《仓颉语言网络编程》、《仓颉语言元编程》、《仓颉语言实战》、《鲲鹏架构入门与实战》等书籍,清华大学出版社出版。

评论

发布
暂无评论
鸿蒙网络编程系列35-通过数据包结束标志解决TCP粘包问题_DevEco Studio_长弓三石_InfoQ写作社区