WebSocket协议作为HTML5规范的一部分,彻底改变了传统Web应用的通信模式。与HTTP这种"一问一答"的请求-响应模式不同,WebSocket在建立连接后始终保持双向通信通道,服务器可以主动向客户端推送数据,特别适合需要实时更新的场景。
我最早接触WebSocket是在开发一个在线协作编辑系统时。当时用HTTP轮询方案导致服务器压力巨大,平均每个客户端每3秒就要发起一次请求,500人在线时服务器几乎瘫痪。切换到WebSocket后,CPU负载直接下降了80%,这让我深刻认识到实时通信协议的价值。
先看几个典型场景的数据对比:
| 方案 | 延迟 | 吞吐量 | 连接开销 | 适用场景 |
|---|---|---|---|---|
| HTTP短轮询 | 高(1-5s) | 低 | 高 | 更新频率低的简单场景 |
| HTTP长轮询 | 中(0.5-2s) | 中 | 中 | 早期实时性要求不高的系统 |
| Server-Sent Events | 低 | 中高 | 低 | 服务器单向推送 |
| WebSocket | 极低(<100ms) | 高 | 极低 | 双向实时交互系统 |
WebSocket建立连接需要经过标准的HTTP升级握手:
http复制GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
http复制HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
这个握手过程我遇到过两个典型问题:一是Nginx默认配置可能拦截Upgrade头,需要在配置中添加:
nginx复制proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
二是某些防火墙会阻断非标准HTTP端口的长连接,这时可以考虑在80/443端口上使用WebSocket,或者采用WebSocket over TLS加密传输。
创建Spring Boot项目时,除了基础的web starter,还需要添加websocket依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置类需要实现WebSocketConfigurer接口。这里有个容易忽略的点:如果前端通过SockJS连接,需要显式配置允许跨域:
java复制@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/ws")
.setAllowedOrigins("*") // 生产环境应指定具体域名
.withSockJS(); // 启用SockJS回退选项
}
@Bean
public WebSocketHandler myHandler() {
return new MyWebSocketHandler();
}
}
自定义Handler需要继承TextWebSocketHandler类,下面是带连接管理的实现示例:
java复制public class MyWebSocketHandler extends TextWebSocketHandler {
private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String userId = extractUserId(session);
sessions.put(userId, session);
log.info("用户 {} 连接成功,当前在线 {}", userId, sessions.size());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String payload = message.getPayload();
// 消息处理逻辑
broadcast("用户说:" + payload);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String userId = extractUserId(session);
sessions.remove(userId);
log.info("用户 {} 断开连接,原因:{}", userId, status);
}
private void broadcast(String message) {
sessions.values().forEach(session -> {
try {
if (session.isOpen()) {
session.sendMessage(new TextMessage(message));
}
} catch (IOException e) {
log.error("消息发送失败", e);
}
});
}
}
重要提示:实际项目中一定要实现心跳机制,可以通过前端定时发送ping消息,后端检测超时未响应的连接并主动关闭。
对于复杂消息场景,建议使用STOMP子协议。首先添加消息代理配置:
java复制@Configuration
@EnableWebSocketMessageBroker
public class StompConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic"); // 客户端订阅前缀
config.setApplicationDestinationPrefixes("/app"); // 服务端接收前缀
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp")
.setAllowedOrigins("*")
.withSockJS();
}
}
然后创建消息控制器:
java复制@Controller
public class ChatController {
@MessageMapping("/chat.send")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage message) {
return message;
}
@MessageExceptionHandler
@SendToUser("/queue/errors")
public String handleException(Throwable exception) {
return exception.getMessage();
}
}
单机WebSocket在横向扩展时会遇到连接状态同步问题。我们曾用Redis发布订阅实现集群通知:
java复制@Bean
public RedisMessageListenerContainer redisContainer(RedisConnectionFactory factory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
container.addMessageListener((message, pattern) -> {
String msg = new String(message.getBody());
// 处理集群消息广播
}, new ChannelTopic("websocket.cluster"));
return container;
}
更成熟的方案是采用专业的消息中间件如RabbitMQ,配合STOMP协议实现跨节点消息路由。
通过压力测试发现几个关键瓶颈点:
优化后的线程池配置示例:
java复制@Bean(destroyMethod = "shutdown")
public Executor asyncMessageExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("ws-exec-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
java复制public class AuthHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) {
String token = extractToken(request);
if (!tokenService.validate(token)) {
throw new RuntimeException("认证失败");
}
return super.beforeHandshake(request, response, wsHandler, attributes);
}
}
yaml复制spring:
websocket:
max-text-message-size: 8192
max-binary-message-size: 8192
java复制private final RateLimiter limiter = RateLimiter.create(10); // 每秒10条
@MessageMapping("/chat")
public void handleMessage(SimpMessageHeaderAccessor accessor, @Payload String message) {
if (!limiter.tryAcquire()) {
throw new RateLimitExceededException();
}
// 处理消息
}
核心功能实现要点:
java复制public String assignAgent(String userId) {
return agents.entrySet().stream()
.filter(e -> e.getValue().isAvailable())
.min(Comparator.comparingInt(e -> e.getValue().getCurrentLoad()))
.map(Map.Entry::getKey)
.orElseThrow(() -> new NoAgentAvailableException());
}
java复制public class ChatSession {
private String sessionId;
private String customerId;
private String agentId;
private Queue<Message> history = new ConcurrentLinkedQueue<>();
private Instant lastActive;
public void addMessage(Message message) {
history.add(message);
lastActive = Instant.now();
if (history.size() > 100) {
history.poll(); // 防止内存泄漏
}
}
}
关键技术点:
java复制public class WhiteboardOperation {
private OperationType type;
private Point start;
private Point end;
private String color;
public WhiteboardOperation transform(WhiteboardOperation other) {
// 实现操作冲突解决逻辑
}
}
java复制public void saveSnapshot(String boardId) {
List<Operation> operations = boardState.getOperations();
String snapshot = compress(operations);
redisTemplate.opsForValue().set("snapshot:" + boardId, snapshot);
redisTemplate.opsForList().trim("history:" + boardId, 0, 99);
}
优化方案对比:
| 方案 | 延迟 | 带宽消耗 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 全量推送 | 低 | 极高 | 低 | 少量数据 |
| 增量差分 | 低 | 中 | 中 | 结构化数据 |
| 压缩二进制协议 | 中 | 低 | 高 | 高频更新 |
| 客户端拉取+服务端推送 | 可变 | 最优 | 最高 | 混合场景 |
二进制消息编码示例:
java复制public byte[] encodeStockData(Stock stock) {
ByteBuffer buffer = ByteBuffer.allocate(32);
buffer.putInt(stock.getCode());
buffer.putDouble(stock.getPrice());
buffer.putDouble(stock.getChange());
buffer.putLong(stock.getTimestamp());
return buffer.array();
}
必须监控的核心指标:
prometheus复制# HELP websocket_connections Current WebSocket connections
# TYPE websocket_connections gauge
websocket_connections{instance="app1"} 253
prometheus复制# HELP websocket_messages_total Total messages processed
# TYPE websocket_messages_total counter
websocket_messages_total{direction="inbound"} 124532
websocket_messages_total{direction="outbound"} 287654
yaml复制groups:
- name: websocket.rules
rules:
- alert: HighErrorRate
expr: rate(websocket_errors_total[5m]) > 0.1
for: 10m
labels:
severity: warning
常见问题排查步骤:
确保WebSocket连接平滑迁移的方案:
java复制@HandshakeInterceptor
public class VersionInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(/*...*/) {
attributes.put("version", request.getHeaders().get("X-Client-Version"));
return true;
}
}
java复制public void sendMessage(WebSocketSession session, String message) {
try {
session.sendMessage(new TextMessage(message));
// 旧版本兼容
if (isLegacyVersion(session)) {
sendCompatibleMessage(session, message);
}
} catch (IOException e) {
handleSendError(session, e);
}
}
在真实项目中,WebSocket的实现往往会遇到各种边界情况。比如我们曾遇到一个棘手的问题:某些安卓设备在网络切换时不会触发标准的连接关闭事件,导致服务端残留僵尸连接。最终解决方案是结合TCP keepalive和应用层心跳包双重检测,将超时时间设置为移动网络平均切换时间的2倍(约60秒)。这类经验往往需要通过实际踩坑才能积累,这也是实时通信系统开发的挑战与乐趣所在。