diff --git a/binding/golang/README.md b/binding/golang/README.md index f949fe3..98792a1 100644 --- a/binding/golang/README.md +++ b/binding/golang/README.md @@ -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 } diff --git a/binding/golang/README_zh.md b/binding/golang/README_zh.md index f924966..121f0af 100644 --- a/binding/golang/README_zh.md +++ b/binding/golang/README_zh.md @@ -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 } diff --git a/binding/golang/go.mod b/binding/golang/go.mod index 130ba4c..704b599 100644 --- a/binding/golang/go.mod +++ b/binding/golang/go.mod @@ -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 \ No newline at end of file +require github.com/mitchellh/go-homedir v1.1.0 diff --git a/binding/golang/main.go b/binding/golang/main.go index 8d1d200..4e39333 100644 --- a/binding/golang/main.go +++ b/binding/golang/main.go @@ -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 { diff --git a/binding/golang/service/ip2region.go b/binding/golang/service/ip2region.go index e89438d..2120c6f 100644 --- a/binding/golang/service/ip2region.go +++ b/binding/golang/service/ip2region.go @@ -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) - if err != nil { - return "", err +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 "", 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 { diff --git a/binding/golang/service/searcher_pool_test.go b/binding/golang/service/searcher_pool_test.go index af2503c..af93ad3 100644 --- a/binding/golang/service/searcher_pool_test.go +++ b/binding/golang/service/searcher_pool_test.go @@ -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) } diff --git a/binding/golang/xdb/searcher.go b/binding/golang/xdb/searcher.go index eb46b3d..4c5172b 100644 --- a/binding/golang/xdb/searcher.go +++ b/binding/golang/xdb/searcher.go @@ -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) - if err != nil { - return "", err +// 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 "", 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) } diff --git a/binding/golang/xdb/util.go b/binding/golang/xdb/util.go index 4b48113..6df4813 100644 --- a/binding/golang/xdb/util.go +++ b/binding/golang/xdb/util.go @@ -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. diff --git a/binding/golang/xdb/util_test.go b/binding/golang/xdb/util_test.go index 75696a7..930fcbc 100644 --- a/binding/golang/xdb/util_test.go +++ b/binding/golang/xdb/util_test.go @@ -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 {