更新 2026年8月16日入门

IP 属地是怎么查出来的?本地查询与在线查询方案对比

科普 IP 属地查询的原理:IP 段与归属地的映射、纯本地离线查询为何能做到毫秒级,对比 ip2region、在线 API 与 MaxMind 三种方案的优劣,并附登录记录场景的 Go 实战与 IPv6 盲点提醒。

IP网络科普
IP 属地是怎么查出来的?本地查询与在线查询方案对比

写在前面

很多网站会在「登录记录」「安全中心」里显示你的登录 IP 归属地,比如「福建·福州 · 中国移动」。你可能好奇过:服务器是怎么根据一个 IP 就判断出我在哪个城市、哪个运营商的?

这篇科普文章讲清楚三件事:

  1. IP 属地到底查的是什么;
  2. 纯本地查询为什么能做到毫秒级且不联网;
  3. 几种主流查询方案的优缺点对比,方便你按场景选型。

说明:文中用到的 IP 均为 RFC 5737 保留的文档示例段(TEST-NET-2,198.51.100.0/24),不会对应任何真实地址。

先想清楚:IP 属地查的是什么

一个常见误区是「每个 IP 对应一个具体地址」。实际上 IP 属地的本质是 IP 段与归属地的映射

运营商和机构在申请 IP 时,是按网段(CIDR)成块分配的。比如 198.51.100.0/24 这一个段,一整块都归属同一地区、同一运营商。所以归属地数据天然可以按段存储

IP 起始          IP 结束         归属地
────────────────────────────────────────
198.51.100.0     198.51.100.255  中国|福建|福州|移动
198.51.101.0     198.51.101.255  中国|浙江|杭州|电信

全球公网 IP 约有 42 亿个,但网段只有几十万条。正因为存的是「段」而不是「每个 IP」,完整的归属地数据文件才只有几 MB(ip2region 的 .xdb 约 11MB)。这是「本地离线查询」可行的第一前提。

所以,IP 属地从来不是精确到街道的定位,它返回的是段级的「省|市|运营商」粗粒度信息。

纯本地查询的原理

以开源的 ip2region 为例,它的查询完全在本地完成,不联网、不请求第三方,几十微秒到毫秒级返回。

第一步:把 IP 变成整数

198.51.100.42 为例,它会转成一个 32 位整数 0xC633642A。IP 的四个数字本质就是 4 个字节,拼起来就是一个整数,这一步没有任何玄学。

第二步:二分查找

在按起始 IP 排序的段列表里做二分查找,找到满足 段的起始IP ≤ 目标整数 ≤ 段的结束IP 的那一段。

二分查找的复杂度是 O(log N),N 是几十万条,所以很快——这就是「不靠网络也能毫秒返回」的核心。

第三步:.xdb 文件的两级结构

ip2region 的 .xdb 不是简单顺序文件,内部分两个区,进一步加速:

┌─────────────────────────────┐
│  索引区 (Index)             │  // 每段一条定长记录:startIP / endIP / 数据区指针 + 数据长度
│  …… 二分快速定位段 ……       │
├─────────────────────────────┤
│  数据区 (Data)              │  // 真正存放各段的地点字符串
│  ……                         │
└─────────────────────────────┘

先二分索引区定位到「是哪一段」,再按指针去数据区读出地点字符串,一次磁盘读就能拿到结果。

为什么「离线数据」不会很快过期

IP 归属取决于注册机构和运营商的分配,一个网段通常长期稳定属于某地区、某运营商。所以:

  • 下载一次数据文件,可以用很久;
  • 归属偶尔变化时,每月更新一次 .xdb 文件即可(替换文件、不用重启服务,见下文实战);
  • 数据源来自 IANA/APNIC 等机构的分配表与公开归属信息,不是实时探测。

主流查询方案对比

实际开发中,查询 IP 属地主要有三条路线。

方案一:ip2region(纯本地离线,推荐)

// Go 中的用法(github.com/lionsoul2014/ip2region/binding/golang)
region, err := searcher.SearchByStr(ip)
// 返回类似 "中国|福建省|福州市|移动"
优点缺点
免费、无网络依赖、无 QPS 限制段级粒度,个别段可能显示到相邻地区
毫秒级查询,永不因外部服务挂掉数据按月更新,最新分配的段可能短暂缺失
数据文件可随版本更新,不重启只返回省市区+运营商,无经纬度;仅支持 IPv4

适用:博客/后台的「登录记录显示归属地」这类展示用途。

方案二:在线 API(ip-api / 高德 / 腾讯位置服务)

// 以 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" 时同样降级
优点缺点
数据最新、可能更精确有频率限制(免费版限 QPS)
部分服务还返回经纬度/时区依赖第三方,断网/被墙/限流时拿不到
无需维护数据文件每次查询都要发一次 HTTP 请求

适用:对精度要求高、且能接受外部依赖与限流的场景。务必加超时和降级,否则第三方一抖,你自己的页面就卡住。

方案三:MaxMind GeoLite2(离线 .mmdb)

优点缺点
全球数据,覆盖广、精度较好免费但需注册账号并遵守 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. 进程启动时加载数据文件

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. 查询一次

func ipRegion(ip string) string {
    region, err := searcher.Load().SearchByStr(ip)
    if err != nil {
        return "未知"
    }
    return region // "中国|福建省|福州市|移动"
}

3. 加一层内存缓存

登录记录里同一个 IP 可能反复出现,用 sync.Map 按 IP 缓存结果,一次查询终身复用:

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 每月发布新数据。下载新文件后重新 NewWithBufferStore 覆盖即可,进程完全不用重启:

func reloadSearcher(newContent []byte) error {
    s, err := xdb.NewWithBuffer(newContent)
    if err != nil {
        return err
    }
    searcher.Store(s) // 原子切换,老查询继续走旧数据,新查询走新数据
    return nil
}

局限与诚实提醒

无论用哪种方案,都要清楚 IP 属地的边界:

  • 段级精度:网段归属是「绝大多数」准确,个别 IP(大型企业自建段、跨省调度段)可能显示到相邻地区或只到省。
  • 运营商级,不是定位:拿到的是「省|市|运营商」,不是精确街道或基站位置——精确位置需要 GPS 或运营商三角定位,纯 IP 归属做不到。
  • 动态 IP:家宽通常是动态 IP,重拨会变,所以「上次登录在福州,这次在广州」不一定是异常,可能是 IP 变了。
  • 别用于安全判定:用 IP 段做封禁或风控判定风险自负,段级数据不适合当精确依据。
  • 隐私与合规:IP 地址属于个人信息(《个人信息保护法》)。归属地展示建议只出现在用户本人可见的页面(登录记录、个人中心);若在评论区等公开场合展示,建议只显示到省市并明确告知用户。

小结

一句话总结:

IP 属地 = 几十万条「IP 段 → 归属地」映射 + 一次二分查找。 它查的是网段归属,不是精确位置。

  • 想省心、免费、稳定,用 ip2region 本地离线查询(注意 IPv6 盲点);
  • 想要最新最细、能接受外部依赖,用在线 API(记得超时降级);
  • 面向全球用户、要 IPv6 覆盖,用 MaxMind GeoLite2

对个人博客「登录记录显示归属地」这种场景,本地离线方案通常是性价比最高的选择。

评论