用源代码简单透析Websocket背后的真相:二. 数据发送

文章目录

  • 数据包的封装
  • 总结

Websocket同时支持“字符串”及“二进制”数据的发送操作,因为在其发送的时候,都要进行二进制数据类型的转换。

这篇文章将会重点介绍数据包格式的重要性,以及Websocket在数据包定义上的特点。

良好的数据包结构,可以为数据解析提供良好协议基础。在实现上逻辑上提供更多的异常处理上的条件参考。

数据包的封装

在正常数据交换过程中,对发送的数据进行封装是必不可少的。对于上面进行的“连接文本”的发送相对来说只是一个特例。有空的话,找一个Websocket服务端的库进行源代码的分析。

我们常用的通讯方式,几乎都是基于TCP/UDP的方式进行数据发关的,其中TCP基于连接的发送方式比较常用。无论基于哪种方式,在数据接收端都是基于接收缓存的方式,接收一定数量的包后回调到应用层进行处理。这个时候,会产生以下几种情况:

  1. 数据包很大,接收缓存无法容纳完整的情况下回调到上层应用方法进行数据的处理。这个时候数据包不完整,要等下一次回调再进行合并后再进行包完整性的判断。当接一个包的数据完整后,进行数据分离,再对这个完整的数据包进行更上层的逻辑处理。

  2. 数据很小,发送的数据包比较频繁的情况下,接收缓存同时接收到多个完整的数据包(当然,最后一个包可能只是接收到一部分),在上层回调接口收到数据时,其实是多个包合在一起的数据。这个时候就要对数据进行分拆,分拆成一个个完整的数据包,把没接收完整的数据包数据先缓存起来,等待下一次的回调,然后再合并数据再进行分包操作。

基于上面的两种情况,在不定义数据包格式的情况下我们很难进行分包处理,所以如何定义数据包就显得尤为重要。数据格式的定义如下图:

  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 ...                  |
 +-----------------------------------------------------------------+

RSV1, RSV2, RSV3各占1位,一般情况下全为0,当客户端、服务端协商采用WebSocket扩展时,这三个标志位可以非0。

Opcode:占4位, Opcode是操作代码,用于标识这个数据包的操作指令,可选的操作代码如下:

  • %x0:表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
  • %x1:表示这是一个文本帧(frame)
  • %x2:表示这是一个二进制帧(frame)
  • %x3-7:保留的操作代码,用于后续定义的非控制帧。
  • %x8:表示连接断开。
  • %x9:表示这是一个ping操作。
  • %xA:表示这是一个pong操作。
  • %xB-F:保留的操作代码,用于后续定义的控制帧。

Payload len:数据载荷的长度,占用一个字节。但真实的包长度不一定只用这一个字节的值表示。

  • 长度<=126:Payload len = 包长度值
  • 长度 > 126 && 长度 < UInt16.max:后面开启多两个字节的长度,用后面两个字节存放包长度,Payload len赋值126
  • 长度 > UInt16.max:后面开启我多8个字节的长度,用于储存包的长度,最高位为0,Payload len赋值127

在这里,掩码操作就不作说明了,志在了解大概的过程。具体的代码如下:

  /**
   *Used to write things to the stream
   */
  private func dequeueWrite(_ data: Data, code: OpCode, writeCompletion: (() -> ())? = nil) {
        let operation = BlockOperation()
        operation.addExecutionBlock { [weak self, weak operation] in
            //stream isn't ready, let's wait
            guard let self = self else { return }
            guard let sOperation = operation else { return }
            var offset = 2
            var firstByte:UInt8 = self.FinMask | code.rawValue
            var data = data
            if [.textFrame, .binaryFrame].contains(code), let compressor = self.compressionState.compressor {
                do {
                    data = try compressor.compress(data)
                    if self.compressionState.clientNoContextTakeover {
                        try compressor.reset()
                    }
                    firstByte |= self.RSV1Mask
                } catch {
                    // TODO: report error?  We can just send the uncompressed frame.
                }
            }
            let dataLength = data.count
            let frame = NSMutableData(capacity: dataLength + self.MaxFrameSize)
            let buffer = UnsafeMutableRawPointer(frame!.mutableBytes).assumingMemoryBound(to: UInt8.self)
            buffer[0] = firstByte
            if dataLength < 126 {
                buffer[1] = CUnsignedChar(dataLength)
            } else if dataLength <= Int(UInt16.max) {
                buffer[1] = 126
                WebSocket.writeUint16(buffer, offset: offset, value: UInt16(dataLength))
                offset += MemoryLayout<UInt16>.size
            } else {
                buffer[1] = 127
                WebSocket.writeUint64(buffer, offset: offset, value: UInt64(dataLength))
                offset += MemoryLayout<UInt64>.size
            }
            buffer[1] |= self.MaskMask
            let maskKey = UnsafeMutablePointer<UInt8>(buffer + offset)
            _ = SecRandomCopyBytes(kSecRandomDefault, Int(MemoryLayout<UInt32>.size), maskKey)
            offset += MemoryLayout<UInt32>.size

            for i in 0..<dataLength {
                buffer[offset] = data[i] ^ maskKey[i % MemoryLayout<UInt32>.size]
                offset += 1
            }
            var total = 0
            while !sOperation.isCancelled {
                if !self.readyToWrite {
                    self.doDisconnect(WSError(type: .outputStreamWriteError, message: "output stream had an error during write", code: 0))
                    break
                }
                let stream = self.stream
                let writeBuffer = UnsafeRawPointer(frame!.bytes+total).assumingMemoryBound(to: UInt8.self)
                let len = stream.write(data: Data(bytes: writeBuffer, count: offset-total))
                if len <= 0 {
                    self.doDisconnect(WSError(type: .outputStreamWriteError, message: "output stream had an error during write", code: 0))
                    break
                } else {
                    total += len
                }
                if total >= offset {
                    if let callback = writeCompletion {
                        self.callbackQueue.async {
                            callback()
                        }
                    }
                    break
                }
            }
        }
        writeQueue.addOperation(operation)
    }

  • 在上面的代码中可以知道,当进行数据压缩时,RSV1的值为 1

在某些文章上说,在数据发送时,会对数据进行再分包操作,最后一个分包的FIN = 1,非最后一个的分包FIN = 0。在Starscream的实现中,我没找到相交的逻辑,FIN的值都固定赋值为1以表示接收完这个包,就是一个完整的包。

在以往的项目开发中,我个人感觉这个也不进行业务层分包操作也是可以的。以后有空的话,拿一个成熟的Websocket服务端库进行研究,查看其接收数据的逻辑,看是否存在对这一块逻辑的处理。

总结

对数据进行发送之前,Websocket要求对发送的数据添加固定格式的包头,用于处理分包拆包时提供必要的依据。在过去文章里,其实差不多的代码逻辑也提到过,通过对比Starscream的分包拆包逻辑,个人感觉Starscream在接收数据方面的逻辑也是略显粗糙。展望未来,感觉Websocket协议进行下一代的标定时,可以给包头添加标识符,以加强在数据解析时的 健壮性提供协议上的条件支撑。当然,足够短的包头设计也算是Websocket一个特点了,在应付分包拆包逻辑上已经处于够用的状态。

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