首页 > 编程语言 >鸿蒙网络编程系列36-固定包头可变包体解决TCP粘包问题

鸿蒙网络编程系列36-固定包头可变包体解决TCP粘包问题

时间:2024-10-27 11:19:34浏览次数:3  
标签:36 TCP 粘包 let 缓冲区 数据包 服务端

1. TCP数据传输粘包简介

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

随后解释了粘包背后的可能原因,并给出了解决TCP传输粘包问题的两种思路,第一种是指定数据包结束标志,在本系列第35篇《鸿蒙网络编程系列35-通过数据包结束标志解决TCP粘包问题》中给出了具体的实现,第二种是通过固定包头指定包的长度,本文将通过一个示例演示这种思路的实现。

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

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

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

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

3. 固定包头可变包体解决TCP粘包问题示例编写

下面详细介绍创建该示例的步骤。
步骤1:创建Empty Ability项目。

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

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

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

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

import { socket } from '@kit.NetworkKit';
import { Decimal, util, buffer } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct Index {
  @State title: string = '固定包头可变包体演示示例';
  //服务端端口号
  @State port: number = 9990
  //服务端IP地址
  @State serverIp: string = ""
  //操作日志
  @State msgHistory: string = ''
  //最大缓存长度
  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 textEncoder = new util.TextEncoder();
    let encodeValue = textEncoder.encodeInto(msg)
    let sendBuf = buffer.alloc(2 + encodeValue.byteLength)
    //写入固定包头中的长度信息
    sendBuf.writeUInt16LE(encodeValue.byteLength)
    //写入可变包体信息
    sendBuf.write(msg, 2)
    await tcpSocket.send({ data: sendBuf.buffer })
  }

  //读取服务端发送过来的数据
  receiveMsgFromServer(value: socket.SocketMessageInfo) {
    //把接收到的数据复制到缓冲区有效数据尾部
    let copyCount = buffer.from(value.message).copy(this.receivedDataBuf, this.receivedDataLen)
    this.receivedDataLen += copyCount
    //至少写入了3个字节才需要解析
    if (this.receivedDataLen < 3) {
      return;
    }

    //当前数据包长度
    let packLen = this.receivedDataBuf.readUInt16LE()
    let textDecoder = util.TextDecoder.create("utf-8");
    //当前数据包长度加上固定包体的2字节,如果小于等于缓冲区已使用长度,就可以解析
    while ((packLen + 2) <= this.receivedDataLen) {
      //把可变包体中的数据转换为字符串
      let msgArray = new Uint8Array(this.receivedDataBuf.subarray(2, packLen + 2).buffer);
      let msg = textDecoder.decodeToString(msgArray)
      //剩余的未解析数据
      let leaveBufData = this.receivedDataBuf.subarray(packLen + 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"
      //至少写入了3个字节才需要解析,否则跳出循环
      if (this.receivedDataLen < 3) {
        break;
      }
      //开始查找下一个固定包头中的可变包体长度
      packLen = this.receivedDataBuf.readUInt16LE()
    }
    this.scroller.scrollEdge(Edge.Bottom)
  }
}

//休眠指定的毫秒数
function sleep(time: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, time));
}

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

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

4. 代码分析

本示例的关键点在于构造数据包的格式,具体数据包的格式是这样的,前两个字节为固定的包长度,使用小端的16位无符号整数表示,后面是包内容。以发送数据包为例,代码如下所示:

  async sendMsg2Server(tcpSocket: socket.TCPSocket, msg: string) {
    let textEncoder = new util.TextEncoder();
    let encodeValue = textEncoder.encodeInto(msg)
    let sendBuf = buffer.alloc(2 + encodeValue.byteLength)
    //写入固定包头中的长度信息
    sendBuf.writeUInt16LE(encodeValue.byteLength)
    //写入可变包体信息
    sendBuf.write(msg, 2)
    await tcpSocket.send({ data: sendBuf.buffer })
  }

这里首先把要发送的内容编码为Uint8Array类型,然后为缓冲区分配长度,长度为内容编码后的长度加上2,随后把内容长度作为无符号数写入缓冲区,然后把发送的内容也写入缓冲区,最后使用TCP客户端发送缓冲区到服务端。

接收时,首先把所有收到的数据都复制到接收缓冲区中,然后从缓冲区头部取两个字节作为数据包内容长度,然后判断接收缓冲区中已接收的数据是不是大于等于数据包内容长度加2,如果是,说明接收到了完整的数据包,就可以从中提取内容了,提取完毕把剩下的缓冲区数据移动到缓冲区头部,继续下一次循环,从缓冲区中提取完整数据包的数据,知道已接收的缓冲区小于数据包长度加2为止。相关代码位于方法receiveMsgFromServer中,源码包含了详细的注释,这里就不再赘述了。

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

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

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

