前言
WebSocket的用途是什么?
想象一个场景,有一些数据实时变化,前端需要在数据变化时刷新界面
此时我们第一反应,前端定时使用HTTP协议调用后端接口,刷新界面。OK,需求实现,下班回家!
然后我们就被前端套麻袋打了一顿。
那么如何优雅的让前端知道数据发生了变化呢?就需要用到WebSocket由后端将数据推送给前端
正文
具体实现
一、引入依赖
代码语言:txt复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>3.0.4</version>
</dependency>
二、配置WebSocket
创建一个config类,配置类代码为固定写法,主要用于告诉SpringBoot我有使用WebSocket的需求,
注意我加了@ServerEndpoint注解的类
代码语言:java复制//ServerEndpointExporter 是springBoot的用于自动注册和暴露 WebSocket 端点的类
//暴露ServerEndpointExporter类后,所有使用@ServerEndpoint("/websocket")的注解都可以用来发送和接收WebSocket请求
@Component
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
三、WebSocket逻辑实现
话不多说,直接上代码
代码语言:java复制@Component // 交给Spring管理
@ServerEndpoint("/websocket") // 告知SpringBoot,这是WebSocket的实现类
@Slf4j
public class WebSocketServer {
//静态变量,用来记录当前在线连接数
private static AtomicInteger onlineCount = new AtomicInteger(0);
//concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。
private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private List<String> ids = new ArrayList<>();
//连接建立成功调用的方法
@OnOpen
public void onOpen(Session session) {
this.session = session;
webSocketSet.add(this);
// ps:后端接参示例代码
// 这样接参,前端对应传参方式为
// var client = new window.WebSocket(this.address "?tunnelId=" tunnelId);
Map<String, List<String>> map = session.getRequestParameterMap();
String id = map.get("tunnelId").get(0);
ids = Arrays.asList(id.split(","));
addOnlineCount(); //在线数加1
try {
sendMessage("连接成功");
} catch (IOException e) {
log.error("websocket IO异常");
}
}
// 连接关闭调用的方法
@OnClose
public void onClose() {
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
log.info("有一连接关闭!当前在线人数为" getOnlineCount());
}
// 收到客户端消息后调用的方法
@OnMessage
public void onMessage(String message, Session session) {
// 心跳检测,看连接是否意外断开
// ps:现在uniapp等前端好像自动带有心跳包,但是web端一般还需要心跳包确保连接一直未断开
if ("heart".equals(message)) {
try {
sendMessage("heartOk");
} catch (IOException e) {
e.printStackTrace();
}
}
}
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
// 实现服务器主动推送
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
// 群发自定义消息
public static void sendInfo(String message, String id) {
for (WebSocketServer item : webSocketSet) {
try {
if (id == null) {
item.sendMessage(message);
} else if (item.ids.contains(id)) {
item.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static int getOnlineCount() {
return onlineCount.get();
}
public static void addOnlineCount() {
WebSocketServer.onlineCount.incrementAndGet();
}
public static void subOnlineCount() {
if (getOnlineCount() > 0) {
WebSocketServer.onlineCount.decrementAndGet();
}
}
}
ok,到这里,一个基本的WebSocket服务端就搭建完成了
下面是前端测试代码(前端就是一个html的demo)
代码语言:html复制<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<title>websocket测试页面</title>
</head>
<body>
<div class="panel panel-default">
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-addon">ws地址</span>
<input type="text" id="address" class="form-control" placeholder="ws地址"
aria-describedby="basic-addon1" value="ws://localhost:9700/tunnel/websocket">
<div class="input-group-btn">
<button class="btn btn-default" type="submit" id="connect">连接</button>
</div>
</div>
</div>
</div>
<div class="row" style="margin-top: 10px;display: none;" id="msg-panel">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-addon">消息</span>
<input type="text" id="msg" class="form-control" placeholder="消息内容" aria-describedby="basic-addon1">
<div class="input-group-btn">
<button class="btn btn-default" type="submit" id="send">发送</button>
</div>
</div>
</div>
</div>
<div class="row" style="margin-top: 10px; padding: 10px;">
<div class="panel panel-default">
<div class="panel-body" id="log" style="height: 450px;overflow-y: auto;">
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>
<script type="text/javascript">
$(function () {
var _socket;
$("#connect").click(function () {
var tunnelId = "2"; // 设置需要传递的参数
_socket = new _websocket($("#address").val(), tunnelId);
_socket.init();
});
$("#send").click(function () {
var _msg = $("#msg").val();
output("发送消息:" _msg);
_socket.client.send(_msg);
});
});
function output(e) {
var _text = $("#log").html();
$("#log").html(_text "<br>" e);
}
function _websocket(address, tunnelId) {
this.address = address;
this.tunnelId = tunnelId;
console.log(address)
console.log(tunnelId)
this.client;
this.init = function () {
if (!window.WebSocket) {
this.websocket = null;
return;
}
var _this = this;
// var _client = new window.WebSocket(_this.address "/" _this.tunnelId);// 路径传参(没跑通)
// 注意这里的名字要和后端接参数的名字对应上
var _client = new window.WebSocket(_this.address "?tunnelId=" tunnelId);
_client.onopen = function () {
output("websocket打开");
$("#msg-panel").show();
};
_client.onclose = function () {
_this.client = null;
output("websocket关闭");
$("#msg-panel").hide();
};
_client.onmessage = function (evt) {
output(evt.data);
};
_this.client = _client;
};
return this;
}
</script>
</body>
</html>
进阶
以上内容实现了基本的推送消息到前端,也是网上大部分文章讲解的深度,但是实际开发中,笔者不可能不进行Spring的依赖注入,然后查询数据库拿到一些数据。此时我们就会发现,为什么空指针啊???为什么啊?
下面是笔者当时的排查思路
第一步:空指针?bean没被Spring管理呗。
看我三下五除二,要不就是@Component注解没加,要不就是SpringBoot启动类的扫描路径有问题,根本难不倒我
?都加了啊,为什么还是不行啊?开始怀疑人生
后来,因为我同时和小程序端还有web端对接,突然反应过来会不会是因为Spring默认单例,只会创造一个对象,但是WebSocket大概率都会有多个客户端,按照这个方向去尝试的话,直接手动获取bean对象是不是就不会空指针了呢?
我写了一个工具类获取bean对象
代码语言:java复制@Component
public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware
{
private static ConfigurableListableBeanFactory beanFactory;
private static ApplicationContext applicationContext;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
{
SpringUtils.beanFactory = beanFactory;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
{
SpringUtils.applicationContext = applicationContext;
}
public static <T> T getBean(Class<T> clz) throws BeansException
{
T result = (T) beanFactory.getBean(clz);
return result;
}
}
在我们的WebSocket类中使用以下代码进行依赖注入
代码语言:java复制EmergencyTypeService emergencyTypeService = SpringUtils.getBean(EmergencyTypeService.class);
ok,到此,我们就解决了空指针的问题,真是泪目。
我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表