Files
5.24BuYu/Assets/MyGame/Scripts/Net/WebSocketManager.cs
T
2026-10-06 01:14:13 +08:00

404 lines
15 KiB
C#
Raw 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.
using Assets.MyGame.Scripts.Net.WebResponses;
using Best.WebSockets;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Assets.MyGame.Scripts.Net
{
public enum SocketType
{
login,
room,
game,
chess,
customer,
}
public partial class WebSocketManager : MonoSingleton<WebSocketManager>
{
private readonly Dictionary<SocketType, WebSocket> sockets = new();
private readonly Dictionary<SocketType, string> socketPaths = new();
private readonly Dictionary<SocketType, string> socketUrls = new();
private readonly Dictionary<SocketType, Action> handshakeActions = new();
private readonly HashSet<SocketType> handshakeCompleted = new();
private readonly HashSet<SocketType> reconnecting = new();
private readonly HashSet<SocketType> manualClosing = new();
/// <summary>登出标志:true 时所有 socket 关闭后都不触发自动重连。
/// Logout 开始时设 true,切回登录界面后设 false,避免关连接时误触发重连访问已 null 的 user。</summary>
public bool isLoggingOut = false;
/// <summary>每个 socketType 正在运行的重连协程引用,用于主动 StopCoroutine</summary>
private readonly Dictionary<SocketType, Coroutine> reconnectCoroutines = new();
/// <summary>重连状态变化事件(type=哪个 socket;true=开始自动重连 / false=重连结束-成功或取消)。
/// 界面(如游戏面板的 Hl_WaitTip"正在连接服务器"提示)据此显隐;与重连生命周期严格一致。</summary>
public static event Action<SocketType, bool> OnReconnectingChanged;
/// <summary>统一维护重连集合并广播变化:reconnecting 的所有 Add/Remove 入口都走这里,避免漏发事件</summary>
private bool SetReconnecting(SocketType type, bool value)
{
bool changed = value ? reconnecting.Add(type) : reconnecting.Remove(type);
if (changed) OnReconnectingChanged?.Invoke(type, value);
return changed;
}
private float nextHeartbeatTime;
private const float HeartbeatIntervalSeconds = 20f;
#if NewIP
private const string baseUrl = "ws://47.109.34.50";
//private const string baseUrl = "ws://47.109.132.12";
#else
private const string baseUrl = "ws://47.109.132.12";
//private const string baseUrl = "ws://47.109.34.50";
#endif
public bool isGame = false;
protected override void Awake()
{
base.Awake();
socketPaths[SocketType.game] = "/fishinghub";
socketPaths[SocketType.room] = "/fishingroomhub";
socketPaths[SocketType.login] = "/loginhub";
socketPaths[SocketType.chess] = "/chessgamehub";
socketPaths[SocketType.customer] = "/customerHub";
nextHeartbeatTime = Time.realtimeSinceStartup + HeartbeatIntervalSeconds;
}
/// <summary>获取指定类型 Socket 的完整连接地址(含 baseUrl,供外部拼接查询参数,如象棋 Hub 的 ?userId=)</summary>
public string GetSocketPath(SocketType type)
{
return baseUrl + socketPaths[type];
}
private void Update()
{
if (Time.realtimeSinceStartup < nextHeartbeatTime)
return;
nextHeartbeatTime = Time.realtimeSinceStartup + HeartbeatIntervalSeconds;
foreach (var pair in sockets)
{
if (pair.Value != null && pair.Value.IsOpen && handshakeCompleted.Contains(pair.Key))
pair.Value.Send("{\"type\":6}\u001e");
}
}
/// <summary>
/// 创建或返回已存在的 WebSocket
/// </summary>
public WebSocket Connect(SocketType type, Action action = null)
{
return Connect(type, null, action);
}
/// <summary>
/// 创建或返回已存在的 WebSocket(可指定动态 URL,用于 /api/Room/Assign 返回的房间专属地址)
/// </summary>
public WebSocket Connect(SocketType type, string url, Action action = null)
{
// 已存在则直接返回
if (sockets.TryGetValue(type, out var exist))
{
if (exist.IsOpen)
{
#if UNITY_EDITOR
Debug.Log($"[WebSocket] {type} 已连接,直接返回");
#endif
switch (type)
{
case SocketType.login:
break;
case SocketType.room:
break;
case SocketType.game:
break;
case SocketType.chess:
UIManager.Instance.Get<ChineseChessMainPanel>(UIPanel.MainGamePanelZGXQ).subUIManager.ShowAsync(UIPanel.ZGXQDistrict);
break;
case SocketType.customer:
break;
default:
break;
}
if (action != null)
action?.Invoke();
return exist;
}
}
// 未指定 url 时用默认 baseUrl + socketPaths
if (string.IsNullOrEmpty(url))
url = baseUrl + socketPaths[type];
socketUrls[type] = url;
handshakeActions[type] = action;
var ws = new WebSocket(new Uri(url));
// Best.WebSockets 1.22.3 bug:服务器返回 3xx 重定向时,InternalRequest_OnBeforeRedirection
// 会再次调用 StartOpen(),但实例已 open 过,抛 "Open already called!"。
// 这里禁用内部 HTTP 请求的重定向跟随来规避。
ws.OnInternalRequestCreated += (socket, req) =>
{
req.RedirectSettings.MaxRedirects = 0;
};
sockets[type] = ws;
// 事件绑定
ws.OnOpen += (web) =>
{
#if UNITY_EDITOR
Debug.Log($"[WebSocket] {type} 已连接: {url}");
#endif
SetReconnecting(type, false);
handshakeCompleted.Remove(type);
// SignalR 握手完成前不能发送 Bind、JoinRoom 等 Hub 调用。
web.Send("{\"protocol\":\"json\",\"version\":1}\u001e");
};
ws.OnClosed += (web, code, reason) =>
{
#if UNITY_EDITOR
Debug.Log($"[WebSocket] {type} 连接关闭 Code:{code} Reason:{reason}");
#endif
handshakeCompleted.Remove(type);
if (sockets.TryGetValue(type, out var current) && ReferenceEquals(current, web))
sockets.Remove(type);
if (manualClosing.Remove(type))
return;
// ⭐ 登出中或未登录:不触发自动重连。
// isLoggingOut 可能因 ws.Close 异步导致 OnClosed 触发时已重置,
// 所以同时检查 user==null 做双重保险(登出后 user 必为 null)
if (isLoggingOut || GameManager.Instance.user == null)
return;
// 自动重连
switch (type)
{
case SocketType.login:
StartReconnect(type);
break;
case SocketType.room:
break;
case SocketType.game:
if (isGame)
{
int sitpos = 0;
foreach (var item in GameManager.Instance.gameData.currRoomData.players)
{
if (item.userId == GameManager.Instance.user.id)
{
sitpos = item.position; break;
}
}
StartReconnect(type, () =>
{
WebSocketSendList.JoinRoom(GameManager.Instance.gameData.currRoomData.roomId, GameManager.Instance.user.id, sitpos);
});
}
break;
case SocketType.chess:
break;
case SocketType.customer:
break;
default:
break;
}
};
ws.OnMessage += (web, msg) =>
{
OnMessage(web, msg, type);
};
// 保存
sockets[type] = ws;
// 发起连接
ws.Open();
return ws;
}
/// <summary>
/// 发送消息
/// </summary>
public void Send(SocketType type, string msg)
{
if (sockets.TryGetValue(type, out var ws) && ws.IsOpen)
{
ws.Send(msg + "\u001e");
#if UNITY_EDITOR
Debug.LogWarning($"[WebSocket] {type}Send :" + msg);
#endif
}
else
{
#if UNITY_EDITOR
Debug.LogWarning($"[WebSocket] {type} 未连接,无法发送消息" + msg);
UIManager.Instance.ShowBoardError($"[WebSocket] {type} 未连接,无法发送消息");
#endif
//switch (type)
//{
// case SocketType.login:
// break;
// case SocketType.room:
// break;
// case SocketType.game:
// //等待界面添加
// UIManager.Instance.ShowAsync(UIPanel.HomeMainPanel, GameManager.Instance.gameData.currGameType);
// //UIManager.Instance.ShowBoardGongGao("网络异常,请重新进入房间");
// break;
// case SocketType.chess:
// break;
// case SocketType.customer:
// break;
// default:
// break;
//}
}
}
/// <summary>
/// 关闭某个 socket
/// </summary>
public void Close(SocketType type)
{
if (sockets.TryGetValue(type, out var ws))
{
//manualClosing.Add(type);
handshakeCompleted.Remove(type);
ws.Close(WebSocketStatusCodes.NormalClosure, "正常关闭"); // 或者 CloseStatusCode.Normal
//ws.Close();
sockets.Remove(type);
}
}
/// <summary>
/// 关闭所有 socket
/// </summary>
public void CloseAll()
{
// ⭐ 先停止所有重连协程:socket 因 ClosedAbnormally 关闭时已从 sockets 移除,
// CloseAll 遍历 sockets.Keys 会漏掉它,导致重连协程继续运行。这里统一停止。
StopAllReconnects();
foreach (var type in new List<SocketType>(sockets.Keys))
{
Close(type);
}
}
/// <summary>
/// 自动重连
/// </summary>
private void StartReconnect(SocketType type, Action action = null)
{
if (!SetReconnecting(type, true))
return;
var co = StartCoroutine(Reconnect(type, action));
reconnectCoroutines[type] = co;
}
/// <summary>
/// 主动停止所有正在等待的重连协程并清空状态。
/// 用于 Logout 时彻底取消所有待重连任务,不依赖 isLoggingOut 时序。
/// </summary>
public void StopAllReconnects()
{
foreach (var kv in reconnectCoroutines)
{
if (kv.Value != null)
StopCoroutine(kv.Value);
}
reconnectCoroutines.Clear();
// 逐个触发"重连取消",界面等待提示能同步收起
foreach (var type in new List<SocketType>(reconnecting))
SetReconnecting(type, false);
}
private System.Collections.IEnumerator Reconnect(SocketType type, Action action = null)
{
#if UNITY_EDITOR
Debug.Log($"[WebSocket] {type} 3 秒后尝试重连...");
#endif
yield return new WaitForSeconds(3f);
if (manualClosing.Contains(type))
{
SetReconnecting(type, false);
reconnectCoroutines.Remove(type);
yield break;
}
// ⭐ 登出中或未登录:取消已排队等待的重连
if (isLoggingOut || GameManager.Instance.user == null)
{
SetReconnecting(type, false);
reconnectCoroutines.Remove(type);
yield break;
}
reconnectCoroutines.Remove(type);
socketUrls.TryGetValue(type, out var url);
Connect(type, url, action);
}
private void OnHandshakeCompleted(SocketType type)
{
if (!handshakeCompleted.Add(type))
return;
switch (type)
{
case SocketType.login:
// ⭐ 防御:登出后 user 可能为 null,避免 NRE
if (GameManager.Instance.user == null || string.IsNullOrEmpty(GameManager.Instance.user.id))
{
Debug.LogWarning("[WebSocket] login 握手完成但 user 为 null,跳过 Bind(可能登出中)");
return;
}
WebSocketSendList.Bind(GameManager.Instance.user.id);
break;
case SocketType.chess:
UIManager.Instance.ShowAsync(UIPanel.MainGamePanelZGXQ);
break;
}
if (handshakeActions.TryGetValue(type, out var action) && action != null)
{
handshakeActions[type] = null;
action.Invoke();
}
}
/// <summary>
/// 关闭所有 socket
/// </summary>
private void OnDestroy()
{
StopAllReconnects();
foreach (var type in new List<SocketType>(sockets.Keys))
manualClosing.Add(type);
foreach (var ws in new List<WebSocket>(sockets.Values))
{
ws.Close();
}
sockets.Clear();
}
}
}