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 { private readonly Dictionary sockets = new(); private readonly Dictionary socketPaths = new(); private readonly Dictionary socketUrls = new(); private readonly Dictionary handshakeActions = new(); private readonly HashSet handshakeCompleted = new(); private readonly HashSet reconnecting = new(); private readonly HashSet manualClosing = new(); /// 登出标志:true 时所有 socket 关闭后都不触发自动重连。 /// Logout 开始时设 true,切回登录界面后设 false,避免关连接时误触发重连访问已 null 的 user。 public bool isLoggingOut = false; /// 每个 socketType 正在运行的重连协程引用,用于主动 StopCoroutine private readonly Dictionary reconnectCoroutines = new(); /// 重连状态变化事件(type=哪个 socket;true=开始自动重连 / false=重连结束-成功或取消)。 /// 界面(如游戏面板的 Hl_WaitTip"正在连接服务器"提示)据此显隐;与重连生命周期严格一致。 public static event Action OnReconnectingChanged; /// 统一维护重连集合并广播变化:reconnecting 的所有 Add/Remove 入口都走这里,避免漏发事件 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; } /// 获取指定类型 Socket 的完整连接地址(含 baseUrl,供外部拼接查询参数,如象棋 Hub 的 ?userId=) 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"); } } /// /// 创建或返回已存在的 WebSocket /// public WebSocket Connect(SocketType type, Action action = null) { return Connect(type, null, action); } /// /// 创建或返回已存在的 WebSocket(可指定动态 URL,用于 /api/Room/Assign 返回的房间专属地址) /// 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(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; } /// /// 发送消息 /// 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; //} } } /// /// 关闭某个 socket /// 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); } } /// /// 关闭所有 socket /// public void CloseAll() { // ⭐ 先停止所有重连协程:socket 因 ClosedAbnormally 关闭时已从 sockets 移除, // CloseAll 遍历 sockets.Keys 会漏掉它,导致重连协程继续运行。这里统一停止。 StopAllReconnects(); foreach (var type in new List(sockets.Keys)) { Close(type); } } /// /// 自动重连 /// private void StartReconnect(SocketType type, Action action = null) { if (!SetReconnecting(type, true)) return; var co = StartCoroutine(Reconnect(type, action)); reconnectCoroutines[type] = co; } /// /// 主动停止所有正在等待的重连协程并清空状态。 /// 用于 Logout 时彻底取消所有待重连任务,不依赖 isLoggingOut 时序。 /// public void StopAllReconnects() { foreach (var kv in reconnectCoroutines) { if (kv.Value != null) StopCoroutine(kv.Value); } reconnectCoroutines.Clear(); // 逐个触发"重连取消",界面等待提示能同步收起 foreach (var type in new List(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(); } } /// /// 关闭所有 socket /// private void OnDestroy() { StopAllReconnects(); foreach (var type in new List(sockets.Keys)) manualClosing.Add(type); foreach (var ws in new List(sockets.Values)) { ws.Close(); } sockets.Clear(); } } }