news 2026/9/9 22:50:08

多源并发+缓存:打造稳定高效的公网IP获取模块

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多源并发+缓存:打造稳定高效的公网IP获取模块

简介:获取公网IP是许多网络脚本和运维场景中的常见需求,get-public-ip即为Node.js环境下的轻量级解决方案。该模块基于DNS、HTTP、HTTPS三种渠道实现公共IP快速检索,并特别兼容0.10.x、4.4.x、6.9.x等多个Node.js历史版本,适合需要运行在旧版或嵌入式Node.js环境的开发者使用。压缩包共6个文件,包含主要JavaScript实现与测试入口、package.json与配置文件、LICENSE及readme文档,整体仅4KB,结构精简便于快速集成到现有项目中。已有175人学习使用这份资源。通过阅读源码与文档,可以掌握多机制获取公网IP的实现思路,了解DNS解析与HTTP请求在IP探测中的不同取舍,也可直接复用模块节省开发时间。 很多项目的起点,都是从“拿个公网IP”开始的。我之前在做一个故障演练平台的时候,每台执行节点在跑演练前都要先上报出口公网IP;后来做一个设备远程重启工具,开机时也要把当前公网IP推给服务端。这个需求简单到几乎所有人都会直接拼一行curl ifconfig.me,但真到了生产环境,你会发现“简单”只是表象:数据源今天通明天不通,解析出来的格式五花八门,IPv6 环境下直接返回一个冒号分隔的长串把下游干崩溃。踩过几轮之后,我把这段逻辑抽成了一个独立模块,也就是标题里这个get-public-ip,专门解决“快速、稳定、不脏手”地拿到公网IP这件事。

这篇文章会把整个设计过程和踩坑记录都摊开讲一遍。内容不依赖具体编程语言,但代码示例我用 Python 写,因为这是我最常用的实现方式。无论你是在写部署脚本、监控采集器,还是自建一个小工具,这套思路应该都能直接抄走用。

1. 看起来简单的需求,为什么容易做成一堆烂摊子

1.1 “curl一下”方案的三个隐藏问题

最直观的做法是找一台公网服务,发请求拿返回体。很多教程让你直接curl ifconfig.me,也确实能拿到IP。但这个方案扔到代码里,会暴露出几个平时注意不到的问题。

第一是单点盲区ifconfig.me这类公共服务并不是 SLA 99.99% 的商业基础设施,它可能因为维护、限流、域名过期而随时不可用。一旦你写死单一数据源,它挂掉你的程序也跟着挂,而且排查起来很尴尬:服务本身没问题,就是IP查不到。

第二是响应格式不稳定。有的服务返回纯文本,有的返回 JSON,有的在接口路径后面加不加?format=json返回不同结构。更麻烦的是,当服务商在前端套了 CDN 或防火墙,偶尔会直接吐给你一段 HTML 错误页。如果你直接把响应正文当IP用,轻则解析失败,重则把<html>...</html>当IP存进数据库。

第三是网络环境适配。办公网、家庭宽带、云主机、容器环境,这四种网络对公网的访问行为完全不一样。家里可能同时有 IPv4 和 IPv6,办公网可能把某些域名给拦截了,云主机返回的是弹性公网IP还是内网IP取决于你的网络命名空间配置。这些问题只用一条 curl 命令根本管不过来。

1.2 当我决定把它变成模块时,想清楚的四件事

后来第三个项目又要复制粘贴这段逻辑的时候,我痛定思痛,决定写一个独立的get-public-ip模块。动手之前,我给自己定了四条硬性要求:

  • 对外只暴露一个函数,返回标准格式的IP字符串,调用方不需要关心数据源是什么。
  • 必须支持多数据源并发竞速,谁先返回合法的公网IP就用谁,避免单个数据源拖垮整体。
  • 内置短时缓存,短时间重复调用直接命中,不重复发网络请求。
  • 对返回内容做严格校验,不合法就当成这个数据源失败,继续等下一个结果。

这四条看着简单,但每一条落地时都有对应的坑,后面几节我会逐个展开。先把数据源选型说清楚,因为这是整个模块的地基。

2. 数据源选型:准确率、延迟与隐私的三方博弈

2.1 主流的公开IP接口到底有几类

市面上能用的公开IP查询服务不少,我按返回格式大致分成了三类:

数据类型代表服务返回示例优点缺点
纯文本ifconfig.me、icanhazip、ip.sb203.0.113.10解析简单,几乎没有额外开销纯文本接口有的不太稳定,容易被误判为爬虫
JSONipify、ipinfo.io{"ip":"203.0.113.10"}结构标准,扩展字段多需要多一次 JSON 解析,部分服务有请求频率限制
多字段接口ip-api.com、ipinfo.io包含IP、位置、ASN等能顺便拿位置和运营商信息响应体大,延迟偏高,不适合只取IP的场景

