前言
WebSocket的用途是什么?
想象一个场景,有一些数据实时变化,前端需要在数据变化时刷新界面
此时我们第一反应,前端定时使用HTTP协议调用后端接口,刷新界面。OK,需求实现,下班回家!
然后我们就被前端套麻袋打了一顿。
那么如何优雅的让前端知道数据发生了变化呢?就需要用到WebSocket由后端将数据推送给前端
正文
具体实现
一、引入依赖
org.springframework.boot
spring-boot-starter-websocket
3.0.4
二、配置WebSocket
创建一个config类,配置类代码为固定写法,主要用于告诉SpringBoot我有使用WebSocket的需求,
注意我加了@ServerEndpoint注解的类
/**
* ServerEndpointExporter 是springBoot的用于自动注册和暴露 WebSocket 端点的类
* 暴露ServerEndpointExporter类后,所有使用@ServerEndpoint("/websocket")的注解都可以用来发送和接收WebSocket请求
*/
@Component
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
三、WebSocket逻辑实现
话不多说,直接上代码
@Component // 交给Spring管理
@ServerEndpoint("/websocket") // 告知SpringBoot,这是WebSocket的实现类
@Slf4j
public class WebSocketServer {
//静态变量,用来记录当前在线连接数
private static AtomicInteger onlineCount = new AtomicInteger(0);
//concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。
private static CopyOnWriteArraySet webSocketSet = new CopyOnWriteArraySet();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private List 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 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());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
// 心跳检测,看连接是否意外断开
// ps:现在uniapp等前端好像自动带有心跳包,但是web端一般还需要心跳包确保连接一直未断开
if ("heart".equals(message)) {
try {
sendMessage("heartOk");
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* @param session
* @param error
*/
@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)
websocket测试页面
ws地址
连接
消息
发送
$(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 + "" + 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;
}
进阶
以上内容实现了基本的推送消息到前端,也是网上大部分文章讲解的深度,但是实际开发中,笔者不可能不进行Spring的依赖注入,然后查询数据库拿到一些数据。此时我们就会发现,为什么空指针啊???为什么啊?
下面是笔者当时的排查思路
第一步:空指针?bean没被Spring管理呗。
看我三下五除二,要不就是@Component注解没加,要不就是SpringBoot启动类的扫描路径有问题,根本难不倒我
?都加了啊,为什么还是不行啊?开始怀疑人生
后来,因为我同时和小程序端还有web端对接,突然反应过来会不会是因为Spring默认单例,只会创造一个对象,但是WebSocket大概率都会有多个客户端,按照这个方向去尝试的话,直接手动获取bean对象是不是就不会空指针了呢?
我写了一个工具类获取bean对象
@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;
}
/**
* 获取类型为requiredType的对象
*
* @param clz
* @return
* @throws org.springframework.beans.BeansException
*
*/
public static T getBean(Class clz) throws BeansException
{
T result = (T) beanFactory.getBean(clz);
return result;
}
}
在我们的WebSocket类中使用以下代码进行依赖注入
EmergencyTypeService emergencyTypeService = SpringUtils.getBean(EmergencyTypeService.class);
ok,到此,我们就解决了空指针的问题,真是泪目。