一. 什么是 WebSocket
WebSocket 是一种全新的协议。它将 TCP 的 Socket(套接字)应用在了web page上,从而使通信双方建立起一个保持在活动状态的连接通道,并且属于全双工通信(双方同时进行双向通信)
。
二. WebSocket 的特点
WebSocket 的最大特点是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
其他特点包括:
- 建立在 TCP 协议之上,服务器端的实现比较容易。
- 与 HTTP 协议有着良好的兼容性。默认端口是 80 和 443 ,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
三. WebSocket 的优势
目前,很多网站都使用 Ajax 轮询方式来实现消息推送。
轮询是指在特定的的时间间隔(如每秒),由浏览器对服务器发出 HTTP 请求,然后由服务器返回最新的数据给客户端的浏览器。
这种传统的模式带来了很明显的缺点,即浏览器需要不断地向服务器发出请求,然而 HTTP 请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,会浪费很多的带宽资源。
而 WebSocket 允许服务端主动向客户端推送数据,这就使得客户端和服务器之间的数据交换变得更加简单。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
四. WebSocket 的实现
1.建SpringBoot项目
2.pom.xml
<!-- SpringBoot Websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
3.新建配置类 WebSocketConfig,开启 WebSocket 支持
package org.springblade.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @ClassName: WebSocketConfig
* @author: 〆、dyh
* @since: 2022/8/23 20:26
*/
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
4.实现核心服务类 WebSocketServer
package org.springblade.modules.home.stats.service;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springblade.modules.rpa.service.SunPurchasePayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
//注册成组件
@Component
//定义websocket服务器端,它的功能主要是将目前的类定义成一个websocket服务器端。注解的值将被用于监听用户连接的终端访问URL地址
@ServerEndpoint("/websocket/{tenantId}")
@Slf4j
public class WebSocketServer {
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private static SunPurchasePayService sunPurchasePayService;
@Autowired
public void setSunPurchasePayService(SunPurchasePayService sunPurchasePayService) {
WebSocket.sunPurchasePayService = sunPurchasePayService;
}
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
//虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
// 注:底下WebSocket是当前类名
private static CopyOnWriteArraySet<WebSocket> webSockets = new CopyOnWriteArraySet<>();
// 用来存在线连接数
private static Map<String, Session> sessionPool = new HashMap<String, Session>();
/**
* 链接成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "tenantId") String tenantId) {
try {
this.session = session;
webSockets.add(this);
sessionPool.put(tenantId, session);
log.info("【websocket消息】有新的连接,总数为:" + webSockets.size());
} catch (Exception e) {
}
}
/**
* 链接关闭调用的方法
*/
@OnClose
public void onClose() {
try {
webSockets.remove(this);
log.info("【websocket消息】连接断开,总数为:" + webSockets.size());
} catch (Exception e) {
}
}
/**
* 收到客户端消息后调用的方法
*
* @param message
*/
@OnMessage
public void onMessage(String message, @PathParam(value = "tenantId") String tenantId) {
log.info("tenantId:{}", tenantId);
log.info("message:" + message);
JSONObject jsonObject = JSONObject.parseObject(message);
if (StringUtils.isNotBlank(jsonObject.getString("type")) && "getBase64Str".equals(jsonObject.getString("type"))) {
sunPurchasePayService.getBase64Str();
}
JSONObject pong = new JSONObject();
pong.put("type", "pong");
sendAllMessage(JSONObject.toJSONString(pong));
log.info("【websocket消息】收到客户端消息:{}" + message);
}
/**
* 发送错误时的处理
*
* @param error
*/
@OnError
public void onError(Throwable error) {
log.error("用户错误,原因:" + error.getMessage());
error.printStackTrace();
}
// 此为广播消息
public void sendAllMessage(String message) {
log.info("【websocket消息】广播消息:" + message);
for (WebSocket webSocket : webSockets) {
try {
if (webSocket.session.isOpen()) {
webSocket.session.getAsyncRemote().sendText(message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 此为单点消息
public void sendOneMessage(String tenantId, String message) {
Session session = sessionPool.get(tenantId);
if (session != null && session.isOpen()) {
try {
log.info("【websocket消息】 单点消息:" + message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 此为单点消息(多人)
public void sendMoreMessage(String[] tenantIds, String message) {
for (String userId : tenantIds) {
Session session = sessionPool.get(userId);
if (session != null && session.isOpen()) {
try {
log.info("【websocket消息】 单点消息:" + message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
五.WebSocket 的问题描述
在具体的业务场景中,需要等用户连接成功后,从库表中先获取10条数据,作为默认的初始化数据进行显示。
我们想当然的通过 @Autowired 注解将对应 Service 进行依赖注入。却发现报了空指针的异常,也就是说,所需要的 Service 没有被成功注入。
当前写法如下:
//注册成组件
@Component
//定义websocket服务器端,它的功能主要是将目前的类定义成一个websocket服务器端。注解的值将被用于监听用户连接的终端访问URL地址
@ServerEndpoint("/websocket/{tenantId}")
@Slf4j
public class WebSocketServer {
@Autowired
private SunPurchasePayService sunPurchasePayService;
}
六.WebSocket 的问题分析
Spring管理采用单例模式(singleton)
,而 WebSocket 是多对象的,即每个客户端对应后台的一个 WebSocket 对象,也可以理解成 new 了一个 WebSocket,这样当然是不能获得自动注入的对象了,因为这两者刚好冲突。
@Autowired 注解注入对象操作是在启动时执行的,而不是在使用时,而 WebSocket 是只有连接使用时才实例化对象,且有多个连接就有多个对象。
所以我们可以得出结论,这个 Service 根本就没有注入到 WebSocket 当中。
七.WebSocket 的解决方案
使用 static 静态对象
将需要注入的 Service 改为静态,让它属于当前类,然后通过 setSunPurchasePayService 方法进行注入即可解决。
private static SunPurchasePayService sunPurchasePayService;
@Autowired
public void setSunPurchasePayService(SunPurchasePayService sunPurchasePayService) {
WebSocket.sunPurchasePayService = sunPurchasePayService;
}