websocket與socket.io比較與分析

大家參與的項目裏多少都會有web server與browser需要長連接互聯的場景,當然我也是,之前沒有進行太多方案的調研(比如深入理解通信協議和通用的一些解決方案),所以websocket就不假思索地直接用了,包括去年寫的框架xframe裏也是直接嵌入了官方websocket的library來實現的。

兩週前遇到個場景,前端大佬提需求說能否支持socket.io,之前寫的websocket的server組件不能用,當然是欣然答應,花了半天的時間重寫了之前的websocket server支持socket.io的協議。但是爲什麼socketio不能兼容websocket呢?作爲一名合格的工程師,不能知其然而不知其所以然,websocket是什麼,它與socket.io有什麼區別呢?他們又分別適合怎樣的場景?爲什麼有了websocket還需要有socket.io?

於是花了幾天時間(工作之餘)研究下websocket的RFC6455上的定義,擼一遍golang版websocket的源碼實現,比對比對Socket.IO。

本文介紹內容大致如下:

  • weboscket
    • weboscket握手機制細節
    • websocket數據幀
    • websocket代碼分析
  • socket.io
    • socket.io特性介紹
    • socket.io握手機制
  • websocket與socket.io使用場景

Websocket

Websocket是全雙工的基於TCP層的通信協議,爲瀏覽器及網站服務器提供處理流式推送消息的方式。它不同於HTTP協議,但仍依賴HTTP的Upgrade頭部進行協議的轉換。

Websocket Handshake

websocket協議通信分爲兩個部分,先是握手,再是數據傳輸。

如下就是一個基本的websocket握手的請求與回包。

websocket handshake請求

        GET /chat HTTP/1.1
        Host: server.example.com
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
        Origin: http://example.com
        Sec-WebSocket-Protocol: chat, superchat
        Sec-WebSocket-Version: 13

websocket handshake返回

        HTTP/1.1 101 Switching Protocols
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
        Sec-WebSocket-Protocol: chat

websocket消息根據RFC6455統稱爲"messages", 一個message可以由多個frame構成,其中frame可以爲文本數據,二進制數據或者控制幀等,websocket官方有6種類型並預留了10種類型用於未來的擴展。

Websocket協議中如何確保客戶端與服務端接收到握手請求呢?

這裏就要說到HTTP的兩個頭部字段,Sec-Websocket-Key與Sec-Websocket-Accept。

SecWebsocketAccept=Base64(SHA1(SecWebsocketKey+GUID)) {SecWebsocketAccept}=Base64(SHA1(SecWebsocketKey+GUID))

首先客戶端發起請求,在頭部Sec-Websocket-Key中隨機生成base64的字符串;服務端收到請求後,根據頭部Sec-Websocket-Key與約定的GUID"258EAFA5-E914-47DA-
95CA-C5AB0DC85B11"拼接;使用SHA-1hash算法編碼拼接的字符串,最後用base64編碼放入頭部Sec-Websocket-Accept返回客戶端做認證。

更詳細的說明可以看RFC說明,服務端與客戶端都有更詳細的入參限制。

Websocket數據幀

瞭解完websocket握手的大致過程後,這個部分介紹下websocket數據幀(這比理解TCP/IP數據幀看着簡單很多吧)與分片傳輸的方式。

websocket數據幀

  • FIN: 表示是否爲最後一個數據幀的標記位
  • opcode: 表示傳輸的數據格式,例如1表示純文本(utf8)數據幀,2表示二進制數據幀
  • MASK: 表示是否需要掩碼的標記位,在websocket協議裏,從客戶端發送給服務端的包需要通過後面的making-key與payload data數據進行異或操作,防止一些惡意程序直接獲取傳輸內容內容。
  • Payload len:傳輸數據內容的長度
  • Payload Data: 傳輸數據

Websocket握手及數據幀的收發(以Golang爲例)

首先對使用者最外層暴露處理ws連接的handler,該handler是http定義的interface的具體實現,這樣也是符合websocket基於http協議完成協議升級的定義。(但是這個庫沒有看出是怎麼分片處理的)

//在websocket庫定義了處理ws連接的alias函數類型
type Handler Func(*Conn)

func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {}

//自定義Handler(_wsOnMessage)連接處理函數
func _wsOnMessage(c *Conn) {
    //接收ws連接推送的每個frame
    buf := make([]byte, 0)
    n, err := websocket.Message.Receive(buf)

    //buf解析(純文本或者二進制) + 業務處理
}

//最後在定義http server的handler
http.Server.Handler = http.Handler(websocket.Handler(_wsOnMessage))
http.Server.ListenAndServe("0.0.0.0:80")

其次,在Handler.ServeHTTP裏都有哪些邏輯呢?

//檢查http的request裏與websocket相關的頭部信息
  var hs serverHandshaker = &hybiServerHandshaker{Config: config}                                                                                                                                                                           
    code, err := hs.ReadHandshake(buf.Reader, req)
    if err == ErrBadWebSocketVersion {
        fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
        fmt.Fprintf(buf, "Sec-WebSocket-Version: %s\r\n", SupportedProtocolVersion)
        buf.WriteString("\r\n")
        buf.WriteString(err.Error())
        buf.Flush()
        return
    }   
    if err != nil {
        fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
        buf.WriteString("\r\n")
        buf.WriteString(err.Error())
        buf.Flush()
        return
    }   

