diff --git a/binding/golang/service/ip2region.go b/binding/golang/service/ip2region.go new file mode 100644 index 0000000..e89438d --- /dev/null +++ b/binding/golang/service/ip2region.go @@ -0,0 +1,182 @@ +// Copyright 2022 The Ip2Region Authors. All rights reserved. +// Use of this source code is governed by a Apache2.0-style +// license that can be found in the LICENSE file. + +package service + +import ( + "fmt" + "time" + + "github.com/lionsoul2014/ip2region/binding/golang/xdb" +) + +// --- +// Ip2Region service +// 1. Unified query interface to IPv4 and IPv6 address. +// 2. Concurrency search support. +// +// @Author Lion +// @Date 2025/12/05 + +type Ip2Region struct { + // v4 pool for cache policy vIndex or NoCache + v4Pool *SearcherPool + + // v4 xdb searcher for full in-memeory search + v4InMemSearcher *xdb.Searcher + + // v6 pool for cache policy vIndex or NoCache:w + v6Pool *SearcherPool + + // v6 xdb searcher for full in-memeory search + v6InMemSearcher *xdb.Searcher +} + +// create a new Ip2Region service with specified v4 and v6 config. +// set it to nil to disabled the specified search for the specified version. +func NewIp2Region(v4Config *Config, v6Config *Config) (*Ip2Region, error) { + var err error + + // check and init the v4 pool or in-memory searcher + var v4Pool *SearcherPool + var v4InMemSearcher *xdb.Searcher + if v4Config == nil { + // with IPv4 disabled ? + v4Pool = nil + v4InMemSearcher = nil + } else if v4Config.cachePolicy == BufferCache { + v4Pool = nil + v4InMemSearcher, err = xdb.NewWithBuffer(v4Config.ipVersion, v4Config.cBuffer) + if err != nil { + return nil, fmt.Errorf("failed to create v4 in-memory searcher: %w", err) + } + } else { + v4InMemSearcher = nil + v4Pool, err = NewSearcherPool(v4Config) + if err != nil { + return nil, fmt.Errorf("failed to create v4 searcher pool: %w", err) + } + } + + // check and init the v6 pool or in-memory searcher + var v6Pool *SearcherPool + var v6InMemSearcher *xdb.Searcher + if v6Config == nil { + v6Pool = nil + v6InMemSearcher = nil + } else if v6Config.cachePolicy == BufferCache { + v6Pool = nil + v6InMemSearcher, err = xdb.NewWithBuffer(v6Config.ipVersion, v6Config.cBuffer) + if err != nil { + return nil, fmt.Errorf("failed to create v6 in-memory searcher: %w", err) + } + } else { + v6InMemSearcher = nil + v6Pool, err = NewSearcherPool(v6Config) + if err != nil { + return nil, fmt.Errorf("failed to create v6 in-memeory searcher pool: %w", err) + } + } + + return &Ip2Region{ + v4Pool: v4Pool, + v4InMemSearcher: v4InMemSearcher, + + v6Pool: v6Pool, + v6InMemSearcher: v6InMemSearcher, + }, nil +} + +// create the ip2region search service with the specified v4 & v6 xdb path. +// with default cache policy VIndexCache and default searchers = 20 +func NewIp2RegionWithPath(v4XdbPath string, v6XdbPath string) (*Ip2Region, error) { + var err error + + // create v4 config with default config items + var v4Config *Config + if v4XdbPath == "" { + v4Config = nil + } else { + v4Config, err = NewV4Config(VIndexCache, v4XdbPath, 20) + if err != nil { + return nil, fmt.Errorf("failed to create v4 config: %w", err) + } + } + + // create v6 config with default config items + var v6Config *Config + if v6XdbPath == "" { + v6Config = nil + } else { + v6Config, err = NewV6Config(VIndexCache, v6XdbPath, 20) + if err != nil { + return nil, fmt.Errorf("failed to create v6 config: %w", err) + } + } + + return NewIp2Region(v4Config, v6Config) +} + +func (ip2r *Ip2Region) SearchByStr(ipStr string) (string, error) { + ipBytes, err := xdb.ParseIP(ipStr) + if err != nil { + return "", err + } + + 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 { + return ip2r.v6Search(ipBytes) + } else { + return "", fmt.Errorf("invalid byte ip address with len=%d", l) + } +} + +func (ip2r *Ip2Region) v4Search(ipBytes []byte) (string, error) { + if ip2r.v4InMemSearcher != nil { + return ip2r.v4InMemSearcher.Search(ipBytes) + } + + // v4 search is disabled + if ip2r.v4Pool == nil { + return "", nil + } + + v4Searcher := ip2r.v4Pool.BorrowSearcher() + defer ip2r.v4Pool.ReturnSearcher(v4Searcher) + return v4Searcher.Search(ipBytes) +} + +func (ip2r *Ip2Region) v6Search(ipBytes []byte) (string, error) { + if ip2r.v6InMemSearcher != nil { + return ip2r.v6InMemSearcher.Search(ipBytes) + } + + // v6 search is disabled + if ip2r.v6Pool == nil { + return "", nil + } + + v6Searcher := ip2r.v6Pool.BorrowSearcher() + defer ip2r.v6Pool.ReturnSearcher(v6Searcher) + return v6Searcher.Search(ipBytes) +} + +func (ip2r *Ip2Region) Close() { + ip2r.CloseTimeout(time.Second * 10) +} + +func (ip2r *Ip2Region) CloseTimeout(d time.Duration) { + if ip2r.v4Pool != nil { + ip2r.v4Pool.CloseTimeout(d) + } + + if ip2r.v6Pool != nil { + ip2r.v6Pool.CloseTimeout(d) + } +} diff --git a/binding/golang/service/ip2region_test.go b/binding/golang/service/ip2region_test.go new file mode 100644 index 0000000..4d84351 --- /dev/null +++ b/binding/golang/service/ip2region_test.go @@ -0,0 +1,273 @@ +// Copyright 2022 The Ip2Region Authors. All rights reserved. +// Use of this source code is governed by a Apache2.0-style +// license that can be found in the LICENSE file. + +package service + +import ( + "fmt" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/lionsoul2014/ip2region/binding/golang/xdb" +) + +func TestConfigCreate(t *testing.T) { + v4Config, err := NewV4Config(VIndexCache, "../../../data/ip2region_v4.xdb", 10) + if err != nil { + t.Fatalf("failed to create v4 config: %s", err) + } + + v6Config, err := NewV6Config(VIndexCache, "../../../data/ip2region_v6.xdb", 10) + if err != nil { + t.Fatalf("failed to create v6 config: %s", err) + } + + ip2region, err := NewIp2Region(v4Config, v6Config) + if err != nil { + t.Fatalf("failed to create ip2region service: %s", err) + } + + v4Bytes, err := xdb.ParseIP("219.133.110.197") + if err != nil { + t.Fatal("invalid ipv4 address") + } + + v6Bytes, err := xdb.ParseIP("240e:3b7:3275:f090:d2a3:7d1a:dd90:c3b6") + if err != nil { + t.Fatalf("invalid ipv6 address") + } + + for i := 0; i < 20; i++ { + v4Bytes = xdb.IPAddOne(v4Bytes) + v6Bytes = xdb.IPAddOne(v6Bytes) + v4Region, err := ip2region.Search(v4Bytes) + if err != nil { + t.Fatalf("failed to search(%s): %s", xdb.IP2String(v4Bytes), err) + } + + v6Region, err := ip2region.Search(v6Bytes) + if err != nil { + t.Fatalf("failed to search(%s): %s", xdb.IP2String(v6Bytes), err) + } + + fmt.Printf( + "%2d->search(%s)=%s, search(%s)=%s\n", + i, xdb.IP2String(v4Bytes), v4Region, xdb.IP2String(v6Bytes), v6Region, + ) + } + + ip2region.Close() + fmt.Print("ip2region closed gracefully") +} + +func TestPathCreate(t *testing.T) { + ip2region, err := NewIp2RegionWithPath("../../../data/ip2region_v4.xdb", "../../../data/ip2region_v6.xdb") + if err != nil { + t.Fatalf("failed to create ip2region with path: %s", err) + } + + v4Bytes, err := xdb.ParseIP("219.133.110.197") + if err != nil { + t.Fatal("invalid ipv4 address") + } + + v6Bytes, err := xdb.ParseIP("240e:3b7:3275:f090:d2a3:7d1a:dd90:c3b6") + if err != nil { + t.Fatalf("invalid ipv6 address") + } + + for i := 0; i < 20; i++ { + v4Bytes = xdb.IPAddOne(v4Bytes) + v6Bytes = xdb.IPAddOne(v6Bytes) + v4Region, err := ip2region.Search(v4Bytes) + if err != nil { + t.Fatalf("failed to search(%s): %s", xdb.IP2String(v4Bytes), err) + } + + v6Region, err := ip2region.Search(v6Bytes) + if err != nil { + t.Fatalf("failed to search(%s): %s", xdb.IP2String(v6Bytes), err) + } + + fmt.Printf( + "%2d->search(%s)=%s, search(%s)=%s\n", + i, xdb.IP2String(v4Bytes), v4Region, xdb.IP2String(v6Bytes), v6Region, + ) + } + + ip2region.Close() + fmt.Print("ip2region closed gracefully") +} + +func TestInMemSearch(t *testing.T) { + v4Config, err := NewV4Config(BufferCache, "../../../data/ip2region_v4.xdb", 10) + if err != nil { + t.Fatalf("failed to create v4 config: %s", err) + } + + v6Config, err := NewV6Config(BufferCache, "../../../data/ip2region_v6.xdb", 10) + if err != nil { + t.Fatalf("failed to create v6 config: %s", err) + } + + ip2region, err := NewIp2Region(v4Config, v6Config) + if err != nil { + t.Fatalf("failed to create ip2region service: %s", err) + } + + v4Bytes, err := xdb.ParseIP("219.133.110.197") + if err != nil { + t.Fatal("invalid ipv4 address") + } + + v6Bytes, err := xdb.ParseIP("240e:3b7:3275:f090:d2a3:7d1a:dd90:c3b6") + if err != nil { + t.Fatalf("invalid ipv6 address") + } + + for i := 0; i < 20; i++ { + v4Bytes = xdb.IPAddOne(v4Bytes) + v6Bytes = xdb.IPAddOne(v6Bytes) + v4Region, err := ip2region.Search(v4Bytes) + if err != nil { + t.Fatalf("failed to search(%s): %s", xdb.IP2String(v4Bytes), err) + } + + v6Region, err := ip2region.Search(v6Bytes) + if err != nil { + t.Fatalf("failed to search(%s): %s", xdb.IP2String(v6Bytes), err) + } + + fmt.Printf( + "%2d->search(%s)=%s, search(%s)=%s\n", + i, xdb.IP2String(v4Bytes), v4Region, xdb.IP2String(v6Bytes), v6Region, + ) + } + + ip2region.Close() + fmt.Print("ip2region closed gracefully") +} + +func TestV4Only(t *testing.T) { + v4Config, err := NewV4Config(NoCache, "../../../data/ip2region_v4.xdb", 10) + if err != nil { + t.Fatalf("failed to create v4 config: %s", err) + } + + ip2region, err := NewIp2Region(v4Config, nil) + if err != nil { + t.Fatalf("failed to create ip2region service: %s", err) + } + + v4Bytes, err := xdb.ParseIP("219.133.110.197") + if err != nil { + t.Fatal("invalid ipv4 address") + } + + v6Bytes, err := xdb.ParseIP("240e:3b7:3275:f090:d2a3:7d1a:dd90:c3b6") + if err != nil { + t.Fatalf("invalid ipv6 address") + } + + for i := 0; i < 10; i++ { + v4Bytes = xdb.IPAddOne(v4Bytes) + v6Bytes = xdb.IPAddOne(v6Bytes) + v4Region, err := ip2region.Search(v4Bytes) + if err != nil { + t.Fatalf("failed to search(%s): %s", xdb.IP2String(v4Bytes), err) + } + + v6Region, err := ip2region.Search(v6Bytes) + if err != nil { + t.Fatalf("failed to search(%s): %s", xdb.IP2String(v6Bytes), err) + } + + fmt.Printf( + "%2d->search(%s)=%s, search(%s)=%s\n", + i, xdb.IP2String(v4Bytes), v4Region, xdb.IP2String(v6Bytes), v6Region, + ) + } + + ip2region.Close() + fmt.Print("ip2region closed gracefully") +} + +func TestConcurrentCall(t *testing.T) { + v4Config, err := NewV4Config(VIndexCache, "../../../data/ip2region_v4.xdb", 15) + if err != nil { + t.Fatalf("failed to create v4 config: %s", err) + } + + v6Config, err := NewV6Config(VIndexCache, "../../../data/ip2region_v6.xdb", 15) + if err != nil { + t.Fatalf("failed to create v6 config: %s", err) + } + + v4Bytes, err := xdb.ParseIP("219.133.110.197") + if err != nil { + t.Fatal("invalid ipv4 address") + } + + v6Bytes, err := xdb.ParseIP("240e:3b7:3275:f090:d2a3:7d1a:dd90:c3b6") + if err != nil { + t.Fatalf("invalid ipv6 address") + } + + fmt.Printf("v4Config: %s\n", v4Config) + fmt.Printf("v6Config: %s\n", v6Config) + ip2region, err := NewIp2Region(v4Config, v6Config) + if err != nil { + t.Fatalf("failed to create ip2region service: %s", err) + } + + coroutines := 100 + var wg sync.WaitGroup + var count int64 = 0 + tStart := time.Now() + for i := 0; i < coroutines; i++ { + wg.Add(1) + go func() { + var ipBytes []byte + for i := 0; i < 5000; i++ { + if i%2 == 0 { + ipBytes = v4Bytes + } else { + ipBytes = v6Bytes + } + + region, err := ip2region.Search(ipBytes) + if err != nil { + fmt.Printf("Error: failed to search(%s): %s", xdb.IP2String(ipBytes), err) + break + } + + if l := len(ipBytes); l == 4 { + if region != "中国|广东省|深圳市|电信" { + fmt.Print("Error: region not equals") + break + } + } else { + if region != "中国|广东省|深圳市|家庭宽带" { + fmt.Print("Error: region not equals") + break + } + } + + atomic.AddInt64(&count, 1) + } + + // mark all searches finished for this coroutine + wg.Done() + }() + } + + // wait for all the searches to finished + wg.Wait() + costs := time.Since(tStart) + fmt.Printf("%d searches finished in %s, avg took: %s\n", count, costs, costs/time.Duration(count)) + ip2region.Close() + fmt.Print("ip2region closed gracefully") +}