diff --git a/examples/ipquery/main.go b/examples/ipquery/main.go new file mode 100644 index 0000000..449e405 --- /dev/null +++ b/examples/ipquery/main.go @@ -0,0 +1,47 @@ +// ipquery 包示例:基于 ip2region xdb 的 IP/域名归属地查询 +// 运行: go run . [IP或域名] +package main + +import ( + "fmt" + "os" + + "git.zeroonesoft.cn/golib/zogo/ipquery" +) + +func main() { + if len(os.Args) < 2 { + fmt.Println("用法: ipquery [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) + } +} diff --git a/ipquery/README.md b/ipquery/README.md new file mode 100644 index 0000000..a207205 --- /dev/null +++ b/ipquery/README.md @@ -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 . [IP或域名]`,xdb 从 ip2region 项目下载) + +## 注意 + +- **必须先 `LoadIp`**,未加载直接 Query 会失败。 +- 全内存加载(xdb 数十 MB 起),内存敏感场景注意;数据文件需自行定期更新。 diff --git a/ipquery/ip.go b/ipquery/ip.go new file mode 100644 index 0000000..16d7193 --- /dev/null +++ b/ipquery/ip.go @@ -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 +} diff --git a/ipquery/ip_test.go b/ipquery/ip_test.go new file mode 100644 index 0000000..1410c9b --- /dev/null +++ b/ipquery/ip_test.go @@ -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("无字母数字的字符串应判定非法") + } +}