動態

詳情 返回 返回

Electron 開發:獲取當前客户端 IP - 動態 詳情

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 = null
private lastValidationTime = 0
private 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()
}
user avatar 79px 頭像 segmentfault 頭像 nut 頭像 zhuweitao 頭像 luomg1995 頭像 niumingxin 頭像 thjjames 頭像 ox1dp6ei 頭像 y0i1w 頭像 tiaoyuedelinghun_5b34a58295338 頭像
點贊 10 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.