之前一篇文章通过demo简单介绍了下Springboot 集成 websocket发送消息;因为工作需要,所以深入了解了下具体的使用方法;主要详情讲一对一的消息发送;
1.依赖环境配置
前端是使用的angualr,需要引入sockjs-client和webstomp-client这两个库;具体package.json中配置,然后通过yarn install安装即可;如下所示
后端使用使用Springboot,需要引入spring对websocket的支持
compile "org.springframework.boot:spring-boot-starter-websocket"
引入WebsocketConfiguration配置类,配置websocket的信息;代码如下所示
@Configuration
@EnableWebSocketMessageBroker
public class WebsocketConfiguration implements WebSocketMessageBrokerConfigurer {
public static final String IP_ADDRESS = "IP_ADDRESS";
private final JHipsterProperties jHipsterProperties;
public WebsocketConfiguration(JHipsterProperties jHipsterProperties) {
this.jHipsterProperties = jHipsterProperties;
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
String[] allowedOrigins = Optional.ofNullable(jHipsterProperties.getCors().getAllowedOrigins()).map(origins -> origins.toArray(new String[0])).orElse(new String[0]);
registry.addEndpoint("/websocket/tracker")
.setHandshakeHandler(defaultHandshakeHandler())
.setAllowedOrigins(allowedOrigins)
.withSockJS()
.setInterceptors(httpSessionHandshakeInterceptor());
}
@Bean
public HandshakeInterceptor httpSessionHandshakeInterceptor() {
return new HandshakeInterceptor() {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
attributes.put(IP_ADDRESS, servletRequest.getRemoteAddress());
}
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
};
}
private DefaultHandshakeHandler defaultHandshakeHandler() {
return new DefaultHandshakeHandler() {
@Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
Principal principal = request.getPrincipal();
if (principal == null) {
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.ANONYMOUS));
principal = new AnonymousAuthenticationToken("WebsocketConfiguration", "aaaa", authorities);
}
return principal;
}
};
}
1>config.enableSimpleBroker("/topic", "/queue");配置客户端监听的地址;topic一般是广播类消息,queue是点对点的消息;
2>registry.addEndpoint("/websocket/tracker");客户端websocket建立连接的地址;
3>setHandshakeHandler(defaultHandshakeHandler());设置默认的握手处理器;设置用户和消息的关联;
4>setInterceptors(httpSessionHandshakeInterceptor());设置socket的过滤器;可以来做权限校验,校验连接的安全性,比如登录之后可以连接,返回true,未登录禁止连接,返回false;
5>defaultHandshakeHandler();重写了determineUser方法,建立链接的时候识别用户名和session之间的对应关系,以便做到点对点的消息通知;这里我默认如果principal没有的话,用户是“aaaa”;
2.前端建立连接
const loc = this.$window.nativeWindow.location;
let url;
url = '//' + loc.host + loc.pathname + 'websocket/tracker';
const socket = new SockJS(url);
this.stompClient = Stomp.over(socket);
const headers = {};
headers['X-XSRF-TOKEN'] = this.csrfService.getCSRF('XSRF-TOKEN');
this.stompClient.connect(
headers,
() => {
this.connectedPromise('success');
this.connectedPromise = null;
console.log('connect success!!');
}
);
订阅点对点消息
this.connection.then(() => {
this.subscriber = this.stompClient.subscribe('/user/queue/chat', data => {
this.listenerObserver.next(data);
});
});
3.后端发送消息
@RestController
public class WebsocketController {
private final SimpMessageSendingOperations messagingTemplate;
public WebsocketController(SimpMessageSendingOperations messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
@GetMapping("/web-socket/test")
public ResponseEntity<Void> test(Principal principal) {
this.messagingTemplate.convertAndSendToUser("aaaa", "/queue/chat", "ffffffff");
return new ResponseEntity<>(HttpStatus.OK);
}
}
调用/web-socket/test时,发送消息到前端;这里也可以使用@MessageMapping来通过websocket前端发送消息到后端;
调用convertAndSendToUser发送消息给指定用户;这里有人可能会有疑问了,前端订阅的路径时/user/queue/chat,后端发送的消息路径是/aaaa/queue/chat,这两个路径怎么不一致?
4.源码分析
1>调用convertAndSendToUser方法,最终调用栈到UserDestinationMessageHandler的handleMessage方法中;如下所示
继续往下跟踪到DefaultUserDestinationResolver的resolveDestination方法;代码如下所示
在parse(message)中继续调用到parseMessage,如下所示
继续往下跟踪到
通过user获取对应的session;这里可以看到用户名是aaaa,对应的路径是subscription:sub-1555394040552-443;我们看前端的日志
这里是一致的,也就是说,每个订阅id,会跟用户绑定起来;
获取到用户的sessionId之后,调用getTargetDestination方法来获取用户的目标地址;
这个是最终的路径;
所以前面发送的地址/user/aaaa/queue/chat 到这里转换成了 user/queue/chat-user4lzrnqaz;用户信息被转换成了对应的sessionId;这样就跟一个唯一的前端订阅地址联系起来了;
5.不需要经过握手的点对点消息推送
这种情况也可以实现点对点的消息推送;需要做一些修改;
1>websocketConfiguation不需要设置握手处理器
后端消息发送不变,如图所示
前端订阅消息需要修改,如下
用户aaaa变成路径的一部分了,之前是/user/queue/chat即可;然后是websocketConfiguartion中,后面是“/user“,上一种方式是queue;
跟踪源码下来发现,这种方式区别在于这里
这里的user是空的,最终的sessionIds也是空的,所以后面的路径就是/user/aaaa/queue/chat;跟前端的路径是一致的;