HarmonyOS鸿蒙Next网络编程系列57-仓颉版固定包头可变包体解决TCP粘包问题

HarmonyOS鸿蒙Next网络编程系列57-仓颉版固定包头可变包体解决TCP粘包问题

1. TCP粘包问题解决思路

本系列的上一篇文章《鸿蒙网络编程系列56-仓颉版通过数据包结束标志解决TCP粘包问题》演示了解决粘包问题的一种方案,不过,通过结束标志解决粘包问题有一点缺陷,就是发送的信息里面不能包括结束标志本身,否则就要通过转义的方式解决,这样会带来一些不便。更常用的解决方案是自己构造一种数据包,这种数据包包括固定长度的包头和可变的包体,固定的包头记录了数据包的元信息,比如可变包体的长度等等,可变包体里是实际发送的数据。一个最简单的固定包头可变包体的示意图如下所示,该示意图演示了封装前后的对比:

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

本文将通过这种固定包头可变包体的方法来解决粘包问题,也就是如上图所示,给要发送的信息添加上一个固定2字节的包头,记录包体的实际长度。本示例将使用仓颉语言在API17的环境下编写,下面是详细的示例演示。

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

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

输入TCP回声服务器的IP地址和端口,然后单击“测试”按钮,发送0到98的数字字符串到服务端,服务端会回传收到的信息,本示例在收到服务器信息后在日志区域输出,如图所示: 从图中可以看出,本示例彻底解决了数据粘包问题,收到的信息和发送时保持一致。

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

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 = '固定包头可变包体演示示例';
    //连接、通讯历史记录
    @State
    var msgHistory: String = ''

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

    //数据包结束标志
    var packetEndFlag: String = "\r\n"
    //最大缓存长度
    var maxBufSize: Int64 = 1024 * 8
    //接收数据缓冲区
    var receivedDataBuf: Array<UInt8> = Array<UInt8>(maxBufSize, item: 0)
    //缓冲区已使用长度
    var receivedDataLen: Int64 = 0

    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) {
        while (true) {
            //从socket读取数据
            var readCount = tcpClient.read(receivedDataBuf[receivedDataLen..])
            //如果读取的字节数为0,表明对端关闭,直接退出
            if (readCount == 0) {
                return
            }

            //缓冲区已使用长度加上本次接收的数据长度
            receivedDataLen += readCount

            //缓冲区已读取字节数大于等于2,说明数据包长度已经可以读取了
            while (receivedDataLen >= 2) {
                //读取包体长度
                var packDatalen = Int64(receivedDataBuf[0]) + Int64(UInt16(receivedDataBuf[1]) << 8u8)

                //缓冲区已读取字节数小于可变包体长度+2,说明缓冲区不包含完整的数据包,跳出内循环,从外层循环的套接字继续读取
                if (receivedDataLen < packDatalen + 2) {
                    break
                } else { //缓冲区包含完整的数据包
                    let content = String.fromUtf8(receivedDataBuf[2..packDatalen + 2])
                    //输出接收的消息到日志
                    msgHistory += "S:${content}\r\n"

                    //完整包后未处理的字节数
                    let undealByteLen = receivedDataLen - 2 - packDatalen

                    //把未处理的字节复制到缓冲区头部
                    receivedDataBuf.copyTo(receivedDataBuf, 2 + packDatalen, 0, undealByteLen)

                    //把未处理的字节数作为缓冲区已读取字节数
                    receivedDataLen = undealByteLen
                }
            }
        }
    }

    //粘包测试
    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) {
                    sendMsg2Server(tcpClient, i.toString())
                    //随即休眠不超过10毫秒的时间
                    sleep(Duration.millisecond * m.nextInt64(10))
                }
            } catch (exp: Exception) {
                msgHistory += "发送数据到服务器异常:${exp}\r\n"
            }
        }
    }

    //把msg封装后发送到到服务端
    func sendMsg2Server(tcpClient: TcpSocket, msg: String) {
        //要发送的消息长度
        let length = UInt16(msg.size)
        //长度低字节
        let low = UInt8(length & 0xffu16)
        //长度高字节
        let high = UInt8(length >> 8u8)

        //发送低字节
        tcpClient.write(low)
        //发送高字节
        tcpClient.write(high)
        //发送内容
        tcpClient.write(msg.toArray())
    }
}

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

步骤6:按照本文第2部分“固定包头可变包体解决TCP粘包问题演示”操作即可。

4. 代码分析

本示例的关键点在于构造数据包的格式,通过函数sendMsg2Server实现,具体数据包的格式是这样的,前两个字节为固定的包长度,发送数据时先发送低字节,再发送高字节,最后发送实际的信息。接收时也一样,把接收到的数据的前两个字节作为数据包可变包体的长度,也是低字节在前,高字节在后,当读取到给定长度的数据时,就可以从中提取出完整的信息。

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

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

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


更多关于HarmonyOS鸿蒙Next网络编程系列57-仓颉版固定包头可变包体解决TCP粘包问题的实战教程也可以访问 https://www.itying.com/category-93-b0.html

2 回复

在鸿蒙Next仓颉版中,处理TCP粘包问题可通过固定包头+可变包体方案实现。具体步骤:

  1. 定义固定长度包头(如4字节),存储包体长度;
  2. 读取时先解析包头获取包体长度;
  3. 按长度读取完整包体。

仓颉提供ByteArrayReader/Writer工具类,支持readInt32()等基础类型操作。关键代码片段:

// 发送端
let writer = new util.ByteArrayWriter();
writer.writeInt32(data.length);  // 写入包头(包体长度)
writer.writeBytes(data);         // 写入包体
socket.write(writer.getBytes());

// 接收端
let header = await socket.readBytes(4); 
let bodyLen = new DataView(header.buffer).getInt32(0);
let body = await socket.readBytes(bodyLen);

更多关于HarmonyOS鸿蒙Next网络编程系列57-仓颉版固定包头可变包体解决TCP粘包问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


这是一个很好的TCP粘包问题解决方案示例。关键点在于使用了固定2字节包头来标识包体长度,这种设计比结束标志更可靠,因为:

  1. 包头固定2字节(低字节在前)存储包体长度,解决了数据内容不能包含结束标志的限制
  2. 接收端通过先读取包头获取包体长度,再读取对应长度的数据,确保完整包处理
  3. 处理完一个完整包后,将剩余数据移动到缓冲区头部继续处理,避免数据丢失

代码中的几个关键实现:

  • sendMsg2Server函数实现了封包逻辑,先发送长度低字节,再高字节,最后数据内容
  • readMsgFromServer函数实现了解包逻辑,先读取2字节包头获取长度,再读取对应长度数据
  • 使用receivedDataBuf缓冲区和receivedDataLen记录处理位置

这种方案是TCP网络编程中处理粘包的经典方法,在HarmonyOS仓颉语言中实现也很简洁。相比结束标志方案,能处理任意二进制数据,适用性更广。

回到顶部