Websocket同時支持“字符串”及“二進制”數據的發送操作,因爲在其發送的時候,都要進行二進制數據類型的轉換。
這篇文章將會重點介紹數據包格式的重要性,以及Websocket在數據包定義上的特點。
良好的數據包結構,可以爲數據解析提供良好協議基礎。在實現上邏輯上提供更多的異常處理上的條件參考。
數據包的封裝
在正常數據交換過程中,對發送的數據進行封裝是必不可少的。對於上面進行的“連接文本”的發送相對來說只是一個特例。有空的話,找一個Websocket服務端的庫進行源代碼的分析。
我們常用的通訊方式,幾乎都是基於TCP/UDP的方式進行數據發關的,其中TCP基於連接的發送方式比較常用。無論基於哪種方式,在數據接收端都是基於接收緩存的方式,接收一定數量的包後回調到應用層進行處理。這個時候,會產生以下幾種情況:
-
數據包很大,接收緩存無法容納完整的情況下回調到上層應用方法進行數據的處理。這個時候數據包不完整,要等下一次回調再進行合併後再進行包完整性的判斷。當接一個包的數據完整後,進行數據分離,再對這個完整的數據包進行更上層的邏輯處理。
-
數據很小,發送的數據包比較頻繁的情況下,接收緩存同時接收到多個完整的數據包(當然,最後一個包可能只是接收到一部分),在上層回調接口收到數據時,其實是多個包合在一起的數據。這個時候就要對數據進行分拆,分拆成一個個完整的數據包,把沒接收完整的數據包數據先緩存起來,等待下一次的回調,然後再合併數據再進行分包操作。
基於上面的兩種情況,在不定義數據包格式的情況下我們很難進行分包處理,所以如何定義數據包就顯得尤爲重要。數據格式的定義如下圖:
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一個特點了,在應付分包拆包邏輯上已經處於夠用的狀態。