Files
DFJ/01_文档/WebSocket接口规范与断线重连.md
2025-09-10 18:13:28 +08:00

819 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 打飞机小程序 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服务支持自动重连和消息确认机制。