本文章在经过阅读翻译 Spring WebServlet WebSocket 文档,并结合实践实现 SpringBoot WebSocket Stomp RabbitMQ 整合而完成,其中关键实现部分都可在这里查阅
WebSocket协议提供标准化方法,可通过单个TCP连接在客户端和服务器之间建立全双工双向通信通道,交互始于一个HTTP请求,该请求使用标头 Upgrade
进行升级。
Upgrade: websocket
Connection: Upgrade
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
从Spring Framework 4.1.5开始,WebSocket和SockJS的默认行为是仅接受同源请求。也可以允许所有或指定的来源列表。
示例:
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler")
// 允许指定来源列表
// .setAllowedOrigins("https://mydomain.com");
// 允许所有原点
// .setAllowedOrigins("*");
}
WebSocket协议定义了两种消息类型(文本消息和二进制消息),但是其内容未定义。
Stomp 协议定义一种机制,客户端和服务器可以协商子协议(即高级消息协议),以在WebSocket上使用该协议来定义每种协议可以发送的消息类型,格式,内容。
STOMP(面向简单文本的消息传递协议)。STOMP是面向文本的协议,但是消息有效负载可以是文本或二进制
STOMP是基于帧的协议,其帧以HTTP为模型。STOMP帧的结构:
Command
Header
Body
@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连接
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) { }
@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)));
}
}
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
消息通道:
可修饰类和方法 客户端使用 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 发送消息
可以使用@MessageExceptionHandler方法来处理方法中的异常 @MessageMapping。访问异常实例,可以在注释中声明异常,也可以通过方法参数声明异常
支持 @MessageMapping 修饰的方法相同的方法参数类型和返回值
全局使用可在@ControllerAdvice类声明
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只需要单例类
解决方案可参考:
可使用支持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-netty
和io.netty:netty-all
依赖
@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());
}
}
将消息路由到@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
对于WebSocket握手或SockJS HTTP传输请求,通常可使用HttpServletRequest#getUserPrincipal() 获取可通过访问的经过身份验证的用户
Spring Session 提供了 WebSocket集成 ,确保当WebSocket会话仍处于活动状态时,用户HTTP会话不会过期
使用SockJS后,HandshakeInterceptor 不能通过 headers 获取认证消息,但可通过在WebSocket连接的url添加请求查询参数获取,然而这样是不好的
解决方案:
在 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;
}
});
}
}
应用程序可以发送针对特定用户的消息,Spring的STOMP支持可以识别/user/为此目的加上前缀的目的地
客户端订阅 /user/queue/position-updates,UserDestinationMessageHandler 会根据Principal转换处理目的地。
实际上发送方将消息送到一个或多个用户目的地 /user/{pricipalName}/queue/position-updates。@SendToUser 的 broadcast 属性值为true,则发送所有用户,否则只会发送正在处理的消息的会话
clientOutboundChannel 支持 ThreadPoolExecutor。消息将在不同的线程中处理,且客户端接收到的结果序列可能与发布的确切顺序不匹配。
@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
@Override
protected void configureMessageBroker(MessageBrokerRegistry registry) {
// 启用消息顺序
registry.setPreservePublishOrder(true);
}
}
会产生很小的性能开销,因此,只有在需要时才应启用消息顺序
ApplicationContext通过实现Spring的ApplicationListener接口,可以发布、接收事件:
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());
}
}
提供可用配置选项的概述,以及有关如何进行扩展
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024).setMessageSizeLimit(128 * 1024);
}
使用外部代理