ios开发cocoaAsyncSocket与protobuf的使用swift5版本,含粘包拆包

皇甫卓君
2023-12-01

最近搞了一下即时通讯,为了配合服务器的使用(netty4+protobuf3),在ios客户端捣鼓了一下。

在ios客户端使用protobuf的资料比较少,配合cocoaAsyncSocket使用的更少,swift版本的更加少。

在swift版本中有处理protobuf粘包/拆包的资料基本没有。所以分享一下,希望对一些朋友有帮助

1、首先导入必要的包。

这里使用了Carthage作为管理,分别导入cocoaAsyncSocket和protobuf

//在swift中使用protobuf需要导入swift-protobuf
github "apple/swift-protobuf" ~> 1.0
github "robbiehanson/CocoaAsyncSocket" "master"

如何生成proto文件,这里不介绍,可以自行参考:https://github.com/apple/swift-protobuf

2、直接上关键代码,注释已经写的非常清楚了。

//
//  SocketClient.swift
//  BestTravel
//
//  Created by gj on 16/11/1.
//
//

import UIKit
import SwiftProtobuf

class SocketClient: NSObject{
    
    fileprivate var clientSocket: GCDAsyncSocket!
    //数据缓冲
    fileprivate var receiveData:Data=Data.init();
  
    //单例模式
    static let sharedInstance=SocketClient();
    private override init() {
        super.init();
        clientSocket = GCDAsyncSocket(delegate: self, delegateQueue: DispatchQueue.main)
    }
    
    
}

extension SocketClient {
    // 开始连接
    func startConnect(){
        startReConnectTimer();
    }
    
    //断开连接
    func stopConnect(){
        if(clientSocket.isConnected){
            clientSocket.disconnect()
        }
    }
    

}

//MARK: 监听
extension SocketClient: GCDAsyncSocketDelegate {
    
    // 断开连接
    func socketDidDisconnect(_ sock: GCDAsyncSocket, withError err: Error?) {
        
    }
    
    // 连接成功
    func socket(_ sock: GCDAsyncSocket, didConnectToHost host: String, port: UInt16) {
   
        
    }
    
    
    // 接收到消息
    func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) {
        
        //接收数据放入缓冲区
        self.receiveData.append(data);
        
        //读取data的头部占用字节 和 从头部读取内容长度
        //验证结果:数据比较小时头部占用字节为1,数据比较大时头部占用字节为2
        var headL:Int  = 0;
        let contentL:Int32 = try! ReadRawVarint32Decode.readRawVarint32(buffer: [UInt8](data), bufferPos: &headL)
        
        //如果没有内容,继续接收
        if (contentL < 1){
            sock.readData(withTimeout: -1, tag: 0);
            return;
        }
        
        //拆包情况下:继续接收下一条消息,直至接收完这条消息所有的拆包,再解析
        if (Int32(headL) + contentL > self.receiveData.count){
            sock.readData(withTimeout: -1, tag: 0);
            return;
        }
        
        //当receiveData长度不小于第一条消息内容长度时,开始解析receiveData
        self.parseContentData(headL: Int32(headL), contentL: contentL);
        sock.readData(withTimeout: -1, tag: 0);
    }
    
    
    
    //-----------------------------------------------------------------------------
    
    
    /** 解析二进制数据:NSData --> 自定义模型对象 */
    func parseContentData( headL: Int32, contentL: Int32) {
      
        let range = Range(0...Int(headL + contentL-1)) //本次解析data的范围
        let data = receiveData.subdata(in: range) //本次解析的data
        
        do {
            //把消息转成stream
            let stream = InputStream.init(data: data);
            stream.open()
            var message = Message();
            //注意这段很重要,必须要粘包才能正常解析
            try BinaryDelimited.merge(into:&message, from: stream)
            stream.close();
            
            //处理消息,这可以写你的代码,比如把消息传出去
            
            
            //移除已经解析过的data,避免消息重复解析
            receiveData.removeSubrange(range)
            if (receiveData.count < 1) {
                return
            }
            
            //对于粘包情况下被合并的多条消息,循环递归直至解析完所有消息
            var headL:Int = 0
            let contentL = try ReadRawVarint32Decode.readRawVarint32(buffer: [UInt8](receiveData), bufferPos: &headL)
            if headL + Int(contentL) > receiveData.count {
                return //实际包不足解析,继续接收下一个包
            }
            
        } catch  {
            print(error)
        }
        
        parseContentData(headL: headL, contentL: contentL) //继续解析下一条
    }
    
    
    
}

