Swift CocoaAsyncSocket解决粘包、大小端序转换

开头

ReplayKit2录屏,h264编码后通过socket传给Android车机,其中遇到不少问题,以前的文章也讲过粘包,协议比较简单,今天讲讲再讲讲粘包解决,重点是组装数据发给Android,要转换成大端序,不然Android端解析不了消息体。

通信协议

报文消息由7部分组成,分别是4字节的长度头(限制实际报文长度小于4m,超出4m通过应用层协议报文
处理)、1字节的加密类型头、1字节的回执类型头、2字节的消息类型头、2字节的消息序号头、N字节的实际消息体部分与回车符终结符号(转换过程的二进制数均为有符号位)。

  1. 长度头:实际数值等于1字节的加密类型头+1字节的消息类型头+2字节的消息类型头+2字节的消息序号头+N字节的实际消息体部分与1字节回车符终结符号;在读取数据时可先读取长度字段,再读取对应数组后进行解析。
  2. 加密类型:0-普通消息,不需加解密;1-RSA非对称加密,手机端使用公钥加解密;2-后置加密类型,使用建立连接后的AES256key(加盐、偏移)进行加解密;只需加解密消息体部分!其他数据不需处理,包括换行符也不需处理!
  3. 1字节回执类型:0不需要回执,1需要回执;对于回执类型需要以标准报文回复序号消息,该序号消息不需要加密、不需要回执
  4. 2字节的消息类型头:报文类型,见报文格式定义。
  5. 2字节消息序号头:从0开始,大于等于65535时重置为0,无论是否需要回执,每次发送消息,该值都需要自增1。
  6. N字节的实际消息体部分:消息体,根据消息类型进行解析读取。
  7. 换行符:高效通讯,不加入数据完整性校验,如果读取数据时如果最后一位非换行符,则发送数据错误报文,重建链接!

源码

上面是和车机沟通好的协议,下面按协议开始干活!

  • 发送数据
    消息类型枚举
enum LYMessageType: Int {
    /// 通用消息
    case common = 0
    /// ping
    case ping = 1
    /// 发送SPS关键帧
    case sps = 7
    /// 发送 sampleBuffer
    case sampleBuffer = 9
    ....
    /// 重连
    case retry = 11
}

拓展消息类型,填上1字节的加密类型头、1字节的回执类型头、2字节的消息类型头, ... 是省略部分枚举值

extension LYMessageType {
    public var encryp: Int8 {
        switch self {
        case .common, .ping, .sampleBuffer,:
            return Int8(0)
        case .password:
            return Int8(1)
        case .connect, ... .retry:
            return Int8(2)
        }
    }
    
    public var receiptType: Int8 {
        switch self {
        case .common, .ping, .sampleBuffer:
            return Int8(0)
        default:
            return Int8(1)
        }
    }
    
    public var meesageType: Int16 {
        return Int16(self.rawValue)
    }
}

加密类型枚举,发送消息和解析消息的时候用得着,用对应的方式加解密,需要注意的是base64编码,发送给Android的加密信息加密后要进行base64编码,拿到车机的加密信息也要先base64解码,具体加解密代码无可奉告!

enum LYSocketMessageEncryption: UInt8 {
    /// 不加密
    case none = 0
    /// 公钥key预加密  (rsa)
    case rsa = 1
    /// 车机返回的aes256key 加密 (aes)
    case aes = 2
}

比较重要:解析消息的结构体

struct LYProcessMessageStruct {
    /// 长度头
    var lengthHeader: Int
    /// 加密类型
    var encryptType: Int
    /// 回执类型
    var receiptType: Int
    /// 消息类型
    var messageType: Int
    /// 消息序号
    var messageSeqNum: Int
    /// 消息体实际长度
    var messageLength: Int
    
    init(header: Data) {
        lengthHeader = (Int(header[0]) << 24) + (Int(header[1]) << 16) + (Int(header[2]) << 8) + Int(header[3])
        encryptType = Int(header[4])
        receiptType = Int(header[5])
        messageType = (Int(header[6]) << 8) + Int(header[7])
        messageSeqNum = (Int(header[8]) << 8) + Int(header[9])
        messageLength = lengthHeader - 7 //lengthHeader - encryptType - receiptType - messageType - messageSeqNum
    }
}

//lengthHeader - encryptType - receiptType - messageType - messageSeqNum 这里被注释掉了,换成常量7,为什么呢,因为车机不按协议来! 让我踩了深坑,我说怎么解析出来的消息长度不对,他也不和我说,解析不出来就说是我代码问题,日志也不给看。。。

消息组装

/// 发送数json据包
    public func packagingData(messageType: LYMessageType, message: [String : Any]) {
        // 网络波动
        if missedHeartbeats > maxMissedHeartbeats { return }
        
        messageSendingQueue.async { [weak self] in
            // 消息体转data
            var bodyData: Data
            if messageType == LYMessageType.sps {
                guard let msgData = self?.dictembeddedDataToData(message) else { return }
                bodyData = msgData
            } else {
                guard let messageData = self?.dictToData(message, messageType.encryp) else { return }
                bodyData = messageData
            }
            
            self?.messageWrapped(messageType: messageType, bodyData: bodyData)
        }
    }