如果你只需要IP,纯文本和轻量JSON其实都行。我个人更偏向 JSON 接口,因为字段名明确,不容易出现“多了个换行符”这种脏问题。纯文本接口也保留着,用来做并发兜底。

2.2 我最终留下了哪几家,以及筛选标准

公开接口多,但能进模块的少。我的筛选标准是:HTTPS 必须支持、解析规则稳定、响应体小、近半年没有长期宕机记录。最后我留下了四个:

  • https://api.ipify.org?format=json:ipify,老牌服务,监控比较完善,JSON 返回{"ip":"..."}
  • https://api.ip.sb/ip:ip.sb,国内访问速度不错(在部分网络下很快),纯文本返回。
  • https://ipv4.icanhazip.com/:icanhazip,纯文本,支持指定 IPv4/IPv6,返回干净。
  • https://ifconfig.me/ip:ifconfig.me,经典服务,纯文本,偶尔慢,但胜在稳定。

你可能会发现这些都是国外服务。这其实是隐私和准确率之间的平衡选择:查询公网IP必然要把自己的出口IP暴露给第三方,我倾向于选运营时间长、隐私政策清晰、不会把请求记录拿去搞黑产的服务商。有些免费接口会附带一堆跟踪参数,那种再好用我也不敢放进生产环境。

这里有一个很重要的细节:同一个数据源,在不同网络环境下表现天差地别。ip.sb 在我家宽带上不到 100ms,到办公网就没响应;ifconfig.me 恰恰反过来。所以模块不能写死“A 优先生效”,必须让每个请求公平竞速,这次谁快用谁。下一节我会讲具体怎么实现。

3. 模块核心实现:并发竞速、超时控制与短时缓存

3.1 最基础的版本:串行请求为什么不行

如果你没做过并发,第一版很可能写成这样:依次请求 ipify,失败再请求 ip.sb,再失败请求 icannhazip。这个逻辑最大的问题是,最终耗时取决于“运气最差的那个数据源”。

假设 ipify 恰好被限流,等了 5 秒超时;ip.sb 本来 80ms 就能返回,但因为你的代码是先试 ipify,它再快也得等前一个失败后才能发请求。这完全背离“快速模块”的初衷。

正确做法是把所有数据源一次性扔出去,谁先返回合法结果就用谁。这样整体耗时就约等于最快数据源的延迟,而不是最慢的。

3.2 并发竞速版本的代码骨架

我用 Python 标准库加线程池实现,不引入额外的第三方依赖,这样模块在任何环境都能直接跑。核心代码如下:

import json import re import time import threading from concurrent.futures import ThreadPoolExecutor, as_completed from urllib.request import Request, urlopen IPV4_RE = re.compile(r"^(\d{1,3}\.){3}\d{1,3}$") SOURCES = [ {"name": "ipify", "url": "https://api.ipify.org?format=json", "kind": "json", "field": "ip"}, {"name": "ip.sb", "url": "https://api.ip.sb/ip", "kind": "text"}, {"name": "icanhazip", "url": "https://ipv4.icanhazip.com/", "kind": "text"}, {"name": "ifconfig.me", "url": "https://ifconfig.me/ip", "kind": "text"}, ] def _fetch_one(source: dict, timeout: float) -> str: req = Request(source["url"], headers={"User-Agent": "get-public-ip/1.0"}) with urlopen(req, timeout=timeout) as resp: body = resp.read().decode("utf-8", errors="replace").strip() if source["kind"] == "json": return json.loads(body)[source["field"]] return body def _is_valid_ipv4(ip: str) -> bool: if not IPV4_RE.match(ip): return False parts = ip.split(".") return all(0 <= int(part) <= 255 for part in parts) _cache_lock = threading.Lock() _cache_ip = None _cache_ts = 0.0 _CACHE_TTL = 30 # 秒 def get_public_ip(timeout: float = 2.0, use_cache: bool = True) -> str: global _cache_ip, _cache_ts if use_cache: now = time.time() with _cache_lock: if _cache_ip and now - _cache_ts < _CACHE_TTL: return _cache_ip errors = [] with ThreadPoolExecutor(max_workers=len(SOURCES)) as pool: futures = {pool.submit(_fetch_one, source, timeout): source["name"] for source in SOURCES} for future in as_completed(futures): name = futures[future] try: ip = future.result().strip() except Exception as exc: errors.append(f"{name}: {exc}") continue if _is_valid_ipv4(ip): if use_cache: with _cache_lock: _cache_ip = ip _cache_ts = time.time() return ip raise RuntimeError("all public ip sources failed: " + "; ".join(errors))

