1. 前言

WebSocket是一种在单个TCP连接上进行全双工通信的协议,常用于实时通信的场景。在没有使用高层级线路协议的情况下,直接使用WebSocket是很难实现发布订阅的功能。而STOMP是在WebSocket之上提供了一个基于帧的线路格式层,STOMP客户端可以同时作为生产者和消费者两种模式。为发布订阅的功能提供了基础。

2. STOMP协议

STOMP is a simple text-orientated messaging protocol. It defines an interoperable wire format so that any of the available STOMP clients can communicate with any STOMP message broker to provide easy and widespread messaging interoperability among languages and platforms (the STOMP web site has a list of STOMP client and server implementations.

文档地址:http://jmesnil.net/stomp-websocket/doc/

3. SpringBoot WebSocket集成

SpringBoot集成WebSocket非常方便,只需要简单的三个步骤:导包、配置、提供接口

3.1 导入websocket包

  1. compile('org.springframework.boot:spring-boot-starter-websocket')

3.2 配置WebSocket

第一步:创建WebSocketConfig类,通过@EnableWebSocketMessageBroker 启用代理支持的消息传递。

第二步:重写registerStompEndpoints和configureMessageBroker方法。

第三步:注册对外可访问的stomp端点、访问方式和连接跨域设置。

第四步:配置消息代理。可设置广播模式和点对点通讯。也可以添加订阅通道的前缀。

  1. package com.itdragon.server.config
  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. @Configuration
  8. @EnableWebSocketMessageBroker
  9. class WebSocketConfig : WebSocketMessageBrokerConfigurer {
  10. override fun configureMessageBroker(config: MessageBrokerRegistry) {
  11. // 设置订阅Broker名称,/topic为广播模式
  12. config.enableSimpleBroker("/topic")
  13. // 设置应用程序全局目标前缀
  14. config.setApplicationDestinationPrefixes("/itdragon")
  15. }
  16. override fun registerStompEndpoints(registry: StompEndpointRegistry) {
  17. // 允许使用socketJs方式访问,访问端点为socket,并允许跨域
  18. registry.addEndpoint("/socket").setAllowedOrigins("*").withSockJS()
  19. }
  20. }

注意:

若使用了setApplicationDestinationPrefixes方法,则作用主要体现在@SubscribeMapping和@MessageMapping上。如控制层配置@MessageMapping("/sendToServer"),则客户端发送的地址是 /itdragon/sendToServer

3.3 对外暴露接口

第一步:创建WebSocket的控制层类,并注入用于发送消息的SimpMessagingTemplate。

第二步:配置通过@MessageMapping注解修饰的方法来接收客户端SEND的操作。

第三步:配置通过@SubscribeMapping注解修饰的方法来接收客户端SUBSCRIBE的操作。

第四步:配置通过@SendTo注解的方法来直接将消息推送的指定地址上。

  1. package com.itdragon.server.api.rest
  2. import org.springframework.beans.factory.annotation.Autowired
  3. import org.springframework.messaging.handler.annotation.MessageMapping
  4. import org.springframework.messaging.handler.annotation.SendTo
  5. import org.springframework.messaging.simp.SimpMessagingTemplate
  6. import org.springframework.messaging.simp.annotation.SubscribeMapping
  7. import org.springframework.stereotype.Controller
  8. import org.springframework.web.bind.annotation.RequestMapping
  9. import java.time.Instant
  10. @Controller
  11. class WebSocketController {
  12. @Autowired
  13. lateinit var simpMessagingTemplate: SimpMessagingTemplate
  14. /**
  15. * 订阅广播,服务器主动推给连接的客户端
  16. * 通过Http请求的方式触发订阅操作
  17. */
  18. @RequestMapping("/subscribeTopic")
  19. fun subscribeTopicByHttp() {
  20. while (true) {
  21. // 可以灵活设置成通道地址,实现发布订阅的功能
  22. val channel = "/topic/subscribeTopic"
  23. simpMessagingTemplate.convertAndSend(channel, Instant.now())
  24. Thread.sleep(10*1000)
  25. }
  26. }
  27. /**
  28. * 订阅广播,服务器主动推给连接的客户端
  29. * 通过Websocket的subscribe操作触发订阅操作
  30. */
  31. @SubscribeMapping("/subscribeTopic")
  32. fun subscribeTopicByWebSocket(): Long {
  33. return Instant.now().toEpochMilli()
  34. }
  35. /**
  36. * 服务端接收客户端发送的消息,类似OnMessage方法
  37. */
  38. @MessageMapping("/sendToServer")
  39. fun handleMessage(message: String) {
  40. println("message:{$message}")
  41. }
  42. /**
  43. * 将客户端发送的消息广播出去
  44. */
  45. @MessageMapping("/sendToTopic")
  46. @SendTo("/topic/subscribeTopic")
  47. fun sendToTopic(message: String): String {
  48. return message
  49. }
  50. }

WebSocket的订阅功能,可以用@SubscribeMapping注解,也可以用HTTP的方式触发。ITDragon龙 比较倾向HTTP的方式,因为在实现身份验证的功能上会比较方便。在客户端发送订阅操作之前,先发送HTTP请求做身份验证,验证成功后再返回指定的订阅通道地址。

4. 前端对接测试

在做消息通道对接的测试中,最常见的对话就是:连上了吗?没连上;收到了吗?没收到;收到了吗?收到了,后端报错....... 作为技术人员,我们有必要对各个领域的知识都有一定的了解。只有清楚明白了前端和移动端的开发思维,我们才能提供更合适的接口。

4.1 前端代码

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8" />
  5. <title>WebSocket 发布订阅</title>
  6. <link rel="stylesheet" type="text/css" href="https://cdn.staticfile.org/antd/3.23.6/antd.min.css">
  7. <script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
  8. <script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
  9. <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
  10. </head>
  11. <body>
  12. <h2 id="connStatus"></h2>
  13. <div style="padding-left:20px;">
  14. <p>/topic/subscribeTopic 订阅广播通道;/sendToServer 向服务端推送消息;/sendToTopic 将消息广播出去</p>
  15. <div>
  16. <label>订阅通道: </label> <input type="text" id="subscribeChannel" value=""/><br>
  17. <label>推送通道: </label> <input type="text" id="sendChannel" value=""/><br>
  18. <button id="subscribe" onclick="subscribe()">订阅</button>
  19. <button id="connect" onclick="connect()">连接</button>
  20. <button id="disconnect" onclick="disconnect();">断开连接</button>
  21. </div>
  22. <br>
  23. <div>
  24. <label>用户名称: </label> <input type="text" id="name"/><br>
  25. <label>输入消息: </label> <input type="text" id="message"/><br>
  26. <button id="send" onclick="sendMsg();">发送</button>
  27. <p id="response"></p>
  28. </div>
  29. </div>
  30. <script type="text/javascript">
  31. var stompClient = null;
  32. var host="http://localhost:8809";
  33. function subscribe() {
  34. $.get(host+'/subscribeTopic');
  35. }
  36. function connect() {
  37. var socket = new SockJS(host+'/socket');
  38. stompClient = Stomp.over(socket);
  39. stompClient.connect({}, function(frame) {
  40. $('#connStatus').html('Connected:' + frame)
  41. stompClient.subscribe($('#subscribeChannel').val(), function(response) {
  42. $('#response').html(response.body);
  43. });
  44. },function(err){
  45. console.log(err)
  46. });
  47. }
  48. function disconnect() {
  49. if (stompClient != null) {
  50. stompClient.disconnect();
  51. }
  52. $('#connStatus').html('Disconnected')
  53. }
  54. function sendMsg() {
  55. var message = $('#message').val();
  56. stompClient.send($('#sendChannel').val(), {}, message);
  57. }
  58. </script>
  59. </body>
  60. </html>

4.2 测试效果

简单测试了发布和订阅功能

5. 原生WebSocket配置

有的特殊场景需要检测WebSocket的生命周期,还是会用到原生的WebSocket配置,这里记录一下对应的坑。

5.1 配置类注册Bean

在任意一个配置类中添加ServerEndpointExporter的Bean配置

  1. @Bean
  2. fun serverEndpointExporter(): ServerEndpointExporter {
  3. return ServerEndpointExporter()
  4. }

问题:

  • 1)添加后单元测试启动失败,服务可以正常启动。网上说可以移除代码,由SpringBoot管理。可是移除后websocket链接会出现问题。解决方法目前未找到。

