文章目录
- Swift 处理TCP粘包
- CocoaAsyncSocket
- Swift Data基础
- 写入和读取
- 替换
- 处理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版: