// Package iploc 提供离线 IP 归属地解析(省/市/运营商)。 // 底层数据使用 ip2region 的 xdb(v4) 数据库,通过 go:embed 直接打进二进制, // 无需外部文件、无需联网,单次查询为内存二分查找,性能稳定。 package iploc import ( _ "embed" "encoding/binary" "net" "strings" ) //go:embed ip2region.xdb var xdbContent []byte // xdb v4 文件结构常量(与官方 C binding 保持一致)。 const ( headerInfoLength = 256 // 头部信息长度 vectorIndexCols = 256 // 向量索引列数 vectorIndexSize = 8 // 每条向量索引 8 字节(起始 ptr + 结束 ptr) v4IndexSize = 14 // IPv4 段索引块:4(IP 起) + 4(IP 止) + 2(数据长度) + 4(数据偏移) ) // Locate 解析 IPv4 的归属地,返回形如 "广东 深圳 电信" 的可读字符串。 // 内部/无效/解析失败的 IP 返回空字符串(调用方据此决定展示策略)。 // 备注:当前仅支持 IPv4;IPv6 直接返回空,避免误判。 func Locate(ipStr string) string { ip := net.ParseIP(ipStr) if ip == nil { return "" } v4 := ip.To4() if v4 == nil { // 暂不支持 IPv6 归属地解析,返回空。 return "" } // v4 已是网络序 4 字节 [a,b,c,d],按大端整型比较即可与段索引块中的起止 IP 对齐。 ipUint := binary.BigEndian.Uint32(v4) // 1) 由前两个字节定位向量索引,得到段索引区间 [sPtr, ePtr]。 il0 := int(v4[0]) il1 := int(v4[1]) idx := il0*vectorIndexCols*vectorIndexSize + il1*vectorIndexSize vOff := headerInfoLength + idx if vOff+8 > len(xdbContent) { return "" } sPtr := binary.LittleEndian.Uint32(xdbContent[vOff : vOff+4]) ePtr := binary.LittleEndian.Uint32(xdbContent[vOff+4 : vOff+8]) if sPtr == 0 || ePtr == 0 { // 该前缀段无数据,归属地未知。 return "" } // 2) 在段索引区间中二分查找命中 IP 的段(段索引块按起始 IP 有序)。 l, h := 0, int((ePtr-sPtr)/v4IndexSize) dataPtr, dataLen := uint32(0), uint16(0) for l <= h { m := (l + h) >> 1 p := sPtr + uint32(m)*v4IndexSize if int(p)+v4IndexSize > len(xdbContent) { break } startIP := binary.BigEndian.Uint32(xdbContent[p : p+4]) endIP := binary.BigEndian.Uint32(xdbContent[p+4 : p+8]) switch { case ipUint < startIP: h = m - 1 case ipUint > endIP: l = m + 1 default: // 命中:数据长度在偏移 8(小端 uint16),数据偏移在偏移 10(小端 uint32)。 dataLen = binary.LittleEndian.Uint16(xdbContent[p+8 : p+10]) dataPtr = binary.LittleEndian.Uint32(xdbContent[p+10 : p+14]) l, h = 0, -1 // 退出循环 } } if dataLen == 0 { return "" } if int(dataPtr)+int(dataLen) > len(xdbContent) { return "" } return cleanRegion(string(xdbContent[dataPtr : dataPtr+uint32(dataLen)])) } // cleanRegion 将 "国家|区域|省份|城市|运营商" 中的 0/空 段剔除,拼接为可读归属地。 func cleanRegion(raw string) string { parts := strings.Split(raw, "|") out := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p == "" || p == "0" { continue } out = append(out, p) } return strings.Join(out, " ") }