229 lines
10 KiB
C#
229 lines
10 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Net;
|
||
using System.Net.NetworkInformation;
|
||
using System.Net.Sockets;
|
||
using System.Text;
|
||
|
||
// 底包(AOT)专用:内网 CDN 广播发现。
|
||
// 真机内网测试时 CDN 跑在开发机上,客户端写死的 127.0.0.1 在真机上指向手机自身、连不到开发机;
|
||
// 故用 UDP 广播问开发机网关要它的 LAN CDN 根地址(形如 http://<开发机IP>:15081/)。
|
||
//
|
||
// 协议与热更层 XWorld.Framework.Protocol.DevDiscovery / DevServerDiscovery 完全一致
|
||
// (magic "XWDL"、UDP 48923、LEB128 varuint + UTF8 长度前缀字符串、token=xw-dev),
|
||
// 但 Base 程序集不能引用热更程序集(XWorld.Framework.Shared,运行时才由 LoadDll 加载),
|
||
// 所以这里内联一份最小编解码,改协议时两边须同步。
|
||
public static class LocalCdnDiscovery
|
||
{
|
||
private const int Port = 48923; // = DevDiscovery.Port
|
||
private const uint ProtoVersion = 1; // = DevDiscovery.ProtoVersion
|
||
private static readonly byte[] Magic = { 0x58, 0x57, 0x44, 0x4C }; // "XWDL" = DevDiscovery.Magic
|
||
private const byte TypeQuery = 1; // = DevDiscovery.TypeQuery
|
||
private const byte TypeReply = 2; // = DevDiscovery.TypeReply
|
||
|
||
// 阻塞式发现,须在后台线程调用(会最多阻塞 durationMs)。
|
||
// 在窗口内周期重发查询(向广播 + 127.0.0.1 单播),收到首个合法应答即返回其 ResourceBaseUrl(CDN 根);超时返回 null。
|
||
// extraProbeIp:额外单播探测目标(跨网段联调用,来自 dev_server.txt 或弹框输入,见 DevServerConfig/DevServerPrompt)。
|
||
// 广播/定向广播不被路由器转发(RFC 2644),组播亦已实测不通(路由器未开组播路由),跨网段只能靠单播已知 IP。
|
||
// 注意:本方法跑在后台线程,不能碰 Unity 主线程 API(如 persistentDataPath),配置由调用方在主线程读好传入。
|
||
public static string Discover(string token, int durationMs = 1500, string extraProbeIp = null)
|
||
{
|
||
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 = EncodeQuery(nonce, token);
|
||
// 同机广播常收不到(取决于网卡是否回环广播),故同时向 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, Port),
|
||
new IPEndPoint(IPAddress.Loopback, Port),
|
||
};
|
||
foreach (IPAddress bc in GetDirectedBroadcasts())
|
||
targetList.Add(new IPEndPoint(bc, Port));
|
||
if (!string.IsNullOrEmpty(extraProbeIp) && IPAddress.TryParse(extraProbeIp, out IPAddress extra))
|
||
targetList.Add(new IPEndPoint(extra, Port));
|
||
IPEndPoint[] targets = targetList.ToArray();
|
||
UnityEngine.Debug.Log("[Discovery] targets(" + targets.Length + "): " + string.Join(", ", Array.ConvertAll(targets, t => t.ToString())));
|
||
|
||
DateTime deadline = DateTime.UtcNow.AddMilliseconds(durationMs);
|
||
DateTime nextSend = DateTime.MinValue;
|
||
while (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; } // 250ms 接收超时,继续重发/等待
|
||
if (TryDecodeReply(data, nonce, token, out string resourceBaseUrl)
|
||
&& !string.IsNullOrEmpty(resourceBaseUrl))
|
||
{
|
||
return resourceBaseUrl;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception)
|
||
{
|
||
// 忽略:发现失败由调用方处理(弹框输入或回退 127.0.0.1)
|
||
}
|
||
finally
|
||
{
|
||
try { udp?.Close(); } catch { }
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// 枚举本机所有 Up 状态 IPv4 网卡,按 broadcast = ip | ~mask 算出各自的子网定向广播地址。
|
||
// 关键:Android/Mono 下 IPv4Mask 常拿不到(返回 null/0.0.0.0 甚至抛异常),故对私有网段按 /24 兜底,
|
||
// 且逐地址 try/catch,任何一块网卡取掩码失败都不中断整体枚举(否则会连累外层 return null)。
|
||
private static List<IPAddress> GetDirectedBroadcasts()
|
||
{
|
||
List<IPAddress> list = new List<IPAddress>();
|
||
NetworkInterface[] nics;
|
||
try { nics = NetworkInterface.GetAllNetworkInterfaces(); }
|
||
catch (Exception e) { UnityEngine.Debug.LogWarning("[Discovery] 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);
|
||
UnityEngine.Debug.Log("[Discovery] 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;
|
||
}
|
||
|
||
private static byte[] EncodeQuery(uint nonce, string token)
|
||
{
|
||
var buf = new List<byte>(32);
|
||
for (int i = 0; i < Magic.Length; i++) buf.Add(Magic[i]);
|
||
buf.Add(TypeQuery);
|
||
WriteVarUInt(buf, ProtoVersion);
|
||
WriteVarUInt(buf, nonce);
|
||
WriteString(buf, token);
|
||
return buf.ToArray();
|
||
}
|
||
|
||
private static bool TryDecodeReply(byte[] data, uint expectNonce, string expectToken, out string resourceBaseUrl)
|
||
{
|
||
resourceBaseUrl = null;
|
||
try
|
||
{
|
||
int pos = 0;
|
||
for (int i = 0; i < Magic.Length; i++)
|
||
if (ReadByte(data, ref pos) != Magic[i]) return false;
|
||
if (ReadByte(data, ref pos) != TypeReply) return false;
|
||
if (ReadVarUInt(data, ref pos) != ProtoVersion) return false;
|
||
if (ReadVarUInt(data, ref pos) != expectNonce) return false;
|
||
ReadString(data, ref pos); // ServerName(忽略)
|
||
ReadString(data, ref pos); // GatewayUrl(忽略,LoadDll 只要 CDN)
|
||
string resBase = ReadString(data, ref pos); // ResourceBaseUrl(CDN 根)
|
||
string token = ReadString(data, ref pos);
|
||
if (token != expectToken) return false;
|
||
resourceBaseUrl = resBase;
|
||
return true;
|
||
}
|
||
catch (FormatException)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// ---- LEB128 varuint + UTF8 长度前缀字符串:与 PacketWriter/PacketReader 对齐 ----
|
||
|
||
private static void WriteVarUInt(List<byte> buf, uint v)
|
||
{
|
||
while (v >= 0x80) { buf.Add((byte)(v | 0x80)); v >>= 7; }
|
||
buf.Add((byte)v);
|
||
}
|
||
|
||
private static void WriteString(List<byte> buf, string s)
|
||
{
|
||
if (string.IsNullOrEmpty(s)) { WriteVarUInt(buf, 0); return; }
|
||
byte[] b = Encoding.UTF8.GetBytes(s);
|
||
WriteVarUInt(buf, (uint)b.Length);
|
||
buf.AddRange(b);
|
||
}
|
||
|
||
private static byte ReadByte(byte[] d, ref int pos)
|
||
{
|
||
if (pos >= d.Length) throw new FormatException("read past end");
|
||
return d[pos++];
|
||
}
|
||
|
||
private static uint ReadVarUInt(byte[] d, ref int pos)
|
||
{
|
||
uint result = 0;
|
||
int shift = 0;
|
||
while (true)
|
||
{
|
||
if (shift > 28) throw new FormatException("varuint too long");
|
||
byte b = ReadByte(d, ref pos);
|
||
result |= (uint)(b & 0x7F) << shift;
|
||
if ((b & 0x80) == 0) break;
|
||
shift += 7;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
private static string ReadString(byte[] d, ref int pos)
|
||
{
|
||
uint len = ReadVarUInt(d, ref pos);
|
||
if (len == 0) return string.Empty;
|
||
if (pos + len > d.Length) throw new FormatException("string length exceeds buffer");
|
||
string s = Encoding.UTF8.GetString(d, pos, (int)len);
|
||
pos += (int)len;
|
||
return s;
|
||
}
|
||
}
|