Starscream
的源代码,在接收到接收缓存回调回来的数据后的处理上,在进行多线程处理方面处理得可谓不尽人意:
上面的流程算是比较简单的数据接收后的操作流程。下面给出部分关于数据重新合并方面的源代码以供参考:
/**
* 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 //连接反馈数据未接收完整
}
上面的注释已经写得效为详细,就不一一解析了,常规操作了,代码也很通俗易懂。
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 ... |
+-----------------------------------------------------------------+
guard let baseAddress = buffer.baseAddress else {return emptyBuffer}
if response != nil && bufferLen < 2 {
fragBuffer = Data(buffer: buffer)
return emptyBuffer
}
具体数据为什么放fragBuffer里就不先详细解说了。在分拆包方面,第个人的实现逻辑都不一样,但处理数据的方法思路与想达到的目的都是一样的。
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
}
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
}
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))
}
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"指令,很多人说Websocket内部存在一个心跳机制,项目开发中不用自己再添加自己的心跳机制去监测网络服务是否可用。
本来想多开一个章节去讨论很多人对于Websocket的一些看法对于我来说的一些理解的。后来,因为一口所写了三篇文章,越写到最后,越感觉到说的东西太简单,怕大家看得无聊,所以用源代码简单透析Websocket背后的真相
写到第三节就决定结束了。在结束之前,我就上面问题说说我的个人看法。
我觉得,上面对于"ping" "pong"指令的说法是很不全面的。可以说就当前分析的库而言,只是提供了最基本的“发ping指令”,“发pong指令”, “接收到ping指令回复pong指令”的这些逻辑。在网络不稳定的情况下,或者弱网状态下,服务端发送的 “ping指令”到法到达客户端,客户端这时是无法判断服务是否可用的。从其代码的角度分析,只是为了纯粹去解决因为长时候不发消息被运营商主动断开连接的问题。
我个人认为,在框架外层,自己再维护一套心跳,不用框架内部的心跳会是一个更好的选择。一方面心跳不单单只是为了解决避免长时候不发消息被运营商主动断开连接的问题。很大情况上我们平时的项目中,会利用心跳去做很多额外的逻辑,比如进行版本配对为数据同步提供启动的依据,用户是否在线等业务逻辑。