WebSocket技术解析和应用

引言

在以前的HTTP协议中,如果我们想实现即时消息推送,使用的方法只有两种:

  • ajax轮询技术:客户端每次在指定间隔的时间发送请求,询问服务器是否有新的数据。
  • long polling长轮询技术:客户端向服务器发送一次请求,然后一直处于pedding阻塞状态,直到服务器返回数据。

这两种方法都很简单粗暴,唯一的缺点就是只能建立起HTTP连接,然后被动地接收服务端的数据。要是有成千上万的用户在等待消息推送,突然服务器不堪重负而罢工,这就尴尬了。

WebSocket就是针对这种场景而设计的,伴随着HTTP5而出现的它,其实跟HTTP协议没有任何关系(虽然HTTP1.1出了一个keep-alive的属性,可以将多个请求合并为一个),可以说WebSocket是一个全新的,用于客户端与服务端进行长连接的全双工协议。

虽然WebSocket只需要建立一次就可以让客户端和服务端建立起连接,但是因为是基于TCP协议构建的,所以本质上还是要进行三次握手。

TCP本身是持久连接,三次握手和四次挥手就不老调重弹了。而HTTP之所以是单向的,是因为规范规定了服务器只能响应请求,而不能主动发送数据。所以说WebSocket可以看做是HTTP的一个补丁

协议概览

WebSocket协议在游览器中的显示是这样的:

1595059291537

我们可以看到不同于一般的请求,WebSocket URL前缀为ws,它告诉游览器自己不是HTTP请求,而是WebSocket请求,此时游览器便会自动对协议进行升级。

默认ws端口是80wss端口是443

wss就是通过TLS加密后的ws

不同于一般的HTTP请求,WebSocket请求添加了几个字段来作为应用,主要的有:

  • Sec-WebSocket-AcceptSec-WebSocket-Key:只有当Sec-WebSocket-Key的值经过固定算法加密后的数据和响应头里的Sec-WebSocket-Accept的值保持一致,该连接才会被认可建立,避免跨协议攻击。
  • Sec-WebSocket-Version:这个header字段的值必须为13,因为在它之前有很多测试的版本,比如9、10、11、12,这些版本现在都不被认为是有效的Sec-WebSocket-Version
  • Sec-WebSocket-Extensions:该属性存储客户端的扩展,在连接建立时服务端可以针对该扩展进行处理。
  • Upgrade:告诉游览器该HTTP协议已经升级到了WebSocket

当客户端对服务端发起WebSocket请求时,只有在当前连接已经建立的情况下才能再次建立连接(客户端会对剩下的连接进行排序)。因为WebSocket是长连接,所以客户端需要注意限制同一个主机的连接数量,避免脚本通过创建大量的WebSocket连接来进行DDOS攻击。如果客户端是通过代理访问服务的,那么客户端应该连接到那个代理并且通过这个代理去和服务端建立一个TCP连接。

在服务端接收客户端的WebSocket请求后,需要对该请求进行解析,获取它的Sec-WebSocket-KeySec-WebSocket-VersionSec-WebSocket-Extensions,还有客户端的源地址、请求的资源名称等。当解析完成后,如果能与服务端连接,那么服务端将会返回给客户端一个响应,响应里面包含Sec-WebSocket-Accept,这是与客户端对接的标识符。

当客户端与服务端建立好连接后,两者就可以通信了:

1595060816450

协议结构

WebSocket协议的全局结构大概如下所示,我们来大概解析一下它各个字段的含义:

1595055155176

  • FIN:表示这是消息的最后一个字段(设置为1,默认为0)。

  • RESV1/RESV2/RESV3:标识是否有扩展协议,如果为1,那么在EXTEND PAYLOAD为0的情况下,就会断开WebSocket连接。

  • OPCODE:标识操作码,这是一个操作帧,用来指示WebSocket的动作。默认的标识码有:

    • %x0 :一个连续的消息分片
    • %x1 :一个文本类型的消息分片
    • %x2 :一个二进制类型的消息分片
    • %x3-7:预留给以后的数据帧
    • %x8 :一个关闭连接的指令
    • %x9 :一个ping包
    • %xA :一个pong包
    • %xB-F:预留给以后的控制帧

    ping包pong包是用来做心跳检测的。

  • MASK:标识数据是否有加掩码,如果设置为1,掩码键必须放在MASKING KEY区域。

  • PAYED LENGTH:传输的数据的长度(不包括MASKING-KEY)。

  • MASKING-KEY:掩码键。

  • PAYLOAD DATA:传输的数据(扩展数据EXTEND PAYLOAD+应用数据APPLICATION PAYLOAD)。

    • 扩展数据:自己定义的扩展协议。
    • 应用数据:基础的数据帧。

前端处理

HTML5封装好了处理方法,只需要调用其API就可以了。

直接创建一个WebSocket对象,然后将onopen\onclose等方法绑定到对象。

相关API可以查看MDN WEB——API WebSocket

后端处理

NettyWebSocketServerProtocolHandler举例,当我们创建WebSocket服务时,必然要加入一个WebSocket协议的处理器,将其协议内容封装为一个便于使用的包装类,在Netty中,我们可以这样定制WebSocket服务:

1595061523665

进入WebSocketServerProtocolHandler类,可以看到它定义的属性有:

1595063532677
它两个方法有:

一个是handlerAdded,它会在Channel连接后回调,每次都会插入WebSocketServerProtocolHandshakeHandler

1595061700072

一个是decode,它会针对进行的数据帧进行操作。

1595062084143

在close前要进行frame.retain();,是因为在关闭时需要用到frame,在Netty的所有操作都是异步的情况下,这样就可以防止frame在没有用完时就被释放掉了。

来看下WebSocketServerProtocolHandshakeHandler.channelRead方法:

1595062843801

WebSocketServerProtocolHandshakeHandler.channelRead绑定触发事件时,为了保持兼容性,所以设置了两个,第一个是过时的,下面那个是新的。

对于通过握手器工厂WebSocketServerHandshakerFactory创建的WebSocketServerHandshaker,我们需要注意它的handshake方法。该方法其实就是来发送响应数据的。

它先把跟HTTP聚合,压缩的处理器移除,然后看有没有HttpRequestDecoder,如果没有,那就在前面添加WebSocket的编解码器。如果有HTTP编解码器,就把编解码器替换成WebSocket编解码器,等发送响应成功了,就移除掉HttpServerCodecHttpResponseEncoder

1595063875336

这样处理完之后,就把和HTTP编解码器移除出去了,这样的话就可以保证使用者即使添加了错误的处理器,程序也可以正常执行WebSocket连接。

总结

WebSocket协议用于长连接传输数据,本质也不过是定义了一种协议格式,然后往里面放数据。从功能上来说唯一和HTTP的区别就是客户端和服务端是可以相互推送消息的,而非被动。

之前说过HTTP 1.1添加了一个keep-alive请求头属性,可以作用于长连接。但是这里的长连接和WebSocket的长连接不同。keep-alive的作用是保持连接,可以让其它的HTTP请求可以复用这个通道,每次HTTP请求还是要携带请求头的。而WebSocket的长连接的每一个连接对应一个客户端。

一个很明显的比方就是打电话给客服。keep-alive的表示是一方讲完之后就把电话给了自己身后的人,然后身后的人跟客服反映新的问题。WebSocket表示一方讲完之后,听完客服的反馈就挂掉了电话,两人就断了联系。

对于WebSocket协议想详细了解的话,这里推荐一篇文章:WebSocket 协议 RFC 文档(全中文翻译)

猜你喜欢

转载自juejin.im/post/5f12e375e51d4534ba19f80c