用源代码简单透析Websocket背后的真相:三. 数据的接收及分包处理

文章目录

  • 1.接收数据并缓存
  • 2.对接收的数据进行处理
    • 2.1 当接收的数据包少于两个字节时,不处理。
    • 2.2分拆出头两个字节的数据,生成``Payload len``头部数据
    • 2.3 生成数据包的长度
    • 2.4 对数据进行截取
    • 2.5 生成数据包,返回到上层
  • 3. Ping / Pong

继上一节讨论“数据包的发送”之后,对于数据包的接收处理也是很多网络请求框架实现中最核心的处理逻辑之一。通过查看 Starscream的源代码,在接收到接收缓存回调回来的数据后的处理上,在进行多线程处理方面处理得可谓不尽人意:

  • 一方面对接收数据缓存的数组没有进行原子操作。在实现上,要么进行操作前的加锁操作后解锁,要么放到同步串行队列中操作。
  • 另一方面,在进行数据包拆包操作的过程没有进行队列化操作,个人感觉在运行架构的设计上欠缺考虑。
  • 拆包逻辑与Websocket内部指令处理逻辑混在一起,在逻辑架构上的设计也是不尽人意的。

1.接收数据并缓存

  • 客户端在受到接收缓冲区接收到数据并回调的数据后,在放到自己的内存中进行暂存等待“数据包分析器”对其进行处理。
  • “数据包分析器”经常分拆数据包后,可能内存中还有一部分未接收完整的数据包无法处理。这个时候需要对这些数据进行缓存。
  • 等待下一次接收缓冲区接收到数据并回调后,把之前的“未处理缓存数据”与新接收的数据进行重新合并,给到“数据包分析器”进行分析解包。

上面的流程算是比较简单的数据接收后的操作流程。下面给出部分关于数据重新合并方面的源代码以供参考:

    /**
     * Dequeue the incoming input so it is processed in order.
     */
    private func dequeueInput() {
        while !inputQueue.isEmpty {
            autoreleasepool {
                let data = inputQueue[0]
                var work = data
                if let buffer = fragBuffer {
                    var combine = NSData(data: buffer) as Data
                    combine.append(data)									//对数据进行合并
                    work = combine
                    fragBuffer = nil
                }
                let buffer = UnsafeRawPointer((work as NSData).bytes).assumingMemoryBound(to: UInt8.self)
                let length = work.count
                if !connected {
                    processTCPHandshake(buffer, bufferLen: length)			//逻辑连接未成功进行的反馈处理逻辑
                } else {
                    processRawMessagesInBuffer(buffer, bufferLen: length)	//对数据包进行分拆,把完整的数据回调到上层
                }
                inputQueue = inputQueue.filter{ $0 != data }					//对数据包队列中进行push操作
            }
        }
    }

下面,以逻辑连接返回后的逻辑代码进行解析,以说明对于不完整的数据包进行缓存的操作:

    /**
     * Handle checking the inital connection status
     */
    private func processTCPHandshake(_ buffer: UnsafePointer<UInt8>, bufferLen: Int) {
        let code = processHTTP(buffer, bufferLen: bufferLen)			//连接的反馈的逻辑处理,返回处理结果
        switch code {
        case 0:															//处理成功
            break
        case -1:														//反馈文本未接收完整,缓存并等待下一次的处理
            fragBuffer = Data(bytes: buffer, count: bufferLen)
            break // do nothing, we are going to collect more data
        default:
            doDisconnect(WSError(type: .upgradeError, message: "Invalid HTTP upgrade", code: code))
        }
    }

    /**
     * Finds the HTTP Packet in the TCP stream, by looking for the CRLF.
     */
    private func processHTTP(_ buffer: UnsafePointer<UInt8>, bufferLen: Int) -> Int {
        //连接返回的文本数据由4行文本组成
        let CRLFBytes = [UInt8(ascii: "\r"), UInt8(ascii: "\n"), UInt8(ascii: "\r"), UInt8(ascii: "\n")]
        var k = 0
        var totalSize = 0
        for i in 0..<bufferLen {
            if buffer[i] == CRLFBytes[k] {
                k += 1
                if k == 4 {
                    totalSize = i + 1
                    break
                }
            } else {
                k = 0
            }
        }
        if totalSize > 0 {
            let code = validateResponse(buffer, bufferLen: totalSize)  				//对返回的Accept值进行校验
            if code != 0 {
                return code
            }
            isConnecting = false
            mutex.lock()
            connected = true
            mutex.unlock()
            didDisconnect = false
            if canDispatch {
                callbackQueue.async { [weak self] in
                    guard let self = self else { return }
                    self.onConnect?()
                    self.delegate?.websocketDidConnect(socket: self)				//校验通过后,调用连接成功的回调
                    self.advancedDelegate?.websocketDidConnect(socket: self)
                    let name:NSNotification.Name = NSNotification.Name(WebsocketDidConnectNotification)
                    NotificationCenter.default.post(name:name, object: self)
                }
            }
            //totalSize += 1 //skip the last \n
            let restSize = bufferLen - totalSize
            if restSize > 0 {														//如果还存在附加数据,对数据进行处理
                processRawMessagesInBuffer(buffer + totalSize, bufferLen: restSize)	//对数据进行分拆包操作并返回上层逻辑
            }
            return 0 			//连接成功								
        }
        return -1 				//连接反馈数据未接收完整
    }

上面的注释已经写得效为详细,就不一一解析了,常规操作了,代码也很通俗易懂。

2.对接收的数据进行处理

Starscream对这方面的分包逻辑与部分指令处理逻辑放在一起了,把代码赤裸裸贴出来就太敷衍了事了,所以下面只贴出重要的代码片段。

为了方便解说,再次贴出包的结构图:

  0                1                2                3
  0 1 2 3 4 5 6 7  0 1 2 3 4 5 6 7  0 1 2 3 4 5 6 7  0 1 2 3 4 5 6 7
 +-+-+-+-+---------+-+--------------+--------------+---------------+
 |F|R|R|R| opcode  |M|  Payload len|    Extended payload length    |
 |I|S|S|S|  (4)    |A|     (7)     |             (16/64)           |
 |N|V|V|V|         |S|             |   (if payload len==126/127)   |
 | |1|2|3|         |K|             |                               |
 +-+-+-+-+---------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127    |
 + - - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1    |
 +---------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data           |
 +-------------------------------- - - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                  :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                  |
 +-----------------------------------------------------------------+

2.1 当接收的数据包少于两个字节时,不处理。

guard let baseAddress = buffer.baseAddress else {return emptyBuffer}
if response != nil && bufferLen < 2 {
     fragBuffer = Data(buffer: buffer)
     return emptyBuffer
}

具体数据为什么放fragBuffer里就不先详细解说了。在分拆包方面,第个人的实现逻辑都不一样,但处理数据的方法思路与想达到的目的都是一样的。

2.2分拆出头两个字节的数据,生成Payload len头部数据

let isFin = (FinMask & baseAddress[0])
let receivedOpcodeRawValue = (OpCodeMask & baseAddress[0])
let receivedOpcode = OpCode(rawValue: receivedOpcodeRawValue)		//生成指令值,如“ping”,“pong”,“textFrame”等
let isMasked = (MaskMask & baseAddress[1])
let payloadLen = (PayloadLenMask & baseAddress[1])					//生成playload值
//是否对数据进行了压缩操作
if compressionState.supportsCompression && receivedOpcode != .continueFrame {
   compressionState.messageNeedsDecompression = (RSV1Mask & baseAddress[0]) > 0
}
  • 指令值用于区分当前数据所要进行的操作,如“进行字符串传送”, “进行二进制数据传送”, “ping/pong”等操作。
  • playload的值用于生成数据包的大小,为下面的数据包分拆截取提供依据
  • 是否对数据进行压缩操作的标记位在第一个字节第二位,当值为 1 时表示数据进行过压缩。

2.3 生成数据包的长度

var dataLength = UInt64(payloadLen)
if dataLength == 127 {
     dataLength = WebSocket.readUint64(baseAddress, offset: offset)
     offset += MemoryLayout<UInt64>.size
} else if dataLength == 126 {
     dataLength = UInt64(WebSocket.readUint16(baseAddress, offset: offset))
     offset += MemoryLayout<UInt16>.size
}
if bufferLen < offset || UInt64(bufferLen - offset) < dataLength {
     fragBuffer = Data(bytes: baseAddress, count: bufferLen)
     return emptyBuffer
}
  • payloadLen == 127 :数据包的长度大于 UInt16.max,头数据第二个字节开始的后面8个字节用于存储数据包的长度
  • palyloadLen ==126 :数据包的长度小于 UInt16.max,大于126,头数据第二个字节开始的后面2个字节用于存储数据包的长度
  • 最后一个情况就量数据包的长度少于126,头数据第二个字节用于存储数据包的长度
  • 这里有一个比较重要的代码,就是最后那几行:当发现当前取得的数据长度少于包长度时,先缓存数据,返回,等待下一次的分拆包处理。

