[网络通信协议]websocket
websocket
定义
背景
HTTP协议和HTTPS协议通信过程通常是客户端通过浏览器发出一个请求,服务器接受请求后进行处理并返回结果给客户端,客户端处理结果。
这种机制对于信息变化不是特别频繁的应用可以良好支撑,但对于实时要求高、海量并发的应用来说显得捉襟见肘
WebSocket出现前我们实现推送技术,用的都是轮询,在特定的时间间隔,浏览器自动发出请求,将服务器的消息主动的拉回来,这种情况下,我们需要不断的向服务器发送请求,并且HTTP 请求 的header非常长,里面包含的数据可能只是一个很小的值,这样会占用很多的带宽和服务器资源,并且服务器不能主动向客户端推送数据。在这种情况下需要一种高效节能的双向通信机制来保证数据的实时传输,于是基于HTML5规范的WebSocket应运而生。
HTTP轮询
HTTP实现实时推送用到的轮询,轮询分两种:长轮询和短轮询(传统轮询)
短轮询
短轮询:浏览器定时向服务器发送请求,服务器收到请求不管是否有数据到达都直接响应 请求,隔特定时间,浏览器又会发送相同的请求到服务器, 获取数据响应,如图:
缺点:数据交互的实时性较低,服务端到浏览器端的数据反馈效率低
长轮询
长轮询:浏览器发起请求到服务器,服务器一直保持连接打开,直到有数据可发送。发送完数据之后,浏览器关闭连接,随即又发起一个到服务器的新请求。这一过程在页面打开期间一直持续不断。如图:
缺点:服务器没有数据到达时,http连接会停留一段时间,造成服务器资源浪费,数据交互的实时性也很低
无论是长轮询还是短轮询,浏览器都要先发起对服务器的连接,才能接收数据,并且实时交互性很低。
概念
WebSocket是HTML5下一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的。
HTML5开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。
使用websocket有两种方式:1是使用sockjs,2是使用h5的标准。使用Html5标准自然更方便简单.
实现过程
- 创建WebSocket
- HTTP请求发送到服务器以发起连接
- 取得服务器响应后,建立的连接使用HTTP升级,从HTTP协议交换为WebSocket协议(使用标准的HTTP服务器无法实现WebSocket,只有支持这种协议的专门服务器才能正常工作。)
- 一旦WebSocket连接建立后,后续数据都以帧序列的形式传输
特点
- WebSocket在建立握手连接时,数据是通过http协议传输的,
- 在建立连接之后,真正的数据传输阶段是不需要http协议参与的。
- WebSocket是类似Socket的TCP长连接通讯模式。
- 在客户端断开WebSocket连接或Server端中断连接前,不需要客户端和服务端重新发起连接请求。
- WebSocket的send函数在实现中最终都是通过TCP的系统接口进行传输的。
- WebSocket是真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求。
- 支持双向通信,实时性更强。
- 更好的二进制支持。
- 较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部。
- 支持扩展。ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)
- 基于多线程或多进程的服务器无法适用于 WebSockets,因为它旨在打开连接,尽可能快地处理请求,然后关闭连接。
- 任何实际的 WebSockets 服务器端实现都需要一个异步服务器。
WebSocket与TCP,HTTP的关系
WebSocket与http协议是基于TCP的–可靠的协议–应用层
HTTP协议在Nginx等服务器的解析下,然后再传送给相应的Handler(PHP等)来处理。简单地说,我们有一个非常快速的 接线员(Nginx) ,他负责把问题转交给相应的 客服(Handler) 。
具体关系如下:
组成结构
WebSocket 客户端
在客户端,没有必要为 WebSockets 使用 JavaScript 库。
实现 WebSockets 的 Web 浏览器将通过 WebSockets 对象公开所有必需的客户端功能(主要指支持 Html5 的浏览器)。
客户端 API
以下 API 用于创建 WebSocket 对象。
var Socket = new WebSocket(url, [protocol] );
以上代码中的第一个参数 url, 指定连接的 URL。第二个参数 protocol 是可选的,指定了可接受的子协议。
WebSocket 属性
以下是 WebSocket 对象的属性。假定我们使用了以上代码创建了 Socket 对象:
属性 描述
Socket.readyState 只读属性 readyState 表示连接状态,可以是以下值:0 - 表示连接尚未建立。1 - 表示连接已建立,可以进行通信。2 - 表示连接正在进行关闭。3 - 表示连接已经关闭或者连接不能打开。
Socket.bufferedAmount 只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。
WebSocket 事件
以下是 WebSocket 对象的相关事件。假定我们使用了以上代码创建了 Socket 对象:
事件 事件处理程序 描述
open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发
WebSocket 方法
以下是 WebSocket 对象的相关方法。假定我们使用了以上代码创建了 Socket 对象:
方法 描述
Socket.send() 使用连接发送数据
Socket.close() 关闭连接
示例
// 初始化一个 WebSocket 对象
var ws = new WebSocket(‘ws://localhost:9998/echo’);
// 建立 web socket 连接成功触发事件
ws.onopen = function() {
// 使用 send() 方法发送数据
ws.send(‘发送数据’);
alert(‘数据发送中…’);
};
// 接收服务端数据时触发事件
ws.onmessage = function(evt) {
var received_msg = evt.data;
alert(‘数据已接收…’);
};
// 断开 web socket 连接成功触发事件
ws.onclose = function() {
alert(‘连接已关闭…’);
};
WebSocket 服务端
WebSocket 在服务端的实现非常丰富。
Node.js、Java、C++、Python 等多种语言都有自己的解决方案。
Java
Java 的 web 一般都依托于 servlet 容器。
servlet 容器有
Tomcat、Jetty、Resin。
其中 Tomcat7、Jetty7 及以上版本均开始支持 WebSocket(推荐较新的版本,因为随着版本的更迭,对 WebSocket 的支持可能有变更)。
Spring 框架对 WebSocket 也提供了支持。
Spring
Spring 对于 WebSocket 的支持基于下面的 jar 包:
org.springframework
spring-websocket
${spring.version}
在 Spring 实现 WebSocket 服务器步骤:
- 创建 WebSocket 处理器
扩展 TextWebSocketHandler 或 BinaryWebSocketHandler ,可以覆写指定的方法。
Spring 在收到 WebSocket 事件时,会自动调用事件对应的方法。
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;
public class MyHandler extends TextWebSocketHandler {
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// …
}
}
WebSocketHandler
public interface WebSocketHandler {
- 建立连接后触发的回调
void afterConnectionEstablished(WebSocketSession session) throws Exception; - 收到消息时触发的回调
void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception; - 传输消息出错时触发的回调
void handleTransportError(WebSocketSession session, Throwable exception) throws Exception; - 断开连接后触发的回调
void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception; - 是否处理分片消息
boolean supportsPartialMessages();
}
- 配置 WebSocket
配置有两种方式:注解和 xml 。
作用就是将 WebSocket 处理器添加到注册中心。
注解方式
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), “/myHandler”);
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
xml 方式
websocket:handlers
<websocket:mapping path="/myHandler" handler=“myHandler”/>
</websocket:handlers>
javax.websocket
如果不想使用 Spring 框架的 WebSocket API,你也可以选择基本的 javax.websocket。
首先,需要引入 API jar 包。
// 打开连接触发事件
@OnOpen
public void onOpen(Session session, EndpointConfig config, @PathParam(“id”) String id) {
…
}
// 关闭连接触发事件
@OnClose
public void onClose(Session session, CloseReason closeReason) {
…
}
// 传输消息错误触发事件
@OnError
public void onError(Throwable error) {
…
}
ServerEndpointConfig.Configurator
编写完处理器,你需要扩展 ServerEndpointConfig.Configurator 类完成配置:
public class WebSocketServerConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
}
}
WebSocket 代理
Nginx 从 1.3 版开始正式支持 WebSocket 代理。
Nginx配置,开启 WebSocket 代理功能。
参考配置:
server {
#this section is specific to the WebSockets proxying
location /socket.io {
proxy_pass http://app_server_wsgiapp/socket.io;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection “upgrade”;
proxy_read_timeout 600;
}
}
工作方式
如何建立连接
WebSocket复用了HTTP的握手通道:客户端通过HTTP请求与WebSocket服务端协商升级协议。协议升级完成后,后续的数据交换则遵照WebSocket的协议。
1、客户端:申请协议升级
首先,客户端发起协议升级请求。
采用的是标准的HTTP报文格式,且只支持GET方法。
重点请求首部意义:
• Connection: Upgrade:表示要升级协议
• Upgrade: websocket:表示要升级到websocket协议。
• Sec-WebSocket-Version: 13:表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。
• Sec-WebSocket-Key:与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。
在握手阶段,可以通过相关请求首部进行 安全限制、权限校验等。
2、服务端:响应协议升级
服务端返回内容:
HTTP/1.1 101 Switching Protocols//状态代码101表示协议切换
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
备注:每个header都以\r\n结尾,并且最后一行加上一个额外的空行\r\n。
服务端回应的HTTP状态码只能在握手阶段使用。过了握手阶段后,就只能采用特定的错误码。
完成协议升级,后续的数据交互都按照新的协议来。
Sec-WebSocket-Accept的计算
Sec-WebSocket-Accept根据客户端请求首部的Sec-WebSocket-Key计算出来。
计算公式为:
- 将Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。
- 通过SHA1计算出摘要,并转成base64字符串。
伪代码如下:
toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )
验证下前面的返回结果:
const crypto = require(‘crypto’);
const magic = ‘258EAFA5-E914-47DA-95CA-C5AB0DC85B11’;
const secWebSocketKey = ‘w4v7O6xFTi36lq3RNcgctw==’;
let secWebSocketAccept = crypto.createHash(‘sha1’)
.update(secWebSocketKey + magic)
.digest(‘base64’);
console.log(secWebSocketAccept);
// Oy4NRAQ13jhfONC7bP8dTKb4PTU=
如何交换数据
一旦WebSocket客户端、服务端建立连接后,后续的操作都是基于数据帧的传递。
WebSocket根据opcode来区分操作的类型。
1、数据分片
WebSocket的每条消息可能被切分成多个数据帧。
当WebSocket的接收方收到一个数据帧时,会根据FIN的值来判断,是否已经收到消息的最后一个数据帧。
FIN=1表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,可以对消息进行处理。
FIN=0,则接收方还需要继续监听接收其余的数据帧。
opcode在数据交换的场景下,表示的是数据的类型。
0x01表示文本
0x02表示二进制
0x00表示延续帧(continuation frame),完整消息对应的数据帧还没接收完。
2、数据分片例子
客户端向服务端两次发送消息,服务端收到消息后回应客户端,
客户端往服务端发送的消息。
第一条消息
FIN=1, 表示是当前消息的最后一个数据帧。服务端收到当前数据帧后,可以处理消息。opcode=0x1,表示客户端发送的是文本类型。
第二条消息
- FIN=0,opcode=0x1,表示发送的是文本类型,且消息还没发送完成,还有后续的数据帧。
- FIN=0,opcode=0x0,表示消息还没发送完成,还有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。
- 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!
数据帧格式
WebSocket客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。
- 发送端:将消息切割成多个帧,并发送给服务端;
- 接收端:接收消息帧,并将关联的帧重新组装成完整的消息;
WebSocket数据帧的统一格式。
- 从左到右,单位是比特。比如FIN、RSV1各占据1比特,opcode占据4比特。
- 内容包括了标识、操作代码、掩码、数据、数据长度等。
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 … |
- FIN:1个比特。
如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是是消息(message)的最后一个分片(fragment)。 - RSV1, RSV2, RSV3:各占1个比特。
一般情况下全为0。当客户端、服务端协商采用WebSocket扩展时,这三个标志位可以非0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用WebSocket扩展,连接出错。 - Opcode: 4个比特。
操作代码,Opcode的值决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端应该断开连接(fail the connection)。可选的操作代码如下:
• %x0:表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
• %x1:表示这是一个文本帧(frame)
• %x2:表示这是一个二进制帧(frame)
• %x3-7:保留的操作代码,用于后续定义的非控制帧。
• %x8:表示连接断开。
• %x9:表示这是一个ping操作。
• %xA:表示这是一个pong操作。
• %xB-F:保留的操作代码,用于后续定义的控制帧。 - Mask: 1个比特。
表示是否要对数据载荷进行掩码操作。从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。
如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。
如果Mask是1,那么在Masking-key中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask都是1。
掩码的算法、用途在下一小节讲解。 - 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,重要的位在前)。 - Masking-key:0或4字节(32位)
所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key。
备注:载荷数据的长度,不包括mask key的长度。 - Payload data:(x+y) 字节
载荷数据:包括了扩展数据、应用数据。其中,扩展数据x字节,应用数据y字节。
扩展数据:如果没有协商使用扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。
应用数据:任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就得到应用数据的长度。
掩码算法Masking-key
掩码键(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
如何维持连接
WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。
有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。这个时候,可以采用心跳来实现。
• 发送方->接收方:ping
• 接收方->发送方:pong
ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA。
疑难解惑
Sec-WebSocket-Key/Accept的作用
Sec-WebSocket-Key/Sec-WebSocket-Accept在主要作用在于提供基础的防护,减少恶意连接、意外连接。
作用:
- 避免服务端收到非法的websocket连接(比如http客户端不小心请求连接websocket服务,此时服务端可以直接拒绝连接)
- 确保服务端理解websocket连接。因为ws握手阶段采用的是http协议,因此可能ws连接是被一个http服务器处理并返回的,此时客户端可以通过Sec-WebSocket-Key来确保服务端认识ws协议。(并非百分百保险,比如总是存在那么些无聊的http服务器,光处理Sec-WebSocket-Key,但并没有实现ws协议。。。)
- 用浏览器里发起ajax请求,设置header时,Sec-WebSocket-Key以及其他相关的header是被禁止的。这样可以避免客户端发送ajax请求时,意外请求协议升级(websocket upgrade)
- 可以防止反向代理(不理解ws协议)返回错误的数据。比如反向代理前后收到两次ws连接的升级请求,反向代理把第一次请求的返回给cache住,然后第二次请求到来时直接把cache住的请求给返回(无意义的返回)。
- Sec-WebSocket-Key主要目的并不是确保数据的安全性,因为Sec-WebSocket-Key、Sec-WebSocket-Accept的转换计算公式是公开的,而且非常简单,最主要的作用是预防一些常见的意外情况(非故意的)。
数据掩码的作用
WebSocket协议中,数据掩码的作用是增强协议的安全性。
为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。
代理缓存污染攻击
摘自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 [5]. 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,
下参与者:
• 攻击者、攻击者自己控制的服务器(简称“邪恶服务器”)、攻击者伪造的资源(简称“邪恶资源”)
• 受害者、受害者想要访问的资源(简称“正义资源”)
• 受害者实际想要访问的服务器(简称“正义服务器”)
• 中间代理服务器
攻击步骤一:
- 攻击者浏览器 向 邪恶服务器 发起WebSocket连接。根据前文,首先是一个协议升级请求。
- 协议升级请求 实际到达 代理服务器。
- 代理服务器 将协议升级请求转发到 邪恶服务器。
- 邪恶服务器 同意连接,代理服务器 将响应转发给 攻击者。
由于 upgrade 的实现上有缺陷,代理服务器 以为之前转发的是普通的HTTP消息。因此,当协议服务器 同意连接,代理服务器 以为本次会话已经结束。
攻击步骤二: - 攻击者 在之前建立的连接上,通过WebSocket的接口向 邪恶服务器 发送数据,且数据是精心构造的HTTP格式的文本。其中包含了 正义资源 的地址,以及一个伪造的host(指向正义服务器)。(见后面报文)
- 请求到达 代理服务器 。虽然复用了之前的TCP连接,但 代理服务器 以为是新的HTTP请求。
- 代理服务器 向 邪恶服务器 请求 邪恶资源。
- 邪恶服务器 返回 邪恶资源。代理服务器 缓存住 邪恶资源(url是对的,但host是 正义服务器 的地址)。
到这里,受害者可以登场了: - 受害者 通过 代理服务器 访问 正义服务器 的 正义资源。
- 代理服务器 检查该资源的url、host,发现本地有一份缓存(伪造的)。
- 代理服务器 将 邪恶资源 返回给 受害者。
- 受害者 卒。
附:前面提到的精心构造的“HTTP请求报文”。
Client → Server:
POST /path/of/attackers/choice HTTP/1.1 Host: host-of-attackers-choice.com Sec-WebSocket-Key:
Server → Client:
HTTP/1.1 200 OK
Sec-WebSocket-Accept:
解决方案
对数据载荷进行掩码处理。
限制了浏览器对数据载荷进行掩码处理,但是坏人完全可以实现自己的WebSocket客户端、服务端,不按规则来,攻击可以照常进行。
但是对浏览器加上这个限制后,可以大大增加攻击的难度,以及攻击的影响范围。如果没有这个限制,只需要在网上放个钓鱼网站骗人去访问,一下子就可以在短时间内展开大范围的攻击。
spring boot Websocket
1、pom
使用springboot的websocket功能首先引入springboot组件。
org.springframework.boot
spring-boot-starter-websocket
1.3.5.RELEASE
springboot的高级组件会自动引用基础的组件,像spring-boot-starter-websocket就引入了spring-boot-starter-web和spring-boot-starter,所以不要重复引入。
2、使用@ServerEndpoint创立websocket endpoint
首先要注入ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint。
注意,如果使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理。
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
websocket的具体实现类:
@ServerEndpoint(value = “/websocket”)
@Component
public class MyWebSocket {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet webSocketSet = new CopyOnWriteArraySet();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
- 连接建立成功调用的方法*/
@OnOpen
public void onOpen(Session session) {
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在线数加1
System.out.println(“有新连接加入!当前在线人数为” + getOnlineCount());
try {
sendMessage(CommonConstant.CURRENT_WANGING_NUMBER.toString());
} catch (IOException e) {
System.out.println(“IO异常”);
}
}
/**
- 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
System.out.println(“有一连接关闭!当前在线人数为” + getOnlineCount());
}
/**
- 收到客户端消息后调用的方法
- @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println(“来自客户端的消息:” + message);
//群发消息
for (MyWebSocket item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
- 发生错误时调用
@OnError
public void onError(Session session, Throwable error) {
System.out.println(“发生错误”);
error.printStackTrace();
}
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
//this.session.getAsyncRemote().sendText(message);
}
/**
- 群发自定义消息
- */
public static void sendInfo(String message) throws IOException {
for (MyWebSocket item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
continue;
}
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
MyWebSocket.onlineCount++;
}
public static synchronized void subOnlineCount() {
MyWebSocket.onlineCount–;
}
}
使用springboot的唯一区别是要@Component声明下,而使用独立容器是由容器自己管理websocket的,但在springboot中连容器都是spring管理的。
虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
3、前端代码
Send Close
WebSocket API
创建WebSocket实例
要创建WebSocket,先实例一个WebSocket对象并传入要连接的URL:
var socket = new WebSocket(‘http://localhost:8000’);
执行上面语句后,浏览器会马上尝试创建连接,与XHR类似,WebSocket也有一个表示当前状态的readyState属性。不过,这个属性的值与XHR不相同, socket.readyState值如下:
• 0:正在建立连接, WebSocket.OPENING
• 1:已经建立连接, WebSocket.OPEN
• 2:正在关闭连接, WebSocket.CLOSING
• 3:已经关闭连接, WebSocket.CLOSE
WebSocket没有readystatechange事件,不过,有其他事件对应着不同的状态,readyState的值永远从0开始。
示例如下:
var socket = new WebSocket(‘ws://localhost:8000’);
//正在建立连接
console.log("[readyState]-" + socket.readyState); //0
//连接建立成功回调
socket.onopen = function() {
console.log(‘Connection established.’)
console.log("[readyState]-" + socket.readyState); //1
//发送消息
// socket.send(‘hello world’);
};
//连接失败回调
socket.onerror = function() {
console.log("[readyState]-" + socket.readyState);//3
console.log(‘Connection error.’)
};
//连接关闭回调
socket.onclose = function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
console.log("[readyState]-" + socket.readyState);//3
console.log(‘Connection closed.’)
console.log(code, reason, wasClean)
};
要关闭WebSocket连接,可以在任何时候调用close方法。
socket.close();
调用了close()之后,readyState的值立即变为2(正在关闭),关闭连接后就会变成3。
发送和接收数据
WebSocket连接建立之后,可以通过连接发送和接收数据。
使用send()方法像服务器发送数据,如下:
var socket = new WebSocket(‘ws://localhost:8000’);
socket.send(‘hello world’);
当服务器向客户端发来消息时,WebSocket对象会触发message事件。这个message事件与其他传递消息的协议类似,也是把返回的数据保存在event.data属性中。
socket.onmessage = function(event) {
var data = event.data;
//处理数据
};
其他事件
WebSocket对象还有其他三个事件,在连接生命周期的不同阶段触发。
• open:成功建立连接时触发。
• error:发生错误时触发,连接断开。
• close: 连接关闭时触发。
var socket = new WebSocket(‘ws://localhost:8000’);
socket.onopen = function() {
console.log(‘Connection established.’)
};
socket.onerror = function() {
console.log(‘Connection error.’)
};
socket.onclose = function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
console.log(‘Connection closed.’)
};
这三个事件中,只有close事件的event对象有额外信息,这个事件的事件对象有三个额外的属性:wasClean、code和reason。
其中wasClean是一个布尔值,表示连接是否已经明确的关闭;
code是服务器返回的数值状态码;
reason是一个字符串,包含服务器发回的信息。