Initial commit
This commit is contained in:
819
01_文档/WebSocket接口规范与断线重连.md
Normal file
819
01_文档/WebSocket接口规范与断线重连.md
Normal file
@@ -0,0 +1,819 @@
|
||||
# 打飞机小程序 WebSocket 接口规范与断线重连策略
|
||||
|
||||
## 1. WebSocket 连接规范
|
||||
|
||||
### 1.1 连接地址
|
||||
```
|
||||
wss://your-domain.com/game-hub?token={authToken}&gameId={gameId}
|
||||
```
|
||||
|
||||
### 1.2 .NET Core WebSocket Hub 实现参考
|
||||
```csharp
|
||||
[Authorize]
|
||||
public class GameHub : Hub
|
||||
{
|
||||
private readonly IGameService _gameService;
|
||||
private readonly IConnectionManager _connectionManager;
|
||||
|
||||
public GameHub(IGameService gameService, IConnectionManager connectionManager)
|
||||
{
|
||||
_gameService = gameService;
|
||||
_connectionManager = connectionManager;
|
||||
}
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
var userId = Context.UserIdentifier;
|
||||
var gameId = Context.GetHttpContext().Request.Query["gameId"];
|
||||
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, $"Game_{gameId}");
|
||||
await _connectionManager.RegisterConnection(userId, Context.ConnectionId, gameId);
|
||||
|
||||
// 发送连接成功确认
|
||||
await Clients.Caller.SendAsync("ConnectionConfirmed", new
|
||||
{
|
||||
ConnectionId = Context.ConnectionId,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
GameId = gameId
|
||||
});
|
||||
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception exception)
|
||||
{
|
||||
var userId = Context.UserIdentifier;
|
||||
await _connectionManager.UnregisterConnection(Context.ConnectionId);
|
||||
|
||||
// 通知游戏中的其他玩家
|
||||
await Clients.Others.SendAsync("PlayerDisconnected", new
|
||||
{
|
||||
UserId = userId,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Reason = exception?.Message
|
||||
});
|
||||
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 消息协议定义
|
||||
|
||||
### 2.1 基础消息格式
|
||||
```typescript
|
||||
interface WebSocketMessage {
|
||||
messageId: string // 消息唯一ID,用于确认和重发
|
||||
type: MessageType // 消息类型
|
||||
timestamp: number // 时间戳
|
||||
gameId: string // 游戏ID
|
||||
userId: string // 发送者ID
|
||||
data: any // 消息内容
|
||||
sequenceNumber: number // 序列号,用于消息排序
|
||||
requiresAck?: boolean // 是否需要确认回复
|
||||
}
|
||||
|
||||
enum MessageType {
|
||||
// 连接管理
|
||||
PING = 'PING',
|
||||
PONG = 'PONG',
|
||||
HEARTBEAT = 'HEARTBEAT',
|
||||
CONNECTION_CONFIRMED = 'CONNECTION_CONFIRMED',
|
||||
|
||||
// 游戏房间
|
||||
JOIN_ROOM = 'JOIN_ROOM',
|
||||
LEAVE_ROOM = 'LEAVE_ROOM',
|
||||
ROOM_STATE_UPDATE = 'ROOM_STATE_UPDATE',
|
||||
|
||||
// 游戏操作
|
||||
PLACE_PLANES = 'PLACE_PLANES',
|
||||
ATTACK_POSITION = 'ATTACK_POSITION',
|
||||
ATTACK_RESULT = 'ATTACK_RESULT',
|
||||
GAME_STATE_SYNC = 'GAME_STATE_SYNC',
|
||||
|
||||
// 系统消息
|
||||
PLAYER_RECONNECT = 'PLAYER_RECONNECT',
|
||||
PLAYER_DISCONNECT = 'PLAYER_DISCONNECT',
|
||||
GAME_END = 'GAME_END',
|
||||
ERROR = 'ERROR',
|
||||
|
||||
// 确认消息
|
||||
ACK = 'ACK',
|
||||
MESSAGE_RECEIVED = 'MESSAGE_RECEIVED'
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 关键消息定义
|
||||
|
||||
#### A. 游戏操作消息
|
||||
```csharp
|
||||
// .NET 消息模型
|
||||
public class AttackMessage
|
||||
{
|
||||
public string MessageId { get; set; } = Guid.NewGuid().ToString();
|
||||
public string Type { get; set; } = "ATTACK_POSITION";
|
||||
public long Timestamp { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
public string GameId { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public AttackData Data { get; set; }
|
||||
public int SequenceNumber { get; set; }
|
||||
public bool RequiresAck { get; set; } = true;
|
||||
}
|
||||
|
||||
public class AttackData
|
||||
{
|
||||
public Position Position { get; set; }
|
||||
public string PlayerId { get; set; }
|
||||
}
|
||||
|
||||
public class Position
|
||||
{
|
||||
public int X { get; set; }
|
||||
public int Y { get; set; }
|
||||
public string Coordinate { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### B. 游戏状态同步
|
||||
```csharp
|
||||
public class GameStateSyncMessage
|
||||
{
|
||||
public string MessageId { get; set; } = Guid.NewGuid().ToString();
|
||||
public string Type { get; set; } = "GAME_STATE_SYNC";
|
||||
public long Timestamp { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
public string GameId { get; set; }
|
||||
public GameStateData Data { get; set; }
|
||||
public int SequenceNumber { get; set; }
|
||||
}
|
||||
|
||||
public class GameStateData
|
||||
{
|
||||
public string CurrentPlayer { get; set; }
|
||||
public string GamePhase { get; set; }
|
||||
public List<GameMove> MoveHistory { get; set; }
|
||||
public Dictionary<string, PlayerState> Players { get; set; }
|
||||
public long LastUpdateTime { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
## 3. .NET WebSocket Hub 核心方法
|
||||
|
||||
### 3.1 游戏操作处理
|
||||
```csharp
|
||||
[HubMethodName("AttackPosition")]
|
||||
public async Task HandleAttack(AttackMessage message)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 验证消息
|
||||
if (!await ValidateMessage(message))
|
||||
{
|
||||
await SendError("Invalid attack message", message.MessageId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理攻击逻辑
|
||||
var result = await _gameService.ProcessAttack(
|
||||
message.GameId,
|
||||
message.UserId,
|
||||
message.Data.Position
|
||||
);
|
||||
|
||||
// 发送攻击结果给所有玩家
|
||||
var resultMessage = new AttackResultMessage
|
||||
{
|
||||
MessageId = Guid.NewGuid().ToString(),
|
||||
GameId = message.GameId,
|
||||
Data = result,
|
||||
SequenceNumber = await GetNextSequenceNumber(message.GameId)
|
||||
};
|
||||
|
||||
await Clients.Group($"Game_{message.GameId}")
|
||||
.SendAsync("AttackResult", resultMessage);
|
||||
|
||||
// 发送确认回复
|
||||
if (message.RequiresAck)
|
||||
{
|
||||
await Clients.Caller.SendAsync("MessageReceived", new
|
||||
{
|
||||
OriginalMessageId = message.MessageId,
|
||||
Status = "SUCCESS",
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
});
|
||||
}
|
||||
|
||||
// 检查游戏是否结束
|
||||
if (result.GameEnded)
|
||||
{
|
||||
await HandleGameEnd(message.GameId, result.Winner);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await SendError($"Attack processing failed: {ex.Message}", message.MessageId);
|
||||
}
|
||||
}
|
||||
|
||||
[HubMethodName("PlacePlanes")]
|
||||
public async Task HandlePlacePlanes(PlacePlanesMessage message)
|
||||
{
|
||||
try
|
||||
{
|
||||
var isValid = await _gameService.ValidatePlacement(
|
||||
message.GameId,
|
||||
message.UserId,
|
||||
message.Data.Planes
|
||||
);
|
||||
|
||||
if (!isValid.Success)
|
||||
{
|
||||
await SendError(isValid.ErrorMessage, message.MessageId);
|
||||
return;
|
||||
}
|
||||
|
||||
await _gameService.SavePlacement(message.GameId, message.UserId, message.Data.Planes);
|
||||
|
||||
// 通知房间状态更新
|
||||
var roomState = await _gameService.GetRoomState(message.GameId);
|
||||
await Clients.Group($"Game_{message.GameId}")
|
||||
.SendAsync("RoomStateUpdate", roomState);
|
||||
|
||||
// 确认消息
|
||||
if (message.RequiresAck)
|
||||
{
|
||||
await Clients.Caller.SendAsync("MessageReceived", new
|
||||
{
|
||||
OriginalMessageId = message.MessageId,
|
||||
Status = "SUCCESS"
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await SendError($"Plane placement failed: {ex.Message}", message.MessageId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 心跳和连接管理
|
||||
```csharp
|
||||
[HubMethodName("Heartbeat")]
|
||||
public async Task HandleHeartbeat(HeartbeatMessage message)
|
||||
{
|
||||
await _connectionManager.UpdateLastActivity(Context.ConnectionId);
|
||||
|
||||
// 发送心跳响应
|
||||
await Clients.Caller.SendAsync("HeartbeatResponse", new
|
||||
{
|
||||
ServerTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
ConnectionId = Context.ConnectionId,
|
||||
Status = "ALIVE"
|
||||
});
|
||||
}
|
||||
|
||||
[HubMethodName("RequestGameState")]
|
||||
public async Task HandleGameStateRequest(string gameId)
|
||||
{
|
||||
var gameState = await _gameService.GetCompleteGameState(gameId);
|
||||
|
||||
await Clients.Caller.SendAsync("GameStateSync", new GameStateSyncMessage
|
||||
{
|
||||
GameId = gameId,
|
||||
Data = gameState,
|
||||
SequenceNumber = await GetNextSequenceNumber(gameId)
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 客户端断线重连策略
|
||||
|
||||
### 4.1 连接管理器
|
||||
```typescript
|
||||
export class WebSocketManager {
|
||||
private connection: HubConnection | null = null
|
||||
private reconnectAttempts = 0
|
||||
private readonly maxReconnectAttempts = 5
|
||||
private readonly baseReconnectDelay = 1000
|
||||
private heartbeatInterval: NodeJS.Timeout | null = null
|
||||
private messageQueue: QueuedMessage[] = []
|
||||
private sequenceNumber = 0
|
||||
private lastReceivedSequence = 0
|
||||
|
||||
constructor(
|
||||
private gameId: string,
|
||||
private authToken: string,
|
||||
private onMessage: (message: WebSocketMessage) => void,
|
||||
private onConnectionStateChange: (state: ConnectionState) => void
|
||||
) {}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
try {
|
||||
this.connection = new HubConnectionBuilder()
|
||||
.withUrl(`wss://your-domain.com/game-hub?gameId=${this.gameId}`, {
|
||||
accessTokenFactory: () => this.authToken,
|
||||
transport: HttpTransportType.WebSockets
|
||||
})
|
||||
.withAutomaticReconnect({
|
||||
nextRetryDelayInMilliseconds: (retryContext) => {
|
||||
// 指数退避策略
|
||||
return Math.min(
|
||||
this.baseReconnectDelay * Math.pow(2, retryContext.previousRetryCount),
|
||||
30000
|
||||
)
|
||||
}
|
||||
})
|
||||
.configureLogging(LogLevel.Information)
|
||||
.build()
|
||||
|
||||
// 注册消息处理器
|
||||
this.setupMessageHandlers()
|
||||
|
||||
// 开始连接
|
||||
await this.connection.start()
|
||||
|
||||
this.onConnectionStateChange('CONNECTED')
|
||||
this.startHeartbeat()
|
||||
this.reconnectAttempts = 0
|
||||
|
||||
// 重连后请求完整游戏状态
|
||||
await this.requestGameState()
|
||||
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection failed:', error)
|
||||
await this.handleConnectionError()
|
||||
}
|
||||
}
|
||||
|
||||
private setupMessageHandlers(): void {
|
||||
if (!this.connection) return
|
||||
|
||||
// 连接确认
|
||||
this.connection.on('ConnectionConfirmed', (data) => {
|
||||
console.log('Connection confirmed:', data)
|
||||
this.onConnectionStateChange('CONNECTED')
|
||||
})
|
||||
|
||||
// 攻击结果
|
||||
this.connection.on('AttackResult', (message: WebSocketMessage) => {
|
||||
this.handleReceivedMessage(message)
|
||||
})
|
||||
|
||||
// 游戏状态同步
|
||||
this.connection.on('GameStateSync', (message: WebSocketMessage) => {
|
||||
this.handleReceivedMessage(message)
|
||||
this.syncSequenceNumber(message.sequenceNumber)
|
||||
})
|
||||
|
||||
// 房间状态更新
|
||||
this.connection.on('RoomStateUpdate', (message: WebSocketMessage) => {
|
||||
this.handleReceivedMessage(message)
|
||||
})
|
||||
|
||||
// 玩家断线/重连
|
||||
this.connection.on('PlayerDisconnected', (data) => {
|
||||
this.onMessage({
|
||||
messageId: Date.now().toString(),
|
||||
type: 'PLAYER_DISCONNECT',
|
||||
timestamp: Date.now(),
|
||||
gameId: this.gameId,
|
||||
userId: data.userId,
|
||||
data,
|
||||
sequenceNumber: 0
|
||||
})
|
||||
})
|
||||
|
||||
this.connection.on('PlayerReconnected', (data) => {
|
||||
this.onMessage({
|
||||
messageId: Date.now().toString(),
|
||||
type: 'PLAYER_RECONNECT',
|
||||
timestamp: Date.now(),
|
||||
gameId: this.gameId,
|
||||
userId: data.userId,
|
||||
data,
|
||||
sequenceNumber: 0
|
||||
})
|
||||
})
|
||||
|
||||
// 消息确认
|
||||
this.connection.on('MessageReceived', (ack) => {
|
||||
this.handleMessageAck(ack.originalMessageId)
|
||||
})
|
||||
|
||||
// 心跳响应
|
||||
this.connection.on('HeartbeatResponse', (data) => {
|
||||
// 计算网络延迟
|
||||
const now = Date.now()
|
||||
const latency = now - (data.clientTimestamp || now)
|
||||
console.log(`Network latency: ${latency}ms`)
|
||||
})
|
||||
|
||||
// 错误处理
|
||||
this.connection.on('Error', (error) => {
|
||||
console.error('Server error:', error)
|
||||
this.onMessage({
|
||||
messageId: Date.now().toString(),
|
||||
type: 'ERROR',
|
||||
timestamp: Date.now(),
|
||||
gameId: this.gameId,
|
||||
userId: '',
|
||||
data: error,
|
||||
sequenceNumber: 0
|
||||
})
|
||||
})
|
||||
|
||||
// 连接状态变化
|
||||
this.connection.onreconnecting(() => {
|
||||
this.onConnectionStateChange('RECONNECTING')
|
||||
this.stopHeartbeat()
|
||||
})
|
||||
|
||||
this.connection.onreconnected(() => {
|
||||
this.onConnectionStateChange('CONNECTED')
|
||||
this.startHeartbeat()
|
||||
this.resendQueuedMessages()
|
||||
this.requestGameState() // 重连后同步状态
|
||||
})
|
||||
|
||||
this.connection.onclose(() => {
|
||||
this.onConnectionStateChange('DISCONNECTED')
|
||||
this.stopHeartbeat()
|
||||
})
|
||||
}
|
||||
|
||||
private handleReceivedMessage(message: WebSocketMessage): void {
|
||||
// 检查消息序列号,处理消息丢失
|
||||
if (message.sequenceNumber && message.sequenceNumber > this.lastReceivedSequence + 1) {
|
||||
console.warn(`Message sequence gap detected: expected ${this.lastReceivedSequence + 1}, got ${message.sequenceNumber}`)
|
||||
// 请求缺失的消息或完整状态同步
|
||||
this.requestGameState()
|
||||
}
|
||||
|
||||
this.lastReceivedSequence = Math.max(this.lastReceivedSequence, message.sequenceNumber || 0)
|
||||
this.onMessage(message)
|
||||
}
|
||||
|
||||
async sendMessage(type: MessageType, data: any, requiresAck: boolean = true): Promise<void> {
|
||||
const message: WebSocketMessage = {
|
||||
messageId: this.generateMessageId(),
|
||||
type,
|
||||
timestamp: Date.now(),
|
||||
gameId: this.gameId,
|
||||
userId: this.getCurrentUserId(),
|
||||
data,
|
||||
sequenceNumber: ++this.sequenceNumber,
|
||||
requiresAck
|
||||
}
|
||||
|
||||
if (this.connection?.state === 'Connected') {
|
||||
try {
|
||||
await this.sendDirectMessage(message)
|
||||
|
||||
// 如果需要确认,加入等待队列
|
||||
if (requiresAck) {
|
||||
this.messageQueue.push({
|
||||
message,
|
||||
attempts: 0,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Send message failed:', error)
|
||||
this.queueMessage(message)
|
||||
}
|
||||
} else {
|
||||
this.queueMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
private async sendDirectMessage(message: WebSocketMessage): Promise<void> {
|
||||
switch (message.type) {
|
||||
case 'ATTACK_POSITION':
|
||||
await this.connection!.invoke('AttackPosition', message)
|
||||
break
|
||||
case 'PLACE_PLANES':
|
||||
await this.connection!.invoke('PlacePlanes', message)
|
||||
break
|
||||
case 'HEARTBEAT':
|
||||
await this.connection!.invoke('Heartbeat', message)
|
||||
break
|
||||
default:
|
||||
console.warn(`Unknown message type: ${message.type}`)
|
||||
}
|
||||
}
|
||||
|
||||
private startHeartbeat(): void {
|
||||
this.heartbeatInterval = setInterval(async () => {
|
||||
try {
|
||||
await this.sendMessage('HEARTBEAT', {
|
||||
clientTimestamp: Date.now()
|
||||
}, false)
|
||||
} catch (error) {
|
||||
console.error('Heartbeat failed:', error)
|
||||
}
|
||||
}, 10000) // 10秒心跳
|
||||
}
|
||||
|
||||
private stopHeartbeat(): void {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval)
|
||||
this.heartbeatInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
private async requestGameState(): Promise<void> {
|
||||
if (this.connection?.state === 'Connected') {
|
||||
try {
|
||||
await this.connection.invoke('RequestGameState', this.gameId)
|
||||
} catch (error) {
|
||||
console.error('Request game state failed:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private queueMessage(message: WebSocketMessage): void {
|
||||
this.messageQueue.push({
|
||||
message,
|
||||
attempts: 0,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
private async resendQueuedMessages(): Promise<void> {
|
||||
const pendingMessages = [...this.messageQueue]
|
||||
this.messageQueue = []
|
||||
|
||||
for (const queuedMessage of pendingMessages) {
|
||||
if (queuedMessage.attempts < 3) { // 最多重试3次
|
||||
try {
|
||||
await this.sendDirectMessage(queuedMessage.message)
|
||||
queuedMessage.attempts++
|
||||
|
||||
if (queuedMessage.message.requiresAck) {
|
||||
this.messageQueue.push(queuedMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Resend message failed:', error)
|
||||
queuedMessage.attempts++
|
||||
if (queuedMessage.attempts < 3) {
|
||||
this.messageQueue.push(queuedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleMessageAck(messageId: string): void {
|
||||
this.messageQueue = this.messageQueue.filter(
|
||||
queuedMessage => queuedMessage.message.messageId !== messageId
|
||||
)
|
||||
}
|
||||
|
||||
private generateMessageId(): string {
|
||||
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
private getCurrentUserId(): string {
|
||||
// 从认证token或存储中获取用户ID
|
||||
return 'current-user-id' // 实现细节
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
this.stopHeartbeat()
|
||||
if (this.connection) {
|
||||
await this.connection.stop()
|
||||
}
|
||||
this.onConnectionStateChange('DISCONNECTED')
|
||||
}
|
||||
}
|
||||
|
||||
interface QueuedMessage {
|
||||
message: WebSocketMessage
|
||||
attempts: number
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
type ConnectionState = 'CONNECTING' | 'CONNECTED' | 'RECONNECTING' | 'DISCONNECTED'
|
||||
```
|
||||
|
||||
## 5. 关键 .NET 服务实现
|
||||
|
||||
### 5.1 连接管理服务
|
||||
```csharp
|
||||
public interface IConnectionManager
|
||||
{
|
||||
Task RegisterConnection(string userId, string connectionId, string gameId);
|
||||
Task UnregisterConnection(string connectionId);
|
||||
Task UpdateLastActivity(string connectionId);
|
||||
Task<List<string>> GetUserConnections(string userId);
|
||||
Task<bool> IsUserOnline(string userId);
|
||||
}
|
||||
|
||||
public class ConnectionManager : IConnectionManager
|
||||
{
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly ILogger<ConnectionManager> _logger;
|
||||
|
||||
public ConnectionManager(IMemoryCache cache, ILogger<ConnectionManager> logger)
|
||||
{
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task RegisterConnection(string userId, string connectionId, string gameId)
|
||||
{
|
||||
var connectionInfo = new ConnectionInfo
|
||||
{
|
||||
UserId = userId,
|
||||
ConnectionId = connectionId,
|
||||
GameId = gameId,
|
||||
ConnectedAt = DateTime.UtcNow,
|
||||
LastActivity = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_cache.Set($"connection_{connectionId}", connectionInfo, TimeSpan.FromHours(2));
|
||||
|
||||
// 更新用户连接列表
|
||||
var userConnections = await GetUserConnections(userId);
|
||||
userConnections.Add(connectionId);
|
||||
_cache.Set($"user_connections_{userId}", userConnections, TimeSpan.FromHours(2));
|
||||
|
||||
_logger.LogInformation($"User {userId} connected with connection {connectionId}");
|
||||
}
|
||||
|
||||
public async Task UnregisterConnection(string connectionId)
|
||||
{
|
||||
if (_cache.TryGetValue($"connection_{connectionId}", out ConnectionInfo connectionInfo))
|
||||
{
|
||||
_cache.Remove($"connection_{connectionId}");
|
||||
|
||||
var userConnections = await GetUserConnections(connectionInfo.UserId);
|
||||
userConnections.Remove(connectionId);
|
||||
_cache.Set($"user_connections_{connectionInfo.UserId}", userConnections);
|
||||
|
||||
_logger.LogInformation($"Connection {connectionId} unregistered");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateLastActivity(string connectionId)
|
||||
{
|
||||
if (_cache.TryGetValue($"connection_{connectionId}", out ConnectionInfo connectionInfo))
|
||||
{
|
||||
connectionInfo.LastActivity = DateTime.UtcNow;
|
||||
_cache.Set($"connection_{connectionId}", connectionInfo, TimeSpan.FromHours(2));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetUserConnections(string userId)
|
||||
{
|
||||
if (_cache.TryGetValue($"user_connections_{userId}", out List<string> connections))
|
||||
{
|
||||
return connections;
|
||||
}
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
public async Task<bool> IsUserOnline(string userId)
|
||||
{
|
||||
var connections = await GetUserConnections(userId);
|
||||
return connections.Any();
|
||||
}
|
||||
}
|
||||
|
||||
public class ConnectionInfo
|
||||
{
|
||||
public string UserId { get; set; }
|
||||
public string ConnectionId { get; set; }
|
||||
public string GameId { get; set; }
|
||||
public DateTime ConnectedAt { get; set; }
|
||||
public DateTime LastActivity { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 消息确认和重发机制
|
||||
```csharp
|
||||
public class MessageReliabilityService
|
||||
{
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IHubContext<GameHub> _hubContext;
|
||||
|
||||
public async Task SendReliableMessage(string connectionId, string method, object data)
|
||||
{
|
||||
var messageId = Guid.NewGuid().ToString();
|
||||
var message = new ReliableMessage
|
||||
{
|
||||
MessageId = messageId,
|
||||
Method = method,
|
||||
Data = data,
|
||||
SentAt = DateTime.UtcNow,
|
||||
Attempts = 0
|
||||
};
|
||||
|
||||
// 存储消息等待确认
|
||||
_cache.Set($"pending_message_{messageId}", message, TimeSpan.FromMinutes(5));
|
||||
|
||||
// 发送消息
|
||||
await _hubContext.Clients.Client(connectionId).SendAsync(method, data);
|
||||
|
||||
// 设置超时重发
|
||||
_ = Task.Delay(TimeSpan.FromSeconds(5)).ContinueWith(async _ =>
|
||||
{
|
||||
await CheckAndResendMessage(messageId, connectionId);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ConfirmMessage(string messageId)
|
||||
{
|
||||
_cache.Remove($"pending_message_{messageId}");
|
||||
}
|
||||
|
||||
private async Task CheckAndResendMessage(string messageId, string connectionId)
|
||||
{
|
||||
if (_cache.TryGetValue($"pending_message_{messageId}", out ReliableMessage message))
|
||||
{
|
||||
message.Attempts++;
|
||||
|
||||
if (message.Attempts < 3) // 最多重试3次
|
||||
{
|
||||
await _hubContext.Clients.Client(connectionId).SendAsync(message.Method, message.Data);
|
||||
|
||||
// 更新缓存
|
||||
_cache.Set($"pending_message_{messageId}", message, TimeSpan.FromMinutes(5));
|
||||
|
||||
// 再次设置超时
|
||||
_ = Task.Delay(TimeSpan.FromSeconds(5)).ContinueWith(async _ =>
|
||||
{
|
||||
await CheckAndResendMessage(messageId, connectionId);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// 超过重试次数,移除消息
|
||||
_cache.Remove($"pending_message_{messageId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ReliableMessage
|
||||
{
|
||||
public string MessageId { get; set; }
|
||||
public string Method { get; set; }
|
||||
public object Data { get; set; }
|
||||
public DateTime SentAt { get; set; }
|
||||
public int Attempts { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Startup.cs 配置
|
||||
|
||||
```csharp
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// SignalR配置
|
||||
services.AddSignalR(options =>
|
||||
{
|
||||
options.EnableDetailedErrors = true;
|
||||
options.KeepAliveInterval = TimeSpan.FromSeconds(10);
|
||||
options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
|
||||
options.HandshakeTimeout = TimeSpan.FromSeconds(15);
|
||||
}).AddJsonProtocol(options =>
|
||||
{
|
||||
options.PayloadSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
});
|
||||
|
||||
// CORS配置
|
||||
services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(builder =>
|
||||
{
|
||||
builder
|
||||
.WithOrigins("https://your-miniprogram-domain.com")
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials();
|
||||
});
|
||||
});
|
||||
|
||||
// 服务注册
|
||||
services.AddScoped<IGameService, GameService>();
|
||||
services.AddSingleton<IConnectionManager, ConnectionManager>();
|
||||
services.AddSingleton<MessageReliabilityService>();
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
app.UseCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// SignalR Hub路由
|
||||
app.UseRouting();
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapHub<GameHub>("/game-hub");
|
||||
endpoints.MapControllers();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
这个WebSocket接口规范提供了完整的断线重连策略,包括指数退避重连、消息队列管理、心跳机制、状态同步等关键功能。.NET后端通过SignalR提供了可靠的WebSocket服务,支持自动重连和消息确认机制。
|
||||
Reference in New Issue
Block a user