docs: 完善 IP 属地文章(IPv6 盲点、Go 实战、合规提醒与封面)
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1067" height="600" viewBox="0 0 1067 600">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#f6f4fb"/>
|
||||
<stop offset="1" stop-color="#e9e4f5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="chip" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#7c5cff"/>
|
||||
<stop offset="1" stop-color="#5b3fd4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="1067" height="600" fill="url(#bg)"/>
|
||||
<text x="533" y="86" text-anchor="middle" font-family="sans-serif" font-size="40" font-weight="700" fill="#3b3550">IP 属地是怎么查出来的?</text>
|
||||
<text x="533" y="128" text-anchor="middle" font-family="sans-serif" font-size="24" fill="#8a84a0">IP 段 → 二分查找 → 省|市|运营商</text>
|
||||
|
||||
<!-- IP 输入 -->
|
||||
<rect x="96" y="190" width="240" height="76" rx="16" fill="url(#chip)"/>
|
||||
<text x="216" y="218" text-anchor="middle" font-family="monospace" font-size="24" font-weight="700" fill="#ffffff">198.51.100.42</text>
|
||||
<text x="216" y="248" text-anchor="middle" font-family="sans-serif" font-size="16" fill="#e6e0ff">IP 地址</text>
|
||||
|
||||
<!-- 箭头1 -->
|
||||
<path d="M356 228 h64" stroke="#7c5cff" stroke-width="4" fill="none" marker-end="none"/>
|
||||
<path d="M412 222 l12 6 l-12 6 z" fill="#7c5cff"/>
|
||||
|
||||
<!-- 二进制 -->
|
||||
<rect x="428" y="196" width="252" height="64" rx="16" fill="#ffffff" stroke="#d8d2ec" stroke-width="3"/>
|
||||
<text x="554" y="224" text-anchor="middle" font-family="monospace" font-size="20" font-weight="700" fill="#5b5568">0xC633642A</text>
|
||||
<text x="554" y="248" text-anchor="middle" font-family="sans-serif" font-size="15" fill="#8a84a0">32 位整数(4 字节拼接)</text>
|
||||
|
||||
<!-- 箭头2 -->
|
||||
<path d="M700 228 h64" stroke="#7c5cff" stroke-width="4" fill="none"/>
|
||||
<path d="M756 222 l12 6 l-12 6 z" fill="#7c5cff"/>
|
||||
|
||||
<!-- 段表 -->
|
||||
<g>
|
||||
<rect x="776" y="176" width="220" height="104" rx="14" fill="#ffffff" stroke="#d8d2ec" stroke-width="3"/>
|
||||
<text x="886" y="204" text-anchor="middle" font-family="monospace" font-size="16" fill="#8a84a0">100.0 ~ 100.255</text>
|
||||
<rect x="790" y="216" width="192" height="26" rx="8" fill="#7c5cff" opacity="0.16"/>
|
||||
<text x="886" y="234" text-anchor="middle" font-family="monospace" font-size="16" font-weight="700" fill="#5b3fd4">100.0 ~ 100.255 ✓</text>
|
||||
<text x="886" y="262" text-anchor="middle" font-family="monospace" font-size="16" fill="#8a84a0">101.0 ~ 101.255</text>
|
||||
<text x="886" y="280" text-anchor="middle" font-family="sans-serif" font-size="13" fill="#8a84a0">二分查找 O(log N)</text>
|
||||
</g>
|
||||
|
||||
<!-- 结果 -->
|
||||
<rect x="308" y="360" width="452" height="110" rx="18" fill="#ffffff" stroke="#7c5cff" stroke-width="4"/>
|
||||
<text x="534" y="406" text-anchor="middle" font-family="sans-serif" font-size="26" font-weight="700" fill="#5b3fd4">中国|福建|福州|移动</text>
|
||||
<text x="534" y="440" text-anchor="middle" font-family="sans-serif" font-size="16" fill="#8a84a0">段级归属地(不是精确定位)</text>
|
||||
|
||||
<!-- 底部提示 -->
|
||||
<text x="533" y="540" text-anchor="middle" font-family="sans-serif" font-size="18" fill="#a49cc0">几十万条「IP 段 → 归属地」映射 · 本地毫秒级查询 · 注意 IPv6 盲点</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -1,8 +1,9 @@
|
||||
---
|
||||
title: IP 属地是怎么查出来的?本地查询与在线查询方案对比
|
||||
published: 2026-08-07T12:00:00
|
||||
updated: 2026-08-07
|
||||
description: 科普 IP 属地查询的原理:IP 段与归属地的映射、纯本地离线查询为何能做到毫秒级,并对比 ip2region、在线 API 与 MaxMind 三种方案的优劣与适用场景。
|
||||
updated: 2026-08-16
|
||||
description: 科普 IP 属地查询的原理:IP 段与归属地的映射、纯本地离线查询为何能做到毫秒级,对比 ip2region、在线 API 与 MaxMind 三种方案的优劣,并附登录记录场景的 Go 实战与 IPv6 盲点提醒。
|
||||
image: /posts/ip-geolocation-guide/img/cover.svg
|
||||
tags: ['IP', '网络', '科普']
|
||||
draft: false
|
||||
difficulty: 入门
|
||||
@@ -57,7 +58,7 @@ ip2region 的 `.xdb` 不是简单顺序文件,内部分两个区,进一步
|
||||
|
||||
```text
|
||||
┌─────────────────────────────┐
|
||||
│ 索引区 (Index) │ // 每段一条:startIP / endIP / 数据位置
|
||||
│ 索引区 (Index) │ // 每段一条定长记录:startIP / endIP / 数据区指针 + 数据长度
|
||||
│ …… 二分快速定位段 …… │
|
||||
├─────────────────────────────┤
|
||||
│ 数据区 (Data) │ // 真正存放各段的地点字符串
|
||||
@@ -72,7 +73,7 @@ ip2region 的 `.xdb` 不是简单顺序文件,内部分两个区,进一步
|
||||
IP 归属取决于**注册机构和运营商的分配**,一个网段通常长期稳定属于某地区、某运营商。所以:
|
||||
|
||||
- 下载一次数据文件,可以用很久;
|
||||
- 归属偶尔变化时,**每月更新一次 `.xdb` 文件**即可(替换文件、不用重启服务);
|
||||
- 归属偶尔变化时,**每月更新一次 `.xdb` 文件**即可(替换文件、不用重启服务,见下文实战);
|
||||
- 数据源来自 IANA/APNIC 等机构的分配表与公开归属信息,不是实时探测。
|
||||
|
||||
## 主流查询方案对比
|
||||
@@ -91,14 +92,23 @@ region, err := searcher.SearchByStr(ip)
|
||||
|------|------|
|
||||
| 免费、无网络依赖、无 QPS 限制 | 段级粒度,个别段可能显示到相邻地区 |
|
||||
| 毫秒级查询,永不因外部服务挂掉 | 数据按月更新,最新分配的段可能短暂缺失 |
|
||||
| 数据文件可随版本更新,不重启 | 只返回省市区+运营商,无经纬度 |
|
||||
| 数据文件可随版本更新,不重启 | 只返回省市区+运营商,无经纬度;**仅支持 IPv4** |
|
||||
|
||||
**适用**:博客/后台的「登录记录显示归属地」这类展示用途。
|
||||
|
||||
### 方案二:在线 API(ip-api / 高德 / 腾讯位置服务)
|
||||
|
||||
```http
|
||||
GET https://ipapi.co/198.51.100.42/json/
|
||||
```go
|
||||
// 以 ip-api.com 为例(免费版约 45 次/分钟,非商用)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", "https://ip-api.com/json/198.51.100.42", nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "未知" // 断网/超时降级:展示「未知」而不是报错
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
// 解析 JSON,status != "success" 时同样降级
|
||||
```
|
||||
|
||||
| 优点 | 缺点 |
|
||||
@@ -107,18 +117,104 @@ GET https://ipapi.co/198.51.100.42/json/
|
||||
| 部分服务还返回经纬度/时区 | 依赖第三方,断网/被墙/限流时拿不到 |
|
||||
| 无需维护数据文件 | 每次查询都要发一次 HTTP 请求 |
|
||||
|
||||
**适用**:对精度要求高、且能接受外部依赖与限流的场景。
|
||||
**适用**:对精度要求高、且能接受外部依赖与限流的场景。**务必加超时和降级**,否则第三方一抖,你自己的页面就卡住。
|
||||
|
||||
### 方案三:MaxMind GeoLite2(离线 .mmdb)
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 全球数据,覆盖广、精度较好 | 需要注册账号下载,商用需授权 |
|
||||
| 标准 `.mmdb` 格式,各语言都有库 | 国内 IP 的省市粒度有时不如 ip2region 细 |
|
||||
| 全球数据,覆盖广、精度较好 | 免费但需注册账号并遵守 EULA;商用需 GeoIP2 商业授权 |
|
||||
| 标准 `.mmdb` 格式,各语言都有库,支持 IPv6 | 国内 IP 的省市粒度有时不如 ip2region 细 |
|
||||
| 离线查询 | 数据同样要定期更新 |
|
||||
|
||||
**适用**:面向全球用户的场景。
|
||||
|
||||
## IPv6:本地离线方案的一个盲点
|
||||
|
||||
前面聊的都是 IPv4(32 位)。但家宽和移动网络越来越多地**只下发 IPv6**(128 位)地址,而 **ip2region 目前只支持 IPv4**——拿到一个纯 IPv6 地址是查不出归属地的,本地库会返回空结果。
|
||||
|
||||
实际影响与常见应对:
|
||||
|
||||
1. **判断版本**:Go 里 `net.ParseIP(ip).To4() != nil` 就是 IPv4,先判断再查询;
|
||||
2. **降级展示**:IPv6 直接显示「IPv6 地址」,不显示属地,诚实第一;
|
||||
3. **优先取 IPv4**:有反向代理时,从 `X-Forwarded-For` 里挑一个 IPv4 来查;
|
||||
4. **需要 v6 归属**:换支持 IPv6 的数据源(如 MaxMind GeoLite2)。
|
||||
|
||||
这一点在接入时很容易漏掉,上线前记得拿一个纯 IPv6 地址回归一遍。
|
||||
|
||||
## 实战:给登录记录加 IP 属地
|
||||
|
||||
subLog 博客的登录记录就用了 ip2region。完整接入一共四步。
|
||||
|
||||
### 1. 进程启动时加载数据文件
|
||||
|
||||
```go
|
||||
import (
|
||||
"sync/atomic"
|
||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||
)
|
||||
|
||||
// 用 atomic.Pointer 持有查询器,方便后面热更新
|
||||
var searcher atomic.Pointer[xdb.Searcher]
|
||||
|
||||
func initSearcher() error {
|
||||
content, err := os.ReadFile("./data/ip2region.xdb") // 约 11MB,一次性读入内存
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, err := xdb.NewWithBuffer(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
searcher.Store(s)
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 查询一次
|
||||
|
||||
```go
|
||||
func ipRegion(ip string) string {
|
||||
region, err := searcher.Load().SearchByStr(ip)
|
||||
if err != nil {
|
||||
return "未知"
|
||||
}
|
||||
return region // "中国|福建省|福州市|移动"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 加一层内存缓存
|
||||
|
||||
登录记录里同一个 IP 可能反复出现,用 `sync.Map` 按 IP 缓存结果,一次查询终身复用:
|
||||
|
||||
```go
|
||||
var regionCache sync.Map
|
||||
|
||||
func ipRegionCached(ip string) string {
|
||||
if v, ok := regionCache.Load(ip); ok {
|
||||
return v.(string)
|
||||
}
|
||||
region := ipRegion(ip)
|
||||
regionCache.Store(ip, region)
|
||||
return region
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 数据热更新(不用重启)
|
||||
|
||||
ip2region 每月发布新数据。下载新文件后重新 `NewWithBuffer` 并 `Store` 覆盖即可,进程完全不用重启:
|
||||
|
||||
```go
|
||||
func reloadSearcher(newContent []byte) error {
|
||||
s, err := xdb.NewWithBuffer(newContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
searcher.Store(s) // 原子切换,老查询继续走旧数据,新查询走新数据
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## 局限与诚实提醒
|
||||
|
||||
无论用哪种方案,都要清楚 IP 属地的边界:
|
||||
@@ -127,6 +223,7 @@ GET https://ipapi.co/198.51.100.42/json/
|
||||
- **运营商级,不是定位**:拿到的是「省|市|运营商」,不是精确街道或基站位置——精确位置需要 GPS 或运营商三角定位,纯 IP 归属做不到。
|
||||
- **动态 IP**:家宽通常是动态 IP,重拨会变,所以「上次登录在福州,这次在广州」不一定是异常,可能是 IP 变了。
|
||||
- **别用于安全判定**:用 IP 段做封禁或风控判定风险自负,段级数据不适合当精确依据。
|
||||
- **隐私与合规**:IP 地址属于个人信息(《个人信息保护法》)。归属地展示建议只出现在**用户本人可见**的页面(登录记录、个人中心);若在评论区等公开场合展示,建议只显示到省市并明确告知用户。
|
||||
|
||||
## 小结
|
||||
|
||||
@@ -134,8 +231,8 @@ GET https://ipapi.co/198.51.100.42/json/
|
||||
|
||||
> **IP 属地 = 几十万条「IP 段 → 归属地」映射 + 一次二分查找。** 它查的是网段归属,不是精确位置。
|
||||
|
||||
- 想省心、免费、稳定,用 **ip2region** 本地离线查询;
|
||||
- 想要最新最细、能接受外部依赖,用**在线 API**;
|
||||
- 面向全球用户,用 **MaxMind GeoLite2**。
|
||||
- 想省心、免费、稳定,用 **ip2region** 本地离线查询(注意 IPv6 盲点);
|
||||
- 想要最新最细、能接受外部依赖,用**在线 API**(记得超时降级);
|
||||
- 面向全球用户、要 IPv6 覆盖,用 **MaxMind GeoLite2**。
|
||||
|
||||
对个人博客「登录记录显示归属地」这种场景,本地离线方案通常是性价比最高的选择。
|
||||
|
||||
Reference in New Issue
Block a user