标签:36,TCP,粘包,let,缓冲区,数据包,服务端
From: https://blog.csdn.net/tashanzhishi/article/details/143230066

相关文章

  • SpringBoot面向爱宠人群的宠物资讯系统36as8--(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表用户,宠物资讯,宠物知识开题报告内容一、选题背景与意义随着生活水平的提高和独居人口的增加,宠物已成为许多家庭的重要成员。宠物经济的蓬勃发展催生了对宠物......
  • Tcp协议讲解与守护进程
    TCP协议:面向链接,面向字节流,可靠通信创建tcp_server1.创建套接字域:依旧选择AF_INET连接方式: 选择SOCK_STREAM可靠的2.bind3.监听装置         client要通信,要先建立连接,client主动建立连接,所以服务端要一直等待连接4.获取连接  成功返回新的s......
  • RF/射频器件: CMD246C4 CMD235C4 CMD236C4 CMD254C3 CMD299K4 CMD262 一款5 W GaN MMI
    CMD246C4是一款宽带GaAsMMIC低相位噪声放大器,采用无引脚表贴封装,非常适合军事、航天和通信系统。16GHz时,该器件提供17dB的增益,饱和输出功率为+18dBm,噪声系数为5dB。此外,对于10GHz的输入信号,该放大器在10kHz失调下具有-165dBc/Hz的低相位噪声性能。CMD235C4是一款宽带MMI......
  • 网络协议基础(2):socket套接字及TCP、UDP的实现
    socket套接字及TCP、UDP的实现socket套接字socket的基本概念socket的类型Socket的工作流程Socket的编程接口(C++示例)1.创建Socket2.绑定地址3.监听连接4.接受连接5.连接到服务器6.发送数据7.接收数据8.关闭Socketsocket相关的结构体sockaddr结构体sockaddr......
  • TCP连接状态是TIME_WAIT的场景解析
    在Tomcat处理网络请求时,TIME_WAIT状态通常是TCP连接关闭过程中的一个阶段。这个状态主要与TCP的四次挥手(Four-WayHandshake)有关。以下是在Tomcat处理网络请求时,连接状态变为TIME_WAIT的具体情况:四次挥手过程1.客户端发送FIN包:客户端完成数据传输后,主动调用clos......
  • 【ModbusTCP与Profibus DP双向互转说明】
        Profibusdp和ModbusTCP均为工业通信协议。ModbusTCP为串行通讯协议,已成为工业领域通讯协议的业界标准。Modbus是现在国内工业领域应用最多的协议,不只PLC设备,各种终端设备,比如水控机、水表、电表、工业秤、各种采集设备。而Profibus为自动化技术的现场总线标准,广泛......
  • TCP连接的状态
    TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP连接的状态可以通过一个状态机来描述,这个状态机定义了TCP连接从建立到关闭过程中可能经历的各种状态。一、状态状态名称描述触发条件CLOSED表示没有连接。这是初始状态。无LISTEN服务......
  • 鸿蒙网络编程系列35-通过数据包结束标志解决TCP粘包问题
    1.TCP数据传输粘包简介在本系列的第6篇文章《鸿蒙网络编程系列6-TCP数据粘包表现及原因分析》中,我们演示了TCP数据粘包的表现,如图所示:随后解释了粘包背后的可能原因,并给出了解决TCP传输粘包问题的两种思路,其中一种就是指定数据包结束标志,本节将通过一个示例演示这种思路......
  • 【保姆级IDF】ESP32使用WIFI作为AP模式TCP通信:连接客户端+一对多通信
    #1024程序员节|征文#Tips:抛砖引玉,本文记录ESP32学习过程中遇到的收获。如有不对的地方,欢迎指正。1.前言    关于ESP32的WIFI这部分基础知识,在网上可以找到许多,包括TCP协议、套接字等等,博主之前的文章也有介绍,在此本文不再赘述,直接讲清楚标题功能如何实现,并说明......
  • abc368_G
    G-AddandMultiplyQueries思路开始直接用的线段树,写完才意识到是假的由于题目说答案不会超过\(10^{18}\),所以一个询问区间内的大于2的b的个数不超过64个,这样一个区间内大于2的b的就可以把a分成不超过64个连续的区间,用树状数组维护,b大于2的位置可以用线段树二分或者set的做......