// 檢查完頭部信息後,這裏websocket定義裏要求必須帶上Origin信息
    if handshake != nil {
        err = handshake(config, req)
        if err != nil {
            code = http.StatusForbidden
            fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
            buf.WriteString("\r\n")
            buf.Flush()
            return
        }   
    }   

//握手檢查完後,這裏做handshake成功返回
    err = hs.AcceptHandshake(buf.Writer)
    if err != nil {
        code = http.StatusBadRequest
        fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
        buf.WriteString("\r\n")
        buf.Flush()
        return
    }   
    conn = hs.NewServerConn(buf, rwc, req)
    return

NewServerConn裏是具體的websocket的encoder與decoder

func (cd Codec) Receive(ws *Conn, v interface{}) (err error) {                                                                                                                                                                                
    ws.rio.Lock()
    defer ws.rio.Unlock()
    if ws.frameReader != nil {
        _, err = io.Copy(ioutil.Discard, ws.frameReader)
        if err != nil {
            return err 
        }   
        ws.frameReader = nil 
    }   
again:
// 這裏初始化的reader用於解析具體的frame,就是上述的一個數據幀的內容,e.g FIN/opcode/payload len等
    frame, err := ws.frameReaderFactory.NewFrameReader() 
    if err != nil {
        return err 
    }   
    frame, err = ws.frameHandler.HandleFrame(frame)
    if err != nil {
        return err 
    }   
    if frame == nil {
        goto again
    }   
    payloadType := frame.PayloadType()
    data, err := ioutil.ReadAll(frame)
    if err != nil {
        return err 
    }   
    return cd.Unmarshal(data, payloadType, v)
}

分片傳輸(fragmentation)

當一個完整消息體大小不可知時,websocket支持分片傳輸。這樣可以方便服務端使用可控大小的buffer來傳輸分段數據,減少帶寬壓力,同時可以有效控制服務器內存。

同時在多路傳輸的場景下,可以利用分片技術使不同的namespace的數據能共享對外傳輸通道。不用等待某個大的message傳輸完成,進入等待狀態。


Socket.IO

Socket.IO是js的庫,用於web的開發應用中實現客戶端與服務端建立全雙工通信。SocketIO主要是基於websocket協議進行的上層封裝(包括連接的管理、心跳與維活及提供room的廣播機制與異步io等特性),同時在websocket不可用時,提供長輪詢作爲備選方式獲取數據。

這裏要注意就是Socket.IO不是Websocket的實現,Socker.IO有自己的協議說明,因此和websocket的server不兼容,Socker.IO握手及數據傳輸都有自定義的metadata與認證邏輯,比如頭部的sid,作者在剛使用時上層接了負載均衡,沒有考慮session保持,導致Socket.IO握手時鑑權一直不通過。

Socket.IO特性

  • 可靠性,Socker.IO基於engine.io實現,先建立長輪詢連接後再升級爲基於websocket全雙工的長連接
  • 自動重連與斷連檢查
  • 多路傳輸/多種數據格式傳輸(這個和websocket特性一樣)
  • 廣播機制(這個用法在開發上還是很方便的,開發同學不需要做太多額外的工作,broadcast函數即可,不用像自己實現websocket服務端一樣要做topic和連接管理及併發推送的處理)

Socket.IO Handshake

主要是polling部分,websocket部分參考前一小節。

1. 客戶端發http請求,URL: /${yourpath}?EIO=3&transport=polling&t=abcd

2. 服務端返回並帶上header: set-cookie=xxx

3. 客戶端使用cookie作爲sid,URL: /${yourpath}?EIO=3&transport=polling&t=abcd&sid=xxx

4. 認證完成

其中客戶端每次發起請求使用的t是時間戳參數 (engine.io),設計思路可以參考cache buster的技術實現。


Websocket與Socket.IO適用場景

只從兩個方面分析:

易用性: Socket.IO的易用性更好,對於前端開發來說,沒有太多心智負擔,比如需要關心重連、push轉polling等容錯邏輯; 服務端上也沒有太多的連接管理的設計,Socker.IO已經打包處理了。

靈活性: 個人覺得websocket的靈活性更高一些,不管是前端還是後端,可以做更多的設計與優化,比如連接管理,容錯重連,用戶認證等,至少在提升技術能力上還是很有幫助。

“學會運用輪子的才能成爲一位好司機,懂得如何造輪子纔可能造就一個米其林”

歡迎討論~~

參考內容

  • https://github.com/jollyburger/xframe/tree/master/server/websocket(忘了哪位大神的庫)
  • https://tools.ietf.org/html/rfc6455
    後端,可以做更多的設計與優化,比如連接管理,容錯重連,用戶認證等,至少在提升技術能力上還是很有幫助。

“學會運用輪子的才能成爲一位好司機,懂得如何造輪子纔可能造就一個米其林”

歡迎討論~~

參考內容

  • https://github.com/jollyburger/xframe/tree/master/server/websocket(忘了哪位大神的庫)
  • https://tools.ietf.org/html/rfc6455
  • https://socket.io
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章