说起来 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 做服务注册是必不可少的。以后有空我可以再展开讲讲这块内容,你感兴趣吗?

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