用源代碼簡單透析Websocket背後的真相:三. 數據的接收及分包處理


繼上一節討論“數據包的發送”之後,對於數據包的接收處理也是很多網絡請求框架實現中最核心的處理邏輯之一。通過查看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指令”到法到達客戶端,客戶端這時是無法判斷服務是否可用的。從其代碼的角度分析,只是爲了純粹去解決因爲長時候不發消息被運營商主動斷開連接的問題。

我個人認爲,在框架外層,自己再維護一套心跳,不用框架內部的心跳會是一個更好的選擇。一方面心跳不單單只是爲了解決避免長時候不發消息被運營商主動斷開連接的問題。很大情況上我們平時的項目中,會利用心跳去做很多額外的邏輯,比如進行版本配對爲數據同步提供啓動的依據,用戶是否在線等業務邏輯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章