|
Electron 开发:获取当前客户端 IP
一、背景与需求
1. 项目背景
客户端会自启动一个服务,Web/后端服务通过 IP + port 请求以操作客户端接口
2. 初始方案与问题
2.1. 初始方案:通过代码获取本机 IP
/** * 获取局域网 IP * @returns {string} 局域网 IP */export function getLocalIP(): string { const interfaces = os.networkInterfaces() for (const name of Object.keys(interfaces)) { for (const iface of interfaces[name] || []) { if (iface.family === 'IPv4' && !iface.internal) { log.info('获取局域网 IP:', iface.address) return iface.address } } } log.warn('无法获取局域网 IP,使用默认 IP: 127.0.0.1') return '127.0.0.1'}2.2. 遇到的问题
如果设备开启了代理,可能获取的是代理 IP,导致后端请求失败
二、解决方案设计
1. 总体思路
- 获取本机所有 IP
- 遍历 IP + port 请求客户端服务接口
- 成功响应即为目标 IP
- 缓存有效 IP,避免频繁请求
2. 获取所有可能的 IP
使用 Node.js 的 os.networkInterfaces() 获取所有可用 IP
private getAllPossibleIPs(): string[] { const interfaces = os.networkInterfaces() const result: string[] = [] for (const name of Object.keys(interfaces)) { const lowerName = name.toLowerCase() if (lowerName.includes('vmware') || lowerName.includes('virtual') || lowerName.includes('vpn') || lowerName.includes('docker') || lowerName.includes('vethernet')) { continue } for (const iface of interfaces[name] || []) { if (iface.family === 'IPv4' && !iface.internal) { result.push(iface.address) } } } return result}3. 遍历 IP 请求验证
轮询所有 IP,尝试访问客户端服务,验证是否可用
private async testIPsParallel(ips: string[]): Promise<string | null> { if (ips.length === 0) return null return new Promise((resolve) => { const globalTimeout = setTimeout(() => { resolve(null) }, this.TIMEOUT * 1.5) const controllers = ips.map(() => new AbortController()) let hasResolved = false let completedCount = 0 const testIP = (ip: string, index: number) => { const controller = controllers[index] axios.get(`http://${ip}:${PORT}/api/task-server/ip`, { timeout: this.TIMEOUT, signal: controller.signal, }) .then(() => { if (!hasResolved) { hasResolved = true clearTimeout(globalTimeout) controllers.forEach((c, i) => { if (i !== index) c.abort() }) resolve(ip) } }) .catch(() => { if (!hasResolved) { completedCount++ if (completedCount >= ips.length) { clearTimeout(globalTimeout) resolve(null) } } }) } ips.forEach(testIP) })}4. 添加缓存策略
对成功的 IP 进行缓存,设定缓存有效时间,避免重复请求
private cachedValidIP: string | null = nullprivate lastValidationTime = 0private readonly CACHE_VALID_DURATION = 24 * 60 * 60 * 1000三、完整代码
import os from 'node:os'import axios from 'axios'import { PORT } from '../../enum/env'/** * IP管理器单例类 * 用于获取并缓存本地有效IP地址 */export class IPManager { private static instance: IPManager private cachedValidIP: string | null = null private lastValidationTime = 0 private readonly CACHE_VALID_DURATION = 24 * 60 * 60 * 1000 private readonly TIMEOUT = 200 private isTestingIPs = false private constructor() {} static getInstance(): IPManager { if (!IPManager.instance) { IPManager.instance = new IPManager() } return IPManager.instance } async getLocalIP(): Promise<string> { const now = Date.now() if (this.cachedValidIP && now - this.lastValidationTime < this.CACHE_VALID_DURATION) { console.log('从缓存中获取 IP', this.cachedValidIP) return this.cachedValidIP } if (this.isTestingIPs) { const allIPs = this.getAllPossibleIPs() return allIPs.length > 0 ? allIPs[0] : '127.0.0.1' } this.isTestingIPs = true try { const allIPs = this.getAllPossibleIPs() if (allIPs.length === 0) { return '127.0.0.1' } const validIP = await this.testIPsParallel(allIPs) if (validIP) { this.cachedValidIP = validIP this.lastValidationTime = now return validIP } return allIPs[0] } catch (error) { const allIPs = this.getAllPossibleIPs() return allIPs.length > 0 ? allIPs[0] : '127.0.0.1' } finally { this.isTestingIPs = false } } private getAllPossibleIPs(): string[] { const interfaces = os.networkInterfaces() const result: string[] = [] for (const name of Object.keys(interfaces)) { const lowerName = name.toLowerCase() if (lowerName.includes('vmware') || lowerName.includes('virtual') || lowerName.includes('vpn') || lowerName.includes('docker') || lowerName.includes('vethernet')) { continue } for (const iface of interfaces[name] || []) { if (iface.family === 'IPv4' && !iface.internal) { result.push(iface.address) } } } return result } private async testIPsParallel(ips: string[]): Promise<string | null> { if (ips.length === 0) return null return new Promise((resolve) => { const globalTimeout = setTimeout(() => { resolve(null) }, this.TIMEOUT * 1.5) const controllers = ips.map(() => new AbortController()) let hasResolved = false let completedCount = 0 const testIP = (ip: string, index: number) => { const controller = controllers[index] axios.get(`http://${ip}:${PORT}/api/task-server/ip`, { timeout: this.TIMEOUT, signal: controller.signal, // validateStatus: status => status === 200, }) .then(() => { if (!hasResolved) { hasResolved = true clearTimeout(globalTimeout) controllers.forEach((c, i) => { if (i !== index) c.abort() }) resolve(ip) } }) .catch(() => { if (!hasResolved) { completedCount++ if (completedCount >= ips.length) { clearTimeout(globalTimeout) resolve(null) } } }) } ips.forEach(testIP) }) }}/** * 获取本地有效IP地址 */export async function getLocalIP(): Promise<string> { return IPManager.getInstance().getLocalIP()} |
|