diff --git a/binding/csharp/.editorconfig b/binding/csharp/.editorconfig new file mode 100644 index 0000000..9ff4bca --- /dev/null +++ b/binding/csharp/.editorconfig @@ -0,0 +1,25 @@ +root = true + +# To learn more about .editorconfig see https://aka.ms/editorconfigdocs +[*] +charset = utf-8 +indent_style = space +trim_trailing_whitespace = true +insert_final_newline = true +spelling_exclusion_path = .\exclusion.dic + +[*.csproj] +charset = utf-8 +insert_final_newline = true + +[*.{xml,config,csproj,nuspec,props,resx,targets,yml,tasks,json}] +indent_size = 2 + +[*.sh] +end_of_line = lf + +[*.cs] +csharp_style_namespace_declarations = file_scoped:silent + +[*.cs] +file_header_template = Copyright 2025 The Ip2Region Authors. All rights reserved.\nUse of this source code is governed by a Apache2.0-style\nlicense that can be found in the LICENSE file.\n@Author Alan \n@Date 2023/07/25\nUpdated by Argo Zhang at 2025/11/21 diff --git a/binding/csharp/IP2Region.Net.BenchMark/Benmarks.cs b/binding/csharp/IP2Region.Net.BenchMark/Benmarks.cs new file mode 100644 index 0000000..eabf368 --- /dev/null +++ b/binding/csharp/IP2Region.Net.BenchMark/Benmarks.cs @@ -0,0 +1,50 @@ +// Copyright 2025 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. +// @Author Alan +// @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 + +using BenchmarkDotNet.Attributes; +using IP2Region.Net.XDB; + +namespace IP2Region.Net.BenchMark; + +public class Benchmarks +{ + private static readonly string XdbPathV4 = Path.Combine(AppContext.BaseDirectory, "IP2Region", "ip2region_v4.xdb"); + private static readonly string XdbPathV6 = Path.Combine(AppContext.BaseDirectory, "IP2Region", "ip2region_v6.xdb"); + private static readonly Searcher _contentV4Searcher = new(CachePolicy.Content, XdbPathV4); + private static readonly Searcher _vectorV4Searcher = new(CachePolicy.VectorIndex, XdbPathV4); + private static readonly Searcher _fileV4Searcher = new(CachePolicy.File, XdbPathV4); + private static readonly Searcher _contentV6Searcher = new(CachePolicy.Content, XdbPathV6); + private static readonly Searcher _vectorV6Searcher = new(CachePolicy.VectorIndex, XdbPathV6); + private static readonly Searcher _fileV6Searcher = new(CachePolicy.File, XdbPathV6); + + private readonly string _testIPv4Address = "114.114.114.114"; + private readonly string _testIPv6Address = "240e:3b7:3272:d8d0:db09:c067:8d59:539e"; + + [Benchmark] + [BenchmarkCategory("IPv4")] + public void ContentIPv4() => _contentV4Searcher.Search(_testIPv4Address); + + [Benchmark] + [BenchmarkCategory("IPv4")] + public void VectorIPv4() => _vectorV4Searcher.Search(_testIPv4Address); + + [Benchmark] + [BenchmarkCategory("IPv4")] + public void FileIPv4() => _fileV4Searcher.Search(_testIPv4Address); + + [Benchmark] + [BenchmarkCategory("IPv6")] + public void ContentIPv6() => _contentV6Searcher.Search(_testIPv6Address); + + [Benchmark] + [BenchmarkCategory("IPv6")] + public void VectorIPv6() => _vectorV6Searcher.Search(_testIPv6Address); + + [Benchmark] + [BenchmarkCategory("IPv6")] + public void FileIPv6() => _fileV6Searcher.Search(_testIPv6Address); +} diff --git a/binding/csharp/IP2Region.Net.BenchMark/IP2Region.Net.BenchMark.csproj b/binding/csharp/IP2Region.Net.BenchMark/IP2Region.Net.BenchMark.csproj index 8d4f436..4060806 100644 --- a/binding/csharp/IP2Region.Net.BenchMark/IP2Region.Net.BenchMark.csproj +++ b/binding/csharp/IP2Region.Net.BenchMark/IP2Region.Net.BenchMark.csproj @@ -2,14 +2,14 @@ Exe - net9.0 + net10.0 enable enable - - + + @@ -21,6 +21,10 @@ IP2Region/ip2region_v4.xdb PreserveNewest - + + IP2Region/ip2region_v6.xdb + PreserveNewest + + diff --git a/binding/csharp/IP2Region.Net.BenchMark/Program.cs b/binding/csharp/IP2Region.Net.BenchMark/Program.cs index b079b8a..49bae6f 100644 --- a/binding/csharp/IP2Region.Net.BenchMark/Program.cs +++ b/binding/csharp/IP2Region.Net.BenchMark/Program.cs @@ -1,29 +1,11 @@ -using BenchmarkDotNet.Attributes; +// Copyright 2025 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. +// @Author Alan +// @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 + using BenchmarkDotNet.Running; -using IP2Region.Net.Abstractions; -using IP2Region.Net.XDB; +using IP2Region.Net.BenchMark; -BenchmarkRunner.Run(typeof(Program).Assembly); - -public class CachePolicyCompare -{ - private static readonly string XdbPath = Path.Combine(AppContext.BaseDirectory, "IP2Region", "ip2region_v4.xdb"); - private readonly ISearcher _contentSearcher = new Searcher(CachePolicy.Content, XdbPath); - private readonly ISearcher _vectorSearcher = new Searcher(CachePolicy.VectorIndex,XdbPath); - private readonly ISearcher _fileSearcher = new Searcher(CachePolicy.File,XdbPath); - - private readonly string _testIpAddress = "114.114.114.114"; - - [Benchmark] - [BenchmarkCategory(nameof(CachePolicy.Content))] - public void CachePolicy_Content() => _contentSearcher.Search(_testIpAddress); - - [Benchmark] - [BenchmarkCategory(nameof(CachePolicy.VectorIndex))] - public void CachePolicy_VectorIndex() => _vectorSearcher.Search(_testIpAddress); - - - [Benchmark] - [BenchmarkCategory(nameof(CachePolicy.File))] - public void CachePolicy_File() => _fileSearcher.Search(_testIpAddress); -} \ No newline at end of file +BenchmarkRunner.Run(); diff --git a/binding/csharp/IP2Region.Net.Test/IP2Region.Net.Test.csproj b/binding/csharp/IP2Region.Net.Test/IP2Region.Net.Test.csproj index c2dd005..ac1ed80 100644 --- a/binding/csharp/IP2Region.Net.Test/IP2Region.Net.Test.csproj +++ b/binding/csharp/IP2Region.Net.Test/IP2Region.Net.Test.csproj @@ -1,40 +1,51 @@  - - net9.0 - enable - enable + + net10.0 + enable + enable - false - + false + - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - - + + + - - - TestData/ipv4_source.txt - PreserveNewest - - - TestData/ip2region_v4.xdb - PreserveNewest - - + + + + + + + TestData/ipv4_source.txt + PreserveNewest + + + TestData/ip2region_v4.xdb + PreserveNewest + + + TestData/ipv6_source.txt + PreserveNewest + + + TestData/ip2region_v6.xdb + PreserveNewest + + diff --git a/binding/csharp/IP2Region.Net.Test/SearcherTest.cs b/binding/csharp/IP2Region.Net.Test/SearcherTest.cs index 21aa767..da9f840 100644 --- a/binding/csharp/IP2Region.Net.Test/SearcherTest.cs +++ b/binding/csharp/IP2Region.Net.Test/SearcherTest.cs @@ -1,80 +1,161 @@ +// Copyright 2025 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. +// @Author Alan +// @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 + +using IP2Region.Net.Abstractions; using IP2Region.Net.XDB; +using Microsoft.Extensions.DependencyInjection; +using System.Net; +using Xunit; namespace IP2Region.Net.Test; -[TestFixture] public class SearcherTest { - private readonly string _xdbPath = Path.Combine(AppContext.BaseDirectory, "TestData", "ip2region_v4.xdb"); - - public static IEnumerable Ips() - { - yield return "114.114.114.114"; - yield return "119.29.29.29"; - yield return "223.5.5.5"; - yield return "180.76.76.76"; - yield return "8.8.8.8"; - } + private readonly string _xdbPathV4 = Path.Combine(AppContext.BaseDirectory, "TestData", "ip2region_v4.xdb"); + private readonly string _xdbPathV6 = Path.Combine(AppContext.BaseDirectory, "TestData", "ip2region_v6.xdb"); - [TestCaseSource(nameof(Ips))] - [Parallelizable(ParallelScope.All)] - public void TestSearchCacheContent(string ip) + [Theory] + [InlineData("58.251.27.201", "中国|广东省|深圳市|联通", "v4")] + [InlineData("114.114.114.114", "中国|江苏省|南京市|0", "v4")] + [InlineData("119.29.29.29", "中国|北京|北京市|腾讯", "v4")] + [InlineData("223.5.5.5", "中国|浙江省|杭州市|阿里云", "v4")] + [InlineData("180.76.76.76", "中国|北京|北京市|百度", "v4")] + [InlineData("8.8.8.8", "美国|0|0|Level3", "v4")] + [InlineData("240e:3b7:3272:d8d0:db09:c067:8d59:539e", "中国|广东省|深圳市|家庭宽带", "v6")] + public void TestSearchCacheContent(string ip, string expected, string version) { - var contentSearcher = new Searcher(CachePolicy.Content,_xdbPath); + var _xdbPath = version == "v4" ? _xdbPathV4 : _xdbPathV6; + var contentSearcher = new Searcher(CachePolicy.Content, _xdbPath); var region = contentSearcher.Search(ip); - Console.WriteLine(region); + Assert.Equal(expected, region); } - [TestCaseSource(nameof(Ips))] - [Parallelizable(ParallelScope.All)] - public void TestSearchCacheVector(string ip) + [Theory] + [InlineData("58.251.27.201", "中国|广东省|深圳市|联通", "v4")] + [InlineData("114.114.114.114", "中国|江苏省|南京市|0", "v4")] + [InlineData("119.29.29.29", "中国|北京|北京市|腾讯", "v4")] + [InlineData("223.5.5.5", "中国|浙江省|杭州市|阿里云", "v4")] + [InlineData("180.76.76.76", "中国|北京|北京市|百度", "v4")] + [InlineData("8.8.8.8", "美国|0|0|Level3", "v4")] + [InlineData("240e:3b7:3272:d8d0:db09:c067:8d59:539e", "中国|广东省|深圳市|家庭宽带", "v6")] + public void TestSearchCacheVector(string ip, string expected, string version) { - var vectorSearcher = new Searcher(CachePolicy.VectorIndex,_xdbPath); + var _xdbPath = version == "v4" ? _xdbPathV4 : _xdbPathV6; + var vectorSearcher = new Searcher(CachePolicy.VectorIndex, _xdbPath); var region = vectorSearcher.Search(ip); - Console.WriteLine(region); + Assert.Equal(expected, region); } - [TestCaseSource(nameof(Ips))] - [Parallelizable(ParallelScope.All)] - public void TestSearchCacheFile(string ip) + [Theory] + [InlineData("58.251.0.0", "中国|广东省|深圳市|联通", "v4")] + [InlineData("58.251.255.255", "中国|广东省|深圳市|联通", "v4")] + [InlineData("58.251.27.201", "中国|广东省|深圳市|联通", "v4")] + [InlineData("114.114.114.114", "中国|江苏省|南京市|0", "v4")] + [InlineData("119.29.29.29", "中国|北京|北京市|腾讯", "v4")] + [InlineData("223.5.5.5", "中国|浙江省|杭州市|阿里云", "v4")] + [InlineData("180.76.76.76", "中国|北京|北京市|百度", "v4")] + [InlineData("8.8.8.8", "美国|0|0|Level3", "v4")] + [InlineData("240e:3b7:3272:d8d0:db09:c067:8d59:539e", "中国|广东省|深圳市|家庭宽带", "v6")] + [InlineData("240e:044d:2d00:0000:0000:0000:0000:0000", "美国|加利福尼亚州|洛杉矶|移动网络", "v6")] + public void TestSearchCacheFile(string ip, string expected, string version) { - var fileSearcher = new Searcher(CachePolicy.File,_xdbPath); + var _xdbPath = version == "v4" ? _xdbPathV4 : _xdbPathV6; + var fileSearcher = new Searcher(CachePolicy.File, _xdbPath); var region = fileSearcher.Search(ip); - Console.WriteLine(region); + Assert.Equal(expected, region); } - [TestCase(CachePolicy.Content)] - [TestCase(CachePolicy.VectorIndex)] - [TestCase(CachePolicy.File)] - public void TestBenchSearch(CachePolicy cachePolicy) + [Fact] + public void IoCount_File_Ok() { - Searcher searcher = new Searcher(cachePolicy,_xdbPath); - var srcPath = Path.Combine(AppContext.BaseDirectory, "TestData", "ipv4_source.txt"); + var searcher = new Searcher(CachePolicy.File, _xdbPathV4); + searcher.Search("58.251.27.201"); + Assert.Equal(3, searcher.IoCount); + + searcher.Search("58.251.27.201"); + Assert.Equal(3, searcher.IoCount); + } + + [Fact] + public void IoCount_Vector_Ok() + { + var searcher = new Searcher(CachePolicy.VectorIndex, _xdbPathV4); + searcher.Search("58.251.27.201"); + Assert.Equal(2, searcher.IoCount); + + searcher.Search("58.251.27.201"); + Assert.Equal(2, searcher.IoCount); + } + + [Fact] + public void IoCount_Content_Ok() + { + var searcher = new Searcher(CachePolicy.Content, _xdbPathV4); + searcher.Search("58.251.27.201"); + Assert.Equal(0, searcher.IoCount); + + searcher.Search("58.251.27.201"); + Assert.Equal(0, searcher.IoCount); + } + + [Theory] + [InlineData("58.251.255.255", "中国|广东省|深圳市|联通")] + public void Search_Ip_Ok(string ipStr, string expected) + { + var fileSearcher = new Searcher(CachePolicy.File, _xdbPathV4); + var ipAddress = IPAddress.Parse(ipStr); + var region = fileSearcher.Search(ipAddress); + Assert.Equal(expected, region); + + var ip = XDB.Util.IpAddressToUInt32(ipAddress); + region = fileSearcher.Search(ip); + Assert.Equal(expected, region); + } + + [Theory] + [InlineData("58.251.255.255", "中国|广东省|深圳市|联通")] + public void AddIP2RegionService_Ok(string ipStr, string expected) + { + var services = new ServiceCollection(); + services.AddIP2RegionService(_xdbPathV4, CachePolicy.File); + + var provider = services.BuildServiceProvider(); + var searcher = provider.GetRequiredService(); + var region = searcher.Search(ipStr); + Assert.Equal(expected, region); + + searcher = provider.GetRequiredKeyedService("IP2Region.Net"); + region = searcher.Search(ipStr); + Assert.Equal(expected, region); + } + + [Theory] + [InlineData(CachePolicy.Content, "v4")] + [InlineData(CachePolicy.VectorIndex, "v4")] + [InlineData(CachePolicy.File, "v4")] + [InlineData(CachePolicy.Content, "v6")] + [InlineData(CachePolicy.VectorIndex, "v6")] + [InlineData(CachePolicy.File, "v6")] + public void TestBenchSearch(CachePolicy cachePolicy, string version) + { + var _xdbPath = version == "v4" ? _xdbPathV4 : _xdbPathV6; + var searcher = new Searcher(cachePolicy, _xdbPath); + var srcPath = Path.Combine(AppContext.BaseDirectory, "TestData", $"ip{version}_source.txt"); foreach (var line in File.ReadLines(srcPath)) { var ps = line.Trim().Split("|", 3); + var sip = ps[0]; + var eip = ps[1]; - if (ps.Length != 3) - { - throw new ArgumentException($"invalid ip segment line {line}", nameof(line)); - } - - var sip = Util.IpAddressToUInt32(ps[0]); - var eip = Util.IpAddressToUInt32(ps[1]); - var mip = Util.GetMidIp(sip, eip); - - uint[] temp = { sip, Util.GetMidIp(sip, mip), mip, Util.GetMidIp(mip, eip), eip }; - - foreach (var ip in temp) - { - var region = searcher.Search(ip); - - if (region != ps[2]) - { - throw new Exception($"failed search {ip} with ({region}!={ps[2]})"); - } - } + var s1 = searcher.Search(sip); + var s2 = searcher.Search(eip); + Assert.Equal(s1, ps[2]); + Assert.Equal(s2, ps[2]); } } -} \ No newline at end of file +} diff --git a/binding/csharp/IP2Region.Net.Test/Usings.cs b/binding/csharp/IP2Region.Net.Test/Usings.cs deleted file mode 100644 index cefced4..0000000 --- a/binding/csharp/IP2Region.Net.Test/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using NUnit.Framework; \ No newline at end of file diff --git a/binding/csharp/IP2Region.Net.Test/UtilTest.cs b/binding/csharp/IP2Region.Net.Test/UtilTest.cs index c25fe01..8a4d38b 100644 --- a/binding/csharp/IP2Region.Net.Test/UtilTest.cs +++ b/binding/csharp/IP2Region.Net.Test/UtilTest.cs @@ -1,14 +1,27 @@ -using IP2Region.Net.XDB; +// Copyright 2025 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. +// @Author Alan +// @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 + +using Xunit; namespace IP2Region.Net.Test; -[TestFixture] public class UtilTest { - [TestCase("114.114.114.114")] - public void TestIpAddressToUInt32(string value) + [Fact] + public void IpAddressToUInt32_Ok() { - var uintIp = XDB.Util.IpAddressToUInt32(value); - Console.WriteLine(uintIp); + var uintIp = XDB.Util.IpAddressToUInt32("114.114.114.114"); + Assert.Equal((uint)1920103026, uintIp); + } + + [Fact] + public void GetMidIp_Ok() + { + var uintIp = XDB.Util.GetMidIp(1, 10); + Assert.Equal((uint)5, uintIp); } } \ No newline at end of file diff --git a/binding/csharp/IP2Region.Net.Test/XdbTest.cs b/binding/csharp/IP2Region.Net.Test/XdbTest.cs new file mode 100644 index 0000000..8b92da8 --- /dev/null +++ b/binding/csharp/IP2Region.Net.Test/XdbTest.cs @@ -0,0 +1,49 @@ +// Copyright 2025 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. +// @Author Alan +// @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 + +using Xunit; + +namespace IP2Region.Net.Test; + +public class XdbTest +{ + [Fact] + public async Task VersionIPV4_Ok() + { + var db = Path.Combine(AppContext.BaseDirectory, "TestData", $"ip2region_v4.xdb"); + + var version = await XDB.Util.GetVersionAsync(db); + Assert.Equal(3, version.Ver); + Assert.Equal(1, version.CachePolice); + Assert.Equal("2025-09-06 02:24:16", version.CreatedTime.ToString("yyyy-MM-dd HH:mm:ss")); + Assert.Equal((uint)955933, version.StartIndex); + Assert.Equal((uint)11042415, version.EndIndex); + Assert.Equal(4, version.IPVer); + Assert.Equal(4, version.BytesCount); + } + + [Fact] + public async Task VersionIPV6_Ok() + { + var db = Path.Combine(AppContext.BaseDirectory, "TestData", $"ip2region_v6.xdb"); + var version = await XDB.Util.GetVersionAsync(db); + Assert.Equal(3, version.Ver); + Assert.Equal(1, version.CachePolice); + Assert.Equal("2025-10-17 04:41:04", version.CreatedTime.ToString("yyyy-MM-dd HH:mm:ss")); + Assert.Equal((uint)3094259, version.StartIndex); + Assert.Equal((uint)36258303, version.EndIndex); + Assert.Equal(6, version.IPVer); + Assert.Equal(4, version.BytesCount); + } + + [Fact] + public async Task GetVersionAsync_Error() + { + await Assert.ThrowsAsync(async () => await XDB.Util.GetVersionAsync(null!)); + await Assert.ThrowsAsync(async () => await XDB.Util.GetVersionAsync(Path.Combine(AppContext.BaseDirectory, "test.xdb"))); + } +} diff --git a/binding/csharp/IP2Region.Net.slnx b/binding/csharp/IP2Region.Net.slnx index 559f5de..1dc947d 100644 --- a/binding/csharp/IP2Region.Net.slnx +++ b/binding/csharp/IP2Region.Net.slnx @@ -1,5 +1,6 @@ + diff --git a/binding/csharp/IP2Region.Net/Abstractions/ISearcher.cs b/binding/csharp/IP2Region.Net/Abstractions/ISearcher.cs index ffd64b9..89cd5e5 100644 --- a/binding/csharp/IP2Region.Net/Abstractions/ISearcher.cs +++ b/binding/csharp/IP2Region.Net/Abstractions/ISearcher.cs @@ -1,20 +1,39 @@ -// Copyright 2023 The Ip2Region Authors. All rights reserved. +// Copyright 2025 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. // @Author Alan // @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 using System.Net; namespace IP2Region.Net.Abstractions; +/// +/// IP 转化为地理位置搜索器接口 +/// public interface ISearcher { + /// + /// 搜索方法 + /// + /// IP 地址字符串 如 192.168.0.1 + /// string? Search(string ipStr); + /// + /// 搜索方法 + /// string? Search(IPAddress ipAddress); + /// + /// 搜索方法 仅限 IPv4 使用 + /// + /// IPv4 地址字节数组小端读取 uint 数值 string? Search(uint ipAddress); + /// + /// 获得 内部 IO 访问次数 + /// int IoCount { get; } -} \ No newline at end of file +} diff --git a/binding/csharp/IP2Region.Net/Extensions/ServiceCollectionExtensions.cs b/binding/csharp/IP2Region.Net/Extensions/ServiceCollectionExtensions.cs index 980243a..16fbf05 100644 --- a/binding/csharp/IP2Region.Net/Extensions/ServiceCollectionExtensions.cs +++ b/binding/csharp/IP2Region.Net/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,11 @@ -using IP2Region.Net.Abstractions; +// Copyright 2025 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. +// @Author Alan +// @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 + +using IP2Region.Net.Abstractions; using IP2Region.Net.XDB; using Microsoft.Extensions.DependencyInjection.Extensions; diff --git a/binding/csharp/IP2Region.Net/IP2Region.Net.csproj b/binding/csharp/IP2Region.Net/IP2Region.Net.csproj index badc071..83901fe 100644 --- a/binding/csharp/IP2Region.Net/IP2Region.Net.csproj +++ b/binding/csharp/IP2Region.Net/IP2Region.Net.csproj @@ -2,7 +2,7 @@ IP2Region.Net - 2.1.0 + 2.1.1 IP2Region.Net Alan Lee Apache-2.0 @@ -15,7 +15,7 @@ git enable enable - netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0;net9.0 + netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0;net9.0;net10.0 latest c2f07fe1-a300-4de3-8200-1278ed8cb5b7 @@ -30,30 +30,34 @@ - + - + - + - + - + - + - - + + + + + + CHANGELOG.md diff --git a/binding/csharp/IP2Region.Net/Internal/Abstractions/AbstractCacheStrategy.cs b/binding/csharp/IP2Region.Net/Internal/Abstractions/AbstractCacheStrategy.cs index 13d19e5..909f75c 100644 --- a/binding/csharp/IP2Region.Net/Internal/Abstractions/AbstractCacheStrategy.cs +++ b/binding/csharp/IP2Region.Net/Internal/Abstractions/AbstractCacheStrategy.cs @@ -1,42 +1,33 @@ -// Copyright 2023 The Ip2Region Authors. All rights reserved. +// Copyright 2025 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. // @Author Alan // @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 using System.Buffers; namespace IP2Region.Net.Internal.Abstractions; -internal abstract class AbstractCacheStrategy +internal abstract class AbstractCacheStrategy(string xdbPath) { protected const int HeaderInfoLength = 256; - protected const int VectorIndexRows = 256; - protected const int VectorIndexCols = 256; protected const int VectorIndexSize = 8; - protected readonly FileStream XdbFileStream; - private const int BufferSize = 4096; + private const int BufferSize = 64 * 1024; - internal int IoCount { get; private set; } + public int IoCount { get; private set; } - protected AbstractCacheStrategy(string xdbPath) + protected FileStream XdbFileStream = new(xdbPath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, FileOptions.RandomAccess); + + public void ResetIoCount() { - XdbFileStream = new FileStream(xdbPath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, - useAsync: true); + IoCount = 0; } - protected int GetVectorIndexStartPos(uint ip) - { - var il0 = ip >> 24 & 0xFF; - var il1 = ip >> 16 & 0xFF; - var idx = il0 * VectorIndexCols * VectorIndexSize + il1 * VectorIndexSize; - return (int)idx; - } + public virtual ReadOnlyMemory GetVectorIndex(int offset) => GetData(HeaderInfoLength + offset, VectorIndexSize); - internal abstract ReadOnlyMemory GetVectorIndex(uint ip); - - internal virtual ReadOnlyMemory GetData(int offset, int length) + public virtual ReadOnlyMemory GetData(long offset, int length) { byte[] buffer = ArrayPool.Shared.Rent(length); int totalBytesRead = 0; @@ -45,14 +36,13 @@ internal abstract class AbstractCacheStrategy XdbFileStream.Seek(offset, SeekOrigin.Begin); int bytesRead; - do + while (totalBytesRead < length) { - int bytesToRead = Math.Min(BufferSize, length - totalBytesRead); - bytesRead = XdbFileStream.Read(buffer, totalBytesRead, bytesToRead); + bytesRead = XdbFileStream.Read(buffer, totalBytesRead, length); totalBytesRead += bytesRead; - + IoCount++; - } while (bytesRead > 0 && totalBytesRead < length); + } } finally { @@ -61,4 +51,4 @@ internal abstract class AbstractCacheStrategy return new ReadOnlyMemory(buffer, 0, totalBytesRead); } -} \ No newline at end of file +} diff --git a/binding/csharp/IP2Region.Net/Internal/CacheStrategyFactory.cs b/binding/csharp/IP2Region.Net/Internal/CacheStrategyFactory.cs index 14e8eb0..084a3d0 100644 --- a/binding/csharp/IP2Region.Net/Internal/CacheStrategyFactory.cs +++ b/binding/csharp/IP2Region.Net/Internal/CacheStrategyFactory.cs @@ -1,31 +1,21 @@ -// Copyright 2023 The Ip2Region Authors. All rights reserved. +// Copyright 2025 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. // @Author Alan // @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 using IP2Region.Net.Internal.Abstractions; using IP2Region.Net.XDB; namespace IP2Region.Net.Internal; -internal class CacheStrategyFactory +class CacheStrategyFactory(string xdbPath) { - private readonly string _xdbPath; - - public CacheStrategyFactory(string xdbPath) + public AbstractCacheStrategy CreateCacheStrategy(CachePolicy cachePolicy) => cachePolicy switch { - _xdbPath = xdbPath; - } - - public AbstractCacheStrategy CreateCacheStrategy(CachePolicy cachePolicy) - { - return cachePolicy switch - { - CachePolicy.Content => new ContentCacheStrategy(_xdbPath), - CachePolicy.VectorIndex => new VectorIndexCacheStrategy(_xdbPath), - CachePolicy.File => new FileCacheStrategy(_xdbPath), - _ => throw new ArgumentException(nameof(cachePolicy)) - }; - } -} \ No newline at end of file + CachePolicy.Content => new ContentCacheStrategy(xdbPath), + CachePolicy.VectorIndex => new VectorIndexCacheStrategy(xdbPath), + _ => new FileCacheStrategy(xdbPath), + }; +} diff --git a/binding/csharp/IP2Region.Net/Internal/ContentCacheStrategy.cs b/binding/csharp/IP2Region.Net/Internal/ContentCacheStrategy.cs index e3094e1..135751d 100644 --- a/binding/csharp/IP2Region.Net/Internal/ContentCacheStrategy.cs +++ b/binding/csharp/IP2Region.Net/Internal/ContentCacheStrategy.cs @@ -1,14 +1,15 @@ -// Copyright 2023 The Ip2Region Authors. All rights reserved. +// Copyright 2025 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. // @Author Alan // @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 using IP2Region.Net.Internal.Abstractions; namespace IP2Region.Net.Internal; -internal class ContentCacheStrategy : AbstractCacheStrategy +class ContentCacheStrategy : AbstractCacheStrategy { private readonly ReadOnlyMemory _cacheData; @@ -19,14 +20,5 @@ internal class ContentCacheStrategy : AbstractCacheStrategy XdbFileStream.Dispose(); } - internal override ReadOnlyMemory GetVectorIndex(uint ip) - { - int idx = GetVectorIndexStartPos(ip); - return _cacheData.Slice(HeaderInfoLength + idx, VectorIndexSize); - } - - internal override ReadOnlyMemory GetData(int offset, int length) - { - return _cacheData.Slice(offset, length); - } -} \ No newline at end of file + public override ReadOnlyMemory GetData(long offset = 0, int length = 0) => _cacheData.Slice((int)offset, length); +} diff --git a/binding/csharp/IP2Region.Net/Internal/FileCacheStrategy.cs b/binding/csharp/IP2Region.Net/Internal/FileCacheStrategy.cs index 31f90f3..2a69364 100644 --- a/binding/csharp/IP2Region.Net/Internal/FileCacheStrategy.cs +++ b/binding/csharp/IP2Region.Net/Internal/FileCacheStrategy.cs @@ -1,22 +1,15 @@ -// Copyright 2023 The Ip2Region Authors. All rights reserved. +// Copyright 2025 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. // @Author Alan // @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 using IP2Region.Net.Internal.Abstractions; namespace IP2Region.Net.Internal; -internal class FileCacheStrategy : AbstractCacheStrategy +class FileCacheStrategy(string xdbPath) : AbstractCacheStrategy(xdbPath) { - public FileCacheStrategy(string xdbPath) : base(xdbPath) - { - } - internal override ReadOnlyMemory GetVectorIndex(uint ip) - { - var idx = GetVectorIndexStartPos(ip); - return GetData(HeaderInfoLength + idx, VectorIndexSize); - } -} \ No newline at end of file +} diff --git a/binding/csharp/IP2Region.Net/Internal/VectorIndexCacheStrategy.cs b/binding/csharp/IP2Region.Net/Internal/VectorIndexCacheStrategy.cs index f1d8ae6..b5abc17 100644 --- a/binding/csharp/IP2Region.Net/Internal/VectorIndexCacheStrategy.cs +++ b/binding/csharp/IP2Region.Net/Internal/VectorIndexCacheStrategy.cs @@ -1,26 +1,28 @@ -// Copyright 2023 The Ip2Region Authors. All rights reserved. +// Copyright 2025 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. // @Author Alan // @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 using IP2Region.Net.Internal.Abstractions; namespace IP2Region.Net.Internal; -internal class VectorIndexCacheStrategy : AbstractCacheStrategy +class VectorIndexCacheStrategy : AbstractCacheStrategy { - private readonly ReadOnlyMemory _vectorIndex; + private const int VectorIndexRows = 256; + private const int VectorIndexCols = 256; + + private readonly ReadOnlyMemory _vectorCache; public VectorIndexCacheStrategy(string xdbPath) : base(xdbPath) { - var vectorLength = VectorIndexRows * VectorIndexCols * VectorIndexSize; - _vectorIndex = base.GetData(HeaderInfoLength, vectorLength); + XdbFileStream.Seek(HeaderInfoLength, SeekOrigin.Begin); + var buffer = new byte[VectorIndexRows * VectorIndexCols * VectorIndexSize]; + var length = XdbFileStream.Read(buffer, 0, buffer.Length); + _vectorCache = new ReadOnlyMemory(buffer); } - internal override ReadOnlyMemory GetVectorIndex(uint ip) - { - var idx = GetVectorIndexStartPos(ip); - return _vectorIndex.Slice(idx, VectorIndexSize); - } -} \ No newline at end of file + public override ReadOnlyMemory GetVectorIndex(int offset) => _vectorCache.Slice(offset, VectorIndexSize); +} diff --git a/binding/csharp/IP2Region.Net/XDB/CachePolicy.cs b/binding/csharp/IP2Region.Net/XDB/CachePolicy.cs index 0f4d035..59b6b29 100644 --- a/binding/csharp/IP2Region.Net/XDB/CachePolicy.cs +++ b/binding/csharp/IP2Region.Net/XDB/CachePolicy.cs @@ -1,17 +1,29 @@ +// Copyright 2025 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. +// @Author Alan +// @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 + namespace IP2Region.Net.XDB; +/// +/// 缓存策略枚举 +/// public enum CachePolicy { /// /// no cache /// File, + /// /// cache vector index , reduce the number of IO operations /// VectorIndex, + /// /// default cache policy , cache whole xdb file /// Content -} \ No newline at end of file +} diff --git a/binding/csharp/IP2Region.Net/XDB/Searcher.cs b/binding/csharp/IP2Region.Net/XDB/Searcher.cs index de29dc9..2f0eadb 100644 --- a/binding/csharp/IP2Region.Net/XDB/Searcher.cs +++ b/binding/csharp/IP2Region.Net/XDB/Searcher.cs @@ -1,85 +1,157 @@ -// Copyright 2023 The Ip2Region Authors. All rights reserved. +// Copyright 2025 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. -// @Author Alan Lee -// @Date 2023/07/23 +// @Author Alan +// @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 -using System.Net; -using System.Runtime.InteropServices; -using System.Text; using IP2Region.Net.Abstractions; using IP2Region.Net.Internal; using IP2Region.Net.Internal.Abstractions; +using System.Buffers.Binary; +using System.Net; +using System.Text; namespace IP2Region.Net.XDB; +/// +/// 实现类 +/// public class Searcher : ISearcher { - const int SegmentIndexSize = 14; - private readonly AbstractCacheStrategy _cacheStrategy; + + /// + /// + /// public int IoCount => _cacheStrategy.IoCount; + /// + /// + /// public Searcher(CachePolicy cachePolicy, string dbPath) { var factory = new CacheStrategyFactory(dbPath); _cacheStrategy = factory.CreateCacheStrategy(cachePolicy); } + /// + /// + /// public string? Search(string ipStr) { - var ip = Util.IpAddressToUInt32(ipStr); - return Search(ip); + var ipAddress = IPAddress.Parse(ipStr); + return SearchCore(ipAddress.GetAddressBytes()); } + /// + /// + /// public string? Search(IPAddress ipAddress) { - var ip = Util.IpAddressToUInt32(ipAddress); - return Search(ip); + return SearchCore(ipAddress.GetAddressBytes()); } - public string? Search(uint ip) + /// + /// + /// + public string? Search(uint ipAddress) { - var index = _cacheStrategy.GetVectorIndex(ip); - uint sPtr = MemoryMarshal.Read(index.Span); - uint ePtr = MemoryMarshal.Read(index.Span.Slice(4)); + var bytes = BitConverter.GetBytes(ipAddress); + Array.Reverse(bytes); + return SearchCore(bytes); + } + string? SearchCore(byte[] ipBytes) + { + // 重置 IO 计数器 + _cacheStrategy.ResetIoCount(); + + // 每个 vector 索引项的字节数 + var vectorIndexSize = 8; + + // vector 索引的列数 + var vectorIndexCols = 256; + + // 计算得到 vector 索引项的开始地址。 + var il0 = ipBytes[0]; + var il1 = ipBytes[1]; + var idx = il0 * vectorIndexCols * vectorIndexSize + il1 * vectorIndexSize; + + var vector = _cacheStrategy.GetVectorIndex(idx); + var sPtr = BinaryPrimitives.ReadUInt32LittleEndian(vector.Span); + var ePtr = BinaryPrimitives.ReadUInt32LittleEndian(vector.Span.Slice(4)); + + var length = ipBytes.Length; + var indexSize = length * 2 + 6; + var l = 0; + var h = (ePtr - sPtr) / indexSize; var dataLen = 0; - uint dataPtr = 0; - uint l = 0; - uint h = (ePtr -sPtr) / SegmentIndexSize; + long dataPtr = 0; while (l <= h) { - var mid = Util.GetMidIp(l, h); - var pos = sPtr + mid * SegmentIndexSize; + int m = (int)(l + h) >> 1; - var buffer = _cacheStrategy.GetData((int)pos, SegmentIndexSize); - uint sip = MemoryMarshal.Read(buffer.Span); - uint eip = MemoryMarshal.Read(buffer.Span.Slice(4)); + var p = (int)sPtr + m * indexSize; + var buff = _cacheStrategy.GetData(p, indexSize); - if (ip < sip) + var s = buff.Span.Slice(0, length); + var e = buff.Span.Slice(length, length); + if (ByteCompare(ipBytes, s) < 0) { - h = mid - 1; + h = m - 1; } - else if (ip > eip) + else if (ByteCompare(ipBytes, e) > 0) { - l = mid + 1; + l = m + 1; } else { - dataLen = MemoryMarshal.Read(buffer.Span.Slice(8)); - dataPtr = MemoryMarshal.Read(buffer.Span.Slice(10)); + dataLen = BinaryPrimitives.ReadUInt16LittleEndian(buff.Span.Slice(length * 2, 2)); + dataPtr = BinaryPrimitives.ReadUInt32LittleEndian(buff.Span.Slice(length * 2 + 2, 4)); break; } } - if (dataLen == 0) - { - return default; - } - - var regionBuff = _cacheStrategy.GetData((int)dataPtr,dataLen); + var regionBuff = _cacheStrategy.GetData((int)dataPtr, dataLen); return Encoding.UTF8.GetString(regionBuff.Span.ToArray()); } -} \ No newline at end of file + + static int ByteCompare(byte[] ip1, ReadOnlySpan ip2) => ip1.Length == 4 ? IPv4Compare(ip1, ip2) : IPv6Compare(ip1, ip2); + + static int IPv4Compare(byte[] ip1, ReadOnlySpan ip2) + { + var ret = 0; + for (int i = 0; i < ip1.Length; i++) + { + var ip2Index = ip1.Length - 1 - i; + if (ip1[i] < ip2[ip2Index]) + { + return -1; + } + else if (ip1[i] > ip2[ip2Index]) + { + return 1; + } + } + return ret; + } + + static int IPv6Compare(byte[] ip1, ReadOnlySpan ip2) + { + var ret = 0; + for (int i = 0; i < ip1.Length; i++) + { + if (ip1[i] < ip2[i]) + { + return -1; + } + else if (ip1[i] > ip2[i]) + { + return 1; + } + } + return ret; + } +} diff --git a/binding/csharp/IP2Region.Net/XDB/Util.cs b/binding/csharp/IP2Region.Net/XDB/Util.cs index 1075377..54afc7a 100644 --- a/binding/csharp/IP2Region.Net/XDB/Util.cs +++ b/binding/csharp/IP2Region.Net/XDB/Util.cs @@ -1,8 +1,20 @@ +// Copyright 2025 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. +// @Author Alan +// @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 + +using System.Buffers; +using System.Buffers.Binary; using System.Net; using System.Runtime.InteropServices; namespace IP2Region.Net.XDB; +/// +/// 工具类 +/// public static class Util { public static uint IpAddressToUInt32(string ipAddress) @@ -10,7 +22,7 @@ public static class Util var address = IPAddress.Parse(ipAddress); return IpAddressToUInt32(address); } - + public static uint IpAddressToUInt32(IPAddress ipAddress) { byte[] bytes = ipAddress.GetAddressBytes(); @@ -20,4 +32,60 @@ public static class Util public static uint GetMidIp(uint x, uint y) => (x & y) + ((x ^ y) >> 1); -} \ No newline at end of file + + public static async Task GetVersionAsync(string dbPath, CancellationToken token = default) + { + if (string.IsNullOrEmpty(dbPath)) + { + throw new ArgumentNullException(nameof(dbPath)); + } + + if (!File.Exists(dbPath)) + { + throw new FileNotFoundException("xdb file not found.", dbPath); + } + + using var reader = File.OpenRead(dbPath); + return await GetVersionAsync(reader, token); + } + + internal static async Task GetVersionAsync(FileStream reader, CancellationToken token = default) + { + XdbVersion ret = default; + var buffer = ArrayPool.Shared.Rent(256); + + try + { + var length = await reader.ReadAsync(buffer, 0, 256, token); + if (length == 256) + { + ret = Parse(buffer); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + return ret; + } + + private static XdbVersion Parse(ReadOnlySpan buffer) + { + var ret = new XdbVersion + { + Ver = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(0, 2)), + CachePolice = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2, 2)), + StartIndex = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(8, 4)), + EndIndex = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(12, 4)), + IPVer = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(16, 2)), + BytesCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(18, 2)) + }; + + var createdAt = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(4, 4)); + var dtm = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.FromHours(0)); + ret.CreatedTime = dtm.AddSeconds(createdAt); + + return ret; + } +} diff --git a/binding/csharp/IP2Region.Net/XDB/XdbVersion.cs b/binding/csharp/IP2Region.Net/XDB/XdbVersion.cs new file mode 100644 index 0000000..5630cf8 --- /dev/null +++ b/binding/csharp/IP2Region.Net/XDB/XdbVersion.cs @@ -0,0 +1,49 @@ +// Copyright 2025 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. +// @Author Alan +// @Date 2023/07/25 +// Updated by Argo Zhang at 2025/11/21 + +namespace IP2Region.Net.XDB; + +/// +/// XdbVersion 结构体 +/// +public struct XdbVersion +{ + /// + /// 获得/设置 版本号 + /// + public ushort Ver { get; set; } + + /// + /// 获得/设置 缓存策略 + /// + public ushort CachePolice { get; set; } + + /// + /// 获得/设置 文件生成时间 + /// + public DateTimeOffset CreatedTime { get; set; } + + /// + /// 获得/设置 索引起始地址 + /// + public uint StartIndex { get; set; } + + /// + /// 获得/设置 索引结束地址 + /// + public uint EndIndex { get; set; } + + /// + /// 获得/设置 IP版本 + /// + public ushort IPVer { get; set; } + + /// + /// 获得/设置 指针字节数 + /// + public ushort BytesCount { get; set; } +} diff --git a/binding/csharp/README.md b/binding/csharp/README.md index f203c57..5f1db40 100644 --- a/binding/csharp/README.md +++ b/binding/csharp/README.md @@ -43,20 +43,19 @@ NET8+ support keyed service provider.GetRequiredKeyedService("IP2Region.Net"); ``` +## TargetFrameworks +netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0;net9.0;net10.0 + ## Performance -``` ini -BenchmarkDotNet=v0.13.2, OS=macOS 13.4.1 (c) (22F770820d) [Darwin 22.5.0] -Apple M1, 1 CPU, 8 logical and 8 physical cores -.NET SDK=7.0.306 - [Host] : .NET 6.0.20 (6.0.2023.32017), Arm64 RyuJIT AdvSIMD - DefaultJob : .NET 6.0.20 (6.0.2023.32017), Arm64 RyuJIT AdvSIMD -``` -| Method | Mean | Error | StdDev | -|------------------------ |-------------:|----------:|----------:| -| CachePolicy_Content | 58.32 ns | 0.182 ns | 0.170 ns | -| CachePolicy_File | 16,417.56 ns | 50.569 ns | 47.302 ns | -| CachePolicy_VectorIndex | 9,348.11 ns | 38.492 ns | 65.363 ns | +| Method | Mean | Error | StdDev | Median | +|------------ |------------:|----------:|------------:|------------:| +| ContentIPv4 | 101.5 ns | 2.04 ns | 4.57 ns | 103.1 ns | +| VectorIPv4 | 7,488.1 ns | 222.91 ns | 657.26 ns | 7,819.2 ns | +| FileIPv4 | 11,686.9 ns | 59.81 ns | 55.95 ns | 11,707.6 ns | +| ContentIPv6 | 296.1 ns | 1.84 ns | 1.72 ns | 296.2 ns | +| VectorIPv6 | 15,025.1 ns | 938.17 ns | 2,766.21 ns | 16,642.9 ns | +| FileIPv6 | 19,721.0 ns | 807.55 ns | 2,381.08 ns | 20,905.7 ns | ## Contributing Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. @@ -64,4 +63,4 @@ Pull requests are welcome. For major changes, please open an issue first to disc Please make sure to update tests as appropriate. ## License -[Apache License 2.0](https://github.com/lionsoul2014/ip2region/blob/master/LICENSE.md) \ No newline at end of file +[Apache License 2.0](https://github.com/lionsoul2014/ip2region/blob/master/LICENSE.md)