# 打飞机小程序 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 MoveHistory { get; set; } public Dictionary 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 { 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 { 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 { 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 { 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 { 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 { 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> GetUserConnections(string userId); Task IsUserOnline(string userId); } public class ConnectionManager : IConnectionManager { private readonly IMemoryCache _cache; private readonly ILogger _logger; public ConnectionManager(IMemoryCache cache, ILogger 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> GetUserConnections(string userId) { if (_cache.TryGetValue($"user_connections_{userId}", out List connections)) { return connections; } return new List(); } public async Task 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 _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(); services.AddSingleton(); services.AddSingleton(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); // SignalR Hub路由 app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapHub("/game-hub"); endpoints.MapControllers(); }); } ``` 这个WebSocket接口规范提供了完整的断线重连策略,包括指数退避重连、消息队列管理、心跳机制、状态同步等关键功能。.NET后端通过SignalR提供了可靠的WebSocket服务,支持自动重连和消息确认机制。