1. 前言 WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上.
本文主要讲述在Java技术领域实现websocket服务的五种方式.
2. 第一种使用Java原生代码实现websocket 使用Java原生代码实现websocket服务的方法, 此方法需要引入一个第三方库java-websocket.jar. 截至目前2023/01/01最新版本为1.5.3.
项目源代码位于:
https://github.com/TooTallNate/Java-WebSocket
示例代码位于:
https://github.com/TooTallNate/Java-WebSocket/tree/master/src/main/example
2.1. 首先在项目中引入依赖 如果你的项目使用gradle作为管理工具, 可以添加以下gradle依赖
1 2 3 implementation group: 'org.java-websocket' , name: 'Java-WebSocket' , version: '1.5.3'
如果你的项目使用maven进行管理, 可以添加以下maven依赖
mven依赖
1 2 3 4 5 <dependency > <groupId > org.java-websocket</groupId > <artifactId > Java-WebSocket</artifactId > <version > 1.5.3</version > </dependency >
2.2. 创建WebsocketServer类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.net.InetSocketAddress;import java.net.UnknownHostException;import org.java_websocket.WebSocket;import org.java_websocket.handshake.ClientHandshake;import org.java_websocket.server.WebSocketServer;public class SocketServer extends WebSocketServer { public SocketServer (int port) throws UnknownHostException { super (new InetSocketAddress (port)); } public SocketServer (InetSocketAddress address) { super (address); } @Override public void onOpen (WebSocket conn, ClientHandshake handshake) { conn.send("Welcome to the server!" ); broadcast("new connection: " + handshake .getResourceDescriptor()); System.out.println( conn.getRemoteSocketAddress().getAddress().getHostAddress() + " entered the room!" ); } @Override public void onClose (WebSocket conn, int code, String reason, boolean remote) { broadcast(conn + " has left the room!" ); System.out.println(conn + " has left the room!" ); } @Override public void onMessage (WebSocket conn, String message) { broadcast(message); System.out.println(conn + ": " + message); } @Override public void onError (WebSocket conn, Exception ex) { ex.printStackTrace(); if (conn != null ) { } } @Override public void onStart () { System.out.println("Server started!" ); setConnectionLostTimeout(0 ); setConnectionLostTimeout(100 ); } }
2.3. 启动SocketServer 我们以及创建好了SocketServer, 这个时候我们可以启动它了, 启动代码如下.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void main (String[] args) throws InterruptedException, IOException { int port = 8887 ; SocketServer s = new SocketServer (port); s.start(); System.out.println("ChatServer started on port: " + s.getPort()); BufferedReader sysin = new BufferedReader (new InputStreamReader (System.in)); while (true ) { String in = sysin.readLine(); s.broadcast(in); if (in.equals("exit" )) { s.stop(1000 ); break ; } } }
写好main方法后, 我们可以启动它, 当控制台输出ChatServer started on port: 8887
表示启动成功.
2.4. 测试web socket server 此时web socket server已经监听在了localhost:8887上. 我们可以使用websocket在线调试工具 对其进行测试.
该工具主要是利用html5 的websocket去连接服务端的websocket,因此, 无论你是内网还是外网都可使用!
打开工具在输入框中输入 ws://localhost:8887点击连接, 既可以看到服务器端的反馈, 同时web socket server的控制台也会输出日志信息.
3. 使用Java原生+SpringBoot混合 在此种方式中, SocketServer依然使用原生的java代码编写, 但是SocketServer实例化过程由spring来管理.
此时我们需要引入spring-boot-starter-websocket, 上一节中的依赖包Java-WebSocket已经不需要了. 两种方式采用了不同的机制.
可以参考我的博文创建spring boot项目, 里面有详细讲解 - 如何手动创建一个springBoot项目
3.1. 引入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 plugins { id 'org.springframework.boot' version '2.7.7' id 'io.spring.dependency-management' version '1.0.15.RELEASE' } dependencies { implementation 'org.springframework.boot:spring-boot-starter-websocket' }
此处我们需要在gradle配置文件的plugins闭包内添加两个plugins, 一个复制控制spring boot的版本, 一个负责管理依赖.
对于maven, 需要如下配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.7.7</version > <relativePath /> </parent > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-websocket</artifactId > </dependency > </dependencies >
3.2. 创建ServerEndpoint 创建ServerEndpoint代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import org.springframework.stereotype.Component;import javax.websocket.OnClose;import javax.websocket.OnMessage;import javax.websocket.OnOpen;import javax.websocket.Session;import javax.websocket.server.ServerEndpoint;import java.io.IOException;@ServerEndpoint("/myWs") @Component public class WsServerEndpoint { @OnOpen public void onOpen (Session session) { System.out.println("连接成功" ); } @OnClose public void onClose (Session session) { System.out.println("连接关闭" ); } @OnMessage public String onMsg (String text) throws IOException { return "servet 发送:" + text; } }
说明 这里有几个注解需要注意一下,首先是他们的包都在 javax.websocket下。并不是 spring 提供的,而 jdk 自带的,下面是他们的具体作用。
@ServerEndpoint 通过这个 spring boot 就可以知道你暴露出去的 ws 应用的路径,有点类似我们经常用的@RequestMapping。比如你的启动端口是 8080,而这个注解的值是 ws,那我们就可以通过 ws://127.0.0.1:8080/ws 来连接你的应用
@OnOpen 当 websocket 建立连接成功后会触发这个注解修饰的方法,注意它有一个 Session 参数
@OnClose 当 websocket 建立的连接断开后会触发这个注解修饰的方法,注意它有一个 Session 参数
@OnMessage 当客户端发送消息到服务端时,会触发这个注解修改的方法,它有一个 String 入参表明客户端传入的值
@OnError 当 websocket 建立连接时出现异常会触发这个注解修饰的方法,注意它有一个 Session 参数
服务器主动发送消息 当服务器端要主动给客户端发送, 需要获取到相应客户端与服务器端的session, 通过 session.getBasicRemote().sendText(), 将消息发送到前端. 因此最好在onOpen方法中将session对象保存起来, 这样下次主动连接客户端时能找到相应的session对象.
3.3. 添加Spring配置 有了WsServerEndpoint后我们还要配置ServerEndpointExporter, 将Endpoint 暴露出去让客户端来建立连接.
而配置ServerEndpointExporter的方式非常简单, 只需要创建一个ServerEndpointExporter bean即可, 它会去获取Spring上下文中所有的Endpoint实例, 完成endpoint的注册过程, 并监听在application.properties 的server.port 属性所指定的端口.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.config.annotation.EnableWebSocket;import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration @EnableWebSocket public class WebsocketConfig { @Bean public ServerEndpointExporter serverEndpoint () { return new ServerEndpointExporter (); } }
3.4. 启动应用程序并测试 我们只需要向一般的Spring boot应用一样启动它即可.
1 2 3 4 5 6 7 8 9 @SpringBootApplication public class App { public static void main (String[] args) { SpringApplication.run(App.class, args); } }
测试, 我们依然使用websocket在线调试工具 来测试, 详情可参考上一节中的介绍
与前一种实现方式稍微不同的地方是, 我们可以url中指定endpoint了 ws://127.0.0.1:8080/myWs
4. 使用SpringBoot实现websocket 4.1. 引入依赖 1 implementation 'org.springframework.boot:spring-boot-starter-websocket'
4.2. 实现类 HttpAuthHandler.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import org.springframework.stereotype.Component;import org.springframework.web.socket.CloseStatus;import org.springframework.web.socket.TextMessage;import org.springframework.web.socket.WebSocketSession;import org.springframework.web.socket.handler.TextWebSocketHandler;import java.time.LocalDateTime;@Component public class HttpAuthHandler extends TextWebSocketHandler { @Override public void afterConnectionEstablished (WebSocketSession session) throws Exception { Object sessionId = session.getAttributes().get("session_id" ); if (sessionId != null ) { WsSessionManager.add(sessionId.toString(), session); } else { throw new RuntimeException ("用户登录已经失效!" ); } } @Override protected void handleTextMessage (WebSocketSession session, TextMessage message) throws Exception { String payload = message.getPayload(); Object sessionId = session.getAttributes().get("session_id" ); System.out.println("server 接收到 " + sessionId + " 发送的 " + payload); session.sendMessage(new TextMessage ("server 发送给 " + sessionId + " 消息 " + payload + " " + LocalDateTime.now().toString())); } @Override public void afterConnectionClosed (WebSocketSession session, CloseStatus status) throws Exception { Object sessionId = session.getAttributes().get("session_id" ); if (sessionId != null ) { WsSessionManager.remove(sessionId.toString()); } } }
WsSessionManager.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import org.springframework.web.socket.WebSocketSession;import java.io.IOException;import java.util.concurrent.ConcurrentHashMap;public class WsSessionManager { private static ConcurrentHashMap<String, WebSocketSession> SESSION_POOL = new ConcurrentHashMap <>(); public static void add (String key, WebSocketSession session) { SESSION_POOL.put(key, session); } public static WebSocketSession remove (String key) { return SESSION_POOL.remove(key); } public static void removeAndClose (String key) { WebSocketSession session = remove(key); if (session != null ) { try { session.close(); } catch (IOException e) { e.printStackTrace(); } } } public static WebSocketSession get (String key) { return SESSION_POOL.get(key); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import java.util.Map;import org.apache.logging.log4j.util.Strings;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.stereotype.Component;import org.springframework.web.socket.WebSocketHandler;import org.springframework.web.socket.server.HandshakeInterceptor;@Component public class MyInterceptor implements HandshakeInterceptor { @Override public boolean beforeHandshake (ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { System.out.println("握手开始" ); String hostName = request.getRemoteAddress().getHostName(); String sessionId = hostName+String.valueOf((int )(Math.random()*1000 )); if (Strings.isNotBlank(sessionId)) { attributes.put("session_id" , sessionId); System.out.println("用户 session_id " + sessionId + " 握手成功!" ); return true ; } System.out.println("用户登录已失效" ); return false ; } @Override public void afterHandshake (ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { System.out.println("握手完成" ); } }
4.3. Spring 配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.config.annotation.EnableWebSocket;import org.springframework.web.socket.config.annotation.WebSocketConfigurer;import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;import com.philoenglish.ws.HttpAuthHandler;import com.philoenglish.ws.MyInterceptor;@Configuration @EnableWebSocket public class WebsocketConfig implements WebSocketConfigurer { @Autowired private HttpAuthHandler httpAuthHandler; @Autowired private MyInterceptor myInterceptor; @Override public void registerWebSocketHandlers (WebSocketHandlerRegistry registry) { registry .addHandler(httpAuthHandler, "myWS" ) .addInterceptors(myInterceptor) .setAllowedOrigins("*" ); } }
4.4. 启动与测试 启动代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import org.springframework.boot.CommandLineRunner;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.Bean;@SpringBootApplication public class App { public static void main (String[] args) { SpringApplication.run(App.class, args); } @Bean public CommandLineRunner commandLineRunner (ApplicationContext ctx) { return args -> { System.out.println("application started" ); }; } }
执行main方法启动应用程序
测试依然使用websocket在线调试工具
5. 使用TIO+SpringBoot实现websocket 以下是关于t-io的一些信息, 如果需要更详细的了解tio可以访问以下这些站点
5.1. 添加相应依赖 gradle:
1 2 3 implementation 'org.t-io:tio-websocket-spring-boot-starter:3.6.0.v20200315-RELEASE'
maven:
1 2 3 4 5 <dependency > <groupId > org.t-io</groupId > <artifactId > tio-websocket-spring-boot-starter</artifactId > <version > 3.6.0.v20200315-RELEASE</version > </dependency >
5.2. 编写消息处理类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import org.springframework.stereotype.Component;import org.tio.core.ChannelContext;import org.tio.http.common.HttpRequest;import org.tio.http.common.HttpResponse;import org.tio.websocket.common.WsRequest;import org.tio.websocket.server.handler.IWsMsgHandler;@Component public class MyWebSocketMsgHandler implements IWsMsgHandler { @Override public HttpResponse handshake (HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception { return httpResponse; } @Override public void onAfterHandshaked (HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception { System.out.println("握手成功" ); } @Override public Object onBytes (WsRequest wsRequest, byte [] bytes, ChannelContext channelContext) throws Exception { System.out.println("接收到bytes消息" ); return null ; } @Override public Object onClose (WsRequest wsRequest, byte [] bytes, ChannelContext channelContext) throws Exception { return null ; } @Override public Object onText (WsRequest wsRequest, String s, ChannelContext channelContext) throws Exception { System.out.println("接收到文本消息:" + s); return null ; } }
5.3. 修改配置文件 application.properties
1 2 tio.websocket.server.port =9876 tio.websocket.server.heartbeat-timeout =60000
5.4. 启动tio Websocket Server 启动tio Websocket Server 的方式如下, 执行main方法.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @SpringBootApplication @EnableTioWebSocketServer public class App { public static void main (String[] args) { SpringApplication.run(App.class, args); } @Bean public CommandLineRunner commandLineRunner (ApplicationContext ctx) { return args -> { System.out.println("application started" ); }; } }
6. STOMP实现websocket 6.1. 添加相应依赖 gradle:
1 implementation 'org.springframework.boot:spring-boot-starter-websocket'
maven:
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-websocket</artifactId > </dependency >
WebSocketConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import org.springframework.context.annotation.Configuration;import org.springframework.messaging.simp.config.MessageBrokerRegistry;import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;import org.springframework.web.socket.config.annotation.StompEndpointRegistry;import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints (StompEndpointRegistry registry) { registry.addEndpoint("/ws" ).setAllowedOrigins("*" ); } @Override public void configureMessageBroker (MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic" , "/queue" ); registry.setApplicationDestinationPrefixes("/app" ); registry.setUserDestinationPrefix("/user/" ); } }
6.3. 实现消息请求处理的Controller WSController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import org.springframework.beans.factory.annotation.Autowired;import org.springframework.messaging.handler.annotation.MessageMapping;import org.springframework.messaging.handler.annotation.Payload;import org.springframework.messaging.simp.SimpMessagingTemplate;import org.springframework.messaging.simp.annotation.SendToUser;import org.springframework.stereotype.Controller;@Controller public class WSController { @Autowired private SimpMessagingTemplate simpMessagingTemplate; @MessageMapping("/greeting") @SendToUser("/queue/serverReply") public String greating (@Payload String data) { System.out.println("received greeting: " + data); String msg = "server replys: " + data; return msg; } @MessageMapping("/shout") public void userShout (Shout shout) { String message = shout.getMessage(); System.out.println("收到的消息是:" + message); simpMessagingTemplate.convertAndSend("/queue/notifications" , shout); } }
domain object
Shout.java
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Shout { private String message; public String getMessage () { return message; } public void setMessage (String message) { this .message = message; } }
6.4. 启动 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import org.springframework.boot.CommandLineRunner;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.Bean;@SpringBootApplication public class App { public static void main (String[] args) { SpringApplication.run(App.class, args); } @Bean public CommandLineRunner commandLineRunner (ApplicationContext ctx) { return args -> { System.out.println("application started" ); }; } }
6.5. 实现消息客户端 index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 <!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" > <link href ="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/css/bootstrap.min.css" rel ="stylesheet" > <title > Document</title > <style > .newmessage { color : green; } .errormessage { color : red; } </style > </head > <body > <div class ="row" > <div class ="col-lg-6" > <div class ="input-group" > <input type ="text" id ='urlInput' class ="form-control" placeholder ='输入websocket端点' > <span class ="input-group-btn" > <button class ="btn btn-default" type ="button" onclick ="reconnect();" > 重新连接</button > </span > </div > </div > <div class ="col-lg-8" id ="output" style ="border:1px solid #ccc;height:365px;overflow: auto;margin-left:15px" > </div > <div class ="col-lg-6" > <div class ="input-group" > <input type ="text" id ='messageInput' class ="form-control" placeholder ='待发信息' > <span class ="input-group-btn" > <button class ="btn btn-default" type ="button" onclick ="doSend();" > 发送</button > </span > <span class ="input-group-btn" > <button class ="btn btn-default" type ="button" onclick ="broadcast();" > 广播</button > </span > </div > </div > </div > </body > <script src ="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js" > </script > <script src ="http://cdn.static.runoob.com/libs/jquery/2.1.1/jquery.min.js" > </script > <script src ="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/js/bootstrap.min.js" > </script > <script type ="text/javascript" > var defaultUrl = "ws://localhost:81/ws" document .getElementById ("urlInput" ).value = defaultUrl; var stomp; var appendMessage = function (msg ) { var div = "<div class='newmessage'>" + msg + "</div>" ; $("#output" ).append (div); } var reportError = function (msg ) { var div = "<div class='errormessage'>" + msg + "</div>" ; $("#output" ).append (div); } function connects (url ) { var promise = new Promise (function (resolve, reject ) { var client = Stomp .client (url); var headers = { username : 'admin' , password : 'admin' , 'client-id' : 'stomp-client-id' }; appendMessage ('connecting to ' + url) client.connect (headers, function (frame ) { resolve (client); }, function (error ) { reject (error) }); }); return promise; } var connect_callback = function (client ) { stomp = client appendMessage ('subscribing on ' + '/queue/subscribe' ) client.subscribe ('/user/queue/serverReply' , function (message ) { console .log ('subscribe topic callback:' + message.body ); appendMessage ('subscribe topic callback:' + message.body ) }); appendMessage ('subscribing on ' + '/user/queue/notifications' ) client.subscribe ("/queue/notifications" , function (message ) { var content = message.body ; var obj = JSON .parse (content); console .log ("收到广播消息:" + message.body ); appendMessage ("收到广播消息:" + message.body ); }); var payload = JSON .stringify ({ 'message' : 'greeting to stomp broker!' }); client.send ("/app/greeting" , {}, payload); }; var error_callback = function (error ) { reportError ("连接失败!" ); }; var doSend = function ( ) { var msg = document .getElementById ("messageInput" ).value var payload = JSON .stringify ({ 'message' : msg }); stomp.send ("/app/greeting" , {}, payload); } var broadcast = function ( ) { var msg = document .getElementById ("messageInput" ).value var payload = JSON .stringify ({ 'message' : msg }); stomp.send ("/app/shout" , {}, payload); } function reconnect ( ) { if (stomp) { stomp.disconnect (function ( ) { console .log ("See you next time!" ); }) } connects (document .getElementById ("urlInput" ).value ).then (connect_callback).catch (error_callback) } reconnect (); </script > </html >
7. 相关文章 本技术博客原创文章位于鹏叔的技术博客 - java实现websocket的五种方式 , 要获取最近更新请访问原文.
更多技术博客请访问: 鹏叔的技术博客
更新: 2023/04/14
上面所讲的都是关于Java程序作为服务端与前端通信. 在Websocket完整体系中还缺少了一块, 那就是服务端向客户端推送消息, 这时候则需要java作为客户端, 这里我写了一个java程序作为webSocket客户端的例子, 而不是将服务器代码耦合在WSController中, 希望我的这篇文章对您有所帮助使用stomp的java客户端向前端推送数据
8. 参考文档 websocket - spring boot 集成 websocket 的四种方式
websocket在线调试工具
JAVA实现WebSocket服务器
让网络编程更轻松和有趣 t-io
tio-websocket-spring-boot-starter 的简单使用
STOMP原理与应用开发详解
Spring消息之STOMP
WebSocket详解:技术原理、代码演示和应用案例
WebSocket 是什么原理?为什么可以实现持久连接