Swift 處理TCP粘包
CocoaAsyncSocket
如果使用CocoaAsyncSocket來和服務器端進行TCP通信,那麼它收發TCP數據包都需要通過Data類型來完成。如下:
class IMClient: GCDAsyncSocketDelegate {
// connect
func socket(_ sock: GCDAsyncSocket, didConnectToHost host: String, port: UInt16) {
// 監聽數據
tcpClient?.readData(withTimeout: -1, tag: 0)
}
// disconnect
func socketDidDisconnect(_ sock: GCDAsyncSocket, withError err: Error?) {
}
// receive data
func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) {
// 監聽數據
tcpClient?.readData(withTimeout: -1, tag: 0)
}
}
Data是什麼,怎麼讀和寫?請看下面
Swift Data基礎
寫入和讀取
data.append(other: Data) // 末尾追加Data
data.append(newElement: UInt8) // 末尾追加UInt8
// 拷貝指定區間的數據
let buffer = data.subdata(in: start..<data.count)
// 也可以直接通過下標訪問
// 取1個
let item = data[0]
let itemValue = UInt8(item) // 轉換成UInt8,可以打印
// 取區間
let slice = data[0..<12] // 不包括12,長度12
let sliceBytes = [UInt8](slice) // 轉換成 UInt8數組
替換
也可以像C/C++ memcpy 裏面一樣,拷貝內存
// 聲明一組二進制數組,隨機填入數字
let bytes: [UInt8] = [12, 32, 12, 23, 42, 24, 24, 24, 24, 24, 24, 24, 42, 123, 124, 12, 55, 36, 46, 77, 86]
// 構造data對象
var data = Data()
// Swift [UInt8]數組轉 Data
data.append(Data(bytes: bytes, count: bytes.count))
print("old:\(data)") // old:21 bytes
let start = 5
// 從data裏面讀取指定區間的數據,包下標,start能取到,data.count不會取到
// 模擬讀取了部分數據(在xcode中,鼠標移動到該變量上,可以點擊“!”查看)
let buffer = data.subdata(in: start..<data.count)
print("buffer:\(buffer)") // buffer:16 bytes
// replace
data.replaceSubrange(0..<buffer.count, with: buffer)
print("replace:\(data)")
輸出
// 即 [12, 32, 12, 23, 42, 24, 24, 24, 24, 24, 24, 24, 42, 123, 124, 12, 55, 36, 46, 77, 86]
old:21 bytes
// 5-21:即 [24, 24, 24, 24, 24, 24, 24, 42, 123, 124, 12, 55, 36, 46, 77, 86]
buffer:16 bytes
// 即 [24, 24, 24, 24, 24, 24, 24, 42, 123, 124, 12, 55, 36, 46, 77, 86, 55, 36, 46, 77, 86]
// 使用5-21的數據覆蓋了0-16位置的數據
replace:21 bytes
處理TCP粘包
釋義
造成TCP粘包的原因有很多,我根據自己的理解畫了一下:
第一種情況:
發送發發送一個2048字節大小的包,到接收方CocoaAsyncSocket回調會有2次,可能第一個包爲1408(Data1),第二個爲640(Data2)。
此時需要把2個包合在一起纔算完整。當然不需要考慮亂序的問題,因爲TCP已經幫我們處理了,這也是區別於UDP的地方,不然我們還需要處理亂序的問題。
第二種情況
我沒實測過,不過建議還是處理一下。
解決方案
通常爲了解決這個問題,我們需要定義一個固定長度的頭部,在頭部記錄數據部的長度多大,這樣後續拆包就好處理了。具體見後面:協議頭一節。
實例
// receive data
func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) {
IMLog.debug(item: "IMClient socket receive data,len=\(data.count)")
// 是否足夠長,數據包完整,否則加入到緩衝區
if IMHeader.isAvailable(data: data) {
recvBufferLen = 0 // 重置,即使還有數據,以免持續惡化
let len = _resolveData(data: data)
// 這裏沒有處理第2種情況,即收到了一個大包,裏面包含多個小包,需要拆分。後續發現了修復 FIXME
if data.count != len{
IMLog.error(item: "data is reset,fix me")
}
} else {
IMLog.warn(item: "data is not enough!")
// 追加上去之後,嘗試解析
let newLen = recvBufferLen + data.count
recvBuffer.replaceSubrange(recvBufferLen..<newLen, with: data)
recvBufferLen = newLen
var start = 0
while true {
let reset = recvBuffer.subdata(in: start..<recvBufferLen)
// 不足夠長
if !IMHeader.isAvailable(data: reset) {
break
}
let len = _resolveData(data: reset)
if len == 0 {
IMLog.error(item: "bad data")
} else {
start += len
}
}
// 去除解析過的數據
if start != 0 {
if start == recvBufferLen{
// 讀取完畢,不用拷貝
recvBufferLen = 0
}else{
// 把後面沒有解析的數據移動到最開始
let resetBuffer = data.subdata(in: start..<recvBufferLen)
recvBuffer.replaceSubrange(0..<resetBuffer.count, with: resetBuffer)
recvBufferLen = resetBuffer.count
}
}
}
// 監聽數據
tcpClient?.readData(withTimeout: -1, tag: 0)
}
fileprivate func _resolveData(data: Data) -> Int {
// 解析協議頭
let header = IMHeader()
if !header.readHeader(data: data) {
IMLog.error(item: "readHeader error!")
} else {
IMLog.debug(item: "parse IMHeader success,cmd=\(header.commandId),seq=\(header.seqNumber)")
// 處理消息
let bodyData = data[Int(kHeaderLen)..<data.count] // 去掉頭部,只放裸數據
// 這裏解析完了,可以用了,我這邊是回調出去的
// 回調 FIXME 非線程安全
//for item in delegateDicData {
// item.value.onHandleData(header, bodyData)
//}
return Int(header.length)
}
return 0
}
協議頭
附我使用的頭部解析類,包含寫入和讀取:
//
// IMHeader.swift
// Coffchat
//
// Created by xuyingchun on 2020/3/12.
// Copyright © 2020 Xuyingchun Inc. All rights reserved.
//
import Foundation
/// 協議頭長度
let kHeaderLen: UInt32 = 16
let kProtocolVersion: UInt16 = 1
/// 消息頭部,自定義協議使用TLV格式
class IMHeader {
var length: UInt32 = 0 // 4 byte,消息體長度
var version: UInt16 = 0 // 2 byte,default 1
var flag: UInt16 = 0 // 2byte,保留
var serviceId: UInt16 = 0 // 2byte,保留
var commandId: UInt16 = 0 // 2byte,命令號
var seqNumber: UInt16 = 0 // 2byte,包序號
var reversed: UInt16 = 0 // 2byte,保留
var bodyData: Data? // 消息體
/// 設置消息ID
/// - Parameter cmdId: 消息ID
func setCommandId(cmdId: UInt16) {
commandId = cmdId
}
/// 設置消息體
/// - Parameter msg: 消息體
func setMsg(msg: Data) {
bodyData = msg
}
/// 設置消息序號,請使用 [SeqGen.singleton.gen()] 生成
/// - Parameter seq: 消息序列號
func setSeq(seq: UInt16) {
seqNumber = seq
}
/// 判斷消息體是否完整
/// - Parameter data: 數據
class func isAvailable(data: Data) -> Bool {
if data.count < kHeaderLen {
return false
}
let buffer = [UInt8](data)
// get total len
var len: UInt32 = UInt32(buffer[0])
for i in 0...3 { // 4 Bytes
len = (len << 8) + UInt32(buffer[i])
}
return len <= data.count
}
/// 從二進制數據中嘗試反序列化Header
/// - Parameter data: 消息體
func readHeader(data: Data) -> Bool {
if data.count < kHeaderLen {
return false
}
let buffer = [UInt8](data)
// get total len
// 按big-endian讀取
let len: UInt32 = UInt32(buffer[0]) << 24 + UInt32(buffer[1]) << 16 + UInt32(buffer[2]) << 8 + UInt32(buffer[3])
if len < data.count {
return false
}
// big-endian
// length(43):
// - 0 : 0
// - 1 : 0
// - 2 : 0
// - 3 : 43
//
// version:
// - 4 : 0
// - 5 : 1
//
// flag:
// - 6 : 0
// - 7 : 0
//
// serviceId:
// - 8 : 0
// - 9 : 0
//
// cmdid(257):
// - 10 : 1
// - 11 : 1
//
// seq(3):
// - 12 : 0
// - 13 : 3
//
// reversed:
// - 14 : 0
// - 15 : 0
length = len
version = UInt16(buffer[4]) << 8 + UInt16(buffer[5]) // big-endian
flag = UInt16(buffer[6]) << 8 + UInt16(buffer[7])
serviceId = UInt16(buffer[8]) << 8 + UInt16(buffer[9])
commandId = UInt16(buffer[10]) << 8 + UInt16(buffer[11])
seqNumber = UInt16(buffer[12]) << 8 + UInt16(buffer[13])
reversed = UInt16(buffer[14]) << 8 + UInt16(buffer[15])
return true
}
/// 轉成2字節的bytes
class func uintToBytes(num: UInt16) -> [UInt8] {
// big-endian
var bytes = [UInt8]()
bytes.append(UInt8(num >> 8) )
bytes.append(UInt8(num & 0xFF))
// return [UInt8(truncatingIfNeeded: num << 8), UInt8(truncatingIfNeeded: num)]
return bytes
}
/// 轉成 4字節的bytes
class func uintToFourBytes(num: UInt32) -> [UInt8] {
return [UInt8(truncatingIfNeeded: num << 24), UInt8(truncatingIfNeeded: num << 16), UInt8(truncatingIfNeeded: num << 8), UInt8(truncatingIfNeeded: num)]
}
/// 獲取消息體
func getBuffer() -> Data? {
if bodyData == nil {
return nil
}
// this.seqNumber = SeqGen.singleton.gen();
length = kHeaderLen + UInt32(bodyData!.count)
version = kProtocolVersion
var headerData = Data()
headerData.append(contentsOf: IMHeader.uintToFourBytes(num: length)) // 總長度
headerData.append(contentsOf: IMHeader.uintToBytes(num: version)) // 協議版本號
headerData.append(contentsOf: IMHeader.uintToBytes(num: flag)) // 標誌位
headerData.append(contentsOf: IMHeader.uintToBytes(num: serviceId))
headerData.append(contentsOf: IMHeader.uintToBytes(num: commandId)) // 命令號
headerData.append(contentsOf: IMHeader.uintToBytes(num: seqNumber)) // 消息序號
headerData.append(contentsOf: IMHeader.uintToBytes(num: reversed))
return headerData + bodyData!
}
}
其中,isAvailable() 函數可以用來判斷一個數據包是否完整。
關於
來源於我的開源項目:https://github.com/xmcy0011/CoffeeChat
服務端使用Golang
客戶端使用iOS(Swift)和Flutter(Dart)
目前還在持續完善中。。。
Swift版:
Flutter版: