经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Spring » 查看文章
SpringBoot WebSocket STOMP
来源:cnblogs  作者:KeBoom  时间:2023/9/25 16:49:39  对本文有异议

SpringBoot WebSocket STOMP

关键词:Springboot, WebSocket, STOMP, broadcast, sendToUser, MessageMapping, SubscribeMapping, convertAndSendToUser

STOMP是一种发布订阅的模式,被订阅者发布消息以广播形式发送。如果需要一对一发送或者说指定某个客户端发送,springboot提供了convertAndSendToUser方法去指定user进行发送。

本文实现了既有广播形式,也有指定user发送形式,以做对比。

代码参考

maven

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-websocket</artifactId>
  4. </dependency>

WebSocketConfig

  1. import org.springframework.context.annotation.Bean;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.messaging.simp.config.MessageBrokerRegistry;
  4. import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
  5. import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
  6. import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
  7. import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
  8. /**
  9. * {@code @author:} keboom
  10. * {@code @date:} 2023/9/21
  11. * {@code @description:}
  12. */
  13. @Configuration
  14. @EnableWebSocketMessageBroker
  15. public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
  16. @Override
  17. public void configureMessageBroker(MessageBrokerRegistry config) {
  18. config.enableSimpleBroker("/topic");
  19. config.setApplicationDestinationPrefixes("/app");
  20. }
  21. @Override
  22. public void registerStompEndpoints(StompEndpointRegistry registry) {
  23. registry.addEndpoint("/mobicaster-websocket/{androidID}").setAllowedOrigins("*").
  24. setHandshakeHandler(new CustomHandshakeHandler());
  25. }
  26. @Bean
  27. public ServletServerContainerFactoryBean createWebSocketContainer() {
  28. ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
  29. container.setMaxTextMessageBufferSize(8192);
  30. container.setMaxBinaryMessageBufferSize(8192);
  31. // container.setMaxSessionIdleTimeout(10000L);
  32. return container;
  33. }
  34. }

registry.addEndpoint("/mobicaster-websocket/{androidID}")这个是网页向服务器开启一个websocket连接的url地址。{androidID} (这个名字大家随便起哈,随便叫devId,sessionId都行)是作为一个websocket标识,这样我们服务器想要主动向一个websocket客户端发送message时,可以知道应该向谁发。

config.setApplicationDestinationPrefixes("/app");这个是网页向服务器发送消息的uri前缀

config.enableSimpleBroker("/topic");这个是服务器向网页发送消息的 uri 的“前缀”

setHandshakeHandler(new CustomHandshakeHandler());顾名思义,这是websocket握手时,做一些自定义处理的handler。

ServletServerContainerFactoryBean这个大家根据需求配了。setMaxSessionIdleTimeout这个在网页上的表示时,如果在一定时期没有发送任何消息,那么当前连接断开,然后建立一个新链接。

CustomHandshakeHandler

在这个类里面,我们对每个websocket做标识,标识每个websocket的user是什么,到时候服务器主动发送message,将根据user去发。

  1. import org.springframework.http.server.ServerHttpRequest;
  2. import org.springframework.web.socket.WebSocketHandler;
  3. import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
  4. import java.security.Principal;
  5. import java.util.Map;
  6. /**
  7. * {@code @author:} keboom
  8. * {@code @date:} 2023/9/22
  9. * {@code @description:}
  10. */
  11. public class CustomHandshakeHandler extends DefaultHandshakeHandler {
  12. @Override
  13. protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
  14. String uri = request.getURI().toString();
  15. String androidID = uri.substring(uri.lastIndexOf("/") + 1);
  16. return () -> androidID;
  17. }
  18. }

Controller

接下来看看我们的controller:

  1. import com.cogent.mobicasterserver.controller.vo.DeviceInfo;
  2. import com.cogent.mobicasterserver.controller.vo.FoldbackReq;
  3. import com.cogent.mobicasterserver.controller.vo.LiveReq;
  4. import com.cogent.mobicasterserver.controller.vo.RespVO;
  5. import jakarta.annotation.Resource;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.beans.factory.annotation.Value;
  8. import org.springframework.messaging.handler.annotation.MessageMapping;
  9. import org.springframework.messaging.handler.annotation.Payload;
  10. import org.springframework.messaging.handler.annotation.SendTo;
  11. import org.springframework.messaging.simp.SimpMessagingTemplate;
  12. import org.springframework.messaging.simp.annotation.SendToUser;
  13. import org.springframework.web.bind.annotation.RequestMapping;
  14. import org.springframework.web.bind.annotation.RestController;
  15. import static org.springframework.web.bind.annotation.RequestMethod.GET;
  16. /**
  17. * {@code @author:} keboom
  18. * {@code @date:} 2023/9/21
  19. * {@code @description:}
  20. */
  21. @RestController
  22. public class MobiController {
  23. private SimpMessagingTemplate template;
  24. @Autowired
  25. public MobiController(SimpMessagingTemplate template) {
  26. this.template = template;
  27. }
  28. @RequestMapping(path = "/foldback", method = GET)
  29. public void foldback(FoldbackReq req) {
  30. this.template.convertAndSendToUser(req.getAndroidID(), "/topic/foldback", req.toString());
  31. }
  32. @MessageMapping("/greetings")
  33. @SendTo("/topic/greetings")
  34. public RespVO greetings(@Payload LiveReq liveReq) throws Exception {
  35. // do something ....
  36. return new RespVO(200, "success", null);
  37. }
  38. @MessageMapping("/live")
  39. @SendToUser("/topic/live")
  40. public RespVO live(@Payload LiveReq liveReq) throws Exception {
  41. // do something ....
  42. return new RespVO(200, "success", null);
  43. }
  44. }

