Files
nl-blogs/server/utils/ip.go
2026-01-20 14:31:39 +08:00

175 lines
4.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package utils
import (
"log"
"net"
"os"
"path/filepath"
"sync"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
)
var (
ipBuff []byte // 全局保存 xdb 文件内容,加载后只读,天然线程安全
once sync.Once // 确保只初始化一次
)
// InitIP2Region 初始化 ip2region
// 需要 ip2region.xdb 文件如果不存在则仅支持基本IP解析
// 如果 dbPath 为空,将自动从环境变量或可执行文件目录查找
func InitIP2Region(dbPath string) {
once.Do(func() {
var err error
var finalPath string
// 1. 如果传入了路径,直接使用
if dbPath != "" {
finalPath = dbPath
} else {
// 2. 优先从环境变量读取
finalPath = os.Getenv("IP2REGION_DB_PATH")
if finalPath == "" {
// 3. 尝试在可执行文件同级目录查找
execPath, err := os.Executable()
if err == nil {
execDir := filepath.Dir(execPath)
candidatePath := filepath.Join(execDir, "ip2region.xdb")
if _, err := os.Stat(candidatePath); err == nil {
finalPath = candidatePath
}
}
// 4. 如果还没找到,尝试当前工作目录(兼容开发环境)
if finalPath == "" {
candidatePath := "./ip2region.xdb"
if _, err := os.Stat(candidatePath); err == nil {
finalPath = candidatePath
}
}
}
}
// 如果最终路径为空,说明找不到文件
if finalPath == "" {
log.Printf("IP2Region database file not found. Region lookup will be disabled.")
return
}
// 5. 尝试加载整个xdb到内存
cBuff, err := xdb.LoadContentFromFile(finalPath)
if err != nil {
log.Printf("Failed to load ip2region.xdb from %s: %v. Region lookup will be disabled.", finalPath, err)
return
}
// 验证 cBuff 是否有效
if cBuff == nil || len(cBuff) == 0 {
log.Printf("Invalid ip2region.xdb buffer (nil or empty) from %s. Region lookup will be disabled.", finalPath)
return
}
// 验证 buffer 长度是否合理
if len(cBuff) < 1024 {
log.Printf("ip2region.xdb buffer too small (%d bytes) from %s. Region lookup will be disabled.", len(cBuff), finalPath)
return
}
// 赋值给全局变量
ipBuff = cBuff
log.Printf("Loaded ip2region.xdb buffer: %d bytes from %s", len(ipBuff), finalPath)
log.Printf("IP2Region loaded successfully from %s", finalPath)
})
}
// GetRegion 获取IP归属地
// 返回格式: 国家|区域|省份|城市|ISP
func GetRegion(ip string) string {
// 过滤内网IP
if isPrivateIP(ip) {
return "Internal"
}
// 检查数据是否已加载
if len(ipBuff) == 0 {
return "Unknown"
}
// 核心改进:每次请求创建一个新的 Searcher 对象
// xdb.NewWithBuffer 只是引用了 ipBuff并没有发生内存拷贝所以创建速度极快且开销很小。
// 这样做彻底避免了多个 goroutine 共用同一个 searcher 对象可能导致的内部状态并发问题。
// 根据官方文档,第一个参数应该是 version (xdb.IPv4 或 xdb.IPv6),而不是 nil
searcher, err := xdb.NewWithBuffer(xdb.IPv4, ipBuff)
if err != nil {
log.Printf("Failed to create searcher for IP %s: %v", ip, err)
return "Unknown"
}
// 检查 searcher 是否为 nil防御性编程
// 即使 err == nilsearcher 也可能为 nil需要显式检查
if searcher == nil {
log.Printf("Searcher is nil for IP %s (err was nil, ipBuff length: %d)", ip, len(ipBuff))
return "Unknown"
}
// 注意searcher 是局部变量,用完即毁,无需 Close如果是基于 buffer 创建的)
// 安全调用 SearchByStr
// 依然保留 recover 保护,防止库内部处理特殊 IP 字符串时发生 Panic
var region string
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in searcher.SearchByStr for IP %s: %v", ip, r)
region = "Unknown"
}
}()
region, err = searcher.SearchByStr(ip)
}()
if err != nil {
log.Printf("Error searching region for IP %s: %v", ip, err)
return "Unknown"
}
if region == "" {
return "Unknown"
}
return region
}
// 简单判断内网IP
func isPrivateIP(ipStr string) bool {
ip := net.ParseIP(ipStr)
if ip == nil {
return false // 不是有效IP
}
if ip.IsLoopback() {
return true
}
ip4 := ip.To4()
if ip4 == nil {
return false // 暂不处理IPv6内网判断
}
// 10.0.0.0/8
if ip4[0] == 10 {
return true
}
// 172.16.0.0/12
if ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31 {
return true
}
// 192.168.0.0/16
if ip4[0] == 192 && ip4[1] == 168 {
return true
}
return false
}