HarmonyOS鸿蒙Next网络编程系列55-仓颉版TCP数据粘包表现及原因分析

HarmonyOS鸿蒙Next网络编程系列55-仓颉版TCP数据粘包表现及原因分析

1. TCP粘包简介

在基于TCP协议的端到端通讯中,如果一端连续发送两个或者两个以上的数据包,对端在一次接收时,收到的数据包数量可能大于1个,也可能是几个完整数据包加上一个完整包的一部分数据,这些统称为粘包。在本系列的第6篇文章《鸿蒙网络编程系列6-TCP数据粘包表现及原因分析》中,基于ArkTS语言在API9环境下使用TCPSocket对象演示了数据粘包的表现,因为粘包是和TCP协议直接相关的,所以,在API17下,使用仓颉语言一样可以实现类似的效果。

2. TCP粘包示例演示

本示例的实现思路是这样的:

  • 使用TCP客户端发起到服务端的连接。
  • 服务端为回声服务器,会把收到的信息原样发回给客户端
  • TCP客户端连续发送从0到99(不包括99)的数字字符信息到服务端,每次发送一个数字,发送后休眠随机的几个毫秒。
  • 客户端对于接收到的服务端信息在日志输出,每次一行(也就是在接收信息后面加上回车换行)
  • 如果没有所谓的“粘包”问题,客户端会收到99次回复 要运行本示例,需要预先启动TCP回声服务器,可以参考本示例指定的服务端源码,也可以使用其他类似的回声服务器。

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

输入TCP回声服务器的IP地址和端口,然后单击“测试”按钮,最终日志信息如图所示:

从图中可以看出,整个过程出现了严重的粘包现象,虽然客户端是一个个发送数字字符的,但是接收的时候出现了一次接收多个数字字符的情况。

3. TCP粘包示例编写

下面详细介绍创建该示例的步骤(确保DevEco Studio已安装仓颉插件)。

步骤1:创建[Cangjie]Empty Ability项目。

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

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

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

步骤3:在build-profile.json5配置文件加上仓颉编译架构:

"cangjieOptions": {
      "path": "./src/main/cangjie/cjpm.toml",
      "abiFilters": ["arm64-v8a", "x86_64"]
    }

步骤4:在index.cj文件里添加如下的代码:

package ohos_app_cangjie_entry

import ohos.base.*
import ohos.component.*
import ohos.state_manage.*
import ohos.state_macro_manage.*
import std.collection.HashMap
import std.convert.*
import std.net.*
import std.socket.*
import encoding.base64.toBase64String
import std.sync.sleep
import std.time.Duration
import std.random.*

@Entry
@Component
class EntryView {
    @State
    var title: String = 'TCP粘包演示示例';
    //连接、通讯历史记录
    @State
    var msgHistory: String = ''

    //服务端ip地址
    @State
    var serverIp: String = ""
    //服务端端口
    @State
    var port: UInt16 = 9990

    let scroller: Scroller = Scroller()

    func build() {
        Row {
            Column {
                Text(title)
                    .fontSize(14)
                    .fontWeight(FontWeight.Bold)
                    .width(100.percent)
                    .textAlign(TextAlign.Center)
                    .padding(10)

                Flex(FlexParams(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center)) {
                    Text("服务端地址:").fontSize(14).width(90)

                    TextInput(text: serverIp).onChange({
                        value => serverIp = value
                    }).width(80).fontSize(11).flexGrow(1)
                    Text(":").fontSize(14)

                    TextInput(text: port.toString())
                        .onChange({
                            value => if (value == "") {
                                port = 0
                            } else {
                                port = UInt16.parse(value)
                            }
                        })
                        .setType(InputType.Number)
                        .width(80)
                        .fontSize(11)

                    Button("测试")
                        .onClick {
                            evt => test()
                        }
                        .width(60)
                        .fontSize(14)
                        .enabled(serverIp.split(".", removeEmpty: true).size == 4 && port != 0)
                }.width(100.percent).padding(5)

                Scroll(scroller) {
                    Text(msgHistory)
                        .textAlign(TextAlign.Start)
                        .padding(10)
                        .width(100.percent)
                        .backgroundColor(0xeeeeee)
                }
                    .align(Alignment.Top)
                    .backgroundColor(0xeeeeee)
                    .height(300)
                    .flexGrow(1)
                    .scrollable(ScrollDirection.Vertical)
                    .scrollBar(BarState.On)
                    .scrollBarWidth(20)
            }.width(100.percent).height(100.percent)
        }.height(100.percent)
    }

