Merge pull request #466 from lionsoul2014/opt_golang_search_api

Opt golang search api
This commit is contained in:
Leon / 狮子的魂 2026-04-04 18:43:38 +08:00 committed by GitHub
commit 4d391acac2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 205 additions and 42 deletions

View File

@ -39,8 +39,8 @@ if err != nil {
}
// 4. Export the ip2region service for concurrent dual-version IP address queries, for example:
v4Region, err := ip2region.SearchByStr("113.92.157.29") // Perform IPv4 query
v6Region, err := ip2region.SearchByStr("240e:3b7:3272:d8d0:db09:c067:8d59:539e") // Perform IPv6 query
v4Region, err := ip2region.Search("113.92.157.29") // Perform IPv4 query
v6Region, err := ip2region.Search("240e:3b7:3272:d8d0:db09:c067:8d59:539e") // Perform IPv6 query
// 5. When the parent service needs to be closed, close the ip2region query service as well
@ -60,8 +60,7 @@ ip2region.Close()
The location information query API prototypes are:
```go
SearchByStr(string) (string, error)
Search([]byte) (string, error)
Search(string | []byte) (string, error)
```
If a query fails, the `error` will contain specific error details. If successful, it returns the `region` string. If the specified IP cannot be found, it returns an empty string `""`.
@ -121,9 +120,9 @@ func main() {
var ip = "1.2.3.4" // IPv4
// ip = "240e:3b7:3272:d8d0:db09:c067:8d59:539e" // IPv6
var tStart = time.Now()
region, err := searcher.SearchByStr(ip)
region, err := searcher.Search(ip)
if err != nil {
fmt.Printf("failed to SearchIP(%s): %s\n", ip, err)
fmt.Printf("failed to Search(%s): %s\n", ip, err)
return
}

View File

@ -36,8 +36,8 @@ if err != nil {
}
// 4导出 ip2region 服务进行双版本的IP地址的并发查询例如
v4Region, err := ip2region.SearchByStr("113.92.157.29") // 进行 IPv4 查询
v6Region, err := ip2region.SearchByStr("240e:3b7:3272:d8d0:db09:c067:8d59:539e") // 进行 IPv6 查询
v4Region, err := ip2region.Search("113.92.157.29") // 进行 IPv4 查询
v6Region, err := ip2region.Search("240e:3b7:3272:d8d0:db09:c067:8d59:539e") // 进行 IPv6 查询
// 5在服务需要关闭的时候同时关闭 ip2region 查询服务
@ -55,8 +55,7 @@ ip2region.Close()
### 关于查询 API
定位信息查询 API 原型为:
```go
SearchByStr(string) (string, error)
Search([]byte) (string, error)
Search(string | []byte) (string, error)
```
查询出错则 error 会包含具体的错误信息,查询成功会返回字符串的 `region` 信息,如果指定的 IP 查询不到则会返回空字符串 `""`
@ -111,9 +110,9 @@ func main() {
var ip = "1.2.3.4" // IPv4
// ip = "240e:3b7:3272:d8d0:db09:c067:8d59:539e" // IPv6
var tStart = time.Now()
region, err := searcher.SearchByStr(ip)
region, err := searcher.Search(ip)
if err != nil {
fmt.Printf("failed to SearchIP(%s): %s\n", ip, err)
fmt.Printf("failed to Search(%s): %s\n", ip, err)
return
}

View File

@ -1,5 +1,5 @@
module github.com/lionsoul2014/ip2region/binding/golang
go 1.17
go 1.18
require github.com/mitchellh/go-homedir v1.1.0

View File

@ -239,7 +239,7 @@ type 'quit' to exit
}
tStart := time.Now()
region, err := ip2region.SearchByStr(line)
region, err := ip2region.Search(line)
if err != nil {
fmt.Printf("\x1b[0;31m{err: %s}\x1b[0m\n", err.Error())
} else {
@ -339,7 +339,13 @@ func testBench() {
return
}
for _, ip := range [][]byte{sip, eip} {
mip, err := xdb.IPMiddle(sip, eip)
if err != nil {
fmt.Printf("IPMiddle(%s,%s): %s", xdb.IP2String(sip), xdb.IP2String(eip), err)
return
}
for _, ip := range [][]byte{sip, mip, eip} {
sTime := time.Now()
region, err := searcher.Search(ip)
if err != nil {

View File

@ -118,16 +118,21 @@ func NewIp2RegionWithPath(v4XdbPath string, v6XdbPath string) (*Ip2Region, error
return NewIp2Region(v4Config, v6Config)
}
func (ip2r *Ip2Region) SearchByStr(ipStr string) (string, error) {
ipBytes, err := xdb.ParseIP(ipStr)
func (ip2r *Ip2Region) Search(ip any) (string, error) {
var err error
var ipBytes []byte
switch v := ip.(type) {
case string:
ipBytes, err = xdb.ParseIP(v)
if err != nil {
return "", err
return "", fmt.Errorf("parse ip %s: %w", v, err)
}
case []byte:
ipBytes = v
default:
return "", fmt.Errorf("invalid ip value type %s", v)
}
return ip2r.Search(ipBytes)
}
func (ip2r *Ip2Region) Search(ipBytes []byte) (string, error) {
if l := len(ipBytes); l == 4 {
return ip2r.v4Search(ipBytes)
} else if l == 16 {

View File

@ -23,7 +23,7 @@ func TestV4SearcherPool(t *testing.T) {
ipString := "219.133.110.197"
for i := 0; i < 20; i++ {
searcher := searcherPool.BorrowSearcher()
region, err := searcher.SearchByStr(ipString)
region, err := searcher.Search(ipString)
if err != nil {
t.Fatalf("failed to search(%s): %s", ipString, err)
}
@ -53,7 +53,7 @@ func TestV6SearcherPool(t *testing.T) {
ipString := "240e:3b7:3275:f090:d2a3:7d1a:dd90:c3b6"
for i := 0; i < 20; i++ {
searcher := searcherPool.BorrowSearcher()
region, err := searcher.SearchByStr(ipString)
region, err := searcher.Search(ipString)
if err != nil {
t.Fatalf("failed to search(%s): %s", ipString, err)
}

View File

@ -89,20 +89,24 @@ func (s *Searcher) GetIOCount() int {
return s.ioCount
}
// SearchByStr find the region for the specified ip string
func (s *Searcher) SearchByStr(str string) (string, error) {
ip, err := ParseIP(str)
// Search the region for the specified string or bytes ip address
func (s *Searcher) Search(ip any) (string, error) {
var err error
var ipBytes []byte
switch v := ip.(type) {
case string:
ipBytes, err = ParseIP(v)
if err != nil {
return "", err
return "", fmt.Errorf("parse ip %s: %w", v, err)
}
case []byte:
ipBytes = v
default:
return "", fmt.Errorf("invalid ip value type %s", v)
}
return s.Search(ip)
}
// Search find the region for the specified long ip
func (s *Searcher) Search(ip []byte) (string, error) {
// ip version check
if len(ip) != s.version.Bytes {
if len(ipBytes) != s.version.Bytes {
return "", fmt.Errorf("invalid ip address(%s expected)", s.version.Name)
}
@ -110,7 +114,7 @@ func (s *Searcher) Search(ip []byte) (string, error) {
s.ioCount = 0
// locate the segment index block based on the vector index
var il0, il1 = int(ip[0]), int(ip[1])
var il0, il1 = int(ipBytes[0]), int(ipBytes[1])
var idx = il0*VectorIndexCols*VectorIndexSize + il1*VectorIndexSize
var sPtr, ePtr = uint32(0), uint32(0)
if s.vectorIndex != nil {
@ -139,7 +143,7 @@ func (s *Searcher) Search(ip []byte) (string, error) {
}
// binary search the segment index to get the region
var bytes, dBytes = len(ip), len(ip) << 1
var bytes, dBytes = len(ipBytes), len(ipBytes) << 1
var segIndexSize = uint32(s.version.SegmentIndexSize)
var dataLen, dataPtr = 0, uint32(0)
var buff = make([]byte, segIndexSize)
@ -153,9 +157,9 @@ func (s *Searcher) Search(ip []byte) (string, error) {
}
// decode the data step by step to reduce the unnecessary operations
if s.version.IPCompare(ip, buff[0:bytes]) < 0 {
if s.version.IPCompare(ipBytes, buff[0:bytes]) < 0 {
h = m - 1
} else if s.version.IPCompare(ip, buff[bytes:dBytes]) > 0 {
} else if s.version.IPCompare(ipBytes, buff[bytes:dBytes]) > 0 {
l = m + 1
} else {
dataLen = int(binary.LittleEndian.Uint16(buff[dBytes:]))
@ -171,7 +175,7 @@ func (s *Searcher) Search(ip []byte) (string, error) {
// load and return the region data
var regionBuff = make([]byte, dataLen)
err := s.read(int64(dataPtr), regionBuff)
err = s.read(int64(dataPtr), regionBuff)
if err != nil {
return "", fmt.Errorf("read region at %d: %w", dataPtr, err)
}

View File

@ -82,6 +82,59 @@ func IPSubOne(ip []byte) []byte {
return r
}
// IPSub Sub the spcecified two byte ip
func IPSub(sip, eip []byte) ([]byte, error) {
if len(sip) != len(eip) {
return []byte{}, fmt.Errorf("length of the two ips are not the same")
}
var carry uint16 = 0
var result = make([]byte, len(sip)+1)
for i := len(sip) - 1; i >= 0; i-- {
sum := uint16(sip[i]) + uint16(eip[i]) + carry
result[i+1] = byte(sum) // Store standard 8-bit result
carry = sum >> 8 // Extract the 1-bit carry for the next byte
}
// check and append the carry
if carry > 0 {
result[0] = byte(carry)
return result, nil
} else {
return result[1:], nil
}
}
// IPHalf get the half value of an input byte ip
func IPHalf(ip []byte) []byte {
var length = len(ip)
var result = make([]byte, length)
// Tracks the bit falling off from the previous byte
var carry byte = 0
for i := 0; i < length; i++ {
// 1. Shift current byte right by 1
// 2. Or (|) with the carry from the previous byte (shifted to the MSB position)
result[i] = (ip[i] >> 1) | (carry << 7)
// 3. Capture the Least Significant Bit (LSB) to use as carry for the next byte
carry = ip[i] & 1
}
return result
}
// IPMiddle get the middle value of two input ip address
func IPMiddle(sip, eip []byte) ([]byte, error) {
buf, err := IPSub(sip, eip)
if err != nil {
return []byte{}, fmt.Errorf("IPSub(%s, %s): %w", IP2String(sip), IP2String(eip), err)
}
return IPHalf(buf), nil
}
// Verify if the current Searcher could be used to search the specified xdb file.
// Why do we need this check ?
// The future features of the xdb impl may cause the current searcher not able to work properly.

View File

@ -9,6 +9,7 @@
package xdb
import (
"encoding/binary"
"fmt"
"testing"
"time"
@ -41,6 +42,102 @@ func TestIPCompare(t *testing.T) {
}
}
func TestIPSub(t *testing.T) {
var strToSub = "1.2.3.4"
bytesToSub, err := ParseIP(strToSub)
if err != nil {
t.Fatalf("failed to parse ip %s", strToSub)
}
var intToSub = int(binary.BigEndian.Uint32(bytesToSub))
t.Logf("to sub ip: %d -> %s", intToSub, strToSub)
counter := 0
buf := make([]byte, 4)
for i := 0; i < 0x2FFFFFFF; i++ {
binary.BigEndian.PutUint32(buf, uint32(i))
subVal, err := IPSub(buf, bytesToSub)
if err != nil {
t.Fatalf("failed to IPSub(%s,%s): %s", IP2String(buf), strToSub, err)
}
// do it as two integers
byteSub := int(binary.BigEndian.Uint32(subVal))
intSub := i + intToSub
if byteSub != intSub {
t.Fatal("byte and int sub value are not the same")
}
counter++
}
t.Logf("test done with %d ips", counter)
}
func TestIPHalf(t *testing.T) {
var buf = make([]byte, 4)
for i := 0; i < 0xFFFFFFFF; i++ {
binary.BigEndian.PutUint32(buf, uint32(i))
half := IPHalf(buf)
// do it as two integers
byteMiddle := binary.BigEndian.Uint32(half)
intMidle := i >> 1
if byteMiddle != uint32(intMidle) {
t.Fatal("byte middle and int middle are not the same")
}
}
}
func TestSubOverflow(t *testing.T) {
var ip1Str = "255.255.255.250"
ip1Bytes, err := ParseIP(ip1Str)
if err != nil {
t.Fatalf("failed to ParseIP(%s): %s", ip1Str, err)
}
var buff = make([]byte, 4)
for i := 0; i < 10; i++ {
binary.BigEndian.PutUint32(buff, uint32(i))
ipSub, err := IPSub(ip1Bytes, buff)
if err != nil {
t.Fatalf("failed to IPSub(%s, %s): %s", ip1Str, IP2String(buff), err)
}
t.Logf("IPSub(%s, %s) = %+v", ip1Str, IP2String(buff), ipSub)
}
}
func TestIPMiddle(t *testing.T) {
var sIPStr = "0.0.0.0"
sBytes, err := ParseIP(sIPStr)
if err != nil {
t.Fatalf("failed to parse ip %s", sIPStr)
}
var sInt = int(binary.BigEndian.Uint32(sBytes))
t.Logf("start ip: %d -> %s", sInt, sIPStr)
counter := 0
buf := make([]byte, 4)
for i := 0; i < 0x0FFFFFFF; i++ {
binary.BigEndian.PutUint32(buf, uint32(i))
midVal, err := IPMiddle(sBytes, buf)
if err != nil {
t.Fatalf("failed to IPMiddle(%s,%s): %s", sIPStr, IP2String(buf), err)
}
// do it as two integers
byteMid := int(binary.BigEndian.Uint32(midVal))
intMid := (sInt + i) >> 1
if byteMid != intMid {
t.Fatal("byte and int middle value are not the same")
}
counter++
}
t.Logf("test done with %d ips", counter)
}
func TestLoadVectorIndex(t *testing.T) {
vIndex, err := LoadVectorIndexFromFile("../../../data/ip2region_v4.xdb")
if err != nil {