1. TCP粘包問題解決思路
在本系列的上一篇文章演示了TCP數據粘包的原因以及可能的解決方法,本文將通過其中的添加數據包結束標志的方法來解決這個問題。我們知道,數據粘包的原因是因為發送的時候沒有標明數據包的邊界,那么,我們人為在每一個數據包發送的時候都加上這個邊界就可以了。這個邊界我們稱為數據包結束標志,在發送端發送消息的時候,固定在消息尾部附加上這個標志,同樣,在接收的時候,分析接收到的消息,從中提取出數據包結束標志,那么,這個標志前面的部分就是完整的消息。本示例將使用倉頡語言在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_entryimport 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 {@Statevar title: String = '數據包結束標志演示示例';//連接、通訊歷史記錄@Statevar msgHistory: String = ''//服務端ip地址@Statevar serverIp: String = "*.*.*.*"//服務端端口@Statevar 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 = 0let 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//如果已接收的數據長度小于結束標志的數據長度,直接開始下一輪循環if (receivedDataLen < packetEndFlag.size) {continue}//查找結束標志第一次出現的位置var matchFlagPos = receivedDataBuf[0..receivedDataLen].indexOf(packetEndFlag.toArray())//如果找到了結束標志,就輸出內容,一直循環,直到找不到結束標志,然后從外層循環再次讀取Socketwhile (let Some(pos) <- matchFlagPos) {//把接收到的數據轉換為字符串,不包括結束標志let content = String.fromUtf8(receivedDataBuf[0..pos])//輸出接收到消息到日志msgHistory += "S:${content}\r\n"//結束標志后未處理的字節數let undealByteLen = receivedDataLen - pos - packetEndFlag.size//把未處理的字節復制到緩沖區頭部receivedDataBuf.copyTo(receivedDataBuf, pos + packetEndFlag.size, 0, undealByteLen)//把未處理的字節數作為緩沖區已使用長度receivedDataLen = undealByteLen//查找下一個結束標志matchFlagPos = receivedDataBuf[0..receivedDataLen].indexOf(packetEndFlag.toArray())}}}//粘包測試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"}}}//附加上結束標志后發送數據到服務端func sendMsg2Server(tcpClient: TcpSocket, msg: String) {let senderMsg = msg + packetEndFlagtcpClient.write(senderMsg.toArray())}
}
步驟5:編譯運行,可以使用模擬器或者真機。
步驟6:按照本文第2部分“數據包結束標志解決TCP粘包問題演示”操作即可。
4. 代碼分析
通過數據包結束標志解決粘包問題的關鍵點是數據發送和數據接收,相對來說,數據發送比較簡單,如函數sendMsg2Server所示,只需要把結束標志附件到消息后面即可。
但是,接收的時候,處理就稍微復雜一些。接收時,會把收到的數據都放到緩沖區receivedDataBuf中,并且記錄接收到的數據長度,然后從已接收的數據中查找結束標志,如果找到了結束標志,就以此為界提取標志前的數據為完整消息,然后繼續把余下的數據移動到緩沖區頭部再進行下一次的查找。如果沒有找到結束標志,表示當前接收的數據不完整,還需要接續從套接字讀取數據。詳細的接收代碼在函數readMsgFromServer中,具體執行流程可以參考代碼注釋。
(本文作者原創,除非明確授權禁止轉載)
本文源碼地址:
https://gitee.com/zl3624/harmonyos_network_samples/tree/master/code/tcp/PacketEndFlag4Cj
本系列源碼地址:
https://gitee.com/zl3624/harmonyos_network_samples