    //从服务器读取消息并输出
    func readMsgFromServer(tcpClient: TcpSocket) {
        let buffer = Array<UInt8>(1024, item: 0)

        while (true) {
            //从socket读取数据
            var readCount = tcpClient.read(buffer)
            //把接收到的数据转换为字符串
            let content = String.fromUtf8(buffer[0..readCount])
            msgHistory += "S:${content}\r\n"
        }
    }

    //粘包测试
    func test() {
        let tcpClient = TcpSocket(serverIp, port)

        try {
            tcpClient.connect()
            msgHistory += "C:连接成功!\r\n"
        } catch (err: Exception) {
            msgHistory += "C:连接失败${err.message}!\r\n"
            return
        }

        //启动一个线程读取服务器返回信息
        spawn {
            readMsgFromServer(tcpClient)
        }

        //启动一个线程循环发送0到99的数字字符串到服务端
        spawn {
            try {
                let m: Random = Random()
                for (i in 0..99) {
                    tcpClient.write(i.toString().toArray())
                    //随即休眠不超过10毫秒的时间
                    sleep(Duration.millisecond * m.nextInt64(10))
                }
            } catch (exp: Exception) {
                msgHistory += "发送数据到服务器异常:${exp}\r\n"
            }
        }
    }
}

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

步骤6:按照本文第2部分“TCP粘包示例演示”操作即可。

4. TCP粘包原因分析

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

1)发送端启用了Nagle算法

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

2)TCP的滑动窗口机制

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

3)MSS和MTU分片

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

4)接收端不及时接收

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

另外,为简化代码,本示例假设客户端一次接收服务端数据量最大不超过1024字节,在本示例场景中,这样一般没什么问题,如果是其他场景,需要考虑接收数据可能超过1024字节的情况。

本示例在数据接收的时候,和ArkTS版本差异也较大,本示例启动了一个线程执行readMsgFromServer函数,该函数负责接收服务端消息并通过日志输出。实际的实现通过一个无限循环从套接字读取数据并存入到字节数组buffer中,然后把接收到的数据转换为字符串,最后写入到变量msgHistory中,该变量会显示到日志区域。代码如下:

func readMsgFromServer(tcpClient: TcpSocket) {
    let buffer = Array<UInt8>(1024, item: 0)

    while (true) {
        //从socket读取数据
        var readCount = tcpClient.read(buffer)
        //把接收到的数据转换为字符串
        let content = String.fromUtf8(buffer[0..readCount])
        msgHistory += "S:${content}\r\n"
    }
}

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

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

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


更多关于HarmonyOS鸿蒙Next网络编程系列55-仓颉版TCP数据粘包表现及原因分析的实战教程也可以访问 https://www.itying.com/category-93-b0.html

2 回复

在鸿蒙Next仓颉版中,TCP数据粘包表现为连续发送的小数据包被合并接收。原因主要有三点:1. TCP协议本身的流式传输特性,不保留消息边界;2. Nagle算法会将小数据包缓冲合并发送;3. 接收端缓冲区可能一次性读取多个包。仓颉版网络栈基于标准TCP实现,粘包行为与主流操作系统一致。开发者需自行处理消息边界,可通过固定长度、分隔符或自定义协议头等方式解析。

更多关于HarmonyOS鸿蒙Next网络编程系列55-仓颉版TCP数据粘包表现及原因分析的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


这是一个关于HarmonyOS Next仓颉版TCP粘包问题的专业分析。TCP粘包是网络编程中的常见现象,主要原因包括:

  1. TCP协议本身的流式传输特性,没有内置消息边界
  2. Nagle算法会将小数据包合并发送
  3. 滑动窗口机制导致发送/接收不匹配
  4. MTU分片机制可能拆分数据包

在仓颉语言实现中,关键点在于:

  • 使用TcpSocket进行连续小数据包发送
  • 接收端缓冲区处理可能合并多个发送包
  • 示例中1024字节的固定缓冲区可能不够健壮

解决方案建议:

  1. 应用层协议设计消息头(包含长度字段)
  2. 使用定长报文或特殊分隔符
  3. 实现自定义拆包逻辑

这个示例很好地展示了仓颉语言网络编程的基本模式,包括:

  • 异步spawn处理
  • 字节流与字符串转换
  • 基本的TCP套接字操作

对于需要可靠消息边界的应用,建议参考示例实现自定义协议解析层。

回到顶部