写点什么

鸿蒙网络编程系列 6-TCP 数据粘包表现及原因分析

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

    阅读完需:约 12 分钟

1. 什么是粘包

在基于 TCP 协议的端到端通讯中,如果一端连续发送两个或者两个以上的数据包,对端在一次接收时,收到的数据包数量可能大于 1 个,也可能是几个完整数据包加上一个完整包的一部分数据,这些统称为粘包。

2.TCP 粘包示例

本文将设计一个示例演示数据粘包的表现,具体的思路是这样的:


1)使用鸿蒙应用发起 TCP 客户端到服务端连接。


2)服务端为回声服务器,会把收到的信息原样发回给客户端


3)TCP 客户端连续发送从 0 到 100 的数字字符信息到服务端,每次发送一个数字,发送后休眠 1 毫秒。


4)客户端对于接受到的服务端信息在日志输出,每次一行(也就是在接受信息后面加上回车换行)


5)如果没有所谓的“粘包”问题,客户端会收到 100 次回复


下面详细介绍创建该应用的步骤。


步骤 1:创建 Empty Ability 项目。


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


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


这里分别添加了访问互联网和访问 WIFI 信息的权限。


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


import socket from '@ohos.net.socket';import wifiManager from '@ohos.wifiManager';import systemDateTime from '@ohos.systemDateTime';import util from '@ohos.util';
//说明:本地的IP地址不是必须知道的,绑定时绑定到IP:0.0.0.0即可,显示本地IP地址的目的是方便对方发送信息过来//本地IP的数值形式let ipNum = wifiManager.getIpInfo().ipAddress//本地IP的字符串形式let localIp = (ipNum >>> 24) + '.' + (ipNum >> 16 & 0xFF) + '.' + (ipNum >> 8 & 0xFF) + '.' + (ipNum & 0xFF);
@Entry@Componentstruct Index { //连接、通讯历史记录 @State msgHistory: string = ''
//服务端IP地址 @State serverIp: string = "0.0.0.0" //服务端端口 @State serverPort: number = 9990 scroller: Scroller = new Scroller()
build() { Row() { Column() { Text("TCP通讯粘包示例") .fontSize(14) .fontWeight(FontWeight.Bold) .width('100%') .textAlign(TextAlign.Center) .padding(10)
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { Text("本地IP:") .width(100) .fontSize(14) .flexGrow(0) Text(localIp) .width(110) .fontSize(12) .flexGrow(1)
Button("测试") .onClick(() => { this.sendTestMsg2Server() }) .width(80) .fontSize(14) .flexGrow(0) }.width('100%') .padding(10)
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { Text("服务端地址:") .fontSize(14) .width(90) .flexGrow(0)
TextInput({ text: this.serverIp }) .onChange((value) => { this.serverIp = value }) .width(110) .fontSize(12) .flexGrow(4)
Text(":") .width(5) .flexGrow(0)
TextInput({ text: this.serverPort.toString() }) .type(InputType.Number) .onChange((value) => { this.serverPort = parseInt(value) }) .fontSize(12) .flexGrow(0) .width(70) } .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 sendTestMsg2Server() { //执行TCP通讯的对象 let tcpSocket = socket.constructTCPSocketInstance(); await this.bind2Port(tcpSocket) await this.connect2Server(tcpSocket)
for (let i = 0;i < 100; i++) { await tcpSocket.send({ data: i.toString() }) await sleep(1) } }
//绑定本地端口 async bind2Port(tcpSocket: socket.TCPSocket) { //本地地址 let localAddress = { address: "0.0.0.0", family: 1 }
await tcpSocket.bind(localAddress) .then(() => { this.msgHistory = 'bind success' + "\r\n"; }) .catch((e) => { this.msgHistory = 'bind fail ' + e.message + "\r\n"; })
//收到消息时的处理 tcpSocket.on("message", async (value) => { let msg = buf2String(value.message) this.msgHistory += "S:" + msg + "\r\n" this.scroller.scrollEdge(Edge.Bottom) }) }
//连接服务端 async connect2Server(tcpSocket: socket.TCPSocket) { //本地地址 let serverAddress = { address: this.serverIp, port: this.serverPort, family: 1 }
await tcpSocket.connect({ address: serverAddress }) .then(() => { this.msgHistory = 'connect success ' + "\r\n"; }) .catch((e) => { this.msgHistory = 'connect fail ' + e.message + "\r\n"; }) }}
//ArrayBuffer转utf8字符串function buf2String(buf: ArrayBuffer) { let msgArray = new Uint8Array(buf); let textDecoder = util.TextDecoder.create("utf-8"); return textDecoder.decodeWithStream(msgArray)}
//休眠指定的毫秒数function sleep(time) { return new Promise((resolve) => setTimeout(resolve, time));}
复制代码


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


步骤 5:运行回声服务器(需要读者自行开发或者等我拿到鸿蒙 Next 权限后写一个鸿蒙版本的回声服务器),然后配置客户端的服务端地址,然后单击“测试”按钮,执行后的截图如下所示:



截图清晰表明,出现了严重的粘包情况,客户端虽然是一个个发送的,代码如下所示:


  //发送测试消息到服务端  async sendTestMsg2Server() {    //执行TCP通讯的对象    let tcpSocket = socket.constructTCPSocketInstance();    await this.bind2Port(tcpSocket)    await this.connect2Server(tcpSocket)
for (let i = 0;i < 100; i++) { await tcpSocket.send({ data: i.toString() }) await sleep(1) } }
复制代码


但是接收的时候和发送的不一样,是一堆堆接收的,原因是什么呢?

3.TCP 粘包原因分析

TCP 是一种面向流的数据传输协议,传输的对象是连续的字节流,内容之间并没有明确的分界标志,严格来说,并不存在粘包的问题,而通常所说的粘包,更多的是一种逻辑上的概念,也就是人为的把 TCP 传输的字节流划分成了一个个的数据包,发送端确定了数据包之间的边界,但是接收端并不能保证按照数据包的边界来接收。对于本示例中发送端和接收端不匹配的情况,还可能和下面的原因有关:


1)发送端启用了 Nagle 算法


发送端对于小包,可能会累计起来,到了一定的数据量或者其他条件满足才发送给接收端,这是导致粘包的一个重要原因。


2)TCP 的滑动窗口机制


根据滑动窗口的机制,发送端一次发送数据量的多少并不完全是由自己决定的,还要受接收端的缓存大小限制,这也会导致发送端原本计划一次发送的数据包被分为多次发送。


3)MSS 和 MTU 分片


如果一次需要发送的数据大于 MSS 或者 MTU 时,数据会被拆分成多个包进行传输,这也会导致粘包的产生。


4)接收端不及时接收


如果接收端不能及时接收缓冲区的数据包,那么在其后的某次接收中,就会出现接收多个数据包的情况。

4.TCP 粘包问题解决方案

知道了数据粘包的原因,解决起来就有方向了,大体可以分为两种方案,一种是指定数据包结束标志,另外一种是通过固定包头指定包的长度,不过这两种方案都需要服务端配合才行,因为 API 9 版本的鸿蒙尚未支持 TCP 服务端,虽然更高的 10、11 支持了,但是我还没有这些版本的使用权限,暂时无法编写服务端程序,等以后开放了权限再补充编写粘包问题的解决方案示例吧。


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


本文源码地址:


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


本系列源码地址:


https://gitee.com/zl3624/harmonyos_network_samples


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

长弓三石

关注

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

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

评论

发布
暂无评论
鸿蒙网络编程系列6-TCP数据粘包表现及原因分析_DevEco Studio_长弓三石_InfoQ写作社区