# 内网发现:探测 IP 可配置化 + 移除组播 日期:2026-07-06 状态:已批准(方案 B) 前置:`2026-07-06-multicast-discovery-design.md`(组播方案,实测跨网段不通,本 spec 将其代码移除) ## 背景与问题 2026-07-06 真机实测:手机(172.30.37.x)→ 开发机(192.168.130.40)跨网段,组播 1.5s 无回复——路由器未开组播路由(IGMP/PIM),组播与广播一样出不了网段。跨网段唯一可靠路径是**单播已知 IP**,但该 IP 目前写死在代码里(`DevExtraProbeIps`),换机器/换网段要改代码重打包。 ## 目标 1. **删除全部组播代码**(实测无效,不留死代码)。 2. 探测 IP 改为**设备本地配置**:`persistentDataPath/dev_server.txt`。 3. 底包发现失败且需要目标时,**真机上弹输入框**让开发者直接输入开发机 IP,探测成功自动写入配置,之后重启不再询问。 4. 删除写死的 `192.168.130.40`,完全由配置文件接替。 ## 非目标 - 不做通用设置界面;输入框仅服务于内网发现这一场景。 - 不支持多 IP / IP:端口(端口固定 48923);配置文件只存一行 IPv4。 - 热更层不弹框(底包阶段已处理,热更层只复用配置)。 - 不改发现协议(magic/端口/编解码不变)。 ## 行为规格 ### 底包启动流程(`LoadDll.CoLocalDiscoverThenDownload`,仅 bLocal 内网模式) ``` 第1轮 Discover(1500ms, targets=广播+loopback+定向广播+文件IP(若有)) ─成功→ 用发现的 CDN 根,照旧 DownLoadAssets │失败 ↓ 挂 DevServerPrompt(输入框预填 DevServerConfig.Load() 旧值,可为空) ←──────────┐ │[确定](输入 IP) │ ↓ │ IP 格式非法 → 弹框内红字"IP 格式不对",留在弹框 ─────────────────────────────┤ 第2轮 Discover(1500ms, 含输入IP) ─失败→ 弹框内红字"探测失败",留在弹框可改可重试┘ │成功 ↓ DevServerConfig.Save(输入IP) → 销毁弹框 → 照旧 DownLoadAssets 任意时刻 [跳过] → UpdateServer 回退 http://127.0.0.1:15081/...(现状行为) ``` - 文件**有**记录且第 1 轮失败(开发机换 IP 等)→ 同样弹框,预填旧值(用户确认的行为)。 - 弹框期间加载流程等待,不超时。 - 探测成功但成功来源无法区分(广播/文件 IP),第 1 轮成功**不**写文件——只有弹框输入并验证成功才写。 ### 热更层(`DevServerDiscovery`,GameLauncher/DevServerSwitchUI 用) - 构造探测目标时读 `DevServerConfig.Load()`,非空则加入 targets。不弹框。调用方零改动。 ## 组件设计 ### 1. `Client\Assets\Base\DevServerConfig.cs`(新,静态类,全局命名空间) - `static string FilePath` → `Application.persistentDataPath + "/dev_server.txt"` - `static string Load()`:读首行 → Trim → `IPAddress.TryParse` 通过才返回,否则 null;文件不存在/IO 异常 → null(静默) - `static void Save(string ip)`:整文件写入单行;IO 异常静默吞 - **仅底包使用**。核实发现 `XWorld.Link.asmdef` 引用列表里没有 Base 程序集,热更层引用不到;为 dev 工具改 asmdef 引用不值当,热更层按本项目"内联+两边同步"惯例(协议编解码已如此)在 `DevServerDiscovery` 内联同语义的最小读取实现 - **主线程限制**:`Application.persistentDataPath` 不能在后台线程访问,`Load()/Save()` 只能在主线程调;发现线程需要的值由调用方在主线程读好传入/存字段 ### 2. `Client\Assets\Base\DevServerPrompt.cs`(新,MonoBehaviour,全局命名空间) - 纯 IMGUI(OnGUI),风格参照热更层 `DevServerSwitchUI` 的 box/横排布局 - 字段:`string InputIp`(初始 = 预填值)、`State state`(`Waiting | Confirmed | Skipped`)、`string Error`(非空则红字显示) - OnGUI:提示语("内网发现失败,请输入开发机 IP")+ TextField + [确定] [跳过] - [确定] → `state = Confirmed`(探测由外部做,失败后外部把 `Error` 置上并把 state 拨回 Waiting) - [跳过] → `state = Skipped` - 不做网络、不做文件 IO——只是输入框 ### 3. `Client\Assets\Base\LocalCdnDiscovery.cs`(改) - 删:组播常量/TTL/逐网卡发送/`GetLocalIPv4Addresses`/相关注释、`DevExtraProbeIps` 整个字段 - 改:`Discover(string token, int durationMs)` → `Discover(string token, int durationMs, string extraProbeIp = null)`;`extraProbeIp` 能 TryParse 则加入 targets - 调用方(LoadDll)自行决定传文件值还是弹框输入值 ### 4. `Client\Assets\Script\xmain\MiniGame\DevServerDiscovery.cs`(改) - 删:同上组播相关、`DevExtraProbeIps` - 加:内联 `ReadConfiguredProbeIp()`(读 dev_server.txt 首行+IPv4 校验,与 `DevServerConfig.Load()` 语义一致,见组件 1 的说明);**构造函数**(主线程)里读取存入字段,`Run()`(后台线程)构造 targets 时使用 ### 5. `Client\Assets\Base\LoadDll.cs`(改) - `CoLocalDiscoverThenDownload` 按上面流程图重写:两轮发现 + 弹框等待循环;弹框对象 `gameObject.AddComponent()`,结束后 `Destroy` - 第 1 轮把 `DevServerConfig.Load()` 传入 `Discover` 的 `extraProbeIp` ### 6. 组播代码删除清单 - `Client\Assets\Framework\Shared\Protocol\DevDiscovery.cs`:删 `MulticastGroup` 常量及注释 - `Server\Gateway\DevDiscoveryResponder.cs`:删 `JoinMulticastOnAllNics` 方法、构造函数调用行、`using System.Net.NetworkInformation;` - `Server\Gateway.Tests\DevDiscoveryResponderTests.cs`:删 `MulticastQuery_ReceivesReply` - 两份客户端:删 TTL 设置、组播端点/网卡列表构建、逐网卡发送循环、零网卡兜底、`GetLocalIPv4Addresses`,注释还原为"单播兜底"语义(指向配置文件而非写死 IP) ## 错误处理 - 文件 IO、IP 解析:全部静默容错,失败等同"无配置" - 弹框输入非法 IP:[确定] 直接在弹框显示"IP 格式不对",不发起探测 - 探测线程异常:沿用现有 catch-all,视为失败回到弹框 ## 测试与验收 1. `dotnet test Server/Server.sln` 全量通过(删组播测试后) 2. 编辑器同机:loopback 一轮即中,弹框不出现,行为与改前一致 3. 真机跨网段首启:第 1 轮失败 → 弹框 → 输入 192.168.130.40 → 探测成功 → 写文件 → 正常加载 4. 真机重启:文件已有记录,第 1 轮直接命中,不弹框 5. 开发机换 IP:第 1 轮失败 → 弹框预填旧值 → 改新 IP → 成功覆写 6. [跳过]:回退 127.0.0.1,与现状一致 ## 风险 - IMGUI TextField 在个别 Android 输入法上体验一般(dev 工具可接受;Unity 移动端 IMGUI 会自动唤起软键盘) - 底包改动须重打 APK 才能上真机(部署铁律,验收时注意)