Browse Source

优化WebSocket超时机制和心跳处理器 - 修复WebSocket超时判断逻辑,解决阻塞接收问题 - 重命名MessageSendTimeout为MessageReceiveTimeout - 添加带超时的消息接收机制 - 优化HeartbeatHandlerManager日志输出 - 调整超时时间为5分钟

feature/x1-web-request
hyh 1 week ago
parent
commit
3fdb992be0
  1. 108
      modify.md
  2. 2
      src/X1.WebAPI/Program.cs
  3. 1
      src/X1.WebSocket/Extensions/ServiceCollectionExtensions.cs
  4. 48
      src/X1.WebSocket/Handlers/HeartbeatHandlerManager.cs
  5. 2
      src/X1.WebSocket/Handlers/WebSocketMessageHandlerAdapter.cs
  6. 2
      src/X1.WebSocket/Middleware/WebSocketMiddleware.cs
  7. 4
      src/X1.WebSocket/Models/WebSocketOptions.cs

108
modify.md

@ -315,3 +315,111 @@
- 提供了丰富的统计信息接口
- 添加了详细的日志记录
- 支持连接状态和Channel状态的实时查询
## 2024-12-19 WebSocket超时判断逻辑分析
### 问题分析
检查 `WebSocketMiddleware.cs` 中第320-321行的超时判断逻辑:
```csharp
// 检查消息是否超时
if (IsMessageTimeout(messageStartTime))
```
### 发现的问题
1. **超时判断逻辑不合理**
- `messageStartTime``ProcessWebSocketMessages` 方法开始时设置为连接开始时间
- 在每次处理有效消息后,`messageStartTime` 被更新为当前时间(第350行)
- 但是超时判断使用的是 `MessageSendTimeout`(30秒),这个配置项名称与实际用途不符
2. **配置项命名错误**
- `MessageSendTimeout` 实际上用于控制消息接收超时,而不是发送超时
- 应该重命名为 `MessageReceiveTimeout``MessageProcessingTimeout`
3. **超时逻辑缺陷**
- 当前逻辑会在每次消息处理后重置 `messageStartTime`
- 这意味着如果客户端在30秒内发送任何消息,超时计时器就会重置
- 这可能导致连接永远不会因为超时而关闭,即使客户端长时间不活跃
4. **阻塞接收问题**
- `webSocket.ReceiveAsync()` 是阻塞等待,如果客户端长时间不发送消息,代码会一直阻塞
- 超时检查永远不会执行,导致连接永远不会被关闭
- 存在严重的资源泄漏风险
### 已完成的修复
1. **重命名配置项**
- 将 `WebSocketOptions.MessageSendTimeout` 重命名为 `MessageReceiveTimeout`
- 更新了配置项的注释说明
2. **修改超时判断逻辑**
- 分离了连接超时和消息接收超时的检查
- 添加了 `IsConnectionTimeout()` 方法检查整个连接的超时(5分钟)
- 添加了 `IsMessageReceiveTimeout()` 方法检查消息接收超时(30秒)
- 使用 `lastMessageTime` 来跟踪最后一次接收消息的时间
3. **更新相关方法**
- 修改了 `ProcessWebSocketMessages` 方法签名,添加了 `lastMessageTime` 参数
- 修改了 `ProcessMessage` 方法签名,移除了不必要的 `messageStartTime` 参数
- 添加了 `HandleConnectionTimeout()``HandleMessageReceiveTimeout()` 方法
- 更新了 `WebSocketMessageHandlerAdapter` 中的超时配置引用
4. **改进的超时机制**
- 连接超时:基于连接开始时间,超过5分钟关闭连接
- 消息接收超时:基于最后消息时间,超过30秒没有新消息则关闭连接
- 这样可以确保连接在真正超时时能够正确关闭
5. **修复阻塞接收问题**
- 添加了 `ReceiveMessageWithTimeout()` 方法,实现带超时的消息接收
- 使用 `CancellationTokenSource` 创建带超时的取消令牌
- 动态计算剩余超时时间,使用较小的超时值
- 在超时时正确关闭连接并返回null
- 确保即使客户端长时间不发送消息,连接也能在超时后正确关闭
### 移除超时判断的风险分析
#### **低风险场景**
- **正常的长连接应用**:如聊天、实时监控、推送通知
- **客户端主动管理连接**:客户端会定期发送心跳或主动关闭
- **网络环境稳定**:连接质量好,很少出现网络问题
#### **高风险场景**
- **僵尸连接**:客户端异常断开但服务器不知道
- **资源泄漏**:大量无效连接占用服务器资源
- **网络异常**:网络中断但连接状态未更新
- **客户端崩溃**:客户端进程异常退出
#### **具体风险**
1. **资源泄漏风险** ⚠️
- 每个连接占用内存缓冲区、文件描述符、线程资源
- 大量僵尸连接可能导致服务器内存耗尽
2. **性能下降风险** ⚠️
- 服务器需要维护连接列表、心跳检测、状态同步
- 无效连接越多,性能越差
3. **连接数限制风险** ⚠️
- 僵尸连接占用连接数,新连接无法建立
- 影响系统的可扩展性
#### **建议的解决方案**
1. **保留超时机制**:但使用更合理的超时时间(如5-10分钟)
2. **实现心跳机制**:客户端定期发送心跳包
3. **连接状态检查**:定期检查WebSocket连接的实际状态
4. **应用层超时**:在应用层处理业务超时,而不是传输层
### 当前配置值
- `MessageReceiveTimeout`: 30秒
- `ConnectionTimeout`: 5分钟
### 结论
已成功修复了超时判断逻辑的设计缺陷和阻塞接收问题,现在连接能够正确地在超时后关闭,避免了资源泄漏问题。新的实现确保了:
- 连接超时检查正常工作
- 消息接收超时检查正常工作
- 不会因为阻塞等待而忽略超时
- 资源能够及时释放
**建议保留超时机制**,但可以根据实际业务需求调整超时时间,或者实现更智能的心跳机制。

