开头
ReplayKit2录屏,h264编码后通过socket传给Android车机,其中遇到不少问题,以前的文章也讲过粘包,协议比较简单,今天讲讲再讲讲粘包解决,重点是组装数据发给Android,要转换成大端序,不然Android端解析不了消息体。
通信协议
报文消息由7部分组成,分别是4字节的长度头(限制实际报文长度小于4m,超出4m通过应用层协议报文
处理)、1字节的加密类型头、1字节的回执类型头、2字节的消息类型头、2字节的消息序号头、N字节的实际消息体部分与回车符终结符号(转换过程的二进制数均为有符号位)。
- 长度头:实际数值等于1字节的加密类型头+1字节的消息类型头+2字节的消息类型头+2字节的消息序号头+N字节的实际消息体部分与1字节回车符终结符号;在读取数据时可先读取长度字段,再读取对应数组后进行解析。
- 加密类型:0-普通消息,不需加解密;1-RSA非对称加密,手机端使用公钥加解密;2-后置加密类型,使用建立连接后的AES256key(加盐、偏移)进行加解密;只需加解密消息体部分!其他数据不需处理,包括换行符也不需处理!
- 1字节回执类型:0不需要回执,1需要回执;对于回执类型需要以标准报文回复序号消息,该序号消息不需要加密、不需要回执
- 2字节的消息类型头:报文类型,见报文格式定义。
- 2字节消息序号头:从0开始,大于等于65535时重置为0,无论是否需要回执,每次发送消息,该值都需要自增1。
- N字节的实际消息体部分:消息体,根据消息类型进行解析读取。
- 换行符:高效通讯,不加入数据完整性校验,如果读取数据时如果最后一位非换行符,则发送数据错误报文,重建链接!
源码
上面是和车机沟通好的协议,下面按协议开始干活!
- 发送数据
消息类型枚举
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解析不出消息是真的头疼