Swift 處理TCP粘包

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版:
在這裏插入圖片描述
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章