2
src/X1.WebAPI/Program.cs

@ -55,7 +55,7 @@ builder.Services.AddWebSocketServices(options =>
options.MaxConcurrentConnections = 2000; // 最大并发连接数
options.MaxMessageSize = 1024 * 1024; // 最大消息大小(字节)
options.ConnectionTimeout = TimeSpan.FromMinutes(2); // 连接超时时间
options.HeartbeatInterval = TimeSpan.FromSeconds(15); // 心跳检测间隔
options.HeartbeatInterval = TimeSpan.FromSeconds(30); // 心跳检测间隔
});

1
src/X1.WebSocket/Extensions/ServiceCollectionExtensions.cs

@ -28,6 +28,7 @@ public static class ServiceCollectionExtensions
// 注册 WebSocket 消息处理器
services.AddSingleton<IWebSocketMessageHandler, ChatMessageHandler>();
services.AddSingleton<IWebSocketMessageHandler, HeartbeatHandlerManager>();
services.AddSingleton<IWebSocketMessageHandler, NotificationMessageHandler>();
// 注册后台服务

48
src/X1.WebSocket/Handlers/HeartbeatHandlerManager.cs

@ -0,0 +1,48 @@
using CellularManagement.WebSocket.Models;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace CellularManagement.WebSocket.Handlers
{
public class HeartbeatHandlerManager : IWebSocketMessageHandler
{
private readonly ILogger<ChatMessageHandler> _logger;
public HeartbeatHandlerManager(ILogger<ChatMessageHandler> logger)
{
_logger = logger;
}
public string MessageType => "heartbeat";
public async Task<WebSocketMessage> HandleAsync(WebSocketMessage message)
{
_logger.LogDebug("收到ping心跳,连接ID:{ConnectionId}", message.ConnectionId);
await Task.Delay(100); // 模拟异步处理
var response = new WebSocketMessage
{
ConnectionId = message.ConnectionId,
Data = System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new
{
type = "heartbeat",
payload = new
{
message = "pong",
timestamp = DateTime.UtcNow
}
})),
MessageType = System.Net.WebSockets.WebSocketMessageType.Text
};
_logger.LogDebug("发送pong响应,连接ID:{ConnectionId}", message.ConnectionId);
return response;
}
}
}

2
src/X1.WebSocket/Handlers/WebSocketMessageHandlerAdapter.cs

@ -57,7 +57,7 @@ namespace CellularManagement.WebSocket.Handlers
// 使用超时控制处理消息
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(_options.MessageSendTimeout);
cts.CancelAfter(_options.MessageReceiveTimeout);
var result = await _handler.HandleAsync(message);
_stopwatch.Stop();

2
src/X1.WebSocket/Middleware/WebSocketMiddleware.cs

@ -489,7 +489,7 @@ public class WebSocketMiddleware
/// 检查消息是否超时
/// </summary>
private bool IsMessageTimeout(DateTime messageStartTime) =>
DateTime.UtcNow - messageStartTime > _options.MessageSendTimeout;
DateTime.UtcNow - messageStartTime > _options.MessageReceiveTimeout;
/// <summary>
/// 检查消息类型是否有效

4
src/X1.WebSocket/Models/WebSocketOptions.cs

@ -44,9 +44,9 @@ namespace CellularManagement.WebSocket.Models
public TimeSpan ConnectionTimeout { get; set; } = TimeSpan.FromMinutes(5);
/// <summary>
/// 消息发送超时时间
/// 控制消息的接收和处理超时
/// </summary>
public TimeSpan MessageSendTimeout { get; set; } = TimeSpan.FromSeconds(30);
public TimeSpan MessageReceiveTimeout { get; set; } = TimeSpan.FromMinutes(5);
/// <summary>
/// 消息重试次数

Loading…
Cancel
Save