在这篇博客中,我们将了解如何设置应用程序以通过 WebSocket 连接,发送和接收 STOMP 消息。我们将以 Spring Boot 2 为基础,因为它包含对 STOMP 和 WebSocket 的支持,并且它还提供了一个简单的消息代理
根据 WebSocket RFC,WebSocket 协议支持在受控环境中运行不受信任的代码的客户端与选择加入来自该代码的通信的远程主机之间的双向通信。 那么,这句话是什么意思呢? 简而言之,WebSockets 在客户端和服务器之间创建了一个连接,允许在客户端和服务器之间来回发送消息,而无需重新打开连接或使用长轮询来获取来自服务器的更新。
因此,一旦建立 WebSocket 连接,它就会保持打开状态并允许传输数据。 这是 WebSocket 相对于传统 HTTP 请求的一大好处,传统的 HTTP 请求需要重新打开连接并发送一堆标头和 cookie 数据才能成功完成请求,这种开销减少甚至消除。
Data 通过 WebSocket 作为包含有效负载的消息传输,该有效负载由一个或多个帧组成。 帧由头和主体组成,其中头描述了帧和主体中的应用数据。 基于帧的系统将非有效负载数据减少到最低限度并显着减少延迟,从而允许以非常快速和可靠的方式传输数据。 这可以使 data 在快节奏的环境中平稳运行。
为了建立 WebSocket 连接,一个传统的 HTTP 请求被发送到包含 “升级” 标头的服务器。 如果服务器支持该协议,它会通过在响应中添加 “升级” 标头来执行升级。 因此,socket 现在被打开并允许客户端和服务器之间的 “实时” 通信。 请注意,在此握手之后,初始 HTTP 连接将替换为 WebSocket 连接。 WebSocket URLS 分别使用 ws 和 wss 进行常规连接和安全连接。
消息代理是一种调解应用程序之间通信的架构模式。
消息代理最简单的可视化是中枢神经系统,它将子系统连接到一个大网络上。消息代理负责接收和发送消息,将它们路由到正确的目的地,并在标准消息协议之间转换消息,稍后会详细介绍。
使用消息代理而不是直接连接系统的主要好处是数据流的处理不是由系统本身处理的。消息代理充当其他系统之间的中介,允许发送者在不知道接收者在哪里、有多少个接收者的情况下发送消息,甚至不必担心是否会收到消息。消息代理通过依赖存储和有序消息的消息队列来保证传递消息,直到这些消息被应用程序使用。
已知的消息代理: Apache Kafka 和 RabbitMQ, RocketMQ
WebSocket 是一种消息传递体系结构,不要求任何特定的消息传递协议。它将字节流转换为消息流。应用程序本身需要转换每条消息的含义。出于这个原因,WebSocket RFC 定义了子协议的使用,允许应用程序选择客户端和服务器都能转换的消息格式。这种格式可以是自定义的、某些特定的或标准的消息传递协议。
标准消息传递协议的一个示例是 STOMP。 STOMP 是简单(或流式)文本的消息传递协议。它提供了一种可互操作的有线格式,以使客户端能够与消息代理进行通信,只要它们都识别 STOMP。因此,使用 STOMP 可以在多种语言、多平台和代理之间进行广泛的消息传递。
STOMP 定义了一些映射到 WebSocket 帧的帧类型:
我们将使用 Spring Boot 2 为 WebSocket 握手 STOMP 端点,并在其上发送和接收消息。 Spring 的简单消息代理将用于处理消息的编码和解码以及管理客户端订阅。
org.springframework.boot
spring-boot-starter-websocket
@Slf4j
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config
.setApplicationDestinationPrefixes("/app")
.setUserDestinationPrefix("/user")
.enableStompBrokerRelay("/topic", "/queue")
.setRelayHost("127.0.0.1")
.setRelayPort(61613)
.setSystemLogin("admin")
.setSystemPasscode("a123456")
.setClientLogin("admin")
.setClientPasscode("a123456")
.setSystemHeartbeatSendInterval(10000L)
.setSystemHeartbeatReceiveInterval(10000L)
;
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// Not withSockJs. This will allow you to use ws://localhost:8080/test to establish websocket connection
// withSockJS. This will allow you to use http://localhost:8080/test to establish websocket connection
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS()
.setHeartbeatTime(60_000L)
.setInterceptors(new UserHandshakeInterceptor())
;
}
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setSendTimeLimit(15 * 1000)
.setSendBufferSizeLimit(512 * 1024)
.setMessageSizeLimit(128 * 1024)
;
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new TokenChannelInterceptor());
}
}
上述代码片段简单描述:
@EnableWebSocketMessageBroker 将通过 WebSocket 连接启用代理支持的消息传递。
registerStompEndpoints 允许将 HTTP URL 配置为 WebSocket 握手的端点。因此,端点 /ws 将可供客户端发送 HTTP 请求以将连接升级为 WebSocket 连接。
configureMessageBroker 被配置为允许发生两件事:
例如,如果客户端向 “/app/message” 发送消息,则将调用带有 “/message” 注释的控制器方法。
可以添加一个示例来处理消息。 在下面的代码片段中,描述了一个示例处理路由到前缀为 “/app” 的目的地的消息。
@Slf4j
@Controller
@RequiredArgsConstructor
public class MessagingController {
private final SimpMessagingTemplate simpMessagingTemplate;
/**
* [@SendTo("/topic/mural")]
*/
@MessageMapping("/demo/message")
public void send(@Payload DemoMessage message, Principal user) {
log.info("===>message: {}", message);
GlobalUserPrincipal userPrincipal = (GlobalUserPrincipal) user;
log.info("===>user: {}", userPrincipal);
}
@MessageMapping("/chat/message")
public void send(@Payload ChatMessage message, Principal user) {
log.info("===>message: {}", message);
GlobalUserPrincipal userPrincipal = (GlobalUserPrincipal) user;
log.info("===>user: {}", userPrincipal);
message.setTime(LocalDateTime.now());
simpMessagingTemplate.convertAndSend("/topic/chat." + message.getTo(), message);
}
}
下图展示简单内置代理在应用程序中的消息流。
当通过 WebSocket 连接接收到消息时,它们被解码为 STOMP 帧并转换为 Spring-message-representations。然后将这些 Spring 消息发送到 channel 进行进一步处理。
在 Spring 应用程序示例配置中,发送到目的地 “/topic” 和 “/queue” 的 STOMP 消息直接路由到消息代理,而 “/app” 消息路由到服务被 controllers 处理
调用通过 @MessageMapping 注解方法,将处理 Spring 消息的有效负载,并通过代理通道向 SimpleBroker 发送 Spring 消息来响应 topic 和 queue 上的连接客户端。 SimpleBroker 将根据消息发送到的目的地确保正确的订阅将接收消息。
在客户端通过向 http://localhost:8080/ws 发送带有 “Upgrade” 标头的 HTTP 请求建立 WebSocket 连接
示例:客户端向服务发送消息,服务向所有订阅的客户端广播回复,包括原始发送者。