感覺現在很多做APP開發的朋友,都習慣了對第三方庫的依賴,沒有第三方庫做什麼都無從下手的樣子,無論是面試還是聊天,都口若懸河滔滔不絕地堆出很多第三方的庫,並引以豪。
而且,現在很多人用習慣了別人寫好的東西,就不想去研究技術本身的東西。覺得寫程序也就是寫寫if–else,一但發生一些增長一點的問題之後就不知所措無從下手,只會想是不是第三方的庫選錯了,然後再去償試各種庫,試到問題解決爲止。
其實用一些比較好用穩定的第三方庫去實現一些功能,我個人認爲是可行的,但並不代表就不用去了解一些基礎的概念了。所以這次打算從一個開源庫的源代碼出發,去了解Websocket到底是什麼。
1. 建立連接文本
一言不合,直接上代碼吧
/**
* Private method that starts the connection.
*/
private func createHTTPRequest() {
guard let url = request.url else {return}
var port = url.port
if port == nil {
if supportedSSLSchemes.contains(url.scheme!) {
port = 443
} else {
port = 80
}
}
request.setValue(headerWSUpgradeValue, forHTTPHeaderField: headerWSUpgradeName)
request.setValue(headerWSConnectionValue, forHTTPHeaderField: headerWSConnectionName)
headerSecKey = generateWebSocketKey()
request.setValue(headerWSVersionValue, forHTTPHeaderField: headerWSVersionName)
request.setValue(headerSecKey, forHTTPHeaderField: headerWSKeyName)
if enableCompression {
let val = "permessage-deflate; client_max_window_bits; server_max_window_bits=15"
request.setValue(val, forHTTPHeaderField: headerWSExtensionName)
}
let hostValue = request.allHTTPHeaderFields?[headerWSHostName] ?? "\(url.host!):\(port!)"
request.setValue(hostValue, forHTTPHeaderField: headerWSHostName)
var path = url.absoluteString
let offset = (url.scheme?.count ?? 2) + 3
path = String(path[path.index(path.startIndex, offsetBy: offset)..<path.endIndex])
if let range = path.range(of: "/") {
path = String(path[range.lowerBound..<path.endIndex])
} else {
path = "/"
if let query = url.query {
path += "?" + query
}
}
var httpBody = "\(request.httpMethod ?? "GET") \(path) HTTP/1.1\r\n"
if let headers = request.allHTTPHeaderFields {
for (key, val) in headers {
httpBody += "\(key): \(val)\r\n"
}
}
httpBody += "\r\n"
initStreamsWithData(httpBody.data(using: .utf8)!, Int(port!))
advancedDelegate?.websocketHttpUpgrade(socket: self, request: httpBody)
}
流程大概如下:
-
分析url,確定服務端連接的端口 (加密通道:443, 普通通道:80)
-
設置相關頭信息,然後生成的頭信息字符串,進行發送。
其中相關的頭信息如下:Connection: Upgrade
申請升級協議
Upgrade: websocket
升級協議爲 websocket
Sec-WebSocket-Version: 13
曾經有一段時候,大家都在實現自己協,最好型成標準後,定下13
這個版本號
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
客戶端隨機生成的16個字節的隨機數,再通過base64編碼後的值,算法如下:/** Generate a WebSocket key as needed in RFC. */ private func generateWebSocketKey() -> String { var key = "" let seed = 16 for _ in 0..<seed { let uni = UnicodeScalar(UInt32(97 + arc4random_uniform(25))) key += "\(Character(uni!))" } let data = key.data(using: String.Encoding.utf8) let baseKey = data?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) return baseKey! }
Sec-WebSocket-Extensions : permessage-deflate; client_max_window_bits; server_max_window_bits=15
這個屬於擴展字段,在這裏的意思是 “對發送的數據進行壓縮操作”
Host: 【域名/IP地址】:【端口號】
這個就不用多解釋了
GET / HTTP/1.1
websocket 在發送應用層連接請求時,只能用GET 方法 -
生成請求字符串
在這裏我們看到,前面建立的URLRequest
對像,只是用於對頭段進行存儲,最後還是以健值對的方式取值後生成提交用的字符串。
在實際的運行中,我進行了輸入如下:
2. 發送連接文本
/**
* Start the stream connection and write the data to the output stream.
*/
private func initStreamsWithData(_ data: Data, _ port: Int) {
guard let url = request.url else {
disconnectStream(nil, runDelegate: true)
return
}
disconnectStream(nil, runDelegate: false)
let useSSL = supportedSSLSchemes.contains(url.scheme!)
#if os(Linux)
let settings = SSLSettings(useSSL: useSSL,
disableCertValidation: disableSSLCertValidation,
overrideTrustHostname: overrideTrustHostname,
desiredTrustHostname: desiredTrustHostname),
sslClientCertificate: sslClientCertificate
#else
let settings = SSLSettings(useSSL: useSSL,
disableCertValidation: disableSSLCertValidation,
overrideTrustHostname: overrideTrustHostname,
desiredTrustHostname: desiredTrustHostname,
sslClientCertificate: sslClientCertificate,
cipherSuites: self.enabledSSLCipherSuites)
#endif
certValidated = !useSSL
let timeout = request.timeoutInterval * 1_000_000
stream.delegate = self
stream.connect(url: url, port: port, timeout: timeout, ssl: settings, completion: { [weak self] (error) in
guard let self = self else {return}
if error != nil {
self.disconnectStream(error)
return
}
let operation = BlockOperation()
operation.addExecutionBlock { [weak self, weak operation] in
guard let sOperation = operation, let self = self else { return }
guard !sOperation.isCancelled else { return }
// Do the pinning now if needed
#if os(Linux) || os(watchOS)
self.certValidated = false
#else
if let sec = self.security, !self.certValidated {
let trustObj = self.stream.sslTrust()
if let possibleTrust = trustObj.trust {
self.certValidated = sec.isValid(possibleTrust, domain: trustObj.domain)
} else {
self.certValidated = false
}
if !self.certValidated {
self.disconnectStream(WSError(type: .invalidSSLError, message: "Invalid SSL certificate", code: 0))
return
}
}
#endif
let _ = self.stream.write(data: data)
}
self.writeQueue.addOperation(operation)
})
self.mutex.lock()
self.readyToWrite = true
self.mutex.unlock()
}
從代碼可以看出,這都是我們平常所用到的進行TCP連接的API。 其中值得注意的是:
- 連接文本的發送在TCP建立連接之後,通過TCP通道發送。
在網絡上看過有說這個過程是通過一個HTTP請求完成的,其表達方式上我個人認爲是存在一定誤導性的。很多不知情的人還以爲是完成一個HTTP請求後纔打開連接,很容易認人以爲是通過兩次連接完成的過程。 - 發送的
連接文本
是直接發送的,沒有經過結構性封裝,以及壓縮處理(就算打開壓縮開關的情況下)
3. 處理服務端返回的連接響應信息
服務端接收到連接請求文本
後,同樣以文本的方式返回到客戶端。這裏我只給出其中的處理代碼,其它流程代碼就省略了。
/**
Finds the HTTP Packet in the TCP stream, by looking for the CRLF.
*/
private func processHTTP(_ buffer: UnsafePointer<UInt8>, bufferLen: Int) -> Int {
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)
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)
NotificationCenter.default.post(name: NSNotification.Name(WebsocketDidConnectNotification), object: self)
}
}
//totalSize += 1 //skip the last \n
let restSize = bufferLen - totalSize
if restSize > 0 {
processRawMessagesInBuffer(buffer + totalSize, bufferLen: restSize)
}
return 0 //success
}
return -1 // Was unable to find the full TCP header.
}
這裏我就列一下大概的返回文本如下:
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
返回的Sec-WebSocket-Accept
的值是通過將“Sec-WebSocket-Key”跟“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”進行拼接,通過SHA1計,並轉成base64字符串得出的結果。代碼如下:
let sha = "\(headerSecKey)258EAFA5-E914-47DA-95CA-C5AB0DC85B11".sha1Base64()
在這段代碼中,我們發現:在服務端進行連接回復時,可以額外附帶其它信息放在 上面的文本尾部,在客戶端進行回覆的同時同時進行其它操作
4.連接流程總結
說到這裏,可以稍微對這個連接過程做一下小小的總結:
- Websocket 進連接階段,先建立TCP的連接。
- 在連接成功用建立的TCP通道後向服務端發起“屬於Websocket本身”的上層連接文本。
- 發送連接文本時,不進行數據包形式的封裝,不進行壓縮等處理。
- 服務端進行連接反饋時,同樣以文本的形式返回,不進行數據包形式的封裝,不進行壓縮等處理。
- 反饋文本後面可以附帶其它數據,可以認客戶端進行連帶的邏輯處理。