254 lines
6.5 KiB
Markdown
254 lines
6.5 KiB
Markdown
# 外网 IP 获取工具技术文档
|
||
|
||
```markdown
|
||
# 外网 IP 获取工具(External IP Getter)
|
||
|
||
一个基于多协议尝试获取外网公网 IP 地址的 Python 工具,支持 NAT-PMP、UPnP 以及第三方网页抓取方式。通过多进程隔离输出流的方式安全地获取 IP 地址。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
- [功能概述](#功能概述)
|
||
- [依赖库](#依赖库)
|
||
- [核心模块与函数说明](#核心模块与函数说明)
|
||
- [`pmp_get_external_ip()`](#pmp_get_external_ip)
|
||
- [`upnp_get_external_ip()`](#upnp_get_external_ip)
|
||
- [`ipgetter_get_external_ip()`](#ipgetter_get_external_ip)
|
||
- [`get_external_ip()`](#get_external_ip)
|
||
- [`outip(w)`](#outipw)
|
||
- [`get_ip()`](#get_ip)
|
||
- [`run()`](#run)
|
||
- [主程序入口](#主程序入口)
|
||
- [使用示例](#使用示例)
|
||
- [注意事项](#注意事项)
|
||
- [未来优化建议](#未来优化建议)
|
||
|
||
---
|
||
|
||
## 功能概述
|
||
|
||
本工具旨在可靠地获取当前设备所在的公网 IP 地址,采用多种协议逐级回退策略:
|
||
|
||
1. **NAT-PMP**:适用于支持该协议的路由器(如部分 Apple 设备)。
|
||
2. **UPnP IGD**:通用即插即用互联网网关设备协议,广泛用于家庭路由器。
|
||
3. **IPGetter 网页抓取**:当本地网络协议不可用时,通过访问公网服务获取 IP。
|
||
|
||
为避免跨进程通信中的阻塞和异常影响主流程,使用 `multiprocessing.Pipe` 和子进程隔离执行 IP 获取逻辑。
|
||
|
||
---
|
||
|
||
## 依赖库
|
||
|
||
| 库名 | 用途 |
|
||
|------|------|
|
||
| `natpmp` | 实现 NAT-PMP 协议以获取公网 IP |
|
||
| `upnpclient` | 发现并调用 UPnP IGD 服务 |
|
||
| `appPublic.ipgetter` | 第三方 IP 获取器(基于网页抓取) |
|
||
| `multiprocessing` | 多进程支持,用于隔离 IO 操作 |
|
||
| `os`, `time` | 系统操作与延时控制 |
|
||
|
||
> ⚠️ 安装命令:
|
||
```bash
|
||
pip install natpmp upnpclient app-public
|
||
```
|
||
|
||
---
|
||
|
||
## 核心模块与函数说明
|
||
|
||
### `pmp_get_external_ip()`
|
||
|
||
尝试通过 **NAT-PMP** 协议获取公网 IP。
|
||
|
||
#### 返回值
|
||
- 成功时返回字符串格式的公网 IP(如 `"203.0.113.45"`)
|
||
- 失败或异常时返回 `None`
|
||
|
||
#### 示例
|
||
```python
|
||
ip = pmp_get_external_ip()
|
||
if ip:
|
||
print("NAT-PMP 获取成功:", ip)
|
||
```
|
||
|
||
---
|
||
|
||
### `upnp_get_external_ip()`
|
||
|
||
通过 **UPnP IGD**(Internet Gateway Device)协议从路由器获取公网 IP。
|
||
|
||
#### 流程说明
|
||
1. 调用 `upnpclient.discover()` 自动发现局域网内支持 UPnP 的网关设备。
|
||
2. 提取第一个设备,并查找包含 `'WAN'` 和 `'Conn'` 的服务名(通常是 WANIPConnection 或类似)。
|
||
3. 调用 `GetExternalIPAddress()` 方法获取外部 IP。
|
||
4. 解析响应中 `NewExternalIPAddress` 字段。
|
||
|
||
#### 返回值
|
||
- 成功时返回公网 IP 字符串
|
||
- 出错时打印错误信息并返回 `None`
|
||
|
||
> 📌 注意:若有多台 UPnP 设备,请注意选择正确的设备实例。
|
||
|
||
---
|
||
|
||
### `ipgetter_get_external_ip()`
|
||
|
||
使用第三方网页服务获取公网 IP,作为最后兜底方案。
|
||
|
||
#### 特点
|
||
- 使用 `appPublic.ipgetter.IPgetter` 类进行 HTTP 请求抓取。
|
||
- 内置重试机制,每 0.1 秒尝试一次直到成功。
|
||
- 忽略请求过程中的所有异常。
|
||
|
||
#### 返回值
|
||
- 成功获取后立即返回 IP 字符串
|
||
- 不会永久阻塞
|
||
|
||
---
|
||
|
||
### `get_external_ip()`
|
||
|
||
**主协调函数**:按优先级顺序尝试三种方法获取 IP。
|
||
|
||
#### 执行顺序
|
||
1. NAT-PMP
|
||
2. UPnP
|
||
3. Web 抓取(IPGetter)
|
||
|
||
一旦任一方法成功即返回结果,形成“短路”逻辑。
|
||
|
||
#### 返回值
|
||
- 公网 IP 字符串(推荐始终检查非空)
|
||
- 若全部失败,则返回 `None`
|
||
|
||
---
|
||
|
||
### `outip(w)`
|
||
|
||
在独立进程中运行的函数,将获取到的 IP 输出到指定写入端 `w`。
|
||
|
||
#### 参数
|
||
- `w`: 可写的文件描述符管道(来自 `Pipe()` 的写入端)
|
||
|
||
#### 原理
|
||
- 使用 `os.dup2(w.fileno(), 1)` 将标准输出重定向至管道
|
||
- 调用 `get_external_ip()` 并通过 `print(ip)` 输出
|
||
- 子进程结束后,父进程可通过读取管道获得结果
|
||
|
||
> ✅ 优点:避免复杂对象传递,利用文本流简化 IPC。
|
||
|
||
---
|
||
|
||
### `get_ip()`
|
||
|
||
主进程调用此函数来启动子进程并安全获取 IP。
|
||
|
||
#### 步骤
|
||
1. 创建匿名管道 `Pipe()`
|
||
2. 打开读取端为文件对象
|
||
3. 启动子进程执行 `outip(w)`
|
||
4. 读取子进程输出的第一行
|
||
5. 等待子进程结束
|
||
6. 返回去除了换行符的 IP 字符串
|
||
|
||
#### 返回值
|
||
- 成功:公网 IP 字符串(如 `"8.8.8.8"`)
|
||
- 失败:空字符串(需判断有效性)
|
||
|
||
---
|
||
|
||
### `run()`
|
||
|
||
持续运行的测试循环函数。
|
||
|
||
#### 行为
|
||
- 每 10 秒调用一次 `get_ip()`
|
||
- 若获取到 IP,则打印:`ip='203.0.113.45'`
|
||
- 使用 `time.sleep(10)` 控制频率
|
||
|
||
可用于长期监控公网 IP 变化。
|
||
|
||
---
|
||
|
||
## 主程序入口
|
||
|
||
```python
|
||
if __name__ == '__main__':
|
||
run()
|
||
```
|
||
|
||
当脚本直接运行时,进入无限循环模式,每隔 10 秒输出当前公网 IP。
|
||
|
||
---
|
||
|
||
## 使用示例
|
||
|
||
### 单次获取 IP
|
||
```python
|
||
from your_module import get_ip
|
||
|
||
ip = get_ip()
|
||
print("Current Public IP:", ip)
|
||
```
|
||
|
||
### 集成进其他应用
|
||
可将 `get_external_ip()` 直接导入使用,无需多进程封装:
|
||
|
||
```python
|
||
from your_module import get_external_ip
|
||
|
||
public_ip = get_external_ip()
|
||
if public_ip:
|
||
print(f"Public IP is {public_ip}")
|
||
else:
|
||
print("Failed to retrieve public IP")
|
||
```
|
||
|
||
---
|
||
|
||
## 注意事项
|
||
|
||
1. **权限要求**
|
||
- UPnP/NAT-PMP 需要设备处于同一局域网且路由器启用相应功能。
|
||
- 某些防火墙可能阻止 SSDP(UPnP 发现协议)广播包。
|
||
|
||
2. **性能与延迟**
|
||
- `ipgetter_get_external_ip()` 依赖网络请求,可能较慢。
|
||
- 推荐仅作为备用手段。
|
||
|
||
3. **稳定性问题**
|
||
- `upnpclient.discover()` 可能因网络波动返回空列表,导致索引错误。
|
||
- 建议添加超时处理或重试逻辑。
|
||
|
||
4. **多进程开销**
|
||
- `get_ip()` 每次创建新进程,不适合高频调用(如 >1次/秒)。
|
||
|
||
5. **输出重定向风险**
|
||
- `os.dup2` 修改了子进程的标准输出,确保只在子进程中使用。
|
||
|
||
---
|
||
|
||
## 未来优化建议
|
||
|
||
| 改进项 | 描述 |
|
||
|--------|------|
|
||
| 添加超时机制 | 给每个 IP 获取方法设置最大等待时间 |
|
||
| 缓存最近 IP | 减少重复查询开销 |
|
||
| 支持更多协议 | 如 PCP(Port Control Protocol) |
|
||
| 异常日志记录 | 替代 `print()`,使用 `logging` 模块 |
|
||
| 配置化策略 | 允许用户配置启用哪些协议及顺序 |
|
||
| 异步支持 | 使用 `asyncio` + `aiohttp` 提升效率 |
|
||
|
||
---
|
||
|
||
## 许可证
|
||
|
||
请根据项目实际情况补充 LICENSE 信息。
|
||
|
||
> 示例:MIT License
|
||
```
|
||
|
||
---
|
||
|
||
✅ 文档完成。可保存为 `README.md` 或集成至 Sphinx/Docusaurus 文档系统。 |