鴻蒙網(wǎng)絡(luò)編程系列36-固定包頭可變包體解決TCP粘包問(wèn)題

1. TCP數(shù)據(jù)傳輸粘包簡(jiǎn)介

在本系列的第6篇文章《鴻蒙網(wǎng)絡(luò)編程系列6-TCP數(shù)據(jù)粘包表現(xiàn)及原因分析》中顿仇,我們演示了TCP數(shù)據(jù)粘包的表現(xiàn)忍坷,如圖所示:

image.png

隨后解釋了粘包背后的可能原因,并給出了解決TCP傳輸粘包問(wèn)題的兩種思路笆制,第一種是指定數(shù)據(jù)包結(jié)束標(biāo)志检诗,在本系列第35篇《鴻蒙網(wǎng)絡(luò)編程系列35-通過(guò)數(shù)據(jù)包結(jié)束標(biāo)志解決TCP粘包問(wèn)題》中給出了具體的實(shí)現(xiàn),第二種是通過(guò)固定包頭指定包的長(zhǎng)度逞度,本文將通過(guò)一個(gè)示例演示這種思路的實(shí)現(xiàn)。

2. 固定包頭可變包體解決TCP粘包問(wèn)題演示

本示例運(yùn)行后的界面如圖所示:

image.png

和上一篇文章類似妙啃,輸入服務(wù)端的地址档泽,這里可以使用本系列第25篇文章《鴻蒙網(wǎng)絡(luò)編程系列25-TCP回聲服務(wù)器的實(shí)現(xiàn)》中創(chuàng)建的TCP回聲服務(wù)器袍啡,也可以使用其他類似的回聲服務(wù)器胖秒;然后輸入服務(wù)器端口,最后單擊"測(cè)試"按鈕循環(huán)發(fā)送0到99的數(shù)字字符串到服務(wù)端梨撞,服務(wù)端會(huì)回傳收到的信息燥滑,本示例在收到服務(wù)器信息后在日志區(qū)域輸出渐北,如圖所示:

image.png

從中可以看出,這次也徹底解決了數(shù)據(jù)粘包問(wèn)題铭拧,收到的信息和發(fā)送時(shí)保持一致赃蛛。

3. 固定包頭可變包體解決TCP粘包問(wèn)題示例編寫

下面詳細(xì)介紹創(chuàng)建該示例的步驟。
步驟1:創(chuàng)建Empty Ability項(xiàng)目搀菩。

步驟2:在module.json5配置文件加上對(duì)權(quán)限的聲明:

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

這里添加了訪問(wèn)互聯(lián)網(wǎng)的權(quán)限呕臂。

步驟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 = '固定包頭可變包體演示示例';
  //服務(wù)端端口號(hào)
  @State port: number = 9990
  //服務(wù)端IP地址
  @State serverIp: string = ""
  //操作日志
  @State msgHistory: string = ''
  //最大緩存長(zhǎng)度
  maxBufSize: number = 1024 * 8
  //接收數(shù)據(jù)緩沖區(qū)
  receivedDataBuf: buffer.Buffer = buffer.alloc(this.maxBufSize)
  //緩沖區(qū)已使用長(zhǎng)度
  receivedDataLen: number = 0
  //日志顯示區(qū)域的滾動(dòng)容器
  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("服務(wù)端地址:")
            .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("測(cè)試")
            .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%')
  }

  //測(cè)試
  async test() {
    //服務(wù)端地址
    let serverAddress: socket.NetAddress = { address: this.serverIp, port: this.port, family: 1 }
    //執(zhí)行TCP通訊的對(duì)象
    let tcpSocket: socket.TCPSocket = socket.constructTCPSocketInstance()
    //收到消息時(shí)的處理
    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`;
      })

    //循環(huán)發(fā)送0到99的數(shù)字字符串到服務(wù)端
    for (let i = 0; i < 100; i++) {
      let msg = i.toString()
      await this.sendMsg2Server(tcpSocket, msg)
      let sleepTime = Decimal.random().toNumber() + 0.5
      //休眠sleepTime時(shí)間,大概0.5毫秒到1.5毫秒
      await sleep(sleepTime)
    }
  }

  //發(fā)送數(shù)據(jù)到服務(wù)端
  async sendMsg2Server(tcpSocket: socket.TCPSocket, msg: string) {
    let textEncoder = new util.TextEncoder();
    let encodeValue = textEncoder.encodeInto(msg)
    let sendBuf = buffer.alloc(2 + encodeValue.byteLength)
    //寫入固定包頭中的長(zhǎng)度信息
    sendBuf.writeUInt16LE(encodeValue.byteLength)
    //寫入可變包體信息
    sendBuf.write(msg, 2)
    await tcpSocket.send({ data: sendBuf.buffer })
  }

  //讀取服務(wù)端發(fā)送過(guò)來(lái)的數(shù)據(jù)
  receiveMsgFromServer(value: socket.SocketMessageInfo) {
    //把接收到的數(shù)據(jù)復(fù)制到緩沖區(qū)有效數(shù)據(jù)尾部
    let copyCount = buffer.from(value.message).copy(this.receivedDataBuf, this.receivedDataLen)
    this.receivedDataLen += copyCount
    //至少寫入了3個(gè)字節(jié)才需要解析
    if (this.receivedDataLen < 3) {
      return;
    }

    //當(dāng)前數(shù)據(jù)包長(zhǎng)度
    let packLen = this.receivedDataBuf.readUInt16LE()
    let textDecoder = util.TextDecoder.create("utf-8");
    //當(dāng)前數(shù)據(jù)包長(zhǎng)度加上固定包體的2字節(jié)肪跋,如果小于等于緩沖區(qū)已使用長(zhǎng)度歧蒋,就可以解析
    while ((packLen + 2) <= this.receivedDataLen) {
      //把可變包體中的數(shù)據(jù)轉(zhuǎn)換為字符串
      let msgArray = new Uint8Array(this.receivedDataBuf.subarray(2, packLen + 2).buffer);
      let msg = textDecoder.decodeToString(msgArray)
      //剩余的未解析數(shù)據(jù)
      let leaveBufData = this.receivedDataBuf.subarray(packLen + 2, this.receivedDataLen)
      //剩余的未解析數(shù)據(jù)移動(dòng)到緩沖區(qū)頭部
      for (let pos = 0; pos < leaveBufData.length; pos++) {
        this.receivedDataBuf.writeUInt8(leaveBufData.readUInt8(pos), pos)
      }
      //重新設(shè)置緩沖區(qū)已使用長(zhǎng)度
      this.receivedDataLen = leaveBufData.length
      //輸出接收的數(shù)據(jù)到日志
      this.msgHistory += "S:" + msg + "\r\n"
      //至少寫入了3個(gè)字節(jié)才需要解析,否則跳出循環(huán)
      if (this.receivedDataLen < 3) {
        break;
      }
      //開(kāi)始查找下一個(gè)固定包頭中的可變包體長(zhǎng)度
      packLen = this.receivedDataBuf.readUInt16LE()
    }
    this.scroller.scrollEdge(Edge.Bottom)
  }
}

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

步驟4:編譯運(yùn)行州既,可以使用模擬器或者真機(jī)谜洽。

步驟5:按照本文第2部分“數(shù)據(jù)包結(jié)束標(biāo)志解決TCP粘包問(wèn)題演示”操作即可。

4. 代碼分析

本示例的關(guān)鍵點(diǎn)在于構(gòu)造數(shù)據(jù)包的格式吴叶,具體數(shù)據(jù)包的格式是這樣的褥琐,前兩個(gè)字節(jié)為固定的包長(zhǎng)度,使用小端的16位無(wú)符號(hào)整數(shù)表示晤郑,后面是包內(nèi)容敌呈。以發(fā)送數(shù)據(jù)包為例贸宏,代碼如下所示:

  async sendMsg2Server(tcpSocket: socket.TCPSocket, msg: string) {
    let textEncoder = new util.TextEncoder();
    let encodeValue = textEncoder.encodeInto(msg)
    let sendBuf = buffer.alloc(2 + encodeValue.byteLength)
    //寫入固定包頭中的長(zhǎng)度信息
    sendBuf.writeUInt16LE(encodeValue.byteLength)
    //寫入可變包體信息
    sendBuf.write(msg, 2)
    await tcpSocket.send({ data: sendBuf.buffer })
  }

這里首先把要發(fā)送的內(nèi)容編碼為Uint8Array類型,然后為緩沖區(qū)分配長(zhǎng)度磕洪,長(zhǎng)度為內(nèi)容編碼后的長(zhǎng)度加上2吭练,隨后把內(nèi)容長(zhǎng)度作為無(wú)符號(hào)數(shù)寫入緩沖區(qū),然后把發(fā)送的內(nèi)容也寫入緩沖區(qū)析显,最后使用TCP客戶端發(fā)送緩沖區(qū)到服務(wù)端鲫咽。

接收時(shí),首先把所有收到的數(shù)據(jù)都復(fù)制到接收緩沖區(qū)中谷异,然后從緩沖區(qū)頭部取兩個(gè)字節(jié)作為數(shù)據(jù)包內(nèi)容長(zhǎng)度分尸,然后判斷接收緩沖區(qū)中已接收的數(shù)據(jù)是不是大于等于數(shù)據(jù)包內(nèi)容長(zhǎng)度加2,如果是歹嘹,說(shuō)明接收到了完整的數(shù)據(jù)包箩绍,就可以從中提取內(nèi)容了,提取完畢把剩下的緩沖區(qū)數(shù)據(jù)移動(dòng)到緩沖區(qū)頭部尺上,繼續(xù)下一次循環(huán)材蛛,從緩沖區(qū)中提取完整數(shù)據(jù)包的數(shù)據(jù),知道已接收的緩沖區(qū)小于數(shù)據(jù)包長(zhǎng)度加2為止怎抛。相關(guān)代碼位于方法receiveMsgFromServer中卑吭,源碼包含了詳細(xì)的注釋,這里就不再贅述了马绝。

(本文作者原創(chuàng)豆赏,除非明確授權(quán)禁止轉(zhuǎn)載)

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市富稻,隨后出現(xiàn)的幾起案子河绽,更是在濱河造成了極大的恐慌,老刑警劉巖唉窃,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異纹笼,居然都是意外死亡纹份,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門廷痘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蔓涧,“玉大人,你說(shuō)我怎么就攤上這事笋额≡” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵兄猩,是天一觀的道長(zhǎng)茉盏。 經(jīng)常有香客問(wèn)我鉴未,道長(zhǎng),這世上最難降的妖魔是什么鸠姨? 我笑而不...
    開(kāi)封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任铜秆,我火速辦了婚禮,結(jié)果婚禮上讶迁,老公的妹妹穿的比我還像新娘连茧。我一直安慰自己,他們只是感情好巍糯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布啸驯。 她就那樣靜靜地躺著,像睡著了一般祟峦。 火紅的嫁衣襯著肌膚如雪罚斗。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天搀愧,我揣著相機(jī)與錄音惰聂,去河邊找鬼。 笑死咱筛,一個(gè)胖子當(dāng)著我的面吹牛搓幌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播迅箩,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼溉愁,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了饲趋?” 一聲冷哼從身側(cè)響起拐揭,我...
    開(kāi)封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎奕塑,沒(méi)想到半個(gè)月后堂污,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡龄砰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年盟猖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片换棚。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡式镐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出固蚤,到底是詐尸還是另有隱情娘汞,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布夕玩,位于F島的核電站你弦,受9級(jí)特大地震影響惊豺,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鳖目,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一扮叨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧领迈,春花似錦彻磁、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至尘喝,卻和暖如春磁浇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背朽褪。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工置吓, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缔赠。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓衍锚,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親嗤堰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子戴质,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容