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
@Component
struct 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支持了,但是我还没有这些版本的使用权限,暂时无法编写服务端程序,等以后开放了权限再补充编写粘包问题的解决方案示例吧。
最后
有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)资料用来跟着学习是非常有必要的。
点击领取→【纯血版鸿蒙全套最新学习资料】(安全链接,放心点击)希望这一份鸿蒙学习资料能够给大家带来帮助,有需要的小伙伴自行领取,限时开源,先到先得~无套路领取!!
这份鸿蒙(HarmonyOS NEXT)资料包含了鸿蒙开发必掌握的核心知识要点,内容包含了(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、(南向驱动、嵌入式等)鸿蒙项目实战等等)鸿蒙(HarmonyOS NEXT)技术知识点。
鸿蒙(HarmonyOS NEXT)最新学习路线
有了路线图,怎么能没有学习资料呢,小编也准备了一份联合鸿蒙官方发布笔记整理收纳的一套系统性的鸿蒙(OpenHarmony )学习手册(共计1236页)与鸿蒙(OpenHarmony )开发入门教学视频,内容包含:ArkTS、ArkUI、Web开发、应用模型、资源分类…等知识点。
获取以上完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习资料
HarmonyOS Next 最新全套视频教程
《鸿蒙 (OpenHarmony)开发基础到实战手册》
OpenHarmony北向、南向开发环境搭建
《鸿蒙开发基础》
- ArkTS语言
- 安装DevEco Studio
- 运用你的第一个ArkTS应用
- ArkUI声明式UI开发
- .……
《鸿蒙开发进阶》
- Stage模型入门
- 网络管理
- 数据管理
- 电话服务
- 分布式应用开发
- 通知与窗口管理
- 多媒体技术
- 安全技能
- 任务管理
- WebGL
- 国际化开发
- 应用测试
- DFX面向未来设计
- 鸿蒙系统移植和裁剪定制
- ……
《鸿蒙进阶实战》
- ArkTS实践
- UIAbility应用
- 网络案例
- ……
大厂面试必问面试题
鸿蒙南向开发技术
鸿蒙APP开发必备
鸿蒙生态应用开发白皮书V2.0PDF
总结
总的来说,华为鸿蒙不再兼容安卓,对中年程序员来说是一个挑战,也是一个机会。只有积极应对变化,不断学习和提升自己,他们才能在这个变革的时代中立于不败之地。