SpringBoot WebSocket STOMP 外部消息代理

本文章在经过阅读翻译 Spring WebServlet WebSocket 文档,并结合实践实现 SpringBoot WebSocket Stomp RabbitMQ 整合而完成,其中关键实现部分都可在这里查阅

4. WebSockets

4.1. Introduction to WebSocket

WebSocket协议提供标准化方法,可通过单个TCP连接在客户端和服务器之间建立全双工双向通信通道,交互始于一个HTTP请求,该请求使用标头 Upgrade 进行升级。

Upgrade: websocket 
Connection: Upgrade 

4.1.1. HTTP Versus WebSocket

4.1.2. When to Use WebSockets

4.2. WebSocket API

4.2.1. WebSocketHandler

4.2.2. WebSocket Handshake

4.2.3. Deployment

4.2.4. Server Configuration 服务器配置

@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
    ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
    container.setMaxTextMessageBufferSize(8192);
    container.setMaxBinaryMessageBufferSize(8192);
    return container;
}

4.2.5. Allowed Origins 允许来源

从Spring Framework 4.1.5开始,WebSocket和SockJS的默认行为是仅接受同源请求。也可以允许所有或指定的来源列表。

  • 仅允许相同来源的请求(默认):启用SockJS时,Iframe HTTP响应标头X-Frame-Options设置为SAMEORIGIN,并且JSONP传输被禁用。不允许检查请求的来源。不支持IE6和IE7
  • 允许指定来源列表:每个允许的来源必须以http:// 或开头https://。启用SockJS时,将禁用IFrame传输。不支持IE6至IE9
  • 允许所有原点:提供*作为允许的原点值

示例:

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry.addHandler(myHandler(), "/myHandler")
	// 允许指定来源列表
	// .setAllowedOrigins("https://mydomain.com");
	// 允许所有原点
	// .setAllowedOrigins("*");
}

4.3. SockJS Fallback

4.4. STOMP

WebSocket协议定义了两种消息类型(文本消息和二进制消息),但是其内容未定义。
Stomp 协议定义一种机制,客户端和服务器可以协商子协议(即高级消息协议),以在WebSocket上使用该协议来定义每种协议可以发送的消息类型,格式,内容。

4.4.1. Overview

STOMP(面向简单文本的消息传递协议)。STOMP是面向文本的协议,但是消息有效负载可以是文本或二进制

STOMP是基于帧的协议,其帧以HTTP为模型。STOMP帧的结构:

Command

Header

Body

4.4.2. Benefits

  • 无需发明自定义消息协议和消息格式
  • 可使用STOMP客户端
  • 可(可选)使用消息代理(例如RabbitMQ,ActiveMQ和其他代理)来管理订阅和广播消息
  • 可以在任意数量的@Controller实例中组织应用程序逻辑,并且可以基于STOMP目标标头将消息路由。WebSocketHandler对于给定的连接,可以使用单个消息处理原始WebSocket消息。
  • 可使用Spring Security基于STOMP目的地和消息类型来保护消息

4.4.3. Enable STOMP

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry
		// WebSocket(或SockJS)客户端需要连接到WebSocket握手的终结点的HTTP URL
		.addEndpoint("/portfolio")
		// 允许 SockJS 作为后备
		.withSockJS();  
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
		// 目的标头以STOMP开头的STOMP消息/app将路由到@Controller类中的@MessageMapping方法
        config.setApplicationDestinationPrefixes("/app"); 
		// 内置的消息代理进行订阅和广播,将目标标头以其开头的消息路由/topic `or `/queue到代理
        config.enableSimpleBroker("/topic", "/queue"); 
    }
}
要从浏览器进行连接:
  • SockJS:可使用 sockjs-client
  • STOMP:可使用 webstomp-client

通过SockJS连接

var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = webstomp.over(socket);

stompClient.connect({}, function(frame) { }

通过WebSocket连接 不使用 SockJS(在配置类的registerStompEndpoints()方法中不使用 SockJS 作为后备 即注释 // .withSockJS())

var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);

stompClient.connect({}, function(frame) { }

4.4.4. WebSocket Server

@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
    ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
    container.setMaxTextMessageBufferSize(8192);
    container.setMaxBinaryMessageBufferSize(8192);
    return container;
}

对于Jetty,需要设置在StompEndpointRegistry设置HandshakeHandler和WebSocketPolicy

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler());
    }

    @Bean
    public DefaultHandshakeHandler handshakeHandler() {

        WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
        policy.setInputBufferSize(8192);
        policy.setIdleTimeout(600000);

        return new DefaultHandshakeHandler(
                new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
    }
}

