Springwebsocket+Stomp+SockJS实时通信源码详解
⼀、三者之间的关系
Http连接为⼀次请求(request)⼀次响应(response),必须为同步调⽤⽅式。WebSocket 协议提供了通过⼀个套接字实现全双⼯通信的功能。⼀次连接以后,会建⽴tcp连接,后续客户端与服务器交互为全双⼯⽅式的交互⽅式,客户端可以发送消息到服务端,服务端也可将消息发送给客户端。
SockJS 是 WebSocket 技术的⼀种模拟。为了应对许多浏览器不⽀持WebSocket协议的问题,设计了备选SockJs。开启并使⽤SockJS 后,它会优先选⽤Websocket协议作为传输协议,如果浏览器不⽀持Websocket协议,则会在其他⽅案中,选择⼀个较好的协议进⾏通讯。 -服务端使⽤:
registry.addEndpoint("/endpointChat").withSockJS();
-客户端使⽤: //加载sockjs
<script src="/sockjs-0.3.min.js"></script>
var url = '/chat';
var sock = new SockJS(url);
//SockJS 所处理的 URL是““或““,⽽不是“ws://“or “wss://“
//.....
STOMP 中⽂为: ⾯向消息的简单⽂本协议。websocket定义了两种传输信息类型: ⽂本信息和⼆进制信息。类型虽然被确定,但是他们的传输体是没有规定的。所以,需要⽤⼀种简单的⽂本传输类型来规定传输内容,它可以作为通讯中的⽂本传输协议,即交互中的⾼级协议来定义交互信息。 STOMP本⾝可以⽀持流类型的⽹络传输协议: websocket协议和tcp协议。 Stomp还提供了⼀个stomp.js,⽤于浏览器客户端使⽤STOMP消息协议传输的js库。
STOMP的优点如下:
(1)不需要⾃建⼀套⾃定义的消息格式
(2)现有stomp.js客户端(浏览器中使⽤)可以直接使⽤
(3)能路由信息到指定消息地点
(4)可以直接使⽤成熟的STOMP代理进⾏⼴播 如:RabbitMQ, ActiveMQ
⼆、配置WebsocketStompConfig
import t.annotation.Configuration;
import fig.MessageBrokerRegistry;
import org.springframework.fig.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.fig.annotation.EnableWebSocketMessageBroker;
import org.springframework.fig.annotation.StompEndpointRegistry;
/**
* @EnableWebSocketMessageBroker 注解表明:这个配置类不仅配置了 WebSocket,还配置了基于代理的 STOMP消息;
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
/**
* 复写了 registerStompEndpoints() ⽅法:添加⼀个服务端点,来接收客户端的连接。将"/endpointChat"路径注册为 STOMP 端点。
* 这个路径与发送和接收消息的⽬的路径有所不同,这是⼀个端点,客户端在订阅或发布消息到⽬的地址前,要连接该端点,
* 即⽤户发送请求:url="/127.0.0.1:8080/endpointChat"与 STOMP server 进⾏连接,之后再转发到订阅url;
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//添加⼀个/endpointChat端点,客户端就可以通过这个端点来进⾏连接;withSockJS作⽤是添加SockJS⽀持
registry.addEndpoint("/endpointChat").withSockJS();
}
/**
* 复写了 configureMessageBroker() ⽅法:
* 配置了⼀个简单的消息代理,通俗⼀点讲就是设置消息连接请求的各种规范信息。
* 发送应⽤程序的消息将会带有 “/app” 前缀。
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//定义了⼀个(或多个)客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息
/
/定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀
//registry.setApplicationDestinationPrefixes("/app");
// 点对点使⽤的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/
//registry.setUserDestinationPrefix("/user/");
}
}
注意:
此配置是基于SpringBoot+Shiro的框架,Shiro维护了所有的session,在⽤户登录的时候就通过
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
将⽤户信息注册成为principal。当客户端连接endpointChat成功时,stomp会取java.security.Principal的默认实现类(在我的系统中为shiro的principal)信息注册成为username,然后返回给客户端。这个username对于点对点发送消息⼗分重要,通过服务端和客户端维护相同的username来达到精准推送消
息的⽬的。
2、⾃定义匹配规则
如果采⽤其他架构,没有实现principal,这就需要⾃⼰实现⾃定义的username规则,必须要通过实现⾃⼰的principal类来完成,参考代码如下:
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/endpointChat").setHandshakeHandler(new DefaultHandshakeHandler(){
@Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) { //key就是服务器和客户端保持⼀致的标记,⼀般可以⽤账户
名称,或者是⽤户ID。
return new MyPrincipal("test");
}
})
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//定义了⼀个(或多个)客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息
//定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀
/
/registry.setApplicationDestinationPrefixes("/app");
// 点对点使⽤的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/
//registry.setUserDestinationPrefix("/user/");
}
/**
* ⾃定义的Principal
*/
class MyPrincipal implements Principal{
private String key;
public MyPrincipal(String key) {
this.key = key;
}
@Override
public String getName() {
return key;
}
}
}
然后服务端给客户端发送消息:
客户端订阅服务器发送的消息(控制板打印消息如图1):
stomp.subscribe("/user/queue/notifications", handleFunction);
注意:此处为什么不是“/user/test/queue/notifications”,稍候再讲。
3、连接时验证登录权限
⼀般在连接服务器时,需要验证此连接的安全性,验证⽤户是否登录,如果没有登录,不能连接服务器,订阅消息
/**
* 连接时验证⽤户是否登录
* @author LEITAO
* @date 2018年4⽉18⽇上午10:10:37
*/
public class SessionAuthHandshakeInterceptor implements HandshakeInterceptor{
private final Logger logger = Class());
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
UserDO user = User();
if (user == null) {
<("websocket权限拒绝:⽤户未登录");
return false;
}
//attributes.put("user", user);
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
}
}
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//添加⼀个/endpointChat端点,客户端就可以通过这个端点来进⾏连接;withSockJS作⽤是添加SockJS⽀持
registry.addEndpoint("/endpointChat")
//添加连接登录验证
.addInterceptors(new SessionAuthHandshakeInterceptor())
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//定义了⼀个(或多个)客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息
//定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀
//registry.setApplicationDestinationPrefixes("/app");
/
/ 点对点使⽤的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/
//registry.setUserDestinationPrefix("/user/");
}
}
三、@MessageMapping、@SendTo、@SendToUser注解
@MessageMapping注解和@RequestMapping注解功能类似,只不过@RequestMapping表明此⽅法是Stomp客户端send⽅法的⽬标地址。 使⽤⽅式如下:
@Controller
public class WebSocketController {
@Autowired
public SimpMessagingTemplate template;
@MessageMapping("/hello")
@SendTo("/topic/hello")
public Greeting greeting(Greeting message) throws Exception {
return message;
}
@MessageMapping("/message")
@SendToUser("/message")
public UserMessage userMessage(UserMessage userMessage) throws Exception {
return userMessage;
}
}
第⼀个⽅法,表⽰服务端可以接收客户端通过向地址“/hello”发送过来的消息。@SendTo表⽰此⽅法会向订阅”/topic/hello”的⽤户⼴播message消息。@SendTo ("/topic/hello")注解等同于使⽤
客户端通过
stomp.subscribe("/topic/hello", handleFunction);
⽅法订阅的地⽅都能收到消息。
第⼆个⽅法道理相同,只是注意这⾥⽤的是@SendToUser,这就是发送给单⼀客户端的标志。本例中,客户端接收⼀对⼀消息的主题应该是“/user/message” ,”/user/”是固定的搭配,服务器会⾃动识别。
@SendToUser("/message") 等同于使⽤
websocket和socket客户端通过
stomp.subscribe("/user/message", handleFunction);
⽅法订阅的并且注册时返回的username=Key时才能收到消息。
注意:相关的注解还有很多,此处不⼀⼀描述。
四、点对点发送流程
⼀对多⼴播消息流程⽐较简单,此处不做描述。 点对点发送功能区别不仅仅在使⽤@SendToUser或者是convertAndSendToUser⽅法。最重要的区别,在于底层的实现逻辑上⾯。 当我在刚刚学习的时候遇到了⼀个问题,客户端通过
stomp.subscribe("/user/queue/notifications", handleFunction);
订阅的地址,居然能收到后台使⽤
发布的点对点消息。 通过研究代码,发现convertAndSendToUser底层通过⽅法:
@Override
public void convertAndSendToUser(String user, String destination, Object payload, Map<String, Object> headers,
MessagePostProcessor postProcessor) throws MessagingException {
user = place(user, "/", "%2F");
}
将"/queue/notifications"转换成了"/user/UserDO{userId=1,accountType=0, username='admin',name='超级管理员',...}/queue/notifications"。⽽前端按照⽹上的说法应该通过订阅相同的地址"/user/UserDO{userId=1,accountType=0, username='admin',name='超级管理员',...}/queue/notifications"才能够接受消息才对。通过前辈的指点,加上⾃⼰debug源码才发现其中的奥秘。
系统启动通过
stomp.subscribe("/user/queue/notifications", handleFunction);
订阅的时候,会调⽤ssaging.simp.user.DefaultUserDestinationResolver的resolveDestination⽅法,将连接服务器返回给前端的usern
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论