网上很多资料写的用 @Controller,我这边用的@RestController 完全没问题。

对于 sendToUser的,uri前缀需要加 /user ,这个通过下面的网页端 js 代码更清晰,还有就是我们看浏览器开发者工具的具体websocket的message更清楚,这里就不说每个注解的意思了。

app.js

  1. const stompClient = new StompJs.Client({
  2. brokerURL: 'ws://localhost:8082/mobicaster-websocket/androidId1234'
  3. });
  4. // --------------------------------------------------------------------------------------------
  5. const stompClient2 = new StompJs.Client({
  6. brokerURL: 'ws://localhost:8082/mobicaster-websocket/androidId2345'
  7. });
  8. stompClient2.onConnect = (frame) => {
  9. stompClient2.subscribe('/topic/greetings', (greeting) => {
  10. showGreeting(JSON.parse(greeting.body).content);
  11. });
  12. stompClient2.subscribe('/user/topic/live', (greeting) => {
  13. // showGreeting(JSON.parse(greeting.body).content);
  14. console.log('Live: ' + greeting);
  15. });
  16. stompClient2.subscribe('/user/topic/foldback', (greeting) => {
  17. // showGreeting(JSON.parse(greeting.body).content);
  18. console.log('foldback: ' + greeting);
  19. });
  20. };
  21. stompClient2.onWebSocketError = (error) => {
  22. console.error('Error with websocket', error);
  23. };
  24. stompClient2.onStompError = (frame) => {
  25. console.error('Broker reported error: ' + frame.headers['message']);
  26. console.error('Additional details: ' + frame.body);
  27. };
  28. // --------------------------------------------------------------------------------------------
  29. stompClient.onConnect = (frame) => {
  30. setConnected(true);
  31. console.log('Connected: ' + frame);
  32. stompClient.subscribe('/topic/greetings', (greeting) => {
  33. showGreeting(JSON.parse(greeting.body).content);
  34. });
  35. stompClient.subscribe('/user/topic/live', (greeting) => {
  36. // showGreeting(JSON.parse(greeting.body).content);
  37. console.log('Live: ' + greeting);
  38. });
  39. stompClient.subscribe('/user/topic/foldback', (greeting) => {
  40. // showGreeting(JSON.parse(greeting.body).content);
  41. console.log('foldback: ' + greeting);
  42. });
  43. };
  44. stompClient.onWebSocketError = (error) => {
  45. console.error('Error with websocket', error);
  46. };
  47. stompClient.onStompError = (frame) => {
  48. console.error('Broker reported error: ' + frame.headers['message']);
  49. console.error('Additional details: ' + frame.body);
  50. };
  51. function setConnected(connected) {
  52. $("#connect").prop("disabled", connected);
  53. $("#disconnect").prop("disabled", !connected);
  54. if (connected) {
  55. $("#conversation").show();
  56. } else {
  57. $("#conversation").hide();
  58. }
  59. $("#greetings").html("");
  60. }
  61. function connect() {
  62. stompClient.activate();
  63. stompClient2.activate();
  64. }
  65. function disconnect() {
  66. stompClient.deactivate();
  67. stompClient2.deactivate();
  68. setConnected(false);
  69. console.log("Disconnected");
  70. }
  71. function sendName() {
  72. stompClient.publish({
  73. destination: "/app/greetings",
  74. body: JSON.stringify({'androidId': "abad123svbasd12", 'liveAction': "start"})
  75. });
  76. stompClient.publish({
  77. destination: "/app/live",
  78. body: JSON.stringify({'androidId': "abad123svbasd12", 'liveAction': "start"})
  79. });
  80. stompClient2.publish({
  81. destination: "/app/greetings",
  82. body: JSON.stringify({'androidId': "abad123svbasd12", 'liveAction': "start"})
  83. });
  84. stompClient2.publish({
  85. destination: "/app/live",
  86. body: JSON.stringify({'androidId': "abad123svbasd12", 'liveAction': "start"})
  87. });
  88. }
  89. function showGreeting(message) {
  90. $("#greetings").append("<tr><td>" + message + "</td></tr>");
  91. }
  92. $(function () {
  93. $("form").on('submit', (e) => e.preventDefault());
  94. $("#connect").click(() => connect());
  95. $("#disconnect").click(() => disconnect());
  96. $("#send").click(() => sendName());
  97. });

这里我发起了两个连接,主要对比广播和一对一的user发送的效果。

index.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Hello WebSocket</title>
  5. <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  6. <link href="/main.css" rel="stylesheet">
  7. <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
  8. <script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.0/bundles/stomp.umd.min.js"></script>
  9. <script src="/app.js"></script>
  10. </head>
  11. <body>
  12. <noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
  13. enabled. Please enable
  14. Javascript and reload this page!</h2></noscript>
  15. <div id="main-content" class="container">
  16. <div class="row">
  17. <div class="col-md-6">
  18. <form class="form-inline">
  19. <div class="form-group">
  20. <label for="connect">WebSocket connection:</label>
  21. <button id="connect" class="btn btn-default" type="submit">Connect</button>
  22. <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
  23. </button>
  24. </div>
  25. </form>
  26. </div>
  27. <div class="col-md-6">
  28. <form class="form-inline">
  29. <div class="form-group">
  30. <label for="name">What is your name?</label>
  31. <input type="text" id="name" class="form-control" placeholder="Your name here...">
  32. </div>
  33. <button id="send" class="btn btn-default" type="submit">Send</button>
  34. </form>
  35. </div>
  36. </div>
  37. <div class="row">
  38. <div class="col-md-12">
  39. <table id="conversation" class="table table-striped">
  40. <thead>
  41. <tr>
  42. <th>Greetings</th>
  43. </tr>
  44. </thead>
  45. <tbody id="greetings">
  46. </tbody>
  47. </table>
  48. </div>
  49. </div>
  50. </div>
  51. </body>
  52. </html>

查看浏览器,打开开发者工具

点击connect

image-20230925111630894

查看这两个websocket的url,查看message订阅的uri。

/user/topic/live和/user/topic/foldback是指定user进行发送的。

/topic/greetings是进行广播。

点击send

image-20230925112009004

根据上面 app.js 的代码,或者我们从message中也可以看到,我们向服务器send :/app/greetings和 /app/live。

我们根据controller的代码,看到/app/greetings发送到此方法:

  1. @MessageMapping("/greetings")
  2. @SendTo("/topic/greetings")
  3. public RespVO greetings(@Payload LiveReq liveReq) throws Exception {
  4. return new RespVO(200, "success", null);
  5. }

SendTo注解,意思是广播。

  1. @MessageMapping("/live")
  2. @SendToUser("/topic/live")
  3. public RespVO live(@Payload LiveReq liveReq) throws Exception {
  4. return new RespVO(200, "success", null);
  5. }

SendToUser注解,则标识只向此发送 /app/live 的websocket 返回 /user/topic/live

在 app.js代码中,我们的两个websocket连接分别向服务器发送了 /app/greetings和 /app/live ,因此我们可以看到这两个websocket分别接受到了两个/topic/greetings和一个/user/topic/live。说明 /topic/greetings 确实是广播,/user/topic/live确实一对一的。

主动向浏览器发送message

  1. private SimpMessagingTemplate template;
  2. @Autowired
  3. public MobiController(SimpMessagingTemplate template) {
  4. this.template = template;
  5. }
  6. @RequestMapping(path = "/foldback", method = GET)
  7. public void foldback(FoldbackReq req) {
  8. this.template.convertAndSendToUser(req.getAndroidID(), "/topic/foldback", req.toString());
  9. }

image-20230925113226727

接着查看网页上的两个websocket:
image-20230925113329829

image-20230925113404088

可以看到只有其中对应的websocket接受到了/user/topic/foldback。

other

SubscribeMapping

https://medium.com/swlh/websockets-with-spring-part-3-stomp-over-websocket-3dab4a21f397

此注解当客户端发起订阅后,服务器就立刻发送message给客户端,这种一次性的,主要用来做初始化操作。

endpoint

多个endpoint,虽然在java上可以进行配置,但是网络上并没有看到对此更详细的使用。

参考资料:

https://spring.io/guides/gs/messaging-stomp-websocket/

https://docs.spring.io/spring-framework/reference/web/websocket/stomp/overview.html

原文链接:https://www.cnblogs.com/keboom/p/17727651.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号