5.2 创建WebSocketServer

第一步:通过@ServerEndpoint注解修饰类,表示该类是WebSocket的Server,并对外暴露连接地址。

第二步:通过@OnOpen、@OnClose、@OnMessage、@OnError注解修饰方法,监控WebSocket的生命周期。

第三步:通过静态、私有、ConcurrentHashMap 修饰的变量管理客户端。

第四步:为程序其他类提供发送消息的方法。

  1. package com.itdragon.server.config
  2. import org.slf4j.LoggerFactory
  3. import org.springframework.stereotype.Component
  4. import java.io.IOException
  5. import java.util.concurrent.ConcurrentHashMap
  6. import javax.websocket.*
  7. import javax.websocket.server.PathParam
  8. import javax.websocket.server.ServerEndpoint
  9. @ServerEndpoint("/nativeSocket/{clientKey}")
  10. @Service
  11. class WebSocketServer {
  12. private var logger = LoggerFactory.getLogger(WebSocketServer::class.java)
  13. private var session: Session? = null
  14. private var clientKey = ""
  15. @OnOpen
  16. fun onOpen(session: Session, @PathParam("clientKey") clientKey: String) {
  17. this.session = session
  18. this.clientKey = clientKey
  19. if (webSocketMap.containsKey(clientKey)) {
  20. webSocketMap.remove(clientKey)
  21. webSocketMap[clientKey] = this
  22. } else {
  23. webSocketMap[clientKey] = this
  24. }
  25. logger.info("客户端:$clientKey 连接成功")
  26. }
  27. @OnClose
  28. fun onClose() {
  29. if (webSocketMap.containsKey(clientKey)) {
  30. webSocketMap.remove(clientKey)
  31. }
  32. logger.warn("客户端:$clientKey 连接关闭")
  33. }
  34. @OnMessage
  35. fun onMessage(message: String, session: Session) {
  36. logger.info("客户端:$clientKey 收到消息:$message")
  37. }
  38. @OnError
  39. fun onError(session: Session, error: Throwable) {
  40. logger.error("WebSocket客户端(${this.clientKey})错误: ${error.message}")
  41. }
  42. @Throws(IOException::class)
  43. fun sendMessage(message: String) {
  44. this.session!!.basicRemote.sendText(message)
  45. }
  46. companion object {
  47. private val webSocketMap = ConcurrentHashMap<String, WebSocketServer>()
  48. @Throws(IOException::class)
  49. fun sendMessage(clientKey: String, message: String) {
  50. webSocketMap[clientKey]?.sendMessage(message)
  51. }
  52. fun getStatus(clientKey: String): Boolean? {
  53. return webSocketMap[clientKey]?.session?.isOpen
  54. }
  55. }
  56. }

