websocket与socket.io比较与分析

  • Post author:
  • Post category:其他


大家参与的项目里多少都会有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。





S

e

c

W

e

b

s

o

c

k

e

t

A

c

c

e

p

t

=

B

a

s

e

64

(

S

H

A

1

(

S

e

c

W

e

b

s

o

c

k

e

t

K

e

y

+

G

U

I

D

)

)

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







S


e


c


W


e


b


s


o


c


k


e


t


A


c


c


e


p


t





=








B


a


s


e


6


4


(


S


H


A


1


(


S


e


c


W


e


b


s


o


c


k


e


t


K


e


y




+








G


U


I


D


)


)





首先客户端发起请求,在头部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



版权声明:本文为xiaojax原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。