上面代码主要是把json转data

private func messageWrapped(messageType: LYMessageType, bodyData: Data) {
        var packageData = [UInt8](repeating: 0, count: bodyData.count + 10 + 1)
         //车机这里直接写死7,而不是按协议来
        let length = bodyData.count + 7

        // 写入长度,0
        let lengthArray = convertIntToBigEndianByteArray(value: length, length: 4)
        packageData.replaceSubrange(0..<lengthArray.count, with: lengthArray)

        // 写入加密类型,4
        packageData[4] = UInt8(messageType.encryp)

        // 写入回执类型,5
        packageData[5] = UInt8(messageType.receiptType)

        // 写入消息类型,6
        let msgTypeBytes = convertIntToBigEndianByteArray(value: Int(messageType.meesageType), length: 2)
        packageData.replaceSubrange(6..<8, with: msgTypeBytes)

        // 写入消息序号,8
        let seqNumBytes = convertIntToBigEndianByteArray(value: Int(packageNum), length: 2)
        packageData.replaceSubrange(8..<10, with: seqNumBytes)

        // 写入消息体,10
        packageData.replaceSubrange(10..<(10 + bodyData.count), with: bodyData)

        // 写入结束换行符,10,packageData.count - 1
        let endSymbolArray: [UInt8] = [0x0A] // 换行符
        packageData.replaceSubrange((packageData.count - 1)..<packageData.count, with: endSymbolArray)
        
        let sendPack = Data(packageData)
        /// 切片传输
        sliceTransfer(data: sendPack)
        
        packageNum += 1
   }

这里有个重要辅助方法,将Int转为大端,不然Android解析不出来

/// 将整数转换为大端格式的有符号二进制数,并存储到指定长度的byte数组中
    private func convertIntToBigEndianByteArray(value: Int, length: Int) -> [UInt8] {
        var byteArray = [UInt8](repeating: 0, count: length)
        for i in (0..<length).reversed() {
            byteArray[i] = UInt8((value >> ((length - i - 1) * 8)) & 0xFF)
        }
        return byteArray
    }

数据切片传输, 发送的数据大小上限我设置为1m,
fileprivate let maxDataSize = 1 * 1024 * 1024

private func sliceTransfer(data: Data) {
        var offset = 0
        let totalLength = data.count
        
        while offset < totalLength {
            let length = min(maxDataSize, totalLength - offset)
            let chunk = data.subdata(in: offset..<offset + length)
            
            if isServer {
                clientSocket?.write(chunk, withTimeout: -1, tag: 0)
            } else {
                socket?.write(chunk, withTimeout: -1, tag: 0)
            }
            offset += length
        }
    }
  • 解析数据,解决粘包
// MARK: -  粘包拆包
    func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) {
        // 先存入缓存区
        dataBuffer.append(data)
        
        while true {
            /**
             报文消息由7部分组成,分别是4字节的长度头(限制实际报文长度小于4m,超出4m通过应用层协议报文
             处理)、1字节的加密类型头、1字节的回执类型头、2字节的消息类型头、2字节的消息序号头、N字节的
             实际消息体部分与回车符终结符号
             */
            guard dataBuffer.count >= 11 else { break } // 至少要有11个字节(4 + 1 + 1 + 2 + 2 + 1)
            
            // 读取长度头 2024-02-02、、、Android车机不按协议来,头部没有填其他信息实际值,而是直接 + 7,填的是字节数
            let msgStruct = LYProcessMessageStruct(header: dataBuffer)
            let totalMessageLength = msgStruct.messageLength + 10 + 1 // 消息体第10位起 + 1位结束符
            
            // 检查是否接收到完整消息
            guard dataBuffer.count >= totalMessageLength else { break } // 数据不够完整
              
            // 读取消息体部分
            let messageData = dataBuffer.subdata(in: 10..<(totalMessageLength - 1))
              
            // 处理完整的消息
            processMessage(messageData, msgStruct)
              
            // 移除已经处理过的消息
            if dataBuffer.count >= totalMessageLength {
                dataBuffer = dataBuffer.subdata(in: totalMessageLength..<dataBuffer.count)
            } else {
                dataBuffer.removeAll()
            }
        }
         
         // 继续监听数据
         sock.readData(withTimeout: -1, tag: 0)
    }

processMessage 方法就是根据消息类型执行相应的逻辑,比如:

/// additional是  LYProcessMessageStruct类型
let type = additional.messageType as LYMessageType.RawValue
switch type {
case LYMessageType.ping.rawValue:
 // 处理ping消息
}
····

好了,分享完毕,主要是要转大端,不然Android解析不出消息是真的头疼

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容