问题:

5.3 前端测试

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8" />
  5. <title>WebSocket 简单通讯</title>
  6. <link rel="stylesheet" type="text/css" href="https://cdn.staticfile.org/antd/3.23.6/antd.min.css">
  7. <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
  8. </head>
  9. <body>
  10. <div>
  11. <label>订阅通道: </label> <input type="text" id="clientId" value=""/><br>
  12. <label>推送消息: </label> <input type="text" id="message" value=""/><br>
  13. <button id="subscribe" onclick="openSocket()">开启socket</button>
  14. <button id="connect" onclick="sendMessage()">发送消息</button>
  15. <button id="disconnect" onclick="closeSocket();">关闭socket</button>
  16. </div>
  17. <script>
  18. var socket;
  19. function openSocket() {
  20. if(typeof(WebSocket) == "undefined") {
  21. console.log("您的浏览器不支持WebSocket");
  22. }else{
  23. console.log("您的浏览器支持WebSocket");
  24. var socketUrl="ws://localhost:8809/nativeSocket/"+$("#clientId").val();
  25. if(socket!=null){
  26. socket.close();
  27. socket=null;
  28. }
  29. socket = new WebSocket(socketUrl);
  30. socket.onopen = function() {
  31. console.log("websocket已打开");
  32. };
  33. socket.onmessage = function(msg) {
  34. console.log(msg.data);
  35. };
  36. socket.onclose = function() {
  37. console.log("websocket已关闭");
  38. };
  39. socket.onerror = function() {
  40. console.log("websocket发生了错误");
  41. }
  42. }
  43. }
  44. function sendMessage() {
  45. if(socket!=null){
  46. socket.send($("#message").val());
  47. }
  48. }
  49. function closeSocket() {
  50. socket.close();
  51. }
  52. </script>
  53. </body>
  54. </html>

通过stomp客户端发起的认证操作可以看一下这篇文章:https://www.cnblogs.com/jmcui/p/8999998.html

