首页 > 编程语言 >鸿蒙网络编程系列35-通过数据包结束标志解决TCP粘包问题

鸿蒙网络编程系列35-通过数据包结束标志解决TCP粘包问题

时间:2024-10-25 10:18:41浏览次数:3  
标签:标志 TCP 粘包 let 35 数据包 服务端

1. TCP数据传输粘包简介

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

随后解释了粘包背后的可能原因,并给出了解决TCP传输粘包问题的两种思路,其中一种就是指定数据包结束标志,本节将通过一个示例演示这种思路的实现。

2. 数据包结束标志解决TCP粘包问题演示

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

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

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

3. 数据包结束标志解决TCP粘包问题示例编写

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

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

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

这里添加了读取和设置Wifi配置的权限。

步骤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 = ''
  //数据包结束标志
  packetEndFlag: string = "\r\n"
  //最大缓存长度
  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 senderMsg = msg + this.packetEndFlag
    await tcpSocket.send({ data: senderMsg })
  }

  //读取服务端发送过来的数据
  receiveMsgFromServer(value: socket.SocketMessageInfo) {
    //把接收到的数据复制到缓冲区有效数据尾部
    let copyCount = buffer.from(value.message).copy(this.receivedDataBuf, this.receivedDataLen)
    //缓冲区已使用长度加上本次接收的数据长度
    this.receivedDataLen += copyCount
    //缓冲区数据中数据包结束标志的位置
    let endFlagPos = this.receivedDataBuf.subarray(0, this.receivedDataLen).indexOf(this.packetEndFlag)
    let textDecoder = util.TextDecoder.create("utf-8");
    while (endFlagPos > -1) {
      //把数据包结束标志前面的数据转换为字符串
      let msgArray = new Uint8Array(this.receivedDataBuf.subarray(0, endFlagPos).buffer);
      let msg = textDecoder.decodeToString(msgArray)
      //剩余的未解析数据
      let leaveBufData = this.receivedDataBuf.subarray(endFlagPos + 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"
      //开始查找下一个数据包结束标志
      endFlagPos = this.receivedDataBuf.subarray(0, this.receivedDataLen).indexOf(this.packetEndFlag)
    }
    this.scroller.scrollEdge(Edge.Bottom)
  }
}

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

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

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

4. 代码分析

本示例的关键点在于设置数据包的结束标志,虽然TCP数据本身是没有边界的,但是通过人为添加额外的数据包分界标志,就可以把发送者的意图通过分界标志表示出来。为简单起见,本示例把结束标志设置为回车换行符,读者也可以使用其他的标志,需要注意的是,在发送信息的时候,信息本身不能包含结束标志符号,针对本示例而言,发送信息本身不能包含回车换行符序列,否则会出现意想不到的结果。

发送时添加结束标志的代码如下所示:

    //发送的原始数据后加上数据包结束标志
    let senderMsg = msg + this.packetEndFlag
    await tcpSocket.send({ data: senderMsg })

接收时,首选把所有收到的数据都复制到接收缓冲区中,然后遍历缓冲区查找结束标志,找到后就把结束标志以前的部分作为完整数据包提取出来使用,剩余的部分复制到缓冲区首部进行下一次遍历,直到找不到结束标志为止,相关代码位于方法receiveMsgFromServer中,源码包含了详细的注释,这里就不再赘述了。

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

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

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

标签:标志,TCP,粘包,let,35,数据包,服务端
From: https://blog.csdn.net/tashanzhishi/article/details/143204885

相关文章

  • CF35C. Fire Again 题解 bfs求最短路
    题目链接:https://codeforces.com/problemset/problem/35/C视频讲解:https://www.bilibili.com/video/BV1tZ1FYPELp/?p=4以每个着火的点为起点求最短路,然后输出任意一个距离值最大的点即可。需要注意的是:本题是文件输入输出。示例程序:#include<bits/stdc++.h>usingnamespace......
  • 汽车级DC-DC转换器英飞凌TLF35584
    上汽荣威都在用的汽车级DC-DC转换器英飞凌TLF35584今天平台君从IPBrain数据库中给大家带来的一款由Infineon(英飞凌)推出的一款多路输出安全电源芯片,具备高可靠性和安全性。适用于汽车电子系统中的多种应用场景,如车身控制、安全气囊、防抱死制动系统,电子稳定控制系统等。......
  • P3547 [POI2013] CEN-Price List
    很不错的图论题。考虑对\(a,2a,b\)大小进行讨论。\(2a\leb\),这种情况是简单的,根本不会走\(b\)边,直接bfs即可,此时答案为\(d*a\)。\(a\leb<2a\),这种情况下能走两条\(a\)边就会用\(b\)边替换掉,同时不用担心三元环的情况(因为三元环会出现三个点最短路都是\(1......
  • M68LC302CAF20VCT,MMC2107CFCPU33,MC9S12UF32PUM,S9S12DJ12F1MPVEMCF52235CVM60MAC7121MA
    NXPSemiconductors公司的产品和技术还广泛应用于安全和身份验证领域,包括智能卡、支付系统、身份识别和生物识别技术。此外,该公司还在电源管理、射频技术和传感器领域拥有丰富的经验和专业知识。恩智浦的产品不仅提供高性能和创新的解决方案,还致力于保证产品的安全性。NXPSem......
  • 网络安全人员必知的35个安全框架及模型
    一、概括网络安全专业机构制定的一套标准、准则和程序,旨在帮助组织了解和管理面临的网络安全风险。优秀的安全框架及模型应该为用户提供一种可靠方法,帮助其实现网络安全建设计划。对于那些希望按照行业最佳实践来设计或改进安全策略的组织或个人来说,网络安全框架及模型是不可......
  • COP3502 P2: RLE with Images Python
    COP3502P2:RLEwithImagesPythonOverviewInthisprojectstudentswilldeveloproutinestoencodeanddecodedataforimagesusingrun-lengthencodingRLE).Studentswillimplementencodinganddecodingofrawdata,conversionbetweendataandstring......
  • 为什么你的网卡收不到不同网段的数据包?
    一、前言最近开发过程中遇到个小问题,板子与客户的模块通过一款交换芯片连接,客户的模块会向我的板子发送组播报文,但是模块和我的板子并在同一个网段,默认情况下,这些数据包会被网卡过滤掉,那么我要如果通过套接字接收网卡上不通网段的数据包呢?这就涉及到一个知识点,网卡的混杂模......
  • 洛谷题单指南-字符串-P4735 最大异或和
    原题链接:https://www.luogu.com.cn/problem/P4735题意解读:已知长度为n的数组a[],要在l~r范围找到一个p,使得a[p]^a[p+1]^...^a[n]^x最大,求这个最大的异或值。解题思路:1、利用前缀和将问题转化设s[]是a[]的前缀异或数组,要计算a中一段范围l~r的异或,可以借助于s由于s[r]=a[0]^a[......
  • abc358E Alphabet Tiles
    现有大写英文字母A-Z,个数分别为C[i],总共可以组成多少个长度在[1,K]之间的不同串?答案对998244353取模。1<=K<=1000,0<=C[i]<=1000分析:记dp[i][k]表示前i类字母构成长度为k的不同方案数,枚举第i类字母的个数j进行转移。#include<bits/stdc++.h>usingi64=longlong;templat......
  • 【原创】RK3588/RK3568/RK3562平台 IgH EthercAT主站编译安装
    目录igh主站编译安装说明一、配置内核自带网卡驱动编译为模块1.内核配置编译内核编译内核模块二、交叉编译EtherCAT主站1.普通linux或preempt-rt1.1配置1.2编译1.3安装到TF卡根目录2.xenomai2.1交叉编译xenomai库2.2配置2.3编译2.4安装到TF卡根目录四、安装目录打......