package com.opendance.studio.net import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext import java.net.Inet4Address import java.net.InetSocketAddress import java.net.NetworkInterface import java.net.Socket /** * T-AND-006:主机发现 * * Windows 客户端是**裸 WebSocket 服务端**(不是 HTTP 服务),因此不能靠扫描 * HTTP 端点发现;这里用「TCP 能否连上 8765 端口」作为探测手段。 * * 策略:取手机在 WiFi 网的 IPv4 地址,推导 /24 网段,并发探测该网段内所有 * 主机的目标端口(默认并发 32、单机超时 300ms,整段约 2~3 秒)。 */ object HostDiscovery { private const val TAG = "HostDiscovery" const val DEFAULT_PORT = 8765 private const val CONNECT_TIMEOUT_MS = 300 private const val MAX_CONCURRENCY = 32 /** 发现结果 */ data class Candidate(val ip: String, val port: Int, val latencyMs: Long) /** 手机自身的 WiFi IPv4 地址(形如 192.168.1.23),无网络时返回 null */ suspend fun localIpv4Address(): String? = withContext(Dispatchers.IO) { try { val candidates = mutableListOf() for (nif in NetworkInterface.getNetworkInterfaces()) { if (!nif.isUp || nif.isLoopback) continue val name = nif.name.lowercase() // 排除蜂窝与隧道网卡,优先 WiFi / 以太网 if (name.startsWith("rmnet") || name.startsWith("dummy") || name.startsWith("tun")) continue for (address in nif.inetAddresses) { if (address !is Inet4Address || address.isLoopbackAddress) continue val ip = address.hostAddress ?: continue // 排除链路本地地址(169.254.x.x) if (ip.startsWith("169.254.")) continue candidates.add(ip) } } // 优先返回私有网段地址;否则退回第一个可用地址 candidates.firstOrNull { isPrivateIpv4(it) } ?: candidates.firstOrNull() } catch (e: Exception) { Log.w(TAG, "获取本机地址失败:${e.message}") null } } /** 判断是否为 RFC1918 私有地址 */ fun isPrivateIpv4(ip: String): Boolean { val parts = ip.split(".").mapNotNull { it.toIntOrNull() } if (parts.size != 4) return false val (a, b) = parts[0] to parts[1] return a == 10 || (a == 172 && b in 16..31) || (a == 192 && b == 168) } /** 由本机地址推导 /24 前缀,如 192.168.1.23 → "192.168.1" */ fun subnetPrefix(ip: String): String? { val parts = ip.split(".") if (parts.size != 4) return null return parts.take(3).joinToString(".") } /** * 扫描网段。以 Flow 形式逐步产出候选,便于 UI 边扫边显示。 */ fun scan(prefix: String, port: Int = DEFAULT_PORT): Flow = flow { val semaphore = Semaphore(MAX_CONCURRENCY) val candidates = coroutineScope { (1..254).map { last -> async(Dispatchers.IO) { semaphore.withPermit { probe("$prefix.$last", port) } } }.awaitAll() } // 按延迟排序,最快响应的最可能是目标机 candidates.filterNotNull().sortedBy { it.latencyMs }.forEach { emit(it) } }.flowOn(Dispatchers.IO) /** 探测单个主机:能建立 TCP 连接即认为端口开放 */ private fun probe(ip: String, port: Int): Candidate? { val started = System.currentTimeMillis() return try { Socket().use { socket -> socket.connect(InetSocketAddress(ip, port), CONNECT_TIMEOUT_MS) Candidate(ip, port, System.currentTimeMillis() - started) } } catch (e: Exception) { null } } /** 单个主机连通性检查(连接页「测试连接」用) */ suspend fun checkHost(ip: String, port: Int = DEFAULT_PORT): Candidate? = withContext(Dispatchers.IO) { probe(ip, port) } }