WebSocket
WebSocket 是基于 TCP 的全双工通信协议,适合需要服务器主动推送的实时场景。
WebSocket vs HTTP
| 特性 | WebSocket | HTTP |
|---|---|---|
| 连接方式 | 持久连接 | 短连接 |
| 通信方向 | 全双工 | 半双工 |
| 服务器推送 | 原生支持 | 需轮询/SSE |
| 资源占用 | 较低 | 较高 |
提示
协议关系 WebSocket 握手基于 HTTP,通过 Upgrade 头从 HTTP 升级而来。
核心概念
| 概念 | 说明 |
|---|---|
| WebSocket | 双向通信协议 |
| STOMP | 基于 WebSocket 的消息协议 |
| Topic | 发布/订阅主题,一对多 |
| Queue | 点对点队列,一对一 |
快速开始
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>原生 WebSocket
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private MyWebSocketHandler myHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler, "/ws").setAllowedOrigins("*");
}
}@Component
public class MyWebSocketHandler extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) {
System.out.println("连接建立: " + session.getId());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
session.sendMessage(new TextMessage("响应: " + message.getPayload()));
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
System.out.println("连接关闭: " + session.getId());
}
}STOMP 消息协议
STOMP 比原生 WebSocket 更易用,支持发布/订阅模式。
配置
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS();
}
}消息控制器
@Controller
public class ChatController {
@MessageMapping("/chat")
@SendTo("/topic/public")
public ChatMessage sendMessage(ChatMessage message) {
return message;
}
@MessageMapping("/private")
public void sendPrivateMessage(ChatMessage message) {
messagingTemplate.convertAndSendToUser(
message.getTo(), "/queue/private", message
);
}
}前端示例
const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
stompClient.subscribe('/topic/public', function(message) {
showMessage(JSON.parse(message.body));
});
});
function sendMessage() {
stompClient.send("/app/chat", {},
JSON.stringify({ content: $('#message-input').val(), type: 'CHAT' })
);
}集群部署
| 方案 | 说明 |
|---|---|
| Sticky Session | 同一会话路由到同一节点 |
| Redis Pub/Sub | 消息广播到所有节点(推荐) |
| Spring Session | 会话存储到 Redis |
集群环境推荐 Redis Pub/Sub:每个节点订阅 Redis 频道,收到消息后广播给本地客户端。
安全配置
身份验证
@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 (accessor != null && accessor.getCommand() == StompCommand.CONNECT) {
String authToken = accessor.getFirstNativeHeader("token");
// 验证逻辑
}
return message;
}
});
}方法级授权
@MessageMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminMessage(String message) {
return "Admin message: " + message;
}常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 连接失败 | 反向代理不支持升级 | Nginx 配置 proxy_set_header Upgrade;检查跨域设置 |
| 消息丢失 | 无持久化机制 | 使用 Redis Pub/Sub 作为消息中间件 |
| 集群消息不同步 | 会话不共享 | 使用 Spring Session + Redis 共享会话 |