用源代碼簡單透析Websocket背後的真相:一. 連接流程

感覺現在很多做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)
    }

流程大概如下:

  1. 分析url,確定服務端連接的端口 (加密通道:443, 普通通道:80)

  2. 設置相關頭信息,然後生成的頭信息字符串,進行發送。
    其中相關的頭信息如下:

    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 方法

  3. 生成請求字符串
    在這裏我們看到,前面建立的 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。 其中值得注意的是:

  1. 連接文本的發送在TCP建立連接之後,通過TCP通道發送。
    在網絡上看過有說這個過程是通過一個HTTP請求完成的,其表達方式上我個人認爲是存在一定誤導性的。很多不知情的人還以爲是完成一個HTTP請求後纔打開連接,很容易認人以爲是通過兩次連接完成的過程。
  2. 發送的連接文本是直接發送的,沒有經過結構性封裝,以及壓縮處理(就算打開壓縮開關的情況下)

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本身”的上層連接文本。
- 發送連接文本時,不進行數據包形式的封裝,不進行壓縮等處理。
- 服務端進行連接反饋時,同樣以文本的形式返回,不進行數據包形式的封裝,不進行壓縮等處理。
- 反饋文本後面可以附帶其它數據,可以認客戶端進行連帶的邏輯處理。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章