feat(ipquery): 自 go-hua 迁入 IP/域名归属地查询
- 基于 ip2region xdb 全内存加载, 新增依赖 lionsoul2014/ip2region - 附测试、examples/ipquery 例程与包 README(含 xdb 获取说明)
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
// ipquery 包示例:基于 ip2region xdb 的 IP/域名归属地查询
|
||||||
|
// 运行: go run . <ip2region.xdb 路径> [IP或域名]
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.zeroonesoft.cn/golib/zogo/ipquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Println("用法: ipquery <ip2region.xdb文件路径> [IP或域名]")
|
||||||
|
fmt.Println("xdb 下载: https://github.com/lionsoul2014/ip2region")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dbPath := os.Args[1]
|
||||||
|
|
||||||
|
// 加载整个 xdb 到内存(进程内只需执行一次,之后可并发查询)
|
||||||
|
if err := ipquery.LoadIp(dbPath); err != nil {
|
||||||
|
fmt.Println("加载失败:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := "114.114.114.114"
|
||||||
|
if len(os.Args) > 2 {
|
||||||
|
ip = os.Args[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询详情
|
||||||
|
data, err := ipquery.Query(ip)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("查询失败:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("IP: %s 国家: %s 区域: %s 省份: %s 城市: %s 运营商: %s\n",
|
||||||
|
data.Ip, data.Country, data.Area, data.Province, data.City, data.Isp)
|
||||||
|
|
||||||
|
// 查询简述(中国 IP 返回“省份城市 运营商”,国外返回“国家省份 运营商”)
|
||||||
|
fmt.Println("简述:", ipquery.QueryString(ip))
|
||||||
|
|
||||||
|
// IP 字符串转 uint32
|
||||||
|
if num, err := ipquery.StringToUint32("192.168.1.1"); err == nil {
|
||||||
|
fmt.Printf("192.168.1.1 -> %d\n", num)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# ipquery
|
||||||
|
|
||||||
|
基于 [ip2region](https://github.com/lionsoul2014/ip2region) xdb 数据文件的
|
||||||
|
IP/域名归属地查询:整个 xdb 加载进内存,进程内只加载一次后可并发查询。
|
||||||
|
|
||||||
|
> 迁移自 go-hua/ipquery,代码保持原样。
|
||||||
|
|
||||||
|
## 用法
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "git.zeroonesoft.cn/golib/zogo/ipquery"
|
||||||
|
|
||||||
|
// 加载 xdb(进程内一次即可)
|
||||||
|
if err := ipquery.LoadIp("D:/data/ip2region.xdb"); err != nil { ... }
|
||||||
|
|
||||||
|
// 字符串结果("国家|区域|省份|城市|ISP")
|
||||||
|
fmt.Println(ipquery.QueryString("114.114.114.114"))
|
||||||
|
|
||||||
|
// 结构化结果(Query 支持域名,内部先解析)
|
||||||
|
d, err := ipquery.Query("www.baidu.com")
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
ipquery.StringToUint32("114.114.114.114") // uint32
|
||||||
|
ipquery.RegexpDns("www.example.com") // 域名格式校验
|
||||||
|
```
|
||||||
|
|
||||||
|
完整可运行例程:[examples/ipquery/main.go](../examples/ipquery/main.go)
|
||||||
|
(`go run . <xdb路径> [IP或域名]`,xdb 从 ip2region 项目下载)
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
- **必须先 `LoadIp`**,未加载直接 Query 会失败。
|
||||||
|
- 全内存加载(xdb 数十 MB 起),内存敏感场景注意;数据文件需自行定期更新。
|
||||||
+149
@@ -0,0 +1,149 @@
|
|||||||
|
// Package ipquery 基于 ip2region xdb 数据文件的 IP/域名归属地查询。
|
||||||
|
package ipquery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cBuff []byte
|
||||||
|
|
||||||
|
// LoadIp 加载整个 xdb 数据文件到内存(进程内只需调用一次,之后可并发查询)
|
||||||
|
func LoadIp(dbPath string) error {
|
||||||
|
// 1、从 dbPath 加载整个 xdb 到内存
|
||||||
|
var err error
|
||||||
|
cBuff, err = xdb.LoadContentFromFile(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data 归属地查询结果
|
||||||
|
type Data struct {
|
||||||
|
Ip string `json:"ip"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
Area string `json:"area"`
|
||||||
|
Province string `json:"province"`
|
||||||
|
City string `json:"city"`
|
||||||
|
Isp string `json:"isp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegexpDns 匹配域名
|
||||||
|
func RegexpDns(str string) error {
|
||||||
|
reg, err := regexp.Compile(`[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z]{0,62})\.?`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s := reg.FindAllString(str, -1)
|
||||||
|
if len(s) > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("不是域名")
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryString 查询归属地简述
|
||||||
|
// 中国 IP 返回"省份城市 运营商",国外 IP 返回"国家省份 运营商"
|
||||||
|
func QueryString(ip string) string {
|
||||||
|
data, err := Query(ip)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if data.Country == "中国" {
|
||||||
|
return fmt.Sprintf("%s%s %s", data.Province, data.City, data.Isp)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s%s %s", data.Country, data.Province, data.Isp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query 查询 IP 或域名的归属地详情(域名会先解析为 IP)
|
||||||
|
func Query(ip string) (Data, error) {
|
||||||
|
ipData := Data{}
|
||||||
|
|
||||||
|
_, err := xdb.CheckIP(ip)
|
||||||
|
if err != nil {
|
||||||
|
err = RegexpDns(ip)
|
||||||
|
if err != nil {
|
||||||
|
return ipData, errors.New("不是IP或域名")
|
||||||
|
}
|
||||||
|
ipAddr, err := net.ResolveIPAddr("ip", ip)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ipData, errors.New("域名无法解析")
|
||||||
|
}
|
||||||
|
ipData.Ip = ipAddr.String()
|
||||||
|
} else {
|
||||||
|
ipData.Ip = ip
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2、用全局的 cBuff 创建完全基于内存的查询对象。
|
||||||
|
// 备注:并发使用,每个 goroutine 需要创建一个独立的 searcher 对象。
|
||||||
|
searcher, err := xdb.NewWithBuffer(cBuff)
|
||||||
|
if err != nil {
|
||||||
|
return ipData, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer searcher.Close()
|
||||||
|
|
||||||
|
// 进行搜索
|
||||||
|
//var tStart = time.Now()
|
||||||
|
region, err := searcher.SearchByStr(ipData.Ip)
|
||||||
|
if err != nil {
|
||||||
|
return ipData, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//fmt.Printf("位置: %s, 耗时: %s\n", region, time.Since(tStart))
|
||||||
|
if region == "" {
|
||||||
|
return ipData, errors.New("搜索结果是空的")
|
||||||
|
}
|
||||||
|
|
||||||
|
arr := strings.Split(region, "|")
|
||||||
|
if len(arr) < 5 {
|
||||||
|
return ipData, errors.New("搜索结果字段不足")
|
||||||
|
}
|
||||||
|
|
||||||
|
ipData.Country = arr[0]
|
||||||
|
ipData.Area = arr[1]
|
||||||
|
ipData.Province = arr[2]
|
||||||
|
ipData.City = arr[3]
|
||||||
|
ipData.Isp = arr[4]
|
||||||
|
|
||||||
|
if ipData.Country == "0" {
|
||||||
|
ipData.Country = ""
|
||||||
|
}
|
||||||
|
if ipData.Area == "0" {
|
||||||
|
ipData.Area = ""
|
||||||
|
}
|
||||||
|
if ipData.Province == "0" {
|
||||||
|
ipData.Province = ""
|
||||||
|
}
|
||||||
|
if ipData.City == "0" {
|
||||||
|
ipData.City = ""
|
||||||
|
}
|
||||||
|
if ipData.Isp == "0" {
|
||||||
|
ipData.Isp = ""
|
||||||
|
}
|
||||||
|
return ipData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringToUint32 将 IPv4 字符串转为 uint32 数值
|
||||||
|
func StringToUint32(ipStr string) (uint32, error) {
|
||||||
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip == nil {
|
||||||
|
//fmt.Println("Invalid IP address")
|
||||||
|
return 0, errors.New("Invalid IP address")
|
||||||
|
}
|
||||||
|
|
||||||
|
ipBytes := ip.To4()
|
||||||
|
if ipBytes == nil {
|
||||||
|
//fmt.Println("Not an IPv4 address")
|
||||||
|
return 0, errors.New("Not an IPv4 address")
|
||||||
|
}
|
||||||
|
|
||||||
|
ipNum := (uint32(ipBytes[0]) << 24) + (uint32(ipBytes[1]) << 16) + (uint32(ipBytes[2]) << 8) + uint32(ipBytes[3])
|
||||||
|
//fmt.Println("IP address as number:", ipNum)
|
||||||
|
return ipNum, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package ipquery
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestStringToUint32(t *testing.T) {
|
||||||
|
// 192.168.1.1 = 0xC0A80101
|
||||||
|
got, err := StringToUint32("192.168.1.1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("StringToUint32: %v", err)
|
||||||
|
}
|
||||||
|
if got != 3232235777 {
|
||||||
|
t.Errorf("StringToUint32(192.168.1.1) = %d, want 3232235777", got)
|
||||||
|
}
|
||||||
|
if got2, _ := StringToUint32("0.0.0.0"); got2 != 0 {
|
||||||
|
t.Errorf("StringToUint32(0.0.0.0) = %d, want 0", got2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringToUint32Invalid(t *testing.T) {
|
||||||
|
if _, err := StringToUint32("not-an-ip"); err == nil {
|
||||||
|
t.Error("非法字符串应返回错误")
|
||||||
|
}
|
||||||
|
if _, err := StringToUint32("::1"); err == nil {
|
||||||
|
t.Error("IPv6 无 IPv4 映射应返回错误")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegexpDns(t *testing.T) {
|
||||||
|
if err := RegexpDns("www.baidu.com"); err != nil {
|
||||||
|
t.Errorf("合法域名不应报错: %v", err)
|
||||||
|
}
|
||||||
|
if err := RegexpDns("sub.domain.example.org"); err != nil {
|
||||||
|
t.Errorf("合法域名不应报错: %v", err)
|
||||||
|
}
|
||||||
|
if err := RegexpDns("不是域名!!!"); err == nil {
|
||||||
|
t.Error("无字母数字的字符串应判定非法")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user