概述
衔接前文
在前面的系列文章中,我们系统剖析了 Spring MVC 如何处理 HTTP 请求:从 DispatcherServlet 的全链路调度,到参数解析与类型转换,再到 HTTP 消息转换器、拦截器与过滤器的协作,以及 Spring Web 异常处理全景。这套体系围绕 请求-响应 模型展开,客户端发请求,服务端给响应,连接即用即弃。
然而,现代 Web 应用对实时性的需求让这个模型捉襟见肘。服务器需要主动向客户端推送数据——股价变动、聊天消息、IoT 设备状态。为实现双向通信,WebSocket 协议应运而生。从 HTTP 协议升级而来的 WebSocket 连接,成为一条全双工的底层通道。Spring 并没有止步于裸 WebSocket 的封装,而是通过引入 STOMP 子协议、消息代理抽象 和 注解驱动的消息映射,将 WebSocket 的无状态连接转化为一个可控、可扩展的消息驱动模型。本文将深入 Spring WebSocket 的内部,从协议升级、消息转发到心跳保持,完整揭示其架构设计。
总结性引言
WebSocket 打破了 HTTP 的单向请求限制,让服务器可以随时向客户端推送数据。Spring 对 WebSocket 的支持不仅停留在底层 API 的封装,而是通过 STOMP 消息代理、注解驱动的消息映射和通道拦截器等机制,将实时通信完全纳入 Spring 的消息驱动体系。当一个 HTTP 握手请求到来,Spring 利用 WebSocketHttpRequestHandler 和 HandshakeInterceptor 完成协议升级,建立 WebSocketSession;随后 StompSubProtocolHandler 将文本帧解析为 STOMP 命令,经 clientInboundChannel 流入 @MessageMapping 注解方法;方法执行完毕后的消息通过 brokerChannel 进入消息代理,最终由 clientOutboundChannel 推回客户端。在此过程中,三通道分工明确,SimpMessagingTemplate 让服务器推送像调用本地方法一样简单,而心跳机制则为连接保活。本文将剖析从 HTTP 握手的瞬间开始,WebSocket 连接如何升华为一个双向消息通道,STOMP 帧如何被解析并分发到消息代理,以及 Spring 如何通过三通道设计实现高效、可扩展的实时消息架构。
核心要点
- 协议升级:从 HTTP 到 WebSocket 的握手,以及 Spring 如何适配不同 Servlet 容器。
- STOMP 帧与注解驱动:
@MessageMapping、@SubscribeMapping如何像处理 HTTP 请求一样处理 WebSocket 消息。 - 消息代理三通道:
clientInboundChannel、brokerChannel、clientOutboundChannel的分工与异步协作。 - 广播与单播:
SimpMessagingTemplate的推送机制,以及基于Principal的定向消息。 - 心跳与连接保活:STOMP 心跳如何与 WebSocket 的 Ping/Pong 协同工作。
文章组织架构图
flowchart TD
A[1. WebSocket握手: HTTP升级到WebSocket] --> B[2. 会话与事件: WebSocketHandler生命周期]
B --> C[3. STOMP协议解析与帧处理]
B --> D[4. 注解驱动的消息路由]
C --> D
D --> E[5. 消息代理架构: 三通道设计]
E --> F[6. 服务器推送: SimpMessagingTemplate]
F --> G[7. 心跳机制与连接保活]
G --> H[8. 安全与消息拦截]
H --> I[9. 生产事故排查专题]
I --> J[10. 面试高频专题]
E -.-> F
C -.-> E
subgraph 基础连接层
A
B
C
end
subgraph 消息路由与代理层
D
E
F
end
subgraph 运维与扩展层
G
H
end
subgraph 实战与总结
I
J
end
架构图说明
- 总览说明:全文 10 个模块从 WebSocket 握手起步,依次深入会话管理、STOMP 协议解析、注解路由、消息代理、服务器推送、心跳机制和安全拦截,最后以生产事故排查和面试高频题闭闭环,形成一个从基础连接到生产实践的完整知识链条。
- 逐模块说明:模块 1-3 聚焦连接建立和协议基础,揭示 HTTP 如何升级为 WebSocket,以及裸连接如何适配 STOMP 子协议;模块 4-6 阐述消息如何被注解方法路由,通过三通道代理进行分发和推送;模块 7-8 涉及连接运维与安全扩展;模块 9-10 将原理落地为事故排查与应试能力。
- 关键结论:Spring WebSocket 通过 STOMP 消息代理和通道设计,将无状态连接转化为可管理的消息驱动系统,理解三通道架构是解决消息延迟和丢失问题的关键。
1. WebSocket 握手:从 HTTP 升级到 WebSocket 连接
1.1 HTTP 升级握手协议细节
WebSocket 连接的建立始于一个标准的 HTTP/1.1 请求,客户端通过特殊的请求头要求服务器将协议切换到 WebSocket。关键的请求头包括:
Upgrade: websocket— 表明希望升级的协议。Connection: Upgrade— 要求连接升级。Sec-WebSocket-Key— 一个 Base64 编码的随机 16 字节值,用于证明握手收到。Sec-WebSocket-Version— WebSocket 协议版本,通常为 13。- 可选
Sec-WebSocket-Protocol列出子协议,如v10.stomp, v11.stomp。
服务器收到这样的请求后,如果支持 WebSocket,会返回一个 101 状态码的响应:
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept— 服务器对Sec-WebSocket-Key加一个魔字符串后 SHA-1 哈希并 Base64 编码的结果,证明服务器理解 WebSocket。Sec-WebSocket-Protocol(可选)选定一个子协议。
在 Servlet 容器中,这个升级过程需要 Servlet API 的支持。传统的 HttpServletRequest 无法直接打开 WebSocket,因此 Spring 抽象出了 RequestUpgradeStrategy 来适配不同容器的非标准 API。
1.2 WebSocketHttpRequestHandler 的拦截与升级触发
Spring 中处理 WebSocket 握手请求的入口是 WebSocketHttpRequestHandler,它实现了 HttpRequestHandler 接口,可以直接被 DispatcherServlet 或单独注册的 Servlet 调用。其关键方法为 handleRequest,源码简化如下:
// org.springframework.web.socket.server.support.WebSocketHttpRequestHandler
public void handleRequest(HttpServletRequest servletRequest,
HttpServletResponse servletResponse)
throws ServletException, IOException {
// 如果请求不是 WebSocket 升级请求,可由后备处理器处理(如 SockJS)
if (!WebSocketHandlerRegistry.supportsHandshake(servletRequest)) {
this.handshakeHandler.doHandshake(servletRequest, servletResponse,
this.webSocketHandler, attributes);
} else {
// 委托给 HandshakeHandler 做实际的升级
this.handshakeHandler.doHandshake(servletRequest, servletResponse,
this.webSocketHandler, attributes);
}
}
首先判断请求头是否包含 Upgrade: websocket。若包含,则将请求交给 HandshakeHandler(通常是 DefaultHandshakeHandler)执行握手。
HandshakeInterceptor 的调用时机也在这里:在 doHandshake 执行前后分别调用 beforeHandshake 和 afterHandshake。这允许我们在握手阶段获取 HttpSession,并往 WebSocketSession 的属性中塞入用户信息,比如从 Spring Security 中提取 Principal,传递给 WebSocket 会话。
// HandshakeInterceptor 调用位置伪代码
boolean success = false;
for (HandshakeInterceptor interceptor : interceptors) {
if (!interceptor.beforeHandshake(request, response, wsHandler, attributes)) {
return false;
}
}
success = handshakeHandler.doHandshake(request, response, wsHandler, attributes);
for (HandshakeInterceptor interceptor : interceptors) {
interceptor.afterHandshake(request, response, wsHandler, exception);
}
设计原理映射:这实际上是模板方法模式的变体,握手前后扩展点提供了类似 HTTP 拦截器的钩子,但作用在协议升级阶段。
工程联系与关键结论:握手拦截器是传递用户身份和安全上下文的黄金位置,在握手失败或认证非法时可以提前终止,避免无效的底层连接。
1.3 容器适配 — RequestUpgradeStrategy
不同的 Servlet 容器(Tomcat、Jetty、Undertow)对 WebSocket 的支持 API 不同。Spring 通过 RequestUpgradeStrategy 接口进行抽象:
TomcatRequestUpgradeStrategy:基于 Tomcat 的WsServerContainer和WSServlet。JettyRequestUpgradeStrategy:使用 Jetty 的WebSocketServerFactory。UndertowRequestUpgradeStrategy:利用 Undertow 的WebSocketHttpExchange。
DefaultHandshakeHandler 在构造时可以通过 ServletContext 检测并选择合适的策略。
// org.springframework.web.socket.server.support.DefaultHandshakeHandler
protected RequestUpgradeStrategy initUpgradeStrategy(ServletContext servletContext) {
// 检测 Jetty, Tomcat, Undertow 等
// 若都没检测到,抛出 IllegalStateException
}
1.4 WebSocket 握手与升级序列图
sequenceDiagram
participant C as 客户端
participant WSHandler as WebSocketHttpRequestHandler
participant Interceptor as HandshakeInterceptor
participant Handler as HandshakeHandler
participant Strategy as RequestUpgradeStrategy
participant Container as Servlet容器
C->>WSHandler: HTTP GET (Upgrade: websocket)
WSHandler->>Interceptor: beforeHandshake()
Interceptor-->>WSHandler: true
WSHandler->>Handler: doHandshake()
Handler->>Strategy: upgrade()
Strategy->>Container: 容器API升级
Container-->>Strategy: WebSocketSession (原生)
Strategy-->>Handler: StandardWebSocketSession
Handler-->>WSHandler: WebSocketSession
WSHandler->>Interceptor: afterHandshake()
WSHandler-->>C: 101 Switching Protocols
C->>WSHandler: WebSocket 连接建立
图表主旨概括
该图展示了从客户端发起 HTTP WebSocket 升级请求,到最终返回 101 响应码并建立 WebSocketSession 的完整流程。
逐层/逐元素分解
客户端首先发送带 Upgrade: websocket 头的 HTTP 请求,WebSocketHttpRequestHandler 接收后调用注册的 HandshakeInterceptor 链执行握手前处理。随后 HandshakeHandler 利用容器特定的 RequestUpgradeStrategy 完成底层连接升级,返回一个 Spring 包装的 WebSocketSession。拦截器的 afterHandshake 在升级成功后执行,最后向客户端返回 101 状态码。
设计原理映射
使用了适配器模式(RequestUpgradeStrategy 适配不同容器 API)和责任链模式(HandshakeInterceptor 链)。服务端 WebSocket 连接的建立本质上是 HTTP 事务的一部分,但 Spring 将其包装成一致的编程模型。
工程联系与关键结论
握手阶段的扩展点直接关系到安全上下文的传递和连接属性的初始化,务必利用 HandshakeInterceptor 将用户身份从 HTTP 会话注入 WebSocket 属性,避免后续安全漏洞。
1.5 自定义 HandshakeInterceptor 示例
public class AuthHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes) {
// 从 HTTP Session 或 Token 中提取用户
if (request instanceof ServletServerHttpRequest) {
HttpSession session = ((ServletServerHttpRequest) request)
.getServletRequest().getSession();
Principal user = (Principal) session.getAttribute("user");
if (user != null) {
attributes.put("principal", user);
return true;
}
}
return false; // 拒绝握手
}
@Override
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Exception exception) {
// 可记录握手成功日志
}
}
2. WebSocket 会话与事件处理:WebSocketHandler 的生命周期
2.1 WebSocketHandler 接口
WebSocketHandler 是 Spring 中处理 WebSocket 消息与事件的核心接口,定义了五个主要方法:
afterConnectionEstablished(WebSocketSession session)— 连接建立后触发,此时会话可用来发送消息。handleMessage(WebSocketSession session, WebSocketMessage<?> message)— 收到消息,包括文本、二进制、Pong 消息。handleTransportError(WebSocketSession session, Throwable exception)— 传输过程中出现错误。afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus)— 连接关闭。supportsPartialMessages()— 是否支持部分消息。
这些方法覆盖了 WebSocket 会话的完整生命周期。Spring 提供了几个实现,如 TextWebSocketHandler、BinaryWebSocketHandler,通过 handleTextMessage 和 handleBinaryMessage 简化开发。
2.2 WebSocketSession 与并发安全
WebSocketSession 抽象了一次 WebSocket 连接,提供 getId()、getUri()、getAttributes() 等属性,以及 sendMessage(WebSocketMessage<?> msg) 方法。其实现通常是 StandardWebSocketSession,封装了底层容器的原生会话。
由于 WebSocket 发送消息可能是并发的(例如,多个线程同时向同一连接推送消息),底层容器会话的发送操作可能不是线程安全的。于是 Spring 提供了 ConcurrentWebSocketSessionDecorator 装饰器,内部使用 ConcurrentWebSocketSessionDecorator 包装原会话,对 sendMessage 进行同步控制。
// org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator
public void sendMessage(WebSocketMessage<?> message) throws IOException {
if (this.sendTimeLimit > 0) {
// 可配置发送超时,防止慢客户端阻塞线程
}
synchronized (this.delegate) {
this.delegate.sendMessage(message);
}
}
2.3 事件驱动的处理模型
WebSocketHandler 本身不涉及线程调度,由底层 WebSocketMessageBroker 基础设施或 STOMP 层触发。当非 STOMP 裸 WebSocket 使用时,通常由 WebSocketHandler 被直接包装为一个 WebSocketHttpRequestHandler 并通过调度执行。而在 STOMP 模式下,WebSocketHandler 的实现 SubProtocolWebSocketHandler 负责接收底层消息,然后委托给 SubProtocolHandler(如 StompSubProtocolHandler)处理。
设计模式映射:WebSocketHandler 是典型的观察者模式,底层连接事件触发相应的处理方法。ConcurrentWebSocketSessionDecorator 使用了装饰器模式增加线程安全性。
关键结论:WebSocket 会话本质是异步事件流,自定义 WebSocketHandler 时必须快速返回,避免阻塞 I/O 线程,否则会造成会话堆积和延迟。
2.4 简单 WebSocketHandler 示例
public class LoggingWebSocketHandler extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) {
System.out.println("连接建立: " + session.getId());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message)
throws Exception {
System.out.println("收到: " + message.getPayload());
session.sendMessage(new TextMessage("Echo: " + message.getPayload()));
}
@Override
public void afterConnectionClosed(WebSocketSession session,
CloseStatus status) {
System.out.println("连接关闭: " + session.getId() + " 原因: " + status);
}
}
3. STOMP 协议解析与帧处理
3.1 STOMP 帧格式
STOMP(Simple Text Oriented Messaging Protocol)是一种简单的基于文本的消息协议,定义在 WebSocket 等可靠双向流之上。一个 STOMP 帧格式如下:
COMMAND
header1:value1
header2:value2
Body^@
帧以命令开头,后跟键值头,再一个空行,然后是可选 Body,最后以 NULL 字符 (\0) 结尾。Spring 中定义了 StompCommand 枚举:
CONNECT, CONNECTED, SEND, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, RECEIPT, ERROR, DISCONNECT 等。
3.2 StompSubProtocolHandler 的角色
当客户端在握手时声明子协议 v10.stomp,服务端创建 SubProtocolWebSocketHandler 并委托给 StompSubProtocolHandler。收到文本消息后,StompSubProtocolHandler.handleMessageFromClient 方法开始解析。
// org.springframework.web.socket.messaging.StompSubProtocolHandler
public void handleMessageFromClient(WebSocketSession session,
WebSocketMessage<?> webSocketMessage,
MessageChannel outputChannel) {
// 1. 解析 STOMP 帧
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
// ... 读取帧命令和头 ...
byte[] payload = webSocketMessage.getPayload();
Message<byte[]> message = MessageBuilder.createMessage(payload,
headers.getMessageHeaders());
// 2. 如果是心跳帧直接响应,不转发到通道
if (headers.getCommand() == StompCommand.CONNECT) {
// 处理 CONNECT,生成 CONNECTED 帧,直接通过 session 发送
} else {
// 3. 其他帧发送到 clientInboundChannel
outputChannel.send(message);
}
}
解析出的 Message 的 payload 是 STOMP 帧的 body 字节,headers 包含 STOMP 头如 destination、content-type、subscription-id 等。这条消息将被发送到 clientInboundChannel。
3.3 STOMP 帧解析与消息通道流转序列图
sequenceDiagram
participant WS as WebSocketSession
participant Sub as StompSubProtocolHandler
participant Inbound as clientInboundChannel
participant Exec as ExecutorSubscribableChannel
participant Interceptor as ChannelInterceptor
participant Handler as @MessageMapping
WS->>Sub: 文本消息 (SEND frame)
Sub->>Sub: 解析 STOMP 帧 (StompCommand.SEND)
Sub->>Inbound: send(Message<byte[]>)
Inbound->>Exec: 执行器调度
Exec->>Interceptor: preSend()
Exec->>Handler: 匹配 @MessageMapping 方法
Handler-->>Exec: 返回结果消息
Interceptor->>Interceptor: postSend()
Exec-->>Inbound: 消息投递完成
图表主旨概括
展示了从原始 WebSocket 文本消息到 STOMP 帧解析,再到流入 clientInboundChannel,最后被注解方法处理的完整异步路径。
逐层/逐元素分解
StompSubProtocolHandler 将 WebSocket 消息转化为 Spring Message,然后通过 clientInboundChannel 发送。该通道一般是 ExecutorSubscribableChannel,使用线程池异步将消息分发给订阅者(即 MessageMappingMessageHandler)。在分发过程中,注册的 ChannelInterceptor 可以拦截并修改消息,最终匹配到 @MessageMapping 方法执行业务逻辑。
设计原理映射
利用消息通道模式(Channel)将协议解析与业务处理解耦,ExecutorSubscribableChannel 实现了发布-订阅模式,允许异步、多线程处理。拦截器链再次体现了责任链模式。
工程联系与关键结论
消息进入 clientInboundChannel 后即转为异步,任何阻塞或异常需在业务方法层处理,否则可能导致通道背压或消息丢失。
3.4 CONNECT 与 DISCONNECT 的特殊处理
StompSubProtocolHandler 直接处理 CONNECT 帧,构建 CONNECTED 帧并写回 WebSocket 会话,同时建立用户会话映射。DISCONNECT 帧会触发 SesionDisconnectEvent,可用于清理资源。
4. 注解驱动的消息路由:@MessageMapping 与 @SubscribeMapping
4.1 消息路由的核心 — MessageMappingMessageHandler
Spring 利用 @MessageMapping 注解标注方法,接收指定 destination 的消息。这与 @RequestMapping 的设计高度相似。负责分发的是 MessageMappingMessageHandler,它实现了 MessageHandler,订阅 clientInboundChannel,收到消息后根据 destination 头进行匹配,调用注解方法。
// 简化匹配逻辑
protected HandlerMethod getHandlerInternal(Message<?> message) {
String destination = (String) message.getHeaders().get("destination");
// 遍历 mappingRegistry 中的 @MessageMapping 映射信息
// 使用 AntPathMatcher 匹配 destination,如 "/app/chat"
return handlerMethods.get(destination);
}
匹配到方法后,通过 InvocableHandlerMethod 执行,参数解析器可以利用 @Payload、@Header、@DestinationVariable 等来提取数据。消息转换器(如 MappingJackson2MessageConverter)负责将 payload 字节转换为 Java 对象,类似于 HTTP 消息转换器。
4.2 @SubscribeMapping 的特殊之处
@SubscribeMapping 用于客户端订阅某个目的地时直接返回一条消息,而不经过 Broker。这常用于获取初始数据(如聊天历史)。方法是同步返回,结果消息会通过 clientOutboundChannel 直接发给该客户端,不会广播给其他订阅者。
设计统一性:Spring 将 WebSocket 消息处理与 HTTP 请求处理在编程模型上对齐,开发者可以复用已有的注解驱动开发经验。这种统一性背后是 Spring 的 Message 抽象和 MessageConverter 体系,前文所述的消息转换器在此同样工作。
4.3 聊天室示例:@MessageMapping 与 SimpMessagingTemplate
@Controller
public class ChatController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@MessageMapping("/chat")
public void handleChat(ChatMessage chat, Principal principal) {
chat.setSender(principal.getName());
// 发送到 Broker 特定 topic,由 Broker 广播给所有订阅者
messagingTemplate.convertAndSend("/topic/public", chat);
}
@SubscribeMapping("/chat/history")
public List<ChatMessage> retrieveHistory() {
// 返回初始聊天记录
return chatHistoryRepository.getRecentMessages();
}
}
在这里,客户端发送消息到 /app/chat,被 @MessageMapping("/chat") 拦截,处理后通过 SimpMessagingTemplate 发送到 /topic/public,Broker 负责广播给所有订阅了 /topic/public 的客户端。
5. 消息代理架构:三通道设计与消息流转
5.1 三通道组件
Spring STOMP 消息代理依赖三个关键 MessageChannel,均为 ExecutorSubscribableChannel 的子类:
clientInboundChannel:客户端 -> 应用。接收StompSubProtocolHandler解析后的 STOMP 消息,分发给@MessageMapping等处理器。brokerChannel:应用 -> 代理。@MessageMapping方法通过SimpMessagingTemplate发送的消息进入此通道,再由代理读取并广播。clientOutboundChannel:代理 -> 客户端。代理广播的消息由此通道发送,SubProtocolWebSocketHandler负责取走并写入具体 WebSocket 会话。
这三个通道都基于异步线程池,实现了生产与消费的解耦。
5.2 三通道消息流转流程图
flowchart TD
Client["客户端"] -->|"SEND frame"| Inbound["clientInboundChannel"]
Inbound -->|"消息分发"| Mapping["@MessageMapping 方法"]
Mapping -->|"发送消息"| BrokerIn["brokerChannel"]
BrokerIn --> SimpleBroker["SimpleBrokerMessageHandler"]
SimpleBroker -->|"广播给订阅者"| Outbound["clientOutboundChannel"]
Outbound -->|"写入连接"| Client
classDef channel fill:#e3f2fd,stroke:#1e88e5,stroke-width:2px,color:#0d47a1;
classDef handler fill:#f3e5f5,stroke:#8e24aa,stroke-width:2px,color:#4a148c;
class Inbound,BrokerIn,Outbound channel;
class Mapping,SimpleBroker handler;
图表主旨概括
流程图清晰展示了消息从客户端进入,经过入站通道、应用处理、出站代理通道,最后返回客户端的完整路径。
逐层/逐元素分解
客户端 SEND 帧被 StompSubProtocolHandler 解析后送入 clientInboundChannel,MessageMappingMessageHandler 作为其订阅者消费消息并调用业务方法。业务方法通过 brokerChannel 将消息提交到消息代理。代理(如 SimpleBroker)查找目标目的地上的所有订阅者,通过 clientOutboundChannel 将消息推送到每个客户端的 WebSocket 会话。
设计原理映射
采用管道-过滤器架构。每个通道是管道,订阅者和拦截器是过滤器。这种架构使得处理流程可以灵活扩展,例如添加拦截器实现监控、授权。
工程联系与关键结论
三通道架构是 Spring STOMP 的心脏,性能瓶颈往往集中在通道的线程池大小和队列容量上,必须根据实际负载进行调整。如果不理解三通道分工,难以排查消息积压或丢失。
5.3 简单 Broker(SimpleBrokerMessageHandler)
SimpleBrokerMessageHandler 在内存中维护一个 SubscriptionRegistry,记录每个会话订阅的 destination 模式。当从 brokerChannel 收到消息,查找匹配的订阅者,通过 clientOutboundChannel 发送给它们。
// org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler
protected void handleMessageInternal(Message<?> message) {
String destination = (String) message.getHeaders().get("destination");
// 查找订阅该 destination 的会话
MultiValueMap<String, String> subscriptions =
this.subscriptionRegistry.findSubscriptions(message);
for (String sessionId : subscriptions.values()) {
// 通过 clientOutboundChannel 发送
this.clientOutboundChannel.send(
MessageBuilder.createMessage(payload, headers/*sessionId*/));
}
}
订阅注册是在 SUBSCRIBE 帧处理时完成,UNSUBSCRIBE 或连接断开时移除。
SimpleBroker 消息广播与订阅关系管理图
classDiagram
class SubscriptionRegistry {
+registerSubscription(subscribeEvent)
+unregisterSubscription(unsubscribeEvent)
+findSubscriptions(message) MultiValueMap
}
class SimpleBrokerMessageHandler {
-subscriptionRegistry: SubscriptionRegistry
-clientOutboundChannel: MessageChannel
+handleMessageInternal(Message)
}
class DefaultSubscriptionRegistry {
-destinations: Map<String, List<SessionId>>
}
SubscriptionRegistry <|.. DefaultSubscriptionRegistry
SimpleBrokerMessageHandler --> SubscriptionRegistry
SimpleBrokerMessageHandler --> clientOutboundChannel : uses
图表主旨概括
展示了 SimpleBroker 内部的订阅注册表与消息处理器如何协作完成广播。
逐层/逐元素分解
SubscriptionRegistry 接口定义了订阅的注册、注销和查找。SimpleBrokerMessageHandler 在处理 SUBSCRIBE 帧时调用 registerSubscription 将会话标识加入目标模式的列表;收到来自 brokerChannel 的应用消息时,调用 findSubscriptions 获取所有匹配的会话 ID,然后逐一通过 clientOutboundChannel 推送。
设计原理映射
本质是一个观察者模式的实现:主题(destination)变化时通知所有观察者(订阅的会话)。SubscriptionRegistry 起到订阅存储的作用。
工程联系与关键结论
简单代理仅适用于单实例应用且订阅量不大的场景。集群部署时必须替换为外部 Message Broker,否则消息无法跨节点到达。
5.4 外部 Broker 中继(StompBrokerRelayMessageHandler)
当使用外部 STOMP 代理(如 RabbitMQ、ActiveMQ)时,StompBrokerRelayMessageHandler 通过 TCP 与外部代理建立 STOMP 连接,将客户端的 SUBSCRIBE/UNSUBSCRIBE 帧中继到代理,SEND 帧也转发给代理,代理负责真正的路由和广播。此时 Spring 自身不再直接做广播,只是作为一个中继,这解决了多实例集群下消息共享的问题。
6. 服务器推送:SimpMessagingTemplate 与消息模板
6.1 SimpMessagingTemplate 核心方法
SimpMessagingTemplate 是 Spring 为 STOMP 提供的高级服务器推送 API,封装了对 MessageChannel 的操作。关键方法:
convertAndSend(String destination, Object payload)— 发送到指定 destination,由MessageConverter负责将对象转换为消息。convertAndSendToUser(String user, String destination, Object payload)— 向特定用户发送,目的地会被转换为/user/username/destination。
6.2 内部实现 通道写入
// org.springframework.messaging.simp.SimpMessagingTemplate
public void convertAndSend(String destination, Object payload) {
Message<?> message = doConvert(payload, headers, null);
send(destination, message);
}
protected void send(String destination, Message<?> message) {
MessageHeaders headers = message.getHeaders();
// 添加 destination 头
this.messageChannel.send(
MessageBuilder.createMessage(message.getPayload(),
new MessageHeaders(headers, destination)));
}
这里的 messageChannel 通常是注入的 brokerChannel。方法最终调用 MessageChannel.send(),将其投入到代理通道的异步线程池。发送是异步的,不会阻塞调用线程。
6.3 SimpMessagingTemplate 发送消息的序列图
sequenceDiagram
participant Service as 业务服务
participant Template as SimpMessagingTemplate
participant Converter as MessageConverter
participant Broker as brokerChannel
participant Outbound as clientOutboundChannel
participant Session as WebSocketSession
Service->>Template: convertAndSend("/topic/price", obj)
Template->>Converter: toMessage(obj)
Converter-->>Template: Message<byte[]>
Template->>Broker: send(message)
Broker->>Outbound: 分发消息给订阅者
Outbound->>Session: sendMessage(WebSocketMessage)
图表主旨概括
揭示服务器推送从业务层出发,经过转换、代理通道,最终到达客户端 WebSocket 会话的全流程。
逐层/逐元素分解
业务代码调用 convertAndSend,模板利用注册的 MessageConverter(通常是 JSON 转换器)将对象转化为消息,然后追加 destination 头,发送到 brokerChannel。消息代理接收后,根据订阅表找到所有匹配的 clientOutboundChannel 订阅者,最终通过 SubProtocolWebSocketHandler 写入原生连接。
设计原理映射
采用了门面模式(SimpMessagingTemplate 简化通道操作)和消息转换策略。异步通道保证了发送者不被慢消费者阻塞。
工程联系与关键结论
服务器推送的性能取决于 brokerChannel 和 clientOutboundChannel 的容量和消费者处理速度,大量单播推送时需注意线程池耗尽风险。
6.4 定向用户推送示例
// 向用户 user1 推送私有消息
messagingTemplate.convertAndSendToUser("user1", "/queue/notify", notification);
背后由 DefaultUserDestinationResolver 将 /user/user1/queue/notify 解析为具体的会话消息,本质上是对“单播”场景的语义封装。
7. 心跳机制与连接保活
7.1 STOMP 心跳协商
客户端在 CONNECT 帧中携带 heart-beat:<cx>,<cy>,分别表示客户端发出的心跳和希望服务器发出的心跳(毫秒)。服务器在 CONNECTED 帧中回应实际采用的心跳参数。StompSubProtocolHandler 负责解析和存储这些值。
7.2 心跳处理与超时断开
AbstractSubProtocolHandler 提供了心跳基础支持。服务器端通过定时任务发送心跳帧(一个空行 \n)给客户端,同时监测最后一次接收客户端消息的时间。如果超过 readTimeout(例如 3 * cx),则认为连接死亡,关闭会话。
// 心跳管理关键逻辑(伪代码)
scheduledFuture = taskScheduler.scheduleWithFixedDelay(() -> {
for (WebSocketSession session : sessions) {
long lastRead = session.getLastReadTime();
if (System.currentTimeMillis() - lastRead > readTimeout) {
session.close(CloseStatus.SESSION_NOT_RELIABLE);
} else {
session.sendMessage(HEARTBEAT_MESSAGE); // 发送 heartbeat
}
}
}, 0, heartbeatInterval, TimeUnit.MILLISECONDS);
这些心跳帧都是纯 STOMP 层面的,与底层 WebSocket 的 Ping/Pong 帧可以独立,也可配合。Spring 也可以通过配置 Tomcat 的 PeriodicHeartbeat 使用 WebSocket Ping/Pong 维持代理不切断连接。
7.3 心跳配置验证示例
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 配置 STOMP 心跳,服务端每 10000ms 发一次,期望客户端每 10000ms 发一次
config.setHeartbeatValue(new long[] {10000, 10000});
// 同时配置 WebSocket 层心跳(Tomcat)在 properties 中设置
}
关键结论:心跳参数必须小于代理或负载均衡器的空闲超时,否则会出现诡异的连接假死现象。在生产中建议服务端心跳 10-25 秒,客户端可相应适配。
8. 安全与消息拦截
8.1 ChannelInterceptor
Spring 消息通道提供了 ChannelInterceptor 接口,可以在消息发送到通道前后进行拦截,类似于 HTTP 的 HandlerInterceptor。典型接口:
preSend(Message<?> message, MessageChannel channel)— 发送前,可以修改消息或返回null拒绝。postSend(Message<?> message, MessageChannel channel, boolean sent)— 发送后。afterSendCompletion/preReceive/postReceive。
我们可以为 clientInboundChannel、brokerChannel、clientOutboundChannel 注册不同拦截器。
8.2 与 Spring Security 集成原理
Spring Security 提供了 WebSocketMessageBrokerSecurityInterceptor,它实现 ChannelInterceptor,在 preSend 中通过 AuthorizationManager 基于消息的 destination 和当前 Principal 进行授权。因为 Principal 在握手时已通过拦截器设置到会话属性,当消息到来时可以从 STOMP 头中取出用户信息。这与 HTTP 的 FilterSecurityInterceptor 设计一致,体现 Spring 安全抽象的跨协议复用(详情见安全系列篇章)。
8.3 记录消息耗时的拦截器示例
public class TimingChannelInterceptor implements ChannelInterceptor {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
message.getHeaders().put("startTime", System.currentTimeMillis());
return message;
}
@Override
public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
Long start = (Long) message.getHeaders().get("startTime");
long duration = System.currentTimeMillis() - start;
System.out.println("通道发送耗时: " + duration + " ms");
}
}
9. 生产事故排查专题
9.1 事故一:消息推送延迟严重与积压
现象:聊天应用中,服务器使用 SimpMessagingTemplate 发送消息,高峰时期客户端收到消息延迟达数秒甚至一分钟以上,服务器内存持续升高。
排查思路:
- 查看
clientOutboundChannel线程池(ThreadPoolTaskExecutor)队列是否满,线程数是否打满。 - 检查是否有慢客户端——网络状况差的 WebSocket 连接,发送缓冲区满导致阻塞。
- 查看 STOMP 代理的广播逻辑是否存在过慢的遍历。
根因(结合原理):
clientOutboundChannel 默认是 ExecutorSubscribableChannel,其 TaskExecutor 核心线程数可能很小(如 1)。当线上大量订阅客户端时,广播每条消息需要为每个客户端执行一次发送任务,任务堆积在队列中。更致命的是,默认 ConcurrentWebSocketSessionDecorator 的发送操作没有超时限制或异步写入优化,某个客户端的阻塞会直接卡住任务线程,导致整体推送能力下降形成雪崩。
解决:
- 增加
clientOutboundChannel核心线程数和最大线程数,并设置合理队列容量(有界队列,拒绝策略为CallerRunsPolicy或流控)。 - 为
ConcurrentWebSocketSessionDecorator设置发送超时sendTimeLimit,超过后丢弃消息或关闭连接。 - 启用
WebSocket异步写,检查容器配置是否可以非阻塞写。
最佳实践:为出站通道配置弹性线程池,并设置发送超时和限流。对慢客户端实施强制关闭,保护系统整体稳定性。
事故排查序列图:消息堆积
sequenceDiagram
participant T1 as 推送线程
participant OutChannel as clientOutboundChannel (线程池)
participant Session as 慢客户端 WebSocketSession
participant Queue as 任务队列
participant Mem as JVM内存
T1->>OutChannel: 提交广播任务 (×1000)
OutChannel->>Queue: 任务入队
OutChannel->>Session: 发送消息 (阻塞)
Session-->>OutChannel: 迟迟未确认 (TCP窗口满)
Queue->>Mem: 任务堆积,内存飙升
Note over OutChannel: 线程耗尽,新任务全部进入队列,延迟巨大
9.2 事故二:WebSocket 连接频繁被动断开
现象:生产环境客户端日志显示每隔约 60 秒连接被关闭,服务端也打印传输错误。
排查思路:
- 检查客户端与服务端之间的网络路径,是否有 Nginx、AWS ELB 等反向代理。
- 查看代理的空闲超时设置。
- 检查 STOMP 心跳配置是否启用。
根因: 应用接口部署在 AWS ELB 后面,ELB 的空闲连接超时为 60 秒。而 STOMP 并未配置心跳,或者心跳间隔大于 60 秒。代理检测到连接在 60 秒内无任何数据读写,就会单方面发送 RST 或 FIN 切断连接。服务端和客户端在无感知的情况下误以为连接依然存活,导致后续通信失败并触发错误处理。
解决:
- 在 STOMP 中配置服务端心跳,例如
heartbeat(10000, 10000),确保每 10 秒有数据交换,远低于代理超时。 - 同时在 WebSocket 容器层面启用 Ping/Pong(Tomcat 的
periodicHeartbeat),保持底层连接活跃。
最佳实践:任何生产 WebSocket 部署都必须显式配置心跳,且心跳间隔应不大于网络设施空闲超时的一半,并压测验证。
10. 面试高频专题
1. Spring 如何支持 WebSocket?简述从握手到消息推送的流程。
- 标准回答:Spring 通过
WebSocketHttpRequestHandler拦截升级请求,利用HandshakeHandler与RequestUpgradeStrategy完成容器适配握手,建立WebSocketSession。对于 STOMP 场景,SubProtocolWebSocketHandler委托StompSubProtocolHandler解析帧,送入clientInboundChannel,@MessageMapping方法处理,结果通过brokerChannel交代理广播,最终由clientOutboundChannel写出。 - 追问1:握手阶段如何传递用户身份? 答:使用
HandshakeInterceptor从 HTTP Session 获取Principal存入 WebSocket 属性。 - 追问2:
clientInboundChannel的线程模型是怎样? 答:默认ExecutorSubscribableChannel,使用线程池异步调用订阅者。 - 追问3:如果不用 STOMP,怎么处理? 答:直接实现
WebSocketHandler,通过WebSocketHttpRequestHandler注册即可。 - 加分回答:阐述
RequestUpgradeStrategy的 SPI 机制和在多 Servlet 容器下的自动检测实现。
2. 什么是 STOMP 协议?Spring 为什么要引入 STOMP 支持?
- 标准回答:STOMP 是面向文本的消息协议,提供帧格式和命令如 SEND、SUBSCRIBE,能在 WebSocket 之上实现发布-订阅模型。Spring 引入它为了将 WebSocket 的无状态消息转换为可路由、可拦截、可安全的
Message体系,无缝集成到 Spring Messaging 抽象中。 - 追问:STOMP 帧的结尾标识是什么? 答:NULL 字符
\0。 - 追问:STOMP 比裸 WebSocket 有什么优势? 答:内置目的地路由、订阅管理、消息回执和事务。
- 加分回答:讨论
StompSubProtocolHandler作为协议适配器是 Spring 将不同协议接入统一消息通道的范例。
3. Spring 消息代理的“三通道”是什么?各自的作用?
- 回答:
clientInboundChannel— 从客户端接收消息,分发给应用。brokerChannel— 从应用发送消息到代理。clientOutboundChannel— 从代理发送消息到客户端。三个通道解耦处理阶段,均为异步可扩展。 - 追问:为何需要 brokerChannel? 答:分离应用和代理,允许引入拦截器、安全控制和事务。
- 追问:这些通道默认线程池配置是多少? 答:核心线程1,无限队列,需显式调优。
- 加分回答:点明通道是
ExecutorSubscribableChannel,实际利用了线程池的发布订阅能力。
4. SimpleBroker 和外部 Message Broker 有什么区别?何时应该用外部 Broker?
- 回答:SimpleBroker 内存维护订阅表,单机广播,不支持集群、消息持久化。外部 Broker 如 RabbitMQ 提供可靠路由、集群和多协议。集群或高可靠要求时必须使用外部 Broker。
- 追问:SimpleBroker 如何匹配通配符 destination? 答:
DefaultSubscriptionRegistry使用 Ant 风格匹配。 - 追问:外部代理的配置步骤? 答:配置
StompBrokerRelayMessageHandler,设置 STOMP 代理地址和凭证。 - 加分回答:分析中继模式下 Spring 如何管理到外部代理的 TCP 连接池。
5. @MessageMapping 注解和 @RequestMapping 有什么异同?
- 回答:相同点:均基于消息 destination/URL 匹配,支持路径变量、消息转换器和参数解析。不同点:前者处理 STOMP SEND 消息,返回类型可发往 brokerChannel,后者处理 HTTP 请求,返回写入 HTTP 响应。
- 追问:
@MessageMapping方法能直接返回给调用客户端吗? 答:@SubscribeMapping可以,@MessageMapping通常通过模板返回。 - 追问:两者对应参数解析器有何关联? 答:均继承
HandlerMethodArgumentResolver,但@Payload等针对消息。 - 加分回答:从源码层面对比
RequestMappingHandlerMapping与MessageMappingMessageHandler的继承结构。
6. 如何实现向指定用户推送消息?
- 回答:使用
SimpMessagingTemplate.convertAndSendToUser("user", "/queue/notify", msg),Spring 会转换为/user/user/queue/notify,并由DefaultUserDestinationResolver映射到该用户 WebSocket 会话。 - 追问:用户标识如何获取? 答:握手时通过
HandshakeInterceptor设置Principal。 - 追问:同一个用户多个连接怎么办? 答:默认会发送到所有该用户的会话。
- 加分回答:可自定义
UserDestinationResolver实现更复杂的定位逻辑。
7. WebSocket 和 HTTP 的长轮询(Long Polling)相比有什么优势?Spring 是如何同时支持这两种方式的?
- 回答:WebSocket 全双工,开销低,服务器主动推送无需客户端重复连接。Spring 通过 SockJS 降级方案支持长轮询、流等,统一编程模型。
- 追问:SockJS 协议如何识别? 答:通过
/info获取传输能力后协商。 - 追问:长轮询在 Spring 中如何模拟 WebSocket? 答:将多个 HTTP 请求映射为对应 WebSocket 事件。
- 加分回答:介绍 SockJS 服务端架构中
SockJsHttpRequestHandler的作用。
8. 如何保证 WebSocket 集群下的消息可达性?
- 回答:使用外部 Message Broker 如 RabbitMQ,所有实例通过
StompBrokerRelay共享代理,消息由代理分发到各实例,再推送给各自连接的客户端。 - 追问:外部中继的故障转移如何配置? 答:通常依赖 Broker 集群(RabbitMQ 集群)和客户端的
failover协议。 - 追问:如果必须用 SimpleBroker 如何广播到其他节点? 答:需要自研广播机制,一般不建议。
- 加分回答:讨论多实例下会话粘性和代理节点的路由 key 设计。
9. STOMP 心跳机制是如何工作的?如果不配置心跳会有什么问题?
- 回答:客户端和服务器协商 heartbeat 周期,定时发送空帧维持连接。不配置会导致反向代理空闲切断连接,且无法检测死连接。
- 追问:心跳帧会发送给消息代理吗? 答:不会,STOMP 心跳仅端到端,不进入 message channel。
- 追问:如何设置合理的心跳间隔? 答:小于所有中间件空闲超时,通常是代理超时的 1/2 到 1/3。
- 加分回答:分析 Spring 源码中心跳调度任务的实现细节,如何避免大量连接的心跳风暴。
10. 如何拦截和处理 STOMP 消息?有哪些扩展点?
- 回答:通过
ChannelInterceptor拦截三通道消息,或者实现HandshakeInterceptor拦截握手,也可注册@MessageExceptionHandler处理异常。 - 追问:拦截器可以修改消息目的地吗? 答:可以,在
preSend中修改 headers。 - 追问:想统计消息流量如何做? 答:记录
postSend的sent参数结合 payload 大小。 - 加分回答:结合 Spring Security 的
AuthorizationManager理解消息级授权拦截器。
11. WebSocket 连接建立失败,可能是哪些原因?
- 回答:握手拦截器拒绝(认证失败),跨域配置缺失,代理不支持 WebSocket,路径配置错误,防火墙拦截,STOMP 子协议不匹配等。
- 追问:如何排查? 答:查看浏览器 F12 握手请求的响应码和头,检查服务端日志。
- 追问:
SockJS降级失败怎么办? 答:保证/info等端点可达,确认传输器注册。 - 加分回答:分析 Spring 内部异常
UpgradeStrategyNotFoundException的触发条件。
12. (系统设计题)设计一个能够支持千万级在线用户的实时消息推送系统。请结合 Spring WebSocket、STOMP 外部 Broker、集群方案给出核心架构设计,并说明如何解决会话管理和消息可靠性问题。
- 回答要点:
- 架构:前端负载均衡 → 多实例 Spring Boot 服务,均通过
StompBrokerRelay接入 RabbitMQ/Kafka 集群。客户端 WebSocket 连接粘性到特定实例。 - 会话管理:使用外部 Broker 维护用户订阅映射,实例仅负责维护本地连接列表。
HandshakeInterceptor建立 JWT 验证,生成Principal。 - 消息可靠性:外部 Broker 持久化消息,开启确认机制。客户端失败可重订阅获取离线消息。
- 高性能:
clientInboundChannel和clientOutboundChannel配置合适线程池,启用异步写和零拷贝。 - 可扩展:水平扩展服务节点,Broker 集群分片承载千万订阅。
- 架构:前端负载均衡 → 多实例 Spring Boot 服务,均通过
- 追问:如何做到用户精准单播? 答:通过
convertAndSendToUser配合 Broker 的路由 key。 - 追问:连接突然断开,用户重新连接后如何接收未读消息? 答:外部代理持久队列,
@SubscribeMapping拉取未读。 - 追问:如何监控? 答案:集成 Micrometer,暴露通道线程池指标和消息速率。
- 加分回答:讨论网络分区时的降级策略和客户端自动重连机制。
附录:Spring WebSocket/STOMP 关键接口速查表
| 组件/接口 | 作用 | 位置 |
|---|---|---|
WebSocketHttpRequestHandler | 处理 WebSocket 握手请求 | spring-websocket |
HandshakeInterceptor | 握手前后拦截 | spring-websocket |
HandshakeHandler / DefaultHandshakeHandler | 执行协议升级 | spring-websocket |
RequestUpgradeStrategy | 适配容器 API | spring-websocket |
WebSocketHandler / TextWebSocketHandler | 处理 WebSocket 消息事件 | spring-websocket |
WebSocketSession | 代表一个 WebSocket 连接 | spring-websocket |
ConcurrentWebSocketSessionDecorator | 并发安全包装 Session | spring-websocket |
StompSubProtocolHandler | STOMP 帧解析与协商 | spring-messaging |
StompCommand / StompHeaderAccessor | STOMP 命令与头封装 | spring-messaging |
@MessageMapping / @SubscribeMapping | 消息路由注解 | spring-messaging |
MessageMappingMessageHandler | 注解方法调度 | spring-messaging |
SimpMessagingTemplate | 服务器推送模板 | spring-messaging |
MessageChannel (三通道) | 异步消息传递通道 | spring-messaging |
ExecutorSubscribableChannel | 基于线程池的通道实现 | spring-messaging |
SimpleBrokerMessageHandler | 简单内存代理 | spring-messaging |
SubscriptionRegistry | 订阅存储 | spring-messaging |
StompBrokerRelayMessageHandler | 外部代理中继 | spring-messaging |
ChannelInterceptor | 通道消息拦截 | spring-messaging |
UserDestinationResolver | 用户目标解析 | spring-messaging |