SockJS
SockJS是一个浏览器JavaScript库。
一些浏览器中缺少对WebSocket的支持,因此,回退选项是必要的,而Spring框架提供了基于SockJS协议的透明的回退选项。参考spring整合websocket官方文档
SockJS的一大好处在于提供了浏览器兼容性。优先使用原生WebSocket,如果在不支持websocket的浏览器中,会自动降为轮询的方式。
// 前端代码 <script src="/js/sockjs.min.js"></script> websocket = new SockJS("端点http地址") websocket.onOpen(); // 等方法均可使用。不一定准确。大概是这么个意思 // 后端基于spring的sockJs实现 // withSockJS() 方法 划重点 @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler(), "/myHandler").withSockJS(); } @Bean public WebSocketHandler myHandler() { return new MyHandler(); } }
stomp
stomp是基于帧的协议,它为这些客户机和服务器之间传递的消息定义了一种基于文本的消息格式。stomp由命令 + header + payload组成。
stomp消息协议
stomp协议组成分析
ws中传递的stomp帧
客户端COMMAND
SEND
SUBSCRIBE
UNSUBSCRIBE
BEGIN
COMMIT
ABORT
ACK
NACK
DISCONNECT
服务端COMMAND
MESSAGE
RECEIPT
ERROR
HEADER
在发送特定的消息时,会携带特定的header。同时前端也可以携带header。然后后端接收。
前端传递:
stompClient.send("/app/sendToServer", {"accountId": 1}, msg);
其中accoontId:1 即为携带的header键值对
后端接收:
方法中添加MessageHeaders 参数即可
BODY
Body^@ -- 消息体
spring with stomp demo
// 前端 stompClient.connect(function (frame) { // writeToScreen("connected: " + frame); /** 订阅广播 */ // stompClient.subscribe('/topic/reply', function (response) { // writeToScreen("/topic/:" + response.body); // }); // // /** 订阅点对点 */ // stompClient.subscribe("/topic/"+accountId, function (response) { // writeToScreen("/topic/"+accountId+":" + response.body); // }); }, function (error) { wsCreateHandler && clearTimeout(wsCreateHandler); wsCreateHandler = setTimeout(function () { // console.log("重连..."); // connect(); // console.log("重连完成"); }, 1000); } // 后端配置 @Configuration @EnableWebSocketMessageBroker public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 此时基于http的url. registry.addEndpoint("/stomp").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry config) { // STOMP messages whose destination header begins with /app are routed to @MessageMapping methods in @Controller classes. // 相当于在所有的方法访问前添加 /app 才可以访问 config.setApplicationDestinationPrefixes("/app"); // 相当于直接发给相关的broker通道,订阅的客户端都可以收到 // The simple broker is great for getting started but supports only // a subset of STOMP commands (it does not support acks, receipts, // and some other features), relies on a simple message-sending loop, // and is not suitable for clustering. As an alternative, you can upgrade // your applications to use a full-featured message broker. config.enableSimpleBroker("/topic", "/queue"); } } // 后端回复和发送消息 /** * MessageMapping 注解会结合setApplicationDestinationPrefixes方法形成完整路径 * SendTo 如果不指定,则返回给MessageMapping的/topic/sendToServer和/queue/sendToServer。指定,则返回给指定的topic * 也可以使用SimpMessagingTemplate 指定目的地进行发送 */ @RestController public class StompController { @Autowired SimpMessagingTemplate template; @MessageMapping("/sendToServer") @SendTo("/topic/reply") //MessageHeaders 会携带header信息,其入参中可构造的参数参考spring文档 public String sendToServer(String msg, MessageHeaders headers){ System.out.println("msg = " + msg); // template.convertAndSend("/templateSend", msg); System.out.println("headers = " + JSON.toJSONString(headers)); return "服务端已经收到了" + msg; } // 可以通过MessageExceptionHandler 进行全局的异常处理 @MessageExceptionHandler public ApplicationError handleException(MyException exception) { // ... return appError; } }
spring stomp 鉴权
@Override public void configureClientInboundChannel(ChannelRegistration registration){ registration.interceptors(MyAuthInterceptor); }
package com.example.websocketdemo.spring.stomp; import com.sun.security.auth.UserPrincipal; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.simp.stomp.StompCommand; import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.messaging.support.MessageHeaderAccessor; import org.springframework.stereotype.Component; @Component public class MyAuthInterceptor implements ChannelInterceptor { private static final String TOKEN = "token"; /** * 实际消息发送到频道之前调用 * @param message * @param channel * @return */ @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); StompCommand command = accessor.getCommand(); if (StompCommand.CONNECT.equals(command)) { Object token = accessor.getNativeHeader(TOKEN); if(token == null){ throw new RuntimeException(); } // 也可以自定义Principal类,用于信息存储和校验。 // 还有一种常用的方式是接入spring security UserPrincipal userPrincipal = new UserPrincipal("my name from token"); accessor.setUser(userPrincipal); }else if (StompCommand.SUBSCRIBE.equals(command)) { // 此处可以通过一些存储的信息进行权限校验 UserPrincipal userPrincipal = (UserPrincipal) accessor.getUser(); userPrincipal.getName(); }else if(StompCommand.SEND.equals(command)){ // 此处可以通过一些存储的信息进行权限校验 UserPrincipal userPrincipal = (UserPrincipal) accessor.getUser(); userPrincipal.getName(); } return message; } }
注:其与http鉴权的不同之处在于,ws基于长链接的,会一直保存。因此token信息只需要在connect时一次存储即可,其他时候如果要进行权限校验,则直接通过Accessor中保存的信息即可。
另外websocket本质上建立的链接是本地和服务器直接建立。因此不支持网关统一鉴权。暂时未考证。
spring stomp client
参考spring官网文档4.4.18 stomp client章节
多节点问题处理
使用rabbitmq broker
安装rabbitmq
mac电脑 brew install rabbitmq
开启stomp服务
rabbitmq-plugins enable rabbitmq_web_stomp rabbitmq_web_stomp_examples
启动rabbitmq
rabbitmq-server
登录控制台查看rabbitmq开发的stomp地址
控制台地址: http://localhost:15672/#/
账密:guest guest
默认端口:61613
符合rabbitmq的前缀
/exchange/<exchangeName>/<pattern>
subcribe frame
手动创建exchange,自动创建一个和exchangeName一致的queue,并且指定了routingkey就是pattern
send frame
发送到定义的exchange中,并且指定了其routingkey
/queue/<queueName>
使用默认的exchange,自动创建持久化的queue
无法做到群发,因为使用的是同一个queue,只会有一个队列进行消费。但是其队列是持久化的,包括消息,所以如果在消费的过程中,客户端没有消费到。会在下次连接时,从没有消费的地方进行ws的传输
/amq/queue/<queueName>
需要手动创建queue,更严谨一些。但是一般都使用自动创建。
/topic/<topicName>
subcribe frame 会自动创建AD 非持久的queue,并且根据routingkey为topicName 绑定到queue上,同时实现对该queue的订阅。使用默认的amq.topic exchange
send frame 消息会被发送到amp.topic exchange中,routingkey 是topicName。
智慧物流项目选择使用的方式。此方式使用起来比较便捷
demo
@Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableStompBrokerRelay("/topic/test").setRelayHost(brokerRelayHost) .setRelayPort(brokerRelayPort).setClientLogin("guest").setClientPasscode("guest"); }
原理简述
猜想:每次在客户端进行订阅的时候会根据标识产生当前客户端对应的临时队列。当客户端进行send的时候会产生消息,然后让队列消费,写入到对应的通道中。
nginx接入
构建稳定的连接
断连情况
浏览器页面隐藏导致的断连
tcp断连
stomp客户端自实现ping、pong,不会发生
服务器重启断连
单节点
多节点滚动更新
解决方式
前端尝试重连,但是不能一直重连。建议重连三次。
重连可以解决 浏览器页面隐藏以及多节点滚动更新的问题。单节点问题无法解决。所以重连三次之后不能一直尝试,避免把网关打满,影响其他服务。