1. TCP數(shù)據(jù)傳輸粘包簡(jiǎn)介
在本系列的第6篇文章《鴻蒙網(wǎng)絡(luò)編程系列6-TCP數(shù)據(jù)粘包表現(xiàn)及原因分析》中顿仇,我們演示了TCP數(shù)據(jù)粘包的表現(xiàn)忍坷,如圖所示:
隨后解釋了粘包背后的可能原因,并給出了解決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)行后的界面如圖所示:
和上一篇文章類似妙啃,輸入服務(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ū)域輸出渐北,如圖所示:
從中可以看出,這次也徹底解決了數(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