热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Spring+STOMP实现WebSocket广播订阅、权限认证、一对一通讯(附源码)(转发)

017年11月04日22:28:19杰明Jamin阅读数:3925版权声明:本文为博主原创文章,未经博主允许不得转载。https:blo

017年11月04日 22:28:19 杰明Jamin 阅读数:3925

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/elonpage/article/details/78446695

1. 项目代码

首先,放上项目的代码链接。 
https://github.com/Jamin20/websocket-spring-demo

2. 背景

WebSocket 是 Html5 新增加特性之一,目的是浏览器与服务端建立全双工的通信方式,解决 http 使用 ajax 轮询或 long-polling 请求-响应带来过多的资源消耗,同时对特殊场景应用提供了全新的实现方式,比如聊天、股票交易、游戏等对对实时性要求较高的行业领域。

3. WebSocket 原理

WebSocket是一个持久化的协议,只需要一次HTTP握手就可以进行连接。整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求,这样服务器就不需要反复解析HTTP协议。同时,服务端就可以主动推送信息给客户端。

传统 HTTP 请求响应 
传统 HTTP 请求响应

WebSocket 请求响应 
WebSocket 请求响应

4. STOMP 协议

STOMP是一个简单的互操作协议,用于服务器在客户端之间进行异步消息传递。

客户端可以使用SEND命令来发送消息以及描述消息的内容,用SUBSCRIBE命令来订阅消息以及由谁来接收消息。这样就可以建立一个发布订阅系统,消息可以从客户端发送到服务器进行操作,服务器也可以推送消息到客户端。

5. WebSocket 与 STOMP

WebSocket 是底层协议,STOMP 是适用于WebSocket 的上层协议。直接使用 WebSocket 就类似于使用 TCP 套接字来编写 web 应用,没有高层协议定义消息的语意,不利于开发与维护。同HTTP在TCP套接字上添加请求-响应模型层一样,STOMP在 WebSocket之上提供了一个基于帧的线路格式层,用来定义消息语义。

6. Spring + STOMP

当使用 Spring 实现 STOMP 时,Spring WebSocket 应用程序充当客户端的 STOMP 代理。消息被路由到 @Controller 消息处理方法,或路由到一个简单的内存代理,经过处理后,发送给订阅用户。

另外,还可以配置 Spring 使用专用的 STOMP 代理(例如RabbitMQ,ActiveMQ 等)来实际传播消息。在这种情况下,Spring 维护代理(MQ系统)的 TCP 连接,将消息转发给它,并将消息传递给连接的 WebSocket 客户端。

7. Spring + STOMP 实现广播订阅

通讯过程:

  1. 客户端与服务器进行 HTTP 握手连接,连接点 EndPoint 通过 WebSocketMessageBroker 设置
  2. 客户端通过 subscribe 向服务器订阅消息主题(/topic/demo1/greetings)
  3. 客户端可通过 send 向服务器发送消息,消息通过路径 /app/demo1/hello/10086 达到服务端,服务端将其转发到对应的Controller(根据Controller配置的 @MessageMapping(“/demo1/hello/{typeId}”) 信息)
  4. 服务器一旦有消息发出,将被推送到订阅了相关主题的客户端(Controller中的@SendTo(“/topic/demo1/greetings”)表示将方法中 return 的信息推送到 /topic/demo1/greetings 主题)

7.1. 服务端 WebSocketMessageBroker 配置

  1. 设置对外暴露的 EndPoint ,客户端通过这个 EndPoint 进行业务接入
  2. 设置Broker,配置订阅主题、以及客户端消息的前缀等信息

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {@Overridepublic void configureMessageBroker(MessageBrokerRegistry config) {/** 用户可以订阅来自"/topic"和"/user"的消息,* 在Controller中,可通过@SendTo注解指明发送目标,这样服务器就可以将消息发送到订阅相关消息的客户端** 在本Demo中,使用topic来达到群发效果,使用user进行一对一发送** 客户端只可以订阅这两个前缀的主题*/config.enableSimpleBroker("/topic", "/user");/** 客户端发送过来的消息,需要以"/app"为前缀,再经过Broker转发给响应的Controller*/config.setApplicationDestinationPrefixes("/app");}@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {/** 路径"/webSocketEndPoint"被注册为STOMP端点,对外暴露,客户端通过该路径接入WebSocket服务*/registry.addEndpoint("/webSocketEndPoint").setAllowedOrigins("*").withSockJS();}
}

  • 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

7.2. 服务端 Controller 配置

  1. 配置程序入口 URI @MessageMapping(“/demo1/hello/{typeId}”)
  2. 配置消息推送的目标主题 @SendTo(“/topic/demo1/greetings”)

@Controller
public class GreetingController {/** 使用restful风格*/@MessageMapping("/demo1/hello/{typeId}")@SendTo("/topic/demo1/greetings")public Greeting greeting(@DestinationVariable Integer typeId, HelloMessage message, @Headers Map headers) throws Exception {return new Greeting(headers.get("simpSessionId").toString(), typeId + "---" + message.getMessage());}/** 这里没用@SendTo注解指明消息目标接收者,消息将默认通过@SendTo("/topic/twoWays")交给Broker进行处理* 不推荐不使用@SendTo注解指明目标接受者*/@MessageMapping("/demo1/twoWays")public Greeting twoWays(HelloMessage message) {return new Greeting("这是没有指明目标接受者的消息:", message.getMessage());}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

7.3. 客户端连接与订阅

  1. 配置 WebSocket 连接的URI:/webSocket/webSocketEndPoint
  2. 配置客户端订阅的主题:/topic/demo1/greetings

function connect() {var socket = new SockJS('/webSocket/webSocketEndPoint');stompClient = Stomp.over(socket);var headers={username:'admin',password:'admin'};stompClient.connect(headers, function (frame) {setConnected(true);console.log('Connected: ' + frame);stompClient.subscribe('/topic/demo1/greetings', function (greeting) {showGreeting(JSON.parse(greeting.body).userId, JSON.parse(greeting.body).content);});stompClient.subscribe('/topic/demo1/twoWays', function (greeting) {showGreeting(JSON.parse(greeting.body).userId, JSON.parse(greeting.body).content);});});
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

7.4. 客户端发送消息

  1. 发送消息

function sendName() {stompClient.send("/app/demo1/hello/10086", {}, JSON.stringify({'message': $("#message").val()}));
}

  • 1
  • 2
  • 3

7.5 效果

demo1

8. Spring + STOMP 实现用户验证

8.1. 服务端设置请求拦截器

  1. 为 configureClientInboundChannel 设置拦截器
  2. WebSocket 首次请求连接的时候,获取其 Header 信息,利用Header 里面的信息进行权限认证
  3. 通过认证的用户,使用 accessor.setUser(user); 方法,将登陆信息绑定在该 StompHeaderAccessor 上,在Controller方法上可以获取 StompHeaderAccessor 的相关信息

public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {@Overridepublic void configureClientInboundChannel(ChannelRegistration registration) {registration.setInterceptors(new ChannelInterceptorAdapter() {@Overridepublic Message preSend(Message message, MessageChannel channel) {StompHeaderAccessor accessor =MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);//1. 判断是否首次连接请求if (StompCommand.CONNECT.equals(accessor.getCommand())) {//2. 验证是否登录String username = accessor.getNativeHeader("username").get(0);String password = accessor.getNativeHeader("password").get(0);for (Map.Entry entry : Users.USERS_MAP.entrySet()) {
// System.out.println(entry.getKey() + "---" + entry.getValue());if (entry.getKey().equals(username) && entry.getValue().equals(password)) {//验证成功,登录Authentication user = new Authentication(username); // access authentication header(s)}accessor.setUser(user);return message;}}return null;}//不是首次连接,已经成功登陆return message;}});}
}

  • 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

8.2. 服务端 Controller 可以获取在拦截器中绑定的用户登录信息

  1. 使用 StompHeaderAccessor 获得相关头信息

@Controller
public class GreetingController2 {@MessageMapping("/demo2/hello/{typeId}")@SendTo("/topic/demo2/greetings")public Greeting greeting(HelloMessage message, StompHeaderAccessor headerAccessor) throws Exception {Authentication user = (Authentication) headerAccessor.getUser();String sessionId = headerAccessor.getSessionId();return new Greeting(user.getName(), "sessionId: " + sessionId + ", message: " + message.getMessage());}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

8.3. 客户端登陆时,带上登陆信息

  1. 利用Header,将登陆信息在首次连接时发送到服务端

function connect() {var socket = new SockJS('/webSocket/webSocketEndPoint');stompClient = Stomp.over(socket);var headers={username:$("#username").val(),password:$("#password").val()};stompClient.connect(headers, function (frame) {setConnected(true);console.log('Connected: ' + frame);stompClient.subscribe('/topic/demo2/greetings', function (greeting) {showGreeting(JSON.parse(greeting.body).userId, JSON.parse(greeting.body).content);});});
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

8.4. 效果

demo2

9. Spring + STOMP 实现指定目标发送


  1. 客户端可订阅个人专属的主题:/user/{username}/demo3/greetings
  2. 在 程序 中利用 SendToUser 发送消息到指定的主题: 
    2.1 Controller 注解,发送到自己 @SendToUser(“/demo3/greetings”) 
    2.2 利用 messagingTemplate 发送到指定用户 messagingTemplate.convertAndSendToUser(destUsername, “/demo3/greetings”, greeting);

9.1. 服务端 WebSocketMessageBroker 配置

  1. 增加定向发送的配置(以下代码为configureMessageBroker中需要增加的内容)

public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {@Overridepublic void configureMessageBroker(MessageBrokerRegistry config) {/** 一对一发送的前缀* 订阅主题:/user/{userID}//demo3/greetings* 推送方式:1、@SendToUser("/demo3/greetings")* 2、messagingTemplate.convertAndSendToUser(destUsername, "/demo3/greetings", greeting);*/config.setUserDestinationPrefix("/user");}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

9.2. 服务端 Controller 配置

  1. 必须注入SimpMessagingTemplate
  2. 使用注解和 messagingTemplate 发送消息到指定的订阅主题(也就是目标客户端)
  3. 目标客户端使用 Restful 的方式在请求路径中指定

@Controller
public class GreetingController3 {private final SimpMessagingTemplate messagingTemplate;/** 实例化Controller的时候,注入SimpMessagingTemplate*/@Autowiredpublic GreetingController3(SimpMessagingTemplate messagingTemplate) {this.messagingTemplate = messagingTemplate;}@MessageMapping("/demo3/hello/{destUsername}")@SendToUser("/demo3/greetings")public Greeting greeting(@DestinationVariable String destUsername, HelloMessage message, StompHeaderAccessor headerAccessor) throws Exception {Authentication user = (Authentication) headerAccessor.getUser();String sessionId = headerAccessor.getSessionId();Greeting greeting = new Greeting(user.getName(), "sessionId: " + sessionId + ", message: " + message.getMessage());/** 对目标进行发送信息*/messagingTemplate.convertAndSendToUser(destUsername, "/demo3/greetings", greeting);return new Greeting("系统", new Date().toString() + "消息已被推送。");}}

  • 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

9.3. 客户端订阅

  1. 订阅用户相关消息主题:’/user/’ + $(“#username”).val() + ‘/demo3/greetings’

function connect() {var socket = new SockJS('/webSocket/webSocketEndPoint');stompClient = Stomp.over(socket);var headers = {username: $("#username").val(),password: $("#password").val()};stompClient.connect(headers, function (frame) {setConnected(true);console.log('Connected: ' + frame);stompClient.subscribe('/user/' + $("#username").val() + '/demo3/greetings', function (greeting) {showGreeting(JSON.parse(greeting.body).userId, JSON.parse(greeting.body).content);});});
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

9.4 客户端发送消息

  1. 客户端发送消息,同时在请求路径中指明发送目标客户端

function sendName() {stompClient.send("/app/demo3/hello/" + $("#destUsername").val(), {}, JSON.stringify({'message': $("#message").val()}));
}

  • 1
  • 2
  • 3

9.5. 效果

这里写图片描述


代码下载: 
http://download.csdn.net/download/elonpage/10105442 
https://github.com/Jamin20/websocket-spring-demo

参考文章: 
1. Spring官方WebSocket文档 
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#websocket 
2. WebSocket+SockJs+STMOP 
http://www.jianshu.com/p/4ef5004a1c81 
3. STOMP Over WebSocket(stomp.js) 
http://jmesnil.net/stomp-websocket/doc/


推荐阅读
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • Linux服务器密码过期策略、登录次数限制、私钥登录等配置方法
    本文介绍了在Linux服务器上进行密码过期策略、登录次数限制、私钥登录等配置的方法。通过修改配置文件中的参数,可以设置密码的有效期、最小间隔时间、最小长度,并在密码过期前进行提示。同时还介绍了如何进行公钥登录和修改默认账户用户名的操作。详细步骤和注意事项可参考本文内容。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • Java实战之电影在线观看系统的实现
    本文介绍了Java实战之电影在线观看系统的实现过程。首先对项目进行了简述,然后展示了系统的效果图。接着介绍了系统的核心代码,包括后台用户管理控制器、电影管理控制器和前台电影控制器。最后对项目的环境配置和使用的技术进行了说明,包括JSP、Spring、SpringMVC、MyBatis、html、css、JavaScript、JQuery、Ajax、layui和maven等。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • 本文介绍了九度OnlineJudge中的1002题目“Grading”的解决方法。该题目要求设计一个公平的评分过程,将每个考题分配给3个独立的专家,如果他们的评分不一致,则需要请一位裁判做出最终决定。文章详细描述了评分规则,并给出了解决该问题的程序。 ... [详细]
  • 原文地址:https:www.cnblogs.combaoyipSpringBoot_YML.html1.在springboot中,有两种配置文件,一种 ... [详细]
  • Windows下配置PHP5.6的方法及注意事项
    本文介绍了在Windows系统下配置PHP5.6的步骤及注意事项,包括下载PHP5.6、解压并配置IIS、添加模块映射、测试等。同时提供了一些常见问题的解决方法,如下载缺失的msvcr110.dll文件等。通过本文的指导,读者可以轻松地在Windows系统下配置PHP5.6,并解决一些常见的配置问题。 ... [详细]
author-avatar
萝莉吴文娟
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有