3、这里有个非常关键的类,上用来判断prototbuf消息的,网上基本找不到swift版本的,倒是有个objc的版本。这里放一个自己写的swift版本的:

//
//  SocketDelegate.swift
//  gim
//
//  Created by imac on 2019/6/21.
//  Copyright © 2019 gim. All rights reserved.
//

import UIKit


class ReadRawVarint32Decode: NSObject {
    
    //static var bufferPos:Int = 0
    
    class  func readRawVarint32(buffer:[UInt8],bufferPos:inout Int) throws -> Int32 {
        var tmp = try readRawByte(buffer: buffer,bufferPos: &bufferPos);
        if (tmp >= 0) {
            return Int32(tmp);
        }
        var result : Int32 = Int32(tmp) & 0x7f;
        tmp = try readRawByte(buffer: buffer,bufferPos: &bufferPos)
        if (tmp >= 0) {
            result |= Int32(tmp) << 7;
        } else {
            result |= (Int32(tmp) & 0x7f) << 7;
            tmp = try readRawByte(buffer: buffer,bufferPos: &bufferPos)
            if (tmp >= 0) {
                result |= Int32(tmp) << 14;
            } else {
                result |= (Int32(tmp) & 0x7f) << 14;
                tmp = try readRawByte(buffer: buffer,bufferPos: &bufferPos)
                if (tmp >= 0) {
                    result |= Int32(tmp) << 21;
                } else {
                    result |= (Int32(tmp) & 0x7f) << 21;
                    tmp = try readRawByte(buffer: buffer,bufferPos: &bufferPos)
                    result |= (Int32(tmp) << 28);
                    if (tmp < 0) {
                        // Discard upper 32 bits.
                        for _ in 0..<5 {
                            let byte = try readRawByte(buffer: buffer,bufferPos: &bufferPos)
                            if (byte >= 0) {
                                return result;
                            }
                        }
                        
                        throw ProtocolBuffersError.invalidProtocolBuffer("MalformedVarint")
                    }
                }
            }
        }
        return result;
    }
    
    class func readRawByte(buffer:[UInt8], bufferPos:inout Int) throws -> Int8 {
        if (bufferPos == buffer.count) {
            return -1
        }
        let res = buffer[Int(bufferPos)]
        bufferPos+=1
        
        var convert:Int8 = 0
        convert = convertTypes(convertValue: res, defaultValue: convert)
        return convert
    }
    
    
    class  func convertTypes<T, ReturnType>(convertValue value:T, defaultValue:ReturnType) -> ReturnType
    {
        var retValue = defaultValue
        var curValue = value
        memcpy(&retValue, &curValue, MemoryLayout<T>.size)
        return retValue
    }
    
    
    
    
    
    public enum ProtocolBuffersError: Error {
        case obvious(String)
        //Streams
        case invalidProtocolBuffer(String)
        case illegalState(String)
        case illegalArgument(String)
        case outOfSpace
    }
    
}

4、还有一点要非常注意,因为消息通过二进制传输,所以发的时候,也需要先进行encode,比如如果每次发送的字节上1024,但你的消息长度超过1024,那么消息就会被分包发送,如果不先进行encode。那么接收解析就会有问题。因此,在创建消息的时候,应该要加上protobuf的消息头,看代码:

 var messageBuilder = Message.init();
  //消息要通过以下方式转成data,否则解析会有问题
 let stream1 = OutputStream.toMemory()
            stream1.open()
            //关键
            try BinaryDelimited.serialize(message: messageBuilder, to: stream1);
            stream1.close()
            //转成data
            let nsData = stream1.property(forKey: .dataWrittenToMemoryStreamKey) as! NSData
            data = Data(referencing: nsData)

 

到此,基本上跟服务器端的收发已经完成不是问题了。

源码地址:https://github.com/gogym/gim-ios-sdk

 类似资料: