在以前的HTTP协议中,如果我们想实现即时消息推送,使用的方法只有两种:
ajax轮询技术
:客户端每次在指定间隔的时间发送请求,询问服务器是否有新的数据。long polling长轮询技术
:客户端向服务器发送一次请求,然后一直处于pedding
阻塞状态,直到服务器返回数据。这两种方法都很简单粗暴,唯一的缺点就是只能建立起HTTP连接,然后被动地接收服务端的数据。要是有成千上万的用户在等待消息推送,突然服务器不堪重负而罢工,这就尴尬了。
而WebSocket
就是针对这种场景而设计的,伴随着HTTP5而出现的它,其实跟HTTP协议没有任何关系(虽然HTTP1.1
出了一个keep-alive
的属性,可以将多个请求合并为一个),可以说WebSocket
是一个全新的,用于客户端与服务端进行长连接的全双工协议。
虽然
WebSocket
只需要建立一次就可以让客户端和服务端建立起连接,但是因为是基于TCP
协议构建的,所以本质上还是要进行三次握手。
TCP
本身是持久连接,三次握手和四次挥手就不老调重弹了。而HTTP
之所以是单向的,是因为规范规定了服务器只能响应请求,而不能主动发送数据。所以说WebSocket
可以看做是HTTP的一个补丁
WebSocket
协议在游览器中的显示是这样的:
我们可以看到不同于一般的请求,WebSocket URL
前缀为ws
,它告诉游览器自己不是HTTP
请求,而是WebSocket
请求,此时游览器便会自动对协议进行升级。
默认
ws
端口是80
,wss
端口是443
。
wss
就是通过TLS
加密后的ws
。
不同于一般的HTTP请求,WebSocket
请求添加了几个字段来作为应用,主要的有:
Sec-WebSocket-Accept
和Sec-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-Key
、Sec-WebSocket-Version
、Sec-WebSocket-Extensions
,还有客户端的源地址、请求的资源名称等。当解析完成后,如果能与服务端连接,那么服务端将会返回给客户端一个响应,响应里面包含Sec-WebSocket-Accept
,这是与客户端对接的标识符。
当客户端与服务端建立好连接后,两者就可以通信了:
WebSocket
协议的全局结构大概如下所示,我们来大概解析一下它各个字段的含义:
FIN
:表示这是消息的最后一个字段(设置为1,默认为0)。
RESV1
/RESV2
/RESV3
:标识是否有扩展协议,如果为1,那么在EXTEND PAYLOAD
为0的情况下,就会断开WebSocket
连接。
OPCODE
:标识操作码,这是一个操作帧,用来指示WebSocket
的动作。默认的标识码有:
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。
用Netty
的WebSocketServerProtocolHandler
举例,当我们创建WebSocket
服务时,必然要加入一个WebSocket
协议的处理器,将其协议内容封装为一个便于使用的包装类,在Netty中,我们可以这样定制WebSocket
服务:
进入WebSocketServerProtocolHandler
类,可以看到它定义的属性有:
一个是handlerAdded
,它会在Channel连接后回调,每次都会插入WebSocketServerProtocolHandshakeHandler
。
一个是decode
,它会针对进行的数据帧进行操作。
在close前要进行
frame.retain();
,是因为在关闭时需要用到frame
,在Netty的所有操作都是异步的情况下,这样就可以防止frame
在没有用完时就被释放掉了。
来看下WebSocketServerProtocolHandshakeHandler.channelRead
方法:
在
WebSocketServerProtocolHandshakeHandler.channelRead
绑定触发事件时,为了保持兼容性,所以设置了两个,第一个是过时的,下面那个是新的。
对于通过握手器工厂WebSocketServerHandshakerFactory
创建的WebSocketServerHandshaker
,我们需要注意它的handshake
方法。该方法其实就是来发送响应数据的。
它先把跟HTTP聚合,压缩的处理器移除,然后看有没有HttpRequestDecoder
,如果没有,那就在前面添加WebSocket
的编解码器。如果有HTTP编解码器,就把编解码器替换成WebSocket
编解码器,等发送响应成功了,就移除掉HttpServerCodec
或HttpResponseEncoder
。
这样处理完之后,就把和HTTP编解码器移除出去了,这样的话就可以保证使用者即使添加了错误的处理器,程序也可以正常执行
WebSocket
连接。
WebSocket
协议用于长连接传输数据,本质也不过是定义了一种协议格式,然后往里面放数据。从功能上来说唯一和HTTP的区别就是客户端和服务端是可以相互推送消息的,而非被动。
之前说过HTTP 1.1
添加了一个keep-alive
请求头属性,可以作用于长连接。但是这里的长连接和WebSocket
的长连接不同。keep-alive
的作用是保持连接,可以让其它的HTTP请求可以复用这个通道,每次HTTP请求还是要携带请求头的。而WebSocket
的长连接的每一个连接对应一个客户端。
一个很明显的比方就是打电话给客服。keep-alive
的表示是一方讲完之后就把电话给了自己身后的人,然后身后的人跟客服反映新的问题。WebSocket
表示一方讲完之后,听完客服的反馈就挂掉了电话,两人就断了联系。
对于WebSocket
协议想详细了解的话,这里推荐一篇文章:WebSocket 协议 RFC 文档(全中文翻译)