最近搞了一下即时通讯,为了配合服务器的使用(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)
到此,基本上跟服务器端的收发已经完成不是问题了。