SpringBoot集成WebSocket,实现后台向前端推送信息
对了,现在大家都开始用 WebSocket + Redis 做集群广播了,WebSocket 自己是无状态的,后端要推消息就必须知道每个连接在哪台机器上,这时候用 Redis 做 pub/sub 或者 zk 做服务注册是必不可少的。这地方我没细改,主要是给后端测试用的,你要是做正式项目,建议还是封装一下,带 reconnect 机制,带 ping-pong 监控连接状态,不然生产环境会掉线但你完全
说起来 WebSocket 这东西,我第一次用还真不是在 Java 项目里,而是搞 Node.js 那会儿写个聊天室,结果连服务端都扛不住压力。后来转回 Java,搞 SpringBoot 项目,终于有机会把 WebSocket 玩明白了。这次我想分享一个实战场景:用 WebSocket 实现后台向前端推送消息,场景是消息从 MQTT 到达后台,再实时转发到前端。
先说结论:WebSocket 在 SpringBoot 里集成真不难,几行配置就能跑起来,但坑也真不少,尤其你要用到 Spring 管理的 Bean 的时候,能把人整疯。
WebSocket 本质上是一个 TCP 协议上的“长连接”机制,而且是全双工,服务端也能主动发消息,这和我们平常写的 HTTP 响应拉模型完全不一样。HTTP 是谁请求谁响应,服务端从来没机会主动说话,但有时候我们真就需要服务端主动推东西给前端,比如物联网场景、即时通知、监控告警啥的。这时候 WebSocket 就特别合适。
SpringBoot 支持 WebSocket 的方式挺直接,先加个依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
然后搞个 ServerEndpointExporter
出来,这是关键的一步,如果你不写它,WebSocket 根本就不会跑起来:
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
写到这可能有朋友说了:“为啥不用 @Controller
搞定?为啥还要 @ServerEndpoint
?”因为标准 WebSocket API 是 Java EE 的一套东西,Spring 本身不管,你得用 @ServerEndpoint
手动去注册路径。而这个路径一旦注册成功,前端就能用 ws://host:port/xxx
连接上来。
下面这个 WebSocketServer
是整个推送机制的核心。用 CopyOnWriteArraySet
存所有连接会话,Map userId 到 Session,再通过这个 Session 发消息。这种做法有点暴力,但简单粗暴好用。
@ServerEndpoint("/api/websocket/{sid}")
@Component
publicclass WebSocketServer {
privatestatic CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();
private Session session;
private String sid = "";
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
this.session = session;
webSocketSet.add(this);
this.sid = sid;
sendMessage("conn_success");
}
@OnClose
public void onClose() {
webSocketSet.remove(this);
}
@OnMessage
public void onMessage(String message) {
// 广播功能,纯展示用
for (WebSocketServer item : webSocketSet) {
item.sendMessage(message);
}
}
public void sendMessage(String message) {
this.session.getAsyncRemote().sendText(message);
}
public static void sendInfo(String message, String sid) {
for (WebSocketServer item : webSocketSet) {
if (item.sid.equals(sid)) {
item.sendMessage(message);
}
}
}
}
我一开始就踩在这里:业务逻辑层用的是 Spring 的 @Service
Bean,WebSocket Server 类又不在 Spring 管理之下,结果注入不了!一顿 debug 才意识到 @ServerEndpoint
这货是由容器自己实例化的,不走 Spring 管理。
解决方法其实也不是没办法,要么在 ApplicationContext
启动之后把 Bean 手动注入进去,要么用 @Autowired
静态成员保存上下文,总之各种花活,比较 dirty。但这确实是实战里你一定会遇到的问题。
再说说前端连接部分,老实说代码很土:
websocket = new WebSocket("ws://192.168.100.196:8082/api/websocket/100");
websocket.onmessage = function(event) {
console.log("收到消息", event.data);
};
websocket.onopen = function() {
console.log("WebSocket连接成功");
};
这地方我没细改,主要是给后端测试用的,你要是做正式项目,建议还是封装一下,带 reconnect 机制,带 ping-pong 监控连接状态,不然生产环境会掉线但你完全不知道。
服务端发消息很简单,写个 Controller 暴露接口就行了:
@RequestMapping("/api/socket/push/{cid}")
@ResponseBody
public Map pushToWeb(@PathVariable String cid, String message) {
WebSocketServer.sendInfo(message, cid);
return Map.of("code", cid, "msg", message);
}
这个 Controller 接口可以让你通过浏览器发请求触发后台向前端推送消息,非常方便调试。比如前端开着页面,你调用 curl
模拟后台消息,就能看到前端是否实时收到。虽然不是啥高科技,但真的是个生产力工具。
最后说个容易忽略的点:WebSocket 默认只支持同域,跨域访问容易遇到奇怪的问题。要么让前端通过反向代理绕过,要么配置 WebSocket 的跨域支持。
写到这里其实也差不多了,我自己在项目里就是这么搞的,从 MQTT 接收传感器数据,用 WebSocket 实时推送到前端大屏,效果还挺不错。
不过讲真,WebSocket 的定位不是替代所有通信方式,它更像是一个工具,适合“服务端主动通知”的场景。你要用它做 API 拉数据就不太合适了,长连接维护代价也不小,一定要想清楚。
如果你项目里也有类似需求,不妨试试看,不过一定要注意线程安全和内存管理,连接多了会很可怕的。
对了,现在大家都开始用 WebSocket + Redis 做集群广播了,WebSocket 自己是无状态的,后端要推消息就必须知道每个连接在哪台机器上,这时候用 Redis 做 pub/sub 或者 zk 做服务注册是必不可少的。以后有空我可以再展开讲讲这块内容,你感兴趣吗?
更多推荐
所有评论(0)