Swift 处理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版:
在这里插入图片描述
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
TCP协议中,由于数据传输的不可靠性,数据可能会被拆分成多个小的数据包进行传输,或者多个数据包会被合并成一个大的数据包进行传输,这就是所谓的TCP粘包问题。 libevent是一个高性能事件驱动库,可以用来处理网络通信,包括TCP粘包问题。下面介绍一下libevent处理TCP粘包的方法: 1. 设置TCP_NODELAY选项 在TCP连接建立时,可以设置TCP_NODELAY选项为1,表示禁止Nagle算法,即禁止将数据合并成一个大的数据包进行传输。这样可以避免TCP粘包问题的发生。设置方式如下: ```c int flag = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag)); ``` 2. 使用分隔符 在数据传输时,可以在每个数据包的末尾添加一个分隔符,表示数据包的结束。这样接收端就可以根据分隔符来分割数据包,避免TCP粘包问题。常用的分隔符有"\r\n"和"\0"。设置方式如下: ```c #define DELIMITER "\r\n" char buf[MAX_BUF_SIZE]; int len = recv(fd, buf, MAX_BUF_SIZE, 0); if (len > 0) { buf[len] = '\0'; char *p = strstr(buf, DELIMITER); if (p != NULL) { *p = '\0'; // 处理一个完整的数据包 } } ``` 3. 使用固定长度的数据包 在数据传输时,可以将每个数据包的长度固定为一个固定的值,这样接收端就可以根据固定长度来分割数据包,避免TCP粘包问题。设置方式如下: ```c #define PACKET_SIZE 1024 char buf[MAX_BUF_SIZE]; int len = recv(fd, buf, MAX_BUF_SIZE, 0); if (len > 0) { while (len >= PACKET_SIZE) { // 处理一个完整的数据包 len -= PACKET_SIZE; } } ``` 4. 使用消息头 在数据传输时,可以在每个数据包的开头添加一个消息头,消息头中包含了数据包的长度信息,这样接收端就可以根据消息头中的长度信息来分割数据包,避免TCP粘包问题。设置方式如下: ```c #define HEADER_LENGTH 4 char buf[MAX_BUF_SIZE]; int len = recv(fd, buf, MAX_BUF_SIZE, 0); if (len > 0) { while (len >= HEADER_LENGTH) { int packet_len = *(int *)buf; // 读取消息头中的长度信息 if (len >= HEADER_LENGTH + packet_len) { // 处理一个完整的数据包 len -= HEADER_LENGTH + packet_len; } else { break; } } } ``` 以上是libevent处理TCP粘包问题的几种方法,可以根据具体情况选择合适的方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值