feat(android): T-AND-005/006 WebSocket 推流 + 主机发现
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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<String>()
|
||||
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<Candidate> = 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user