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