Initial commit: Client Doc docs Server Tools
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
using XWorld.Framework.Protocol;
|
||||
|
||||
namespace XGame.MiniGame
|
||||
{
|
||||
public sealed class DevServerDiscovery
|
||||
{
|
||||
private readonly string _token;
|
||||
private readonly string _extraProbeIp;
|
||||
private readonly object _lock = new object();
|
||||
private readonly List<DevDiscoveryReply> _results = new List<DevDiscoveryReply>();
|
||||
private Thread _thread;
|
||||
private volatile bool _running;
|
||||
|
||||
public DevServerDiscovery(string token)
|
||||
{
|
||||
_token = token;
|
||||
// 跨网段联调:广播/定向广播不被路由器转发(RFC 2644),组播亦实测不通(路由器未开组播路由),
|
||||
// 唯一可靠路径是向已知开发机 IP 单播。IP 来自 persistentDataPath/dev_server.txt——底包首启弹框
|
||||
// 录入(见 Base 层 DevServerConfig/DevServerPrompt)。persistentDataPath 不能在后台线程访问,
|
||||
// 故在主线程构造时读好存字段,Run(后台线程)只用字段。
|
||||
_extraProbeIp = ReadConfiguredProbeIp();
|
||||
}
|
||||
|
||||
public bool IsRunning => _running;
|
||||
|
||||
public void Begin(int durationMs = 1200)
|
||||
{
|
||||
if (_running) return;
|
||||
_running = true;
|
||||
_thread = new Thread(() => Run(durationMs)) { IsBackground = true };
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
private void Run(int durationMs)
|
||||
{
|
||||
UdpClient udp = null;
|
||||
try
|
||||
{
|
||||
udp = new UdpClient(AddressFamily.InterNetwork);
|
||||
udp.EnableBroadcast = true;
|
||||
udp.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
udp.Client.Bind(new IPEndPoint(IPAddress.Any, 0));
|
||||
udp.Client.ReceiveTimeout = 250;
|
||||
|
||||
uint nonce = unchecked((uint)Guid.NewGuid().GetHashCode());
|
||||
byte[] query = DevDiscoveryCodec.EncodeQuery(new DevDiscoveryQuery
|
||||
{
|
||||
ProtoVersion = DevDiscovery.ProtoVersion,
|
||||
Nonce = nonce,
|
||||
Token = _token,
|
||||
});
|
||||
// 同机的 UDP 广播常收不到(取决于网卡是否回环广播),故同时向 127.0.0.1 单播;
|
||||
// 并在窗口内周期重发,防响应器(网关)刚起来还没监听。
|
||||
// 255.255.255.255(限制广播)在部分 Android WiFi 驱动上会被丢弃,故再按各网卡 IP|~mask 补发子网定向广播(如 192.168.1.255)。
|
||||
List<IPEndPoint> targetList = new List<IPEndPoint>
|
||||
{
|
||||
new IPEndPoint(IPAddress.Broadcast, DevDiscovery.Port),
|
||||
new IPEndPoint(IPAddress.Loopback, DevDiscovery.Port),
|
||||
};
|
||||
foreach (IPAddress bc in GetDirectedBroadcasts())
|
||||
targetList.Add(new IPEndPoint(bc, DevDiscovery.Port));
|
||||
if (!string.IsNullOrEmpty(_extraProbeIp) && IPAddress.TryParse(_extraProbeIp, out IPAddress extra))
|
||||
targetList.Add(new IPEndPoint(extra, DevDiscovery.Port));
|
||||
IPEndPoint[] targets = targetList.ToArray();
|
||||
Debug.Log("[DevTest] targets(" + targets.Length + "): " + string.Join(", ", Array.ConvertAll(targets, t => t.ToString())));
|
||||
|
||||
DateTime deadline = DateTime.UtcNow.AddMilliseconds(durationMs);
|
||||
DateTime nextSend = DateTime.MinValue;
|
||||
while (_running && DateTime.UtcNow < deadline)
|
||||
{
|
||||
if (DateTime.UtcNow >= nextSend)
|
||||
{
|
||||
foreach (IPEndPoint t in targets)
|
||||
{
|
||||
try { udp.Send(query, query.Length, t); } catch { }
|
||||
}
|
||||
nextSend = DateTime.UtcNow.AddMilliseconds(300);
|
||||
}
|
||||
IPEndPoint from = new IPEndPoint(IPAddress.Any, 0);
|
||||
byte[] data;
|
||||
try { data = udp.Receive(ref from); }
|
||||
catch (SocketException) { continue; }
|
||||
if (!DevDiscoveryCodec.TryDecodeReply(data, out var reply)) continue;
|
||||
if (reply.ProtoVersion != DevDiscovery.ProtoVersion) continue;
|
||||
if (reply.Nonce != nonce) continue;
|
||||
if (reply.Token != _token) continue;
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_results.Exists(r => r.GatewayUrl == reply.GatewayUrl))
|
||||
{
|
||||
_results.Add(reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning("[DevTest] discovery thread error: " + e.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { udp?.Close(); } catch { }
|
||||
_running = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 枚举本机所有 Up 状态 IPv4 网卡,按 broadcast = ip | ~mask 算出各自的子网定向广播地址。
|
||||
// 关键:Android/Mono 下 IPv4Mask 常拿不到(返回 null/0.0.0.0 甚至抛异常),故对私有网段按 /24 兜底,
|
||||
// 且逐地址 try/catch,任何一块网卡取掩码失败都不中断整体枚举。
|
||||
private static List<IPAddress> GetDirectedBroadcasts()
|
||||
{
|
||||
List<IPAddress> list = new List<IPAddress>();
|
||||
NetworkInterface[] nics;
|
||||
try { nics = NetworkInterface.GetAllNetworkInterfaces(); }
|
||||
catch (Exception e) { Debug.LogWarning("[DevTest] GetAllNetworkInterfaces failed: " + e.Message); return list; }
|
||||
|
||||
foreach (NetworkInterface ni in nics)
|
||||
{
|
||||
if (ni.OperationalStatus != OperationalStatus.Up) continue;
|
||||
if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback) continue;
|
||||
|
||||
IPInterfaceProperties props;
|
||||
try { props = ni.GetIPProperties(); }
|
||||
catch { continue; }
|
||||
|
||||
foreach (UnicastIPAddressInformation ua in props.UnicastAddresses)
|
||||
{
|
||||
if (ua.Address.AddressFamily != AddressFamily.InterNetwork) continue; // 只处理 IPv4
|
||||
byte[] ip = ua.Address.GetAddressBytes();
|
||||
if (ip.Length != 4) continue;
|
||||
|
||||
byte[] mk = null;
|
||||
try { IPAddress m = ua.IPv4Mask; if (m != null) mk = m.GetAddressBytes(); }
|
||||
catch { mk = null; } // Android 上访问 IPv4Mask 可能抛,兜住
|
||||
|
||||
// 掩码不可用(null/长度不对/全 0)时,私有网段按 /24 兜底(最常见的家用/办公 WiFi),公网地址不猜。
|
||||
bool maskBad = mk == null || mk.Length != 4 || (mk[0] == 0 && mk[1] == 0 && mk[2] == 0 && mk[3] == 0);
|
||||
if (maskBad)
|
||||
{
|
||||
if (!IsPrivateV4(ip)) continue;
|
||||
mk = new byte[] { 255, 255, 255, 0 };
|
||||
}
|
||||
|
||||
byte[] bc = new byte[4];
|
||||
for (int i = 0; i < 4; i++) bc[i] = (byte)(ip[i] | (~mk[i] & 0xFF));
|
||||
IPAddress bcAddr = new IPAddress(bc);
|
||||
list.Add(bcAddr);
|
||||
Debug.Log("[DevTest] nic=" + ni.Name + " ip=" + ua.Address + " mask=" + string.Join(".", mk) + " bc=" + bcAddr);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static bool IsPrivateV4(byte[] ip)
|
||||
{
|
||||
if (ip[0] == 10) return true; // 10.0.0.0/8
|
||||
if (ip[0] == 192 && ip[1] == 168) return true; // 192.168.0.0/16
|
||||
if (ip[0] == 172 && ip[1] >= 16 && ip[1] <= 31) return true; // 172.16.0.0/12
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<DevDiscoveryReply> TakeResults()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return new List<DevDiscoveryReply>(_results);
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_running = false;
|
||||
}
|
||||
|
||||
// 读 dev_server.txt 首行并校验为 IPv4,失败一律 null。与 Base 层 DevServerConfig.Load() 语义一致——
|
||||
// XWorld.Link 未引用 Base 程序集,按本项目“内联+两边同步”惯例复制最小实现,改格式时两边须同步。
|
||||
private static string ReadConfiguredProbeIp()
|
||||
{
|
||||
try
|
||||
{
|
||||
string path = System.IO.Path.Combine(Application.persistentDataPath, "dev_server.txt");
|
||||
if (!System.IO.File.Exists(path)) return null;
|
||||
string line = System.IO.File.ReadAllText(path).Trim();
|
||||
if (line.Length == 0) return null;
|
||||
if (!IPAddress.TryParse(line, out IPAddress ip)) return null;
|
||||
if (ip.AddressFamily != AddressFamily.InterNetwork) return null; // 只认 IPv4
|
||||
return ip.ToString();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user