2.4 对数据进行截取

if compressionState.messageNeedsDecompression, let decompressor = compressionState.decompressor {
     do {
        data = try decompressor.decompress(bytes: baseAddress+offset, count: Int(len), finish: isFin > 0)
         if isFin > 0 && compressionState.serverNoContextTakeover {
             try decompressor.reset()
         }
     } catch {
         let closeReason = "Decompression failed: \(error)"
         let closeCode = CloseCode.encoding.rawValue
         doDisconnect(WSError(type: .protocolError, message: closeReason, code: Int(closeCode)))
         writeError(closeCode)
         return emptyBuffer
     }
} else {
     data = Data(bytes: baseAddress+offset, count: Int(len))
}
  • 当包的数据长度在于等于包的长度值时,可以进行拆包操作了。
  • 当存在压缩时,先截取数据,再进行解压操作。
  • 没有进行压缩操作的数据,直接截取数据,生成数据包。

2.5 生成数据包,返回到上层

if let response = response {
     response.buffer.append(data)
     response.bytesLeft -= Int(len)
     response.frameCount += 1
     response.isFin = isFin > 0 ? true : false
     if isNew {
        readStack.append(response)
     }
     _ = processResponse(response)
}


/**
 *Process the finished response of a buffer.
 */
private func processResponse(_ response: WSResponse) -> Bool {
   if response.isFin && response.bytesLeft <= 0 {
       if response.code == .ping {
            if respondToPingWithPong {
                 let data = response.buffer! // local copy so it is perverse for writing
                 dequeueWrite(data as Data, code: .pong)
             }
       } else if response.code == .textFrame {
         guard let str = String(data: response.buffer! as Data, encoding: .utf8) else {
             writeError(CloseCode.encoding.rawValue)
             return false
       	}
        if canDispatch {
            callbackQueue.async { [weak self] in
               guard let self = self else { return }
               self.onText?(str)
               self.delegate?.websocketDidReceiveMessage(socket: self, text: str)
               self.advancedDelegate?.websocketDidReceiveMessage(socket: self, text: str, response: response)
            }
        }
      } else if response.code == .binaryFrame {
          if canDispatch {
              let data = response.buffer! // local copy so it is perverse for writing
              callbackQueue.async { [weak self] in
                  guard let self = self else { return }
                  self.onData?(data as Data)
                  self.delegate?.websocketDidReceiveData(socket: self, data: data as Data)
                  self.advancedDelegate?.websocketDidReceiveData(socket: self, data: data as Data, response: response)
             }
          }
      }
      readStack.removeLast()
      return true
  }
  return false
}
  • 当收到 “ping”指令时,回复“pong”指令
  • 当接收的数据是字符串数据,数据包中的二进制数据转换成字符串,回调到框架外层的回调方法中。
  • 当接收的数据是二进制数据,把数据包中的二进制数直接回调到框架外层对应的回调方法中。

3. Ping / Pong

说到"ping" "pong"指令,很多人说Websocket内部存在一个心跳机制,项目开发中不用自己再添加自己的心跳机制去监测网络服务是否可用。

本来想多开一个章节去讨论很多人对于Websocket的一些看法对于我来说的一些理解的。后来,因为一口所写了三篇文章,越写到最后,越感觉到说的东西太简单,怕大家看得无聊,所以用源代码简单透析Websocket背后的真相写到第三节就决定结束了。在结束之前,我就上面问题说说我的个人看法。

我觉得,上面对于"ping" "pong"指令的说法是很不全面的。可以说就当前分析的库而言,只是提供了最基本的“发ping指令”,“发pong指令”, “接收到ping指令回复pong指令”的这些逻辑。在网络不稳定的情况下,或者弱网状态下,服务端发送的 “ping指令”到法到达客户端,客户端这时是无法判断服务是否可用的。从其代码的角度分析,只是为了纯粹去解决因为长时候不发消息被运营商主动断开连接的问题。

我个人认为,在框架外层,自己再维护一套心跳,不用框架内部的心跳会是一个更好的选择。一方面心跳不单单只是为了解决避免长时候不发消息被运营商主动断开连接的问题。很大情况上我们平时的项目中,会利用心跳去做很多额外的逻辑,比如进行版本配对为数据同步提供启动的依据,用户是否在线等业务逻辑。

你可能感兴趣的:(ios,基础)