这段代码里有个容易被忽略的点:as_completed是边完成边返回,代码在拿到第一个合法 IP 时会立刻return,剩下的线程会被ThreadPoolExecutor的上下文管理器等待回收。虽然不会立即中断仍在跑的请求,但已经不会阻塞调用方了。

校验函数_is_valid_ipv4比单纯正则多了一步逐段判断 0-255 的范围,这一步能过滤掉大量“看起来像IP实际不是”的脏数据。比如999.1.1.1能通过正则但过不了数字校验。

3.3 缓存策略决定“快不快”

并发竞速只是把单次请求的耗时压到最快,真正解决“频繁调用”问题的还得是缓存。我给缓存设计的规则很简单:30 秒内同一进程重复调用,直接返回上次结果

IP 地址属于变化频率很低的信息。对大多数场景来说,30 秒甚至 5 分钟内的缓存都不会造成业务问题。但缓存会让调用耗时从百毫秒级别直接降到微秒级别,在很多巡检脚本里这是决定性的体验提升。

另一个细节是加锁。多线程环境下,不加锁的缓存可能导致两个线程同时穿透缓存并发请求,虽然不会出错,但白白浪费资源。用一个threading.Lock成本极低,能保证只有一个线程去更新缓存,其他线程等锁结束后拿到的就是最新缓存值。

4. 最容易翻车的边界:IPv6、脏数据和受限出口

4.1 IPv4/IPv6 偏好,以及自动检测的坑

很多人的第一版模块会写成“请求任意接口,返回什么就信什么”。这个思路在双栈网络里会踩大坑。

我有一次在一条 IPv6 开通的家庭宽带下测试,某个数据源直接返回了 IPv6 地址,比如240e:1234:5678::1。我的调用方程序是以前按 IPv4 写的,截断、存储、展示全按点分十进制来,结果整条流程直接卡死。

解决方式有两个层面。第一,默认模块只认 IPv4,如果调用方明确需要 IPv6,再提供对应的数据源或参数。第二,如果数据源本身支持ipv4ipv6子域名就更好,比如 icanhazip 就有ipv4.icanhazip.comipv6.icanhazip.com,把选择权交给调用方。

我当时把接口设计成了get_public_ip(family="ipv4"),默认 IPv4,传family="ipv6"才走 IPv6 源。这样下游永远不会在不知情的情况下收到“意外格式”。

4.2 数据源返回的“不是IP”的内容

这是我在办公网踩过的一个经典坑。有一段时间模块时不时报校验失败,我一开始以为是数据源问题,后来抓响应正文才发现,出口网关把一个拦截页直接返回给了请求,内容是:

<html><body>Access Denied</body></html>

这类内容经过.strip()之后不是空字符串,正则和数字校验都会把它筛掉。很多人的模块在这里会直接抛异常,但我的设计是“校验失败等于这个数据源失败,继续等其他数据源”,所以外层表现只是偶尔慢几百毫秒,不会崩。

这也解释了为什么模块要多留几分余量:永远不要信任远程服务的返回内容。本地可以做一轮严格校验,宁可多等一个源,也不能把脏数据交给下游。

4.3 在 NAT 出口、云主机和弱网环境下的表现

模块上线后,我分别在家庭宽带、办公网和云主机上跑过。家庭宽带拿到的是运营商 NAT 出口的公共 IP,不是光猫 WAN 口那个内网地址;云主机在默认网络命名空间下拿到的是弹性公网 IP,和你在云控制台看到的一致;办公网则需要走出口网关才能触达公网服务。

这三个环境其实都在验证一件事:模块测到的是“当前网络环境出口的公网IP”,而不是“这台机器的IP”。在容器里也同理,拿到的是宿主机所在网络的出口IP。如果你的业务需要的是本机网卡 IP,应该去查本地路由表,而不是调用这个模块。

弱网环境的坑是超时。我在模块里把默认超时设成 2 秒,实际测试中,超过 2 秒回不来的数据源,哪怕最后返回了正确IP,对整体体验也几乎没有帮助。遇到全部超时的情况,模块会把所有错误信息聚合成一条异常抛出,方便排查。

5. 实测对比:单源串行 vs 多源并发,到底快多少

5.1 测试环境与结果

模块写完之后,我在三张网络上做了对照测试:家庭宽带、云主机、办公网。每个场景分别测了三种调用方式:

  • 单源串行:只请求 ipify。
  • 多源串行:按配置顺序依次请求,失败才换下一个。
  • 多源并发:本文实现的方式。

结果如下:

场景单源串行(ms)多源串行(ms)多源并发(ms)
家庭宽带8412076
云主机121230110
办公网3008(超时)1210508

