package com.xtrh.discovery.util; import com.xtrh.common.config.Settings; import java.net.InetSocketAddress; import java.net.InetAddress; import java.net.Inet4Address; import java.net.NetworkInterface; import java.net.Socket; import java.net.URI; import java.util.Collections; /** * 选择节点对外登记的地址,不把本机回环地址默认当成跨电脑播放地址。 * 多网卡时根据到已认证总控的实际网络路由选择本机地址,而非主机名的首个解析结果。 */ public final class NodeAddress { private NodeAddress() {} public static String host(Settings settings, String coordinator) throws Exception { if (!settings.advertisedHost.isBlank()) { // 特殊网络或播放域名由部署配置覆盖,最终注册仍执行服务根地址校验。 return settings.advertisedHost; } // 主执行端通过本机入口领取任务,对外登记地址必须另外选择,供其他电脑播放。 if (settings.role.equals("coordinator")) return localHost(settings); URI uri = URI.create(coordinator); try (Socket route = new Socket()) { // 连接只用于查询操作系统选出的本机接口,关闭后不会持续占用控制连接。 // HTTPS 未显式写端口时按 443 查询路由,HTTP 按 80。 route.connect(new InetSocketAddress( uri.getHost(), uri.getPort() > 0 ? uri.getPort() : (uri.getScheme().equals("https") ? 443 : 80)), 1000); InetAddress local = route.getLocalAddress(); // 同电脑的独立子节点可能连接 localhost,同样不能把回环默认发布给远端观看者。 return local.isLoopbackAddress() ? localHost(settings) : local.getHostAddress(); } } /** 自注册只访问本实例已绑定的控制入口,网络发送仍走正常签名认证。 */ public static String controlUrl(Settings settings) { if (!settings.httpReady()) throw new IllegalStateException("Waiting for local HTTP server"); String host = wildcard(settings.bindHost) ? "127.0.0.1" : settings.bindHost; return httpUrl(host, settings.port); } // 发布实际 HTTP 端口;主节点的控制和媒体入口共用同一端口,不增开第二个执行进程。 public static String url(Settings settings, String coordinator) throws Exception { return httpUrl(host(settings, coordinator), settings.port); } /** 对外地址优先尊重显式绑定与发现网卡,多网卡部署仍可用 advertised-host 指定。 */ private static String localHost(Settings settings) throws Exception { if (!wildcard(settings.bindHost)) return settings.bindHost; if (!settings.discoveryAddress.isBlank()) { InetAddress configured = InetAddress.getByName(settings.discoveryAddress); if (!configured.isLoopbackAddress() && !configured.isAnyLocalAddress()) return configured.getHostAddress(); } // 只考虑启用的真实 IPv4 网卡,避免默认发布回环、链路本地或虚拟接口的地址。 var interfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); InetAddress preferred = null; try { preferred = InetAddress.getLocalHost(); } catch (Exception ignored) { } String first = null; for (NetworkInterface card : interfaces) { if (!card.isUp() || card.isLoopback() || card.isVirtual()) continue; for (InetAddress address : Collections.list(card.getInetAddresses())) { if (!(address instanceof Inet4Address) || address.isLoopbackAddress() || address.isLinkLocalAddress()) continue; if (address.equals(preferred)) return address.getHostAddress(); if (first == null) first = address.getHostAddress(); } } // 无局域网网卡的纯本机环境仍可运行;跨电脑使用须发布观看端可访问的地址。 return first == null ? "127.0.0.1" : first; } private static boolean wildcard(String host) { return host.isBlank() || host.equals("0.0.0.0") || host.equals("::") || host.equals("[::]"); } // IPv6 字面量需要方括号,确保绑定特定网卡时仍生成合法 URL。 private static String httpUrl(String host, int port) { String authority = host.contains(":") && !host.startsWith("[") ? "[" + host + "]" : host; return "http://" + authority + ":" + port; } }