深入浅出FE(十四)深入浅出websocket
Posted fullstack_lth
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入浅出FE(十四)深入浅出websocket相关的知识,希望对你有一定的参考价值。
一、理论知识
1.1 引言
Websocket是一个持久化的协议 协议分为ws(80端口)协议 和wss(443端口)协议。
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
1.2 设计哲学
RFC草案中已经说明,WebSocket的目的就是为了在基础上保证传输的数据量最少。
Websocket协议是基于Frame而非Stream的。也就是说,数据的传输不是像传统的流式读写一样按字节发送,而是采用一帧一帧的Frame,并且每个Frame都定义了严格的数据结构,因此所有的信息就在这个Frame载体中。
1.3 为什么会有 WebSocket
以前,很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。 而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。 在这种情况下,html5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
1.4 WebSocket 有什么优点
开销少、时时性高、二进制支持完善、支持扩展、压缩更优。
- 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
- 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。 保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有* 状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
- 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
- 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
- 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
1.5 websocket和http协议关系
websocket和http一样都是基于tcp协议的传输 websocket和http是两种不同的东西 客户端要建立 websocket链接时候要在header标记一个Upgrade的HTTP请求表示请求升级 服务端返回响应101的状态码 完成握手以后再发送收据就么有http的事了。
1.6 帧Frame
WebSocket传输的数据都是以Frame
(帧)的形式实现的,就像TCP/UDP协议中的报文段Segment
。下面就是一个Frame:(以bit为单位表示)
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
按照RFC中的描述:
-
FIN: 1 bit
表示这是一个消息的最后的一帧。第一个帧也可能是最后一个。 %x0 : 还有后续帧 %x1 : 最后一帧
-
RSV1、2、3: 1 bit each
除非一个扩展经过协商赋予了非零值以某种含义,否则必须为0 如果没有定义非零值,并且收到了非零的RSV,则websocket链接会失败
-
Opcode: 4 bit
解释说明 “Payload data” 的用途/功能 如果收到了未知的opcode,最后会断开链接 定义了以下几个opcode值: %x0 : 代表连续的帧 %x1 : text帧 %x2 : binary帧 %x3-7 : 为非控制帧而预留的 %x8 : 关闭握手帧 %x9 : ping帧 %xA : pong帧 %xB-F : 为非控制帧而预留的
-
Mask: 1 bit
定义“payload data”是否被添加掩码 如果置1, “Masking-key”就会被赋值 所有从客户端发往服务器的帧都会被置1
-
Payload length: 7 bit | 7+16 bit | 7+64 bit
“payload data” 的长度如果在0~125 bytes范围内,它就是“payload length”, 如果是126 bytes, 紧随其后的被表示为16 bits的2 bytes无符号整型就是“payload length”, 如果是127 bytes, 紧随其后的被表示为64 bits的8 bytes无符号整型就是“payload length”
-
Masking-key: 0 or 4 bytes
所有从客户端发送到服务器的帧都包含一个32 bits的掩码(如果“mask bit”被设置成1),否则为0 bit。一旦掩码被设置,所有接收到的payload data都必须与该值以一种算法做异或运算来获取真实值。(见下文)
-
Payload data: (x+y) bytes
它是"Extension data"和"Application data"的总和,一般扩展数据为空。
-
Extension data: x bytes
除非扩展被定义,否则就是0 任何扩展必须指定其Extension data的长度
-
Application data: y bytes
占据"Extension data"之后的剩余帧的空间
注意:这些数据都是以二进制形式表示的,而非ascii编码字符串
1.7 双端交互流程
客户端与服务端交互流程如下所示:
客户端 - 发起握手请求 - 服务器接到请求后返回信息 - 连接建立成功 - 消息互通
所以,要解决的第一个问题就是握手问题。
握手 - 客户端
关于握手标准,在协议中有说明:
The opening handshake is intended to be compatible with HTTP-based server-side software and intermediaries, so that a single port can be used by both HTTP clients talking to that server and WebSocket clients talking to that server. To this end, the WebSocket client's handshake is an HTTP Upgrade request:
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
In compliance with [RFC2616], header fields in the handshake may be sent by the client in any order, so the order in which different header fields are received is not significant.
WebSocket 握手时使用的并不是 WebSocket 协议,而是 HTTP 协议,握手时发出的请求可以叫做升级请求。客户端在握手阶段通过:
Upgrade: websocket
Connection: Upgrade
Connection 和 Upgrade 这两个头域告知服务端,要求将通信的协议转换为 websocket。其中 Sec-WebSocket-Version、Sec-WebSocket-Protocol 这两个头域表明通信版本和协议约定, Sec-WebSocket-Key 则作为一个防止无端连接的保障(其实并没有什么保障作用,因为 key 的值完全由客户端控制,服务端并无验证机制),其他几个头域则与 HTTP 协议的作用一致。
握手 - 服务端
刚才只是客户端发出一个 HTTP 请求,表明想要握手,服务端需要对信息进行验证,确认以后才算握手成功(连接建立成功,可以双向通信),然后服务端会给客户端回复:"小老弟你好,没有内鬼,连接达成!"
服务端需要回复什么内容呢?
Status Code: 101 Web Socket Protocol Handshake
Sec-WebSocket-Accept: T5ar3gbl3rZJcRmEmBT8vxKjdDo=
Upgrade: websocket
Connection: Upgrade
首先,服务端会给出状态码,101 状态码表示服务器已经理解了客户端的请求,并且回复 Connection 和 Upgrade 表示已经切换成 websocket 协议。Sec-WebSocket-Accept 则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key。
这样,客户端与服务端就完成了握手操作,达成一致,使用 WebSocket 协议进行通信。
1.8 websocket请求和响应格式
(1)客户端发送的握手请求
GET /chat HTTP/1.1
Host: XXX.com
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Sec-WebSocket-key: XXXX
依次介绍下
-
GET /chat HTTP/1.1
可以是是chat 聊天 也可以game 游戏
-
Connection: Upgrade Upgrade: websocket
这告诉服务器给升级到websocket协议
-
Sec-WebSocket-Protocol: chat, superchat
用户自定义的字符串 在同一个url下 不同服务的所需要的协议 比如聊天chat 也可以其他的自定义
-
Sec-WebSocket-Version 告诉服务器所使用的协议版本
-
Sec-WebSocket-Key 是base64加密的字符串 浏览器自动生成
(2)服务端响应客户端握手请求
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
依次介绍下
- HTTP/1.1 101 Switching Protocols 就是返回101状态码
- Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= 对Sec-WebSocket-key的加密 同意握手建立链接 客户端收到 Sec-WebSocket-Accept后 将本地的Sec-WebSocket-key 编码做一个对比来验证
1.9 数据帧格式
1.9.1 概述
客户端、服务端数据的交换,离不开数据帧格式的定义。因此,在实际讲解数据交换之前,我们先来看下WebSocket的数据帧格式。
WebSocket客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。
- 1)发送端:将消息切割成多个帧,并发送给服务端;
- 2)接收端:接收消息帧,并将关联的帧重新组装成完整的消息。
本节的重点,就是讲解数据帧的格式。详细定义可参考 RFC6455 5.2节 。
1.9.2 数据帧格式概览
下面给出了WebSocket数据帧的统一格式。熟悉TCP/IP协议的同学对这样的图应该不陌生。
- 1)从左到右,单位是比特。比如FIN、RSV1各占据1比特,opcode占据4比特;
- 2)内容包括了标识、操作代码、掩码、数据、数据长度等。(下一小节会展开)
1.9.3 数据帧格式详解
针对前面的格式概览图,这里逐个字段进行讲解,如有不清楚之处,可参考协议规范,或留言交流。
_1)FIN:_1个比特。
如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是是消息(message)的最后一个分片(fragment)。
2)RSV1, RSV2, RSV3:各占1个比特。
一般情况下全为0。当客户端、服务端协商采用WebSocket扩展时,这三个标志位可以非0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用WebSocket扩展,连接出错。
3)Opcode:4个比特。
操作代码,Opcode的值决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端应该断开连接(fail the connection)。
可选的操作代码如下:
%x0:表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
%x1:表示这是一个文本帧(frame)
%x2:表示这是一个二进制帧(frame)
%x3-7:保留的操作代码,用于后续定义的非控制帧。
%x8:表示连接断开。
%x9:表示这是一个ping操作。
%xA:表示这是一个pong操作。
%xB-F:保留的操作代码,用于后续定义的控制帧。
4)Mask:1个比特。
表示是否要对数据载荷进行掩码操作。从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。
如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。
如果Mask是1,那么在Masking-key中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask都是1。
掩码的算法、用途在下一小节讲解。
5)Payload length:数据载荷的长度,单位是字节。为7位,或7+16位,或1+64位。
假设数Payload length === x,如果:
x为0~126:数据的长度为x字节。
x为126:后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度。
x为127:后续8个字节代表一个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度。
此外,如果payload length占用了多个字节的话,payload length的二进制表达采用网络序(big endian,重要的位在前)。
6)Masking-key:0或4字节(32位)
所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key。
备注:载荷数据的长度,不包括mask key的长度。
7)Payload data:(x+y) 字节
载荷数据:包括了扩展数据、应用数据。其中,扩展数据x字节,应用数据y字节。
扩展数据:如果没有协商使用扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。
应用数据:任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就得到应用数据的长度。
1.9.4 掩码算法
掩码键(Masking-key)是由客户端挑选出来的32位的随机数。掩码操作不会影响数据载荷的长度。
掩码、反掩码操作都采用如下算法。
首先,假设:
original-octet-i:为原始数据的第i字节;
transformed-octet-i:为转换后的数据的第i字节;
j:为i mod 4的结果;
masking-key-octet-j:为mask key第j字节。
算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
1.10 数据传输
一旦WebSocket客户端、服务端建立连接后,后续的操作都是基于数据帧的传递。
WebSocket根据opcode来区分操作的类型。比如0x8表示断开连接,0x0-0x2表示数据交互。
1.10.1 数据分片
WebSocket的每条消息可能被切分成多个数据帧。当WebSocket的接收方收到一个数据帧时,会根据FIN的值来判断,是否已经收到消息的最后一个数据帧。
FIN=1表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,可以对消息进行处理。FIN=0,则接收方还需要继续监听接收其余的数据帧。
此外,opcode在数据交换的场景下,表示的是数据的类型。0x01表示文本,0x02表示二进制。而0x00比较特殊,表示延续帧(continuation frame),顾名思义,就是完整消息对应的数据帧还没接收完。
1.10.2 数据分片例子
下面例子来自 MDN,可以很好地演示数据的分片。客户端向服务端两次发送消息,服务端收到消息后回应客户端,这里主要看客户端往服务端发送的消息。
第一条消息:
FIN=1, 表示是当前消息的最后一个数据帧。服务端收到当前数据帧后,可以处理消息。opcode=0x1,表示客户端发送的是文本类型。
第二条消息:
- 1)FIN=0,opcode=0x1,表示发送的是文本类型,且消息还没发送完成,还有后续的数据帧;
- 2)FIN=0,opcode=0x0,表示消息还没发送完成,还有后续的数据帧,当前的数据帧需要接在上一条数据帧之后;
- 3)FIN=1,opcode=0x0,表示消息已经发送完成,没有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。服务端可以将关联的数据帧组装成完整的消息。
Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!
1.11 连接保持、心跳
WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。
然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费包括的连接资源。
但不排除有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。
这个时候,可以采用心跳来实现:
发送方->接收方:ping
接收方->发送方:pong
ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA。
举例:WebSocket服务端向客户端发送ping,只需要如下代码(采用ws模块)
ws.ping('', false, true);
1.12 Sec-WebSocket-Key/Accept的作用
前面提到了,Sec-WebSocket-Key/Sec-WebSocket-Accept 在主要作用在于提供基础的防护,减少恶意连接、意外连接。
作用大致归纳如下:
- 1)避免服务端收到非法的websocket连接(比如http客户端不小心请求连接websocket服务,此时服务端可以直接拒绝连接)
- 2)确保服务端理解websocket连接。因为ws握手阶段采用的是http协议,因此可能ws连接是被一个http服务器处理并返回的,此时客户端可以通过Sec-WebSocket-Key来确保服务端认识ws协议。(并非百分百保险,比如总是存在那么些无聊的http服务器,光处理Sec-WebSocket-Key,但并没有实现ws协议。。。)
- 3)用浏览器里发起ajax请求,设置header时,Sec-WebSocket-Key以及其他相关的header是被禁止的。这样可以避免客户端发送ajax请求时,意外请求协议升级(websocket upgrade)
- 4)可以防止反向代理(不理解ws协议)返回错误的数据。比如反向代理前后收到两次ws连接的升级请求,反向代理把第一次请求的返回给cache住,然后第二次请求到来时直接把cache住的请求给返回(无意义的返回)。
- 5)Sec-WebSocket-Key主要目的并不是确保数据的安全性,因为Sec-WebSocket-Key、Sec-WebSocket-Accept的转换计算公式是公开的,而且非常简单,最主要的作用是预防一些常见的意外情况(非故意的)。
强调:Sec-WebSocket-Key/Sec-WebSocket-Accept 的换算,只能带来基本的保障,但连接是否安全、数据是否安全、客户端/服务端是否合法的 ws客户端、ws服务端,其实并没有实际性的保证。
1.13 数据掩码的作用
1.13.1 概述
WebSocket协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。除了加密通道本身,似乎没有太多有效的保护通信安全的办法。
那么为什么还要引入掩码计算呢,除了增加计算机器的运算量外似乎并没有太多的收益(这也是不少同学疑惑的点)。
答案还是两个字:安全。但并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。
1.13.2 代理缓存污染攻击
下面摘自2010年关于安全的一段讲话。其中提到了代理服务器在协议实现上的缺陷可能导致的安全问题(点此查看出处)。
“We show, empirically, that the current version of the WebSocket consent mechanism is vulnerable to proxy cache poisoning attacks. Even though the WebSocket handshake is based on HTTP, which should be understood by most network intermediaries, the handshake uses the esoteric “Upgrade” mechanism of HTTP. In our experiment, we find that many proxies do not implement the Upgrade mechanism properly, which causes the handshake to succeed even though subsequent traffic over the socket will be misinterpreted by the proxy.”
【TALKING】 Huang, L-S., Chen, E., Barth, A., Rescorla, E., and C.
Jackson, "Talking to Yourself for Fun and Profit", 2010,
在正式描述攻击步骤之前,我们假设有如下参与者:
- 1)攻击者、攻击者自己控制的服务器(简称“邪恶服务器”)、攻击者伪造的资源(简称“邪恶资源”);
- 2)受害者、受害者想要访问的资源(简称“正义资源”);
- 3)受害者实际想要访问的服务器(简称“正义服务器”);
- 4)中间代理服务器。
攻击步骤一:
- 1)攻击者浏览器 向 邪恶服务器 发起WebSocket连接。根据前文,首先是一个协议升级请求;
- 2)协议升级请求 实际到达 代理服务器;
- 3)代理服务器 将协议升级请求转发到 邪恶服务器;
- 4)邪恶服务器 同意连接,代理服务器 将响应转发给 攻击者。
由于 upgrade 的实现上有缺陷,代理服务器 以为之前转发的是普通的HTTP消息。因此,当协议服务器 同意连接,代理服务器 以为本次会话已经结束。
攻击步骤二:
- 1)攻击者 在之前建立的连接上,通过WebSocket的接口向 邪恶服务器 发送数据,且数据是精心构造的HTTP格式的文本。其中包含了 正义资源的地址,以及一个伪造的host(指向正义服务器)。(见后面报文)
- 2)请求到达 代理服务器 。虽然复用了之前的TCP连接,但 代理服务器 以为是新的HTTP请求。
- 3)代理服务器 向 邪恶服务器 请求 邪恶资源。
- 4)邪恶服务器 返回 邪恶资源。代理服务器 缓存住 邪恶资源(url是对的,但host是 正义服务器 的地址)。
到这里,受害者可以登场了:
- 1)受害者 通过 代理服务器 访问 正义服务器 的 正义资源。
- 2)代理服务器 检查该资源的url、host,发现本地有一份缓存(伪造的)。
- 3)代理服务器 将 邪恶资源 返回给 受害者。
- 4)受害者 卒。
附:前面提到的精心构造的“HTTP请求报文”。
Client → Server:
POST /path/of/attackers/choice HTTP/1.1 Host: host-of-attackers-choice.com Sec-WebSocket-Key: <connection-key>
Server → Client:
HTTP/1.1 200 OK
Sec-WebSocket-Accept: <connection-key>
1.13.3 当前解决方案
最初的提案是对数据进行加密处理。基于安全、效率的考虑,最终采用了折中的方案:对数据载荷进行掩码处理。
需要注意的是,这里只是限制了浏览器对数据载荷进行掩码处理,但是坏人完全可以实现自己的WebSocket客户端、服务端,不按规则来,攻击可以照常进行。
但是对浏览器加上这个限制后,可以大大增加攻击的难度,以及攻击的影响范围。如果没有这个限制,只需要在网上放个钓鱼网站骗人去访问,一下子就可以在短时间内展开大范围的攻击。
1.14 兼容性
兼容性列表来自MDN
二、Spring boot + Koa2 + websocket实现简易聊天室
用Spring boot完成一个简单的聊天室功能demo,不具备高并发、持久化等功能。
2.1 pom.xml文件配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.2 添加ServerEndpointExporter配置bean
配置类WebSocketConfig,扫描并注册带有@ServerEndpoint注解的所有websocket服务端
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @author Alan Chen
* @description 开启WebSocket支持
* @date 2020-04-08
*/
@Configuration
public class WebSocketConfig
@Bean
public ServerEndpointExporter serverEndpointExporter()
return new ServerEndpointExporter();
2.3 添加service中的类
新建WebSocketServer类,WebSocket服务端是多例的,一次WebSocket连接对应一个实例
/**
* @Auther: karma2014
* @Date: 2020/11/22 17:55
* @Description: websocket的具体实现类
* 使用springboot的唯一区别是要@Component声明下,而使用独立容器是由容器自己管理websocket的,
* 但在springboot中连容器都是spring管理的。
虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,
所以可以用一个静态set保存起来。
*/
@ServerEndpoint(value = "/websocket")
@Component
public class MyWebSocket
//用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session)
this.session = session;
webSocketSet.add(this); //加入set中
System.out.println("有新连接加入!当前在线人数为" + webSocketSet.size());
this.session.getAsyncRemote().sendText("恭喜您成功连接上WebSocket-->当前在线人数为:"+webSocketSet.size());
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose()
webSocketSet.remove(this); //从set中删除
System.out.println("有一连接关闭!当前在线人数为" + webSocketSet.size());
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session)
System.out.println("来自客户端的消息:" + message);
//群发消息
broadcast(message);
/**
* 发生错误时调用
*
*/
@OnError
public void onError(Session session, Throwable error)
System.out.println("发生错误");
error.printStackTrace();
/**
* 群发自定义消息
* */
public void broadcast(String message)
for (MyWebSocket item : webSocketSet)
//this.session.getBasicRemote().sendText(message);
item.session.getAsyncRemote().sendText(message);//异步发送消息.
2.4 前端代码
前端代码可以单独启动一个Nodejs静态服务,将此页面放在静态文件夹即可
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>My WebSocket</title>
<style>
#message
margin-top:40px;
border:1px solid gray;
padding:20px;
</style>
</head>
<body>
<button onclick="conectWebSocket()">连接WebSocket</button>
<button onclick="closeWebSocket()">断开连接</button>
<hr />
<br />
消息:<input id="text" type="text" />
<button onclick="send()">发送消息</button>
<div id="message"></div>
</body>
<script type="text/javascript">
var websocket = null;
function conectWebSocket()
//判断当前浏览器是否支持WebSocket
if ('WebSocket'in window)
websocket = new WebSocket("ws://localhost:8080/websocket");
else
alert('Not support websocket')
//连接发生错误的回调方法
websocket.onerror = function()
setMessageInnerHTML("error");
;
//连接成功建立的回调方法
websocket.onopen = function(event)
setMessageInnerHTML("Loc MSG: 成功建立连接");
//接收到消息的回调方法
websocket.onmessage = function(event)
setMessageInnerHTML(event.data);
//连接关闭的回调方法
websocket.onclose = function()
setMessageInnerHTML("Loc MSG:关闭连接");
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function()
websocket.close();
//将消息显示在网页上
function setMessageInnerHTML(innerHTML)
document.getElementById('message').innerHTML += innerHTML + '<br/>';
//关闭连接
function closeWebSocket()
websocket.close();
//发送消息
function send()
var message = document.getElementById('text').value;
websocket.send(message);
</script>
</html>
项目启动界面:
三、项目设计问题
3.1 接口设计
3.2 数据推送频率
3.3 心跳检测和断线重连
3.4 不支持websocket的浏览器降级为轮询查询
使用一些成熟的框架,如socket.io
3.5 nginx 部署
The WebSocket protocol is different from the HTTP protocol, but the WebSocket handshake is compatible with HTTP, using the HTTP Upgrade facility to upgrade the connection from HTTP to WebSocket.
This allows WebSocket applications to more easily fit into existing infrastructures.
For example, WebSocket applications can use the standard HTTP ports 80 and 443, thus allowing the use of existing firewall rules.
location /websocket
proxy_pass http://xx.xxx.xx.xx; # websocket服务器。不用管 ws://
proxy_http_version 1.1; # http协议切换
proxy_set_header Host $host; # 保留源信息
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade; # 请求协议升级,如果生产环境有报400错误,可以尝试将值设置为websocket
proxy_set_header Connection $connection_upgrade;
3.6 集群部署
四、参考资料
[1] RFC6455:websocket规范
[2] 规范:数据帧掩码细节
[3] 规范:数据帧格式
[4] server-example
[5] 编写websocket服务器
[6] 对网络基础设施的攻击(数据掩码操作所要预防的事情)
[7] Talking to Yourself for Fun and Profit(含有攻击描述)
[8] What is Sec-WebSocket-Key for?
[9] 10.3. Attacks On Infrastructure (Masking)
[10] Talking to Yourself for Fun and Profit
[11] Why are WebSockets masked?
[12] How does websocket frame masking protect against cache poisoning?
[13] What is the mask in a WebSocket frame?
附录:更多Web端即时通讯资料
《 SSE技术详解:一种全新的HTML5服务器推送事件技术》
《 Comet技术详解:基于HTTP长连接的Web端实时通信技术》
《 socket.io实现消息推送的一点实践及思路》
《 LinkedIn的Web端即时通讯实践:实现单机几十万条长连接》
《 Web端即时通讯技术的发展与WebSocket、Socket.io的技术实践》
《 Web端即时通讯安全:跨站点WebSocket劫持漏洞详解(含示例代码)》
《 开源框架Pomelo实践:搭建Web端高性能分布式IM聊天服务器》
《 使用WebSocket和SSE技术实现Web端消息推送》
《 详解Web端通信方式的演进:从Ajax、JSONP 到 SSE、Websocket》
《 MobileIMSDK-Web的网络层框架为何使用的是Socket.io而不是Netty?》
《 理论联系实际:从零理解WebSocket的通信原理、协议格式、安全性》
《 微信小程序中如何使用WebSocket实现长连接(含完整源码)》
《 快速了解Electron:新一代基于Web的跨平台桌面技术》
《 一文读懂前端技术演进:盘点Web前端20年的技术变迁史》
《 Web端即时通讯基础知识补课:一文搞懂跨域的所有问题!》
《 Web端即时通讯实践干货:如何让你的WebSocket断网重连更快速?》
《 WebSocket从入门到精通,半小时就够!》
以上是关于深入浅出FE(十四)深入浅出websocket的主要内容,如果未能解决你的问题,请参考以下文章