最明显的是办公网那一行。ipify 在办公网下被出口网关干扰,单源直接等满 3 秒超时;多源并发因为并行了四个数据源,最终由 ip.sb 在 508ms 返回了合法结果,整体没有失败。

这个测试也说明了一个实战理念:“快”不是因为某一个接口快,而是因为多个接口竞争后,任何时刻总有一个快的能顶上。

5.2 两个调试故事:完整排查链路

第一个问题是我自己栽的:IPv6 地址混入缓存。当时在反馈里看到一条规律,只有家庭宽带的机器会出现一个怪异的长字符串。排查步骤是先看日志,确认下游收到的就是类似240e:...的 IPv6 地址;接着复现请求,确认来自某数据源;最后发现是模块没有按family参数过滤数据源,导致 IPv6 源的返回被当成了合法 IPv4 返回。修法就是前面说的,校验函数只认 IPv4,并且数据源按 family 分组。

第二个问题是同事反馈“偶尔变慢”。我一开始怀疑是线程池创建开销,但后来抓日志发现,每次变慢都是因为 ip.sb 先返回了非法内容,模块在等第二个源的过程中浪费了时间。优化方式是把校验不通过的源及时从本轮竞速中排除,并通过as_completed的返回值顺序,优先处理完成且合法的结果,而不是先到先处理。

5.3 一些实际调优经验

  • 如果调用频率很高,可以把_CACHE_TTL从 30 秒调到 60 秒甚至 300 秒。IP 变了最多晚 5 分钟感知,但对外部服务商的压力会小很多。
  • 数据源的顺序不重要,因为并发请求本身没有顺序依赖。但配置顺序最好按维护频率排,方便后续增删。
  • 不要把所有数据源都塞给同一个运营商。我见过有人选了三个接口,结果全部指向同一家 CDN,一宕全宕。挑数据源时要看背后的服务商和网络链路是否独立。

最后再啰嗦一句

如果你只是本地临时用一次,curl ifconfig.me当然没问题。但只要这段逻辑会进代码库、会被多个环境调用、会被别人长期维护,它就值得一个独立的模块身份。把校验、超时、竞速、缓存这些细节固化下来,以后不管谁接到这个需求,都不需要重新踩一遍我踩过的坑。这大概就是写工具类模块的最大价值。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 22:49:40

MFC+OpenGL CAD二次开发:经典源码实战与图形学底层解析

简介&#xff1a;这套配套源码DEMO源自王清辉、李静蓉合著的《CAD应用程序开发详解——Visual C与OpenGL综合应用》&#xff0c;面向有一定C基础、希望深入掌握CAD图形编程的开发者和工科学生。包内共710个文件&#xff0c;包括142个h头文件与131个cpp源文件、43个lib库和47个d…

作者头像 李华
网站建设 2026/9/9 22:48:50

出海营销怎么选?推荐专业海外营销获客公司

摘要&#xff1a;国内B2B制造业企业出海常面临线索流失、品牌落地难、渠道搭建低效等问题&#xff0c;传统人工营销模式难以适配海外市场节奏。本文结合工业出海场景痛点&#xff0c;讲解专业AI出海营销服务的选型逻辑&#xff0c;聚焦深耕行业的星谷云&#xff0c;解析其全链路…

作者头像 李华
网站建设 2026/9/9 22:46:59

Navicat Premium 17创建MySQL触发器保姆级教程:从概念到实战

1. 项目概述与前置准备1.1 触发器到底解决什么问题Navicat Premium 是我日常管理 MySQL、MariaDB、PostgreSQL 这些数据库的主力工具&#xff0c;尤其是图形化建库、导数据、看执行计划这几件事上&#xff0c;比敲命令行舒服太多。但最近被好几个刚入行的同事问到同一个问题&am…

作者头像 李华
网站建设 2026/9/9 22:45:26

Balls of Buma题解:字符串压缩与区间DP破解祖玛消除

最近在洛谷上翻 NERC 2019 的题目&#xff0c;看到 P12935 这道Balls of Buma&#xff0c;第一反应是“哦&#xff0c;又一个祖玛变种”。这类题在区域赛里其实不少见&#xff0c;表面是个点击消除的小游戏&#xff0c;实际是披着字符串壳子的区间 DP。整道题不需要什么高级数据…

作者头像 李华
网站建设 2026/9/9 22:44:40

WorkBuddy办公自动化实战:AI智能体驱动的高频场景全拆解

刚接手一个部门级的自动化需求时&#xff0c;我最大的感受是&#xff1a;工具其实不难找&#xff0c;难的是把日常琐碎的工作流真正串起来。文件整理要写脚本&#xff0c;发票报销要手工录入&#xff0c;周报要翻聊天记录&#xff0c;竞品分析要开十几个网页——这些事情单看都…

作者头像 李华