SpringBoot WebSocket STOMP 广播配置的更多相关文章

  1. springboot+websocket+sockjs进行消息推送【基于STOMP协议】

    springboot+websocket+sockjs进行消息推送[基于STOMP协议] WebSocket是在HTML5基础上单个TCP连接上进行全双工通讯的协议,只要浏览器和服务器进行一次握手,就 ...

  2. springboot websocket集群(stomp协议)连接时候传递参数

    最近在公司项目中接到个需求.就是后台跟前端浏览器要保持长连接,后台主动往前台推数据. 网上查了下,websocket stomp协议处理这个很简单.尤其是跟springboot 集成. 但是由于开始是 ...

  3. 【Java分享客栈】SpringBoot整合WebSocket+Stomp搭建群聊项目

    前言 前两周经常有大学生小伙伴私信给我,问我可否有偿提供毕设帮助,我说暂时没有这个打算,因为工作实在太忙,现阶段无法投入到这样的领域内,其中有两个小伙伴又问到我websocket该怎么使用,想给自己的 ...

  4. spring boot websocket stomp 实现广播通信和一对一通信聊天

    一.前言 玩.net的时候,在asp.net下有一个叫 SignalR 的框架,可以在ASP .NET的Web项目中实现实时通信.刚接触java寻找相关替代品,发现 java 体系中有一套基于stom ...

  5. springboot + websocket + spring-messaging实现服务器向浏览器广播式

    目录结构 pom.xml <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http:// ...

  6. springboot情操陶冶-web配置(四)

    承接前文springboot情操陶冶-web配置(三),本文将在DispatcherServlet应用的基础上谈下websocket的使用 websocket websocket的简单了解可见维基百科 ...

  7. Spring Boot2 系列教程 (十六) | 整合 WebSocket 实现广播

    前言 如题,今天介绍的是 SpringBoot 整合 WebSocket 实现广播消息. 什么是 WebSocket ? WebSocket 为浏览器和服务器提供了双工异步通信的功能,即浏览器可以向服 ...

  8. SpringBoot+WebSocket

    SpringBoot+WebSocket 只需三个步骤 导入依赖 <dependency> <groupId>org.springframework.boot</grou ...

  9. springboot websocket 简单入门

    在没有WebSocket时,大多时候我们在处理服务端主动给浏览器推送消息都是非常麻烦,且有很多弊端,如: 1.Ajax轮循 优点:客户端很容易实现良好的错误处理系统和超时管理,实现成本与Ajax轮询的 ...

随机推荐

  1. Python迭代器和关键字 global ,nonlocal

    1.关键字 global : 可以修改全局变量 可以在局部作用域声明一个全局变量,剪切 : 此时局部作用域没有该变量,全局作用域中有 name = 1 def func(): global name ...

  2. django框架基础-django redis-长期维护-20191220

    ###############   django框架-django redis    ############### # 学习django redis我能得到什么? # 1,项目中广泛使用到redis ...

  3. git本地仓库目录问题

    git安装后修改默认的路径:每次打开git bash后都会进入这个目录 https://blog.csdn.net/weixin_39634961/article/details/79881140 在 ...

  4. Widgets学习

    ListView ListView RecyclerView RecyclerView ExpandableListView 关闭箭头 elvMsg.setGroupIndicator(null); ...

  5. VisualStudioCode通过SSH远程编辑文件

    翻译修改自:https://codepen.io/ginfuru/post/remote-editing-files-with-ssh 在远程服务器上编写文件是一件很糟糕的事情,vim和其他终端编辑器 ...

  6. POJ 2112 Optimal Milking 最短路 二分构图 网络流

    题意:有C头奶牛,K个挤奶站,每个挤奶器最多服务M头奶牛,奶牛和奶牛.奶牛和挤奶站.挤奶站和挤奶站之间都存在一定的距离.现在问满足所有的奶牛都能够被挤奶器服务到的情况下,行走距离的最远的奶牛的至少要走 ...

  7. OSX安装Mysql8.0

    OSX下MySQL的安装非常方便,可以通过官网的dmg包进行安装,也可通过brew进行安装.以下介绍如何通过brew如何安装MySQL. 0X00.安装前的准备 既然要通过brew安装,那么就需要确保 ...

  8. flutter 白板工具Twitter IconFacebook Icon

    flutter 白板工具 Categories: flutter 平常桌面上都放着一些草稿纸,因为经常要整理思路.画画草图啥的.这不是电子时代嘛,就觉得在手机.pad上也可以这样写写画画,我看了有很多 ...

  9. Typora+PicGo+Gitee笔记方案

    前言:需要学习的知识太多,从一开始就在寻找一款能让我完全满意的编辑器,然而一直都没有令我满意的.在前两天Typora新版本更新后,总算是拥有了一套我认为很完美的笔记方案:使用Typora编写markd ...

  10. swoole(1)使用docker安装swoole环境

    1.下载镜像 pull php 镜像 docker pull php:7.3-alpine3.8 创建容器 docker run -it --name test php:7.3-alpine3.8 s ...