Spring+STOMP实现WebSocket广播订阅、权限认证、一对一通讯(附源码)
版声明:本文为博主原创文章,未经博主允许不得转载。 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 请求响应
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 实现广播订阅
通讯过程:
- 客户端与服务器进行 HTTP 握手连接,连接点 EndPoint 通过 WebSocketMessageBroker 设置
- 客户端通过 subscribe 向服务器订阅消息主题(/topic/demo1/greetings)
- 客户端可通过 send 向服务器发送消息,消息通过路径 /app/demo1/hello/10086 达到服务端,服务端将其转发到对应的Controller(根据Controller配置的 @MessageMapping(“/demo1/hello/{typeId}”) 信息)
- 服务器一旦有消息发出,将被推送到订阅了相关主题的客户端(Controller中的@SendTo(“/topic/demo1/greetings”)表示将方法中 return 的信息推送到 /topic/demo1/greetings 主题)
7.1. 服务端 WebSocketMessageBroker 配置
- 设置对外暴露的 EndPoint ,客户端通过这个 EndPoint 进行业务接入
- 设置Broker,配置订阅主题、以及客户端消息的前缀等信息
-
@Configuration
-
@EnableWebSocketMessageBroker
-
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
-
@Override
-
public void configureMessageBroker(MessageBrokerRegistry config) {
-
/*
-
* 用户可以订阅来自"/topic"和"/user"的消息,
-
* 在Controller中,可通过@SendTo注解指明发送目标,这样服务器就可以将消息发送到订阅相关消息的客户端
-
*
-
* 在本Demo中,使用topic来达到群发效果,使用user进行一对一发送
-
*
-
* 客户端只可以订阅这两个前缀的主题
-
*/
-
config.enableSimpleBroker("/topic", "/user");
-
/*
-
* 客户端发送过来的消息,需要以"/app"为前缀,再经过Broker转发给响应的Controller
-
*/
-
config.setApplicationDestinationPrefixes("/app");
-
}
-
@Override
-
public 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 配置
- 配置程序入口 URI @MessageMapping(“/demo1/hello/{typeId}”)
- 配置消息推送的目标主题 @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<String, Object> 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. 客户端连接与订阅
- 配置 WebSocket 连接的URI:/webSocket/webSocketEndPoint
- 配置客户端订阅的主题:/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. 客户端发送消息
- 发送消息
-
function sendName() {
-
stompClient.send("/app/demo1/hello/10086", {}, JSON.stringify({'message': $("#message").val()}));
-
}
- 1
- 2
- 3
7.5 效果
8. Spring + STOMP 实现用户验证
8.1. 服务端设置请求拦截器
- 为 configureClientInboundChannel 设置拦截器
- WebSocket 首次请求连接的时候,获取其 Header 信息,利用Header 里面的信息进行权限认证
- 通过认证的用户,使用 accessor.setUser(user); 方法,将登陆信息绑定在该 StompHeaderAccessor 上,在Controller方法上可以获取 StompHeaderAccessor 的相关信息
-
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
-
@Override
-
public void configureClientInboundChannel(ChannelRegistration registration) {
-
registration.setInterceptors(new ChannelInterceptorAdapter() {
-
@Override
-
public 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<String, String> 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 可以获取在拦截器中绑定的用户登录信息
- 使用 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. 客户端登陆时,带上登陆信息
- 利用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. 效果
9. Spring + STOMP 实现指定目标发送
- 客户端可订阅个人专属的主题:/user/{username}/demo3/greetings
- 在 程序 中利用 SendToUser 发送消息到指定的主题:
2.1 Controller 注解,发送到自己 @SendToUser(“/demo3/greetings”)
2.2 利用 messagingTemplate 发送到指定用户 messagingTemplate.convertAndSendToUser(destUsername, “/demo3/greetings”, greeting);
9.1. 服务端 WebSocketMessageBroker 配置
- 增加定向发送的配置(以下代码为configureMessageBroker中需要增加的内容)
-
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
-
@Override
-
public 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 配置
- 必须注入SimpMessagingTemplate
- 使用注解和 messagingTemplate 发送消息到指定的订阅主题(也就是目标客户端)
- 目标客户端使用 Restful 的方式在请求路径中指定
-
@Controller
-
public class GreetingController3 {
-
private final SimpMessagingTemplate messagingTemplate;
-
/*
-
* 实例化Controller的时候,注入SimpMessagingTemplate
-
*/
-
@Autowired
-
public 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. 客户端订阅
- 订阅用户相关消息主题:’/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 客户端发送消息
- 客户端发送消息,同时在请求路径中指明发送目标客户端
-
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/