4.4.5. Flow of Messages

消息传递抽象:
  • Message: Simple representation for a message, including headers and payload.

  • MessageHandler: Contract for handling a message.

  • MessageChannel: Contract for sending a message that enables loose coupling between producers and consumers.

  • SubscribableChannel: MessageChannel with MessageHandler subscribers.

  • ExecutorSubscribableChannel: SubscribableChannel that uses an Executor for delivering messages.

  • Message 包含消息标头和有效负载

  • MessageHandler 处理消息的协议

  • MessageChannel 发生生产者与消费者键松散耦合的消息的协议

  • SubscribableChannel

  • ExecutorSubscribableChannel

消息通道:

  • clientInboundChannel:用于传递从WebSocket客户端收到的消息
  • clientOutboundChannel:用于向WebSocket客户端发送服务器消息
  • brokerChannel:用于从服务器端应用程序代码内将消息发送到消息代理

4.4.6. Annotated Controllers

  • @MessageMapping
  • @SubscribeMapping
  • @MessageExceptionHandler

@MessageMapping

可修饰类和方法 客户端使用 SEND /app/xx/xx 发送消息到 @MessageMapping("/xx/xx")

支持的方法参数
方法参数 描述
Message 用于访问完整的消息。
MessageHeaders 用于访问中的标头Message。
MessageHeaderAccessor,SimpMessageHeaderAccessor和StompHeaderAccessor 用于通过类型化访问器方法访问标头。
@Payload 为了访问消息的有效负载,由configure转换(例如,从JSON转换) MessageConverter。不需要此注释,因为默认情况下会假定没有其他自变量匹配。可以使用@javax.validation.Valid或Spring的注释有效负载参数@Validated,以使有效负载参数被自动验证。
@Header 用于访问特定的标头值- org.springframework.core.convert.converter.Converter如有必要,还可以使用进行类型转换 。
@Headers 用于访问消息中的所有标题。此参数必须可分配给 java.util.Map。
@DestinationVariable 用于访问从消息目标中提取的模板变量。根据需要将值转换为声明的方法参数类型。
java.security.Principal 反映在WebSocket HTTP握手时登录的用户。
返回值

默认,@MessageMapping修饰的方法的返回值通过匹配被序列化为有效负载,MessageConverter会发送到brokerChannel,前缀为 /topic/[@MessageMapping的值]

可以使用@SendTo和@SendToUser注释来自定义输出消息的目的地。等同于使用 SimpMessagingTemplate 发送消息

@SubscribeMapping

@MessageExceptionHandler

可以使用@MessageExceptionHandler方法来处理方法中的异常 @MessageMapping。访问异常实例,可以在注释中声明异常,也可以通过方法参数声明异常

支持 @MessageMapping 修饰的方法相同的方法参数类型和返回值

全局使用可在@ControllerAdvice类声明

4.4.7. Sending Messages

SimpMessagingTemplate

SimpMessagingTemplate注入后会出现的异常:Consider defining a bean of type ‘org.springframework.messaging.simp.SimpMessagingTemplate’ in your configuration Consider defining a bean of type ‘SimpMessagingTemplate’ in your configuration

若为SimpMessagingTemplate声明Bean,会出现SimpMessagingTemplate只需要单例类

解决方案可参考:

  • https://blog.csdn.net/hefrankeleyn/article/details/89742745
  • 《Spring4实战》代码 第18章

X 4.4.8. Simple Broker

4.4.9. External Broker

可使用支持STOMP协议的消息代理,如RabbitMQ,ActiveMQ等,安装消息代理后,在启用STOMP支持运行

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/topic", "/queue");
        registry.setApplicationDestinationPrefixes("/app");
    }

}

配置消息代理中继是MessageHandler:服务端建立连接外部消息代理的TCP连接,将消息转发到外部消息代理后,通过WebSocket会话将从代理收到的所有消息转发给客户端。充当双向转发消息的“中继”

TCP 连接管理可添加 io.projectreactor.netty:reactor-nettyio.netty:netty-all 依赖

4.4.10. Connecting to a Broker

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    // ...

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient());
        registry.setApplicationDestinationPrefixes("/app");
    }

    private ReactorNettyTcpClient createTcpClient() {
        return new ReactorNettyTcpClient<>(
                client -> client.addressSupplier(() -> ... ),
                new StompReactorNettyCodec());
    }
}

4.4.11. Dots as Separators

将消息路由到@MessageMapping方法时,它们使用 AntPathMatcher 匹配。默认使用斜杠(/)作为分隔符。但对于消息代理中继不支持(RabbitMQ)或习惯消息传递约定,可使用(.)作为分隔符

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    // ...

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setPathMatcher(new AntPathMatcher("."));
        registry.enableStompBrokerRelay("/queue", "/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

使用:

@Controller
@MessageMapping("red")
public class RedController {

    @MessageMapping("blue.{green}")
    public void handleGreen(@DestinationVariable String green) {
        // ...
    }
}

客户端使用 SEND /app/red.blue.green123

4.4.12. Authentication

对于WebSocket握手或SockJS HTTP传输请求,通常可使用HttpServletRequest#getUserPrincipal() 获取可通过访问的经过身份验证的用户

Spring Session 提供了 WebSocket集成 ,确保当WebSocket会话仍处于活动状态时,用户HTTP会话不会过期

4.4.13. Token Authentication

使用SockJS后,HandshakeInterceptor 不能通过 headers 获取认证消息,但可通过在WebSocket连接的url添加请求查询参数获取,然而这样是不好的

解决方案:

  1. 使用STOMP客户端在连接时传递身份验证 headers
  2. 使用 ChannelInterceptor 处理身份验证【需要实现java.security.Principal重写 getName() 】

在 StompCommand 为 CONNECT 时获取 headers 中的认证信息并设置自定义Principal实现 StompHeaderAccessor.setUser(principal)

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message preSend(Message message, MessageChannel channel) {
                StompHeaderAccessor accessor =
                        MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                    // 需要实现java.security.Principal,并重写 getName() 
					Authentication user = ... ; // access authentication header(s)
                    accessor.setUser(user);
                }
                return message;
            }
        });
    }
}

4.4.14. User Destinations

应用程序可以发送针对特定用户的消息,Spring的STOMP支持可以识别/user/为此目的加上前缀的目的地

客户端订阅 /user/queue/position-updates,UserDestinationMessageHandler 会根据Principal转换处理目的地。

  • SimpMessagingTemplate 发送 /queue/position-updates
  • @SendToUser("/queue/position-updates")。

实际上发送方将消息送到一个或多个用户目的地 /user/{pricipalName}/queue/position-updates。@SendToUser 的 broadcast 属性值为true,则发送所有用户,否则只会发送正在处理的消息的会话

4.4.15. Order of Messages

clientOutboundChannel 支持 ThreadPoolExecutor。消息将在不同的线程中处理,且客户端接收到的结果序列可能与发布的确切顺序不匹配。

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    protected void configureMessageBroker(MessageBrokerRegistry registry) {
        // 启用消息顺序
        registry.setPreservePublishOrder(true);
    }

}

会产生很小的性能开销,因此,只有在需要时才应启用消息顺序

4.4.16. Events

ApplicationContext通过实现Spring的ApplicationListener接口,可以发布、接收事件:

  • BrokerAvailabilityEvent
  • SessionConnectEvent
  • SessionConnectedEvent
  • SessionSubscribeEvent
  • SessionUnsubscribeEvent
  • SessionDisconnectEvent

4.4.17. Interception

ChannelInterceptor可拦截处理链中任何部分的任何消息

定制ChannelInterceptor可以使用StompHeaderAccessor或SimpMessageHeaderAccessor 访问有关消息的信息

public class MyChannelInterceptor implements ChannelInterceptor {

    @Override
    public Message preSend(Message message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
        StompCommand command = accessor.getStompCommand();
        // ...
        return message;
    }
}

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new MyChannelInterceptor());
    }
}

4.4.18. STOMP Client

4.4.19. WebSocket Scope

4.4.20. Performance

提供可用配置选项的概述,以及有关如何进行扩展

@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
	registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024).setMessageSizeLimit(128 * 1024);
}

使用外部代理

你可能感兴趣的:(java,websocket,spring,boot,spring)