diff --git a/binding/csharp/.gitignore b/binding/csharp/.gitignore new file mode 100644 index 0000000..2ecf253 --- /dev/null +++ b/binding/csharp/.gitignore @@ -0,0 +1,337 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ \ No newline at end of file diff --git a/binding/csharp/IP2Region.SearchTest/App.config b/binding/csharp/IP2Region.SearchTest/App.config new file mode 100644 index 0000000..56efbc7 --- /dev/null +++ b/binding/csharp/IP2Region.SearchTest/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/binding/csharp/IP2Region.SearchTest/IP2Region.SearchTest.csproj b/binding/csharp/IP2Region.SearchTest/IP2Region.SearchTest.csproj new file mode 100644 index 0000000..d4eb256 --- /dev/null +++ b/binding/csharp/IP2Region.SearchTest/IP2Region.SearchTest.csproj @@ -0,0 +1,59 @@ + + + + + Debug + AnyCPU + {FF136AEA-D41F-4C14-842B-CE597BB97916} + Exe + IP2Region.SearchTest + IP2Region.SearchTest + v4.7.2 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + {483575a5-ffb3-4131-a2dc-6d3ad3bb268f} + IP2Region + + + + \ No newline at end of file diff --git a/binding/csharp/IP2Region.SearchTest/Program.cs b/binding/csharp/IP2Region.SearchTest/Program.cs new file mode 100644 index 0000000..8e650e4 --- /dev/null +++ b/binding/csharp/IP2Region.SearchTest/Program.cs @@ -0,0 +1,263 @@ +using IP2Region.xdb; +using System; +using System.Diagnostics; +using System.IO; + +namespace IP2Region.SearchTest +{ + internal class Program + { + + public static void PrintHelp(String[] args) + { + Console.WriteLine("ip2region xdb searcher"); + Console.WriteLine("IP2Region.SearchTest.exe [command] [command options]"); + Console.WriteLine("Command: "); + Console.WriteLine(" search search input test"); + Console.WriteLine(" bench search bench test"); + } + public static Searcher CreateSearcher(String dbPath, String cachePolicy) + { + if ("file" == cachePolicy) + { + return Searcher.NewWithFileOnly(dbPath); + } + else if ("vectorIndex" == cachePolicy) + { + byte[] vIndex = Searcher.LoadVectorIndexFromFile(dbPath); + return Searcher.NewWithVectorIndex(dbPath, vIndex); + } + else if ("content" == cachePolicy) + { + byte[] cBuff = Searcher.LoadContentFromFile(dbPath); + return Searcher.NewWithBuffer(cBuff); + } + else + { + throw new Exception("invalid cache policy `" + cachePolicy + "`, options: file/vectorIndex/content"); + } + } + + public static void SearchTest(string[] args) + { + String dbPath = "", cachePolicy = "vectorIndex"; + foreach (string r in args) + { + if (r.Length < 5) + { + continue; + } + + if (r.IndexOf("--") != 0) + { + continue; + } + + int sIdx = r.IndexOf('='); + if (sIdx < 0) + { + Console.WriteLine("missing = for args pair `{0}`", r); + return; + } + + String key = r.Substring(2, sIdx - 2); + String val = r.Substring(sIdx + 1); + // Console.WriteLinef("key=%s, val=%s\n", key, val); + if ("db" == key) + { + dbPath = val; + } + else if ("cache-policy" == key) + { + cachePolicy = val; + } + else + { + Console.WriteLine("undefined option `{0}`", r); + return; + } + } + + if (dbPath.Length < 1) + { + Console.WriteLine("IP2Region.SearchTest.exe search [command options]"); + Console.WriteLine("options:"); + Console.WriteLine(" --db string ip2region binary xdb file path"); + Console.WriteLine(" --cache-policy string cache policy: file/vectorIndex/content"); + return; + } + + Searcher searcher = CreateSearcher(dbPath, cachePolicy); + + Console.WriteLine("ip2region xdb searcher test program, cachePolicy: {0}\ntype 'quit' to exit", cachePolicy); + while (true) + { + Console.Write("ip2region>> "); + var line = Console.ReadLine().Trim(); + if (line.Length < 2) continue; + if (line == "quit") break; + try + { + var st = new Stopwatch(); + st.Start(); + var region = searcher.Search(line); + st.Stop(); + var cost = st.ElapsedMilliseconds; + Console.WriteLine("{{region: {0}, ioCount: {1}, took: {2} ms}}", region, searcher.IOCount, cost); + } + catch (Exception e) + { + Console.WriteLine("{{err:{0}, ioCount: {1}}}", e, searcher.IOCount); + } + } + Console.WriteLine("searcher test program exited, thanks for trying"); + + } + public static void BenchTest(String[] args) + { + String dbPath = "", srcPath = "", cachePolicy = "vectorIndex"; + foreach (String r in args) + { + if (r.Length < 5) + { + continue; + } + + if (r.IndexOf("--") != 0) + { + continue; + } + + int sIdx = r.IndexOf('='); + if (sIdx < 0) + { + Console.WriteLine("missing = for args pair `{0}`", r); + return; + } + + String key = r.Substring(2, sIdx - 2); + String val = r.Substring(sIdx + 1); + if ("db" == key) + { + dbPath = val; + } + else if ("src" == key) + { + srcPath = val; + } + else if ("cache-policy" == key) + { + cachePolicy = val; + } + else + { + Console.WriteLine("undefined option `{0}`", r); + return; + } + } + + if (dbPath.Length < 1 || srcPath.Length < 1) + { + Console.WriteLine("IP2Region.SearchTest.exe bench [command options]"); + Console.WriteLine("options:"); + Console.WriteLine(" --db string ip2region binary xdb file path"); + Console.WriteLine(" --src string source ip text file path"); + Console.WriteLine(" --cache-policy string cache policy: file/vectorIndex/content"); + return; + } + + Searcher searcher = CreateSearcher(dbPath, cachePolicy); + long count = 0; + var sw = new Stopwatch(); + var lines = File.ReadAllLines(srcPath); + foreach (var line in lines) + { + String l = line.Trim(); + String[] ps = l.Split(new[] { '|' }, 3); + if (ps.Length != 3) + { + Console.WriteLine("invalid ip segment `{0}`", l); + return; + } + long sip; + try + { + sip = Searcher.checkIP(ps[0]); + } + catch (Exception e) + { + Console.WriteLine("check start ip `{0}`: {1}", ps[0], e); + return; + } + long eip; + try + { + eip = Searcher.checkIP(ps[1]); + } + catch (Exception e) + { + Console.WriteLine("check end ip `{0}`: {1}", ps[1], e); + return; + } + + if (sip > eip) + { + Console.WriteLine("start ip({0}) should not be greater than end ip({1})", ps[0], ps[1]); + return; + } + + long mip = (sip + eip) >> 1; + foreach (var ip in new long[] { sip, (sip + mip) >> 1, mip, (mip + eip) >> 1, eip }) + { + sw.Start(); + String region = searcher.Search(ip); + sw.Stop(); + // check the region info + if (ps[2] != (region)) + { + Console.WriteLine("failed search({0}) with ({1} != {2})\n", Searcher.Long2ip(ip), region, ps[2]); + return; + } + + count++; + } + } + + Console.WriteLine("Bench finished, {{cachePolicy: {0}, total: {1}, took: {2}, cost: {3} ms/op}}", + cachePolicy, count, sw.Elapsed, + count == 0 ? 0 : sw.ElapsedMilliseconds / count); + } + static void Main(string[] args) + { + if (args.Length < 1) + { + PrintHelp(args); + return; + } + switch (args[0]) + { + case "search": + try + { + SearchTest(args); + } + catch (Exception e) + { + Console.WriteLine("failed running search test: {0}", e); + } + break; + case "bench": + try + { + BenchTest(args); + } + catch (Exception e) + { + Console.WriteLine("failed running bench test: {0}", e); + } + break; + default: PrintHelp(args); break; + } + } + } +} diff --git a/binding/csharp/IP2Region.SearchTest/Properties/AssemblyInfo.cs b/binding/csharp/IP2Region.SearchTest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..61714dc --- /dev/null +++ b/binding/csharp/IP2Region.SearchTest/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// 有关程序集的一般信息由以下 +// 控制。更改这些特性值可修改 +// 与程序集关联的信息。 +[assembly: AssemblyTitle("IP2Region.SearchTest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("IP2Region.SearchTest")] +[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// 将 ComVisible 设置为 false 会使此程序集中的类型 +//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 +//请将此类型的 ComVisible 特性设置为 true。 +[assembly: ComVisible(false)] + +// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID +[assembly: Guid("ff136aea-d41f-4c14-842b-ce597bb97916")] + +// 程序集的版本信息由下列四个值组成: +// +// 主版本 +// 次版本 +// 生成号 +// 修订号 +// +//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值 +//通过使用 "*",如下所示: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/binding/csharp/IP2Region.sln b/binding/csharp/IP2Region.sln new file mode 100644 index 0000000..8a1c6d7 --- /dev/null +++ b/binding/csharp/IP2Region.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32228.430 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IP2Region", "IP2Region\IP2Region.csproj", "{483575A5-FFB3-4131-A2DC-6D3AD3BB268F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IP2Region.SearchTest", "IP2Region.SearchTest\IP2Region.SearchTest.csproj", "{FF136AEA-D41F-4C14-842B-CE597BB97916}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {483575A5-FFB3-4131-A2DC-6D3AD3BB268F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {483575A5-FFB3-4131-A2DC-6D3AD3BB268F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {483575A5-FFB3-4131-A2DC-6D3AD3BB268F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {483575A5-FFB3-4131-A2DC-6D3AD3BB268F}.Release|Any CPU.Build.0 = Release|Any CPU + {FF136AEA-D41F-4C14-842B-CE597BB97916}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF136AEA-D41F-4C14-842B-CE597BB97916}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF136AEA-D41F-4C14-842B-CE597BB97916}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF136AEA-D41F-4C14-842B-CE597BB97916}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6AEA440B-0D90-4F6E-BA9A-D42B27A78D7B} + EndGlobalSection +EndGlobal diff --git a/binding/csharp/IP2Region/IP2Region.csproj b/binding/csharp/IP2Region/IP2Region.csproj new file mode 100644 index 0000000..7a87b2b --- /dev/null +++ b/binding/csharp/IP2Region/IP2Region.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0;netcoreapp3.1;net5.0;net6.0 + + + diff --git a/binding/csharp/IP2Region/xdb/Header.cs b/binding/csharp/IP2Region/xdb/Header.cs new file mode 100644 index 0000000..2fc871b --- /dev/null +++ b/binding/csharp/IP2Region/xdb/Header.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IP2Region.xdb +{ + public class Header + { + public int Version { get; } + public int IndexPolicy { get; } + public int CreatedAt { get; } + public int StartIndexPtr { get; } + public int EndIndexPtr { get; } + public byte[] Buffer { get; } + public Header(byte[] buff) + { + if (buff == null) throw new ArgumentNullException(nameof(buff)); + if (buff.Length < 16) throw new ArgumentOutOfRangeException(nameof(buff)); + Version = Searcher.GetInt2(buff, 0); + IndexPolicy = Searcher.GetInt2(buff, 2); + CreatedAt = Searcher.GetInt(buff, 4); + StartIndexPtr = Searcher.GetInt(buff, 8); + EndIndexPtr = Searcher.GetInt(buff, 12); + Buffer = buff; + + } + public override string ToString() + { + return "{" + + "Version: " + Version + ',' + + "IndexPolicy: " + IndexPolicy + ',' + + "CreatedAt: " + CreatedAt + ',' + + "StartIndexPtr: " + StartIndexPtr + ',' + + "EndIndexPtr: " + EndIndexPtr + + '}'; + } + } +} diff --git a/binding/csharp/IP2Region/xdb/Searcher.cs b/binding/csharp/IP2Region/xdb/Searcher.cs new file mode 100644 index 0000000..c04c166 --- /dev/null +++ b/binding/csharp/IP2Region/xdb/Searcher.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IP2Region.xdb +{ + public class Searcher + { + + // constant defined copied from the xdb maker + public static int HeaderInfoLength = 256; + public static int VectorIndexRows = 256; + public static int VectorIndexCols = 256; + public static int VectorIndexSize = 8; + public static int SegmentIndexSize = 14; + + // random access file handle for file based search + private readonly Stream handle; + + private int ioCount = 0; + + public int IOCount => this.ioCount; + + + // vector index. + // use the byte[] instead of VectorIndex entry array to keep + // the minimal memory allocation. + private readonly byte[] vectorIndex; + + // xdb content buffer, used for in-memory search + private readonly byte[] contentBuff; + + + // --- static method to create searchers + + + public static Searcher NewWithFileOnly(String dbPath) + { + return new Searcher(dbPath, null, null); + } + + public static Searcher NewWithVectorIndex(String dbPath, byte[] vectorIndex) + { + return new Searcher(dbPath, vectorIndex, null); + } + + public static Searcher NewWithBuffer(byte[] cBuff) + { + return new Searcher(null, null, cBuff); + } + + // --- End of creator + public Searcher(string dbFile, byte[] vectorIndex, byte[] cBuff) + { + if (cBuff != null) + { + this.handle = null; + this.vectorIndex = null; + this.contentBuff = cBuff; + } + else + { + this.handle = File.OpenRead(dbFile); + this.vectorIndex = vectorIndex; + this.contentBuff = null; + } + } + + public void Close() + { + if (this.handle != null) this.handle.Close(); + } + + public string Search(string ipStr) + { + var ip = checkIP(ipStr); + return Search(ip); + } + + public string Search(long ip) + { + // reset the global counter + this.ioCount = 0; + // locate the segment index block based on the vector index + int sPtr = 0, ePtr = 0; + int il0 = (int)((ip >> 24) & 0xFF); + int il1 = (int)((ip >> 16) & 0xFF); + int idx = il0 * VectorIndexCols * VectorIndexSize + il1 * VectorIndexSize; + if (vectorIndex != null) + { + sPtr = GetInt(vectorIndex, idx); + ePtr = GetInt(vectorIndex, idx + 4); + } + else if (contentBuff != null) + { + sPtr = GetInt(contentBuff, HeaderInfoLength + idx); + ePtr = GetInt(contentBuff, HeaderInfoLength + idx + 4); + } + else + { + byte[] vectorBuff = new byte[VectorIndexSize]; + Read(HeaderInfoLength + idx, vectorBuff); + sPtr = GetInt(vectorBuff, 0); + ePtr = GetInt(vectorBuff, 4); + } + + // binary search the segment index block to get the region info + byte[] buff = new byte[SegmentIndexSize]; + int dataLen = -1, dataPtr = -1; + int l = 0, h = (ePtr - sPtr) / SegmentIndexSize; + while (l <= h) + { + int m = (l + h) >> 1; + int p = sPtr + m * SegmentIndexSize; + + // read the segment index + Read(p, buff); + long sip = GetIntLong(buff, 0); + if (ip < sip) + { + h = m - 1; + } + else + { + long eip = GetIntLong(buff, 4); + if (ip > eip) + { + l = m + 1; + } + else + { + dataLen = GetInt2(buff, 8); + dataPtr = GetInt(buff, 10); + break; + } + } + } + + // empty match interception + // System.out.printf("dataLen: %d, dataPtr: %d\n", dataLen, dataPtr); + if (dataPtr < 0) return null; + + // load and return the region data + byte[] regionBuff = new byte[dataLen]; + Read(dataPtr, regionBuff); + //return new String(regionBuff, "utf-8"); + return Encoding.UTF8.GetString(regionBuff); + } + + protected virtual void Read(int offset, byte[] buffer) + { + // check the in-memory buffer first + if (contentBuff != null) + { + // @TODO: reduce data copying, directly decode the data ? + //System.arraycopy(contentBuff, offset, buffer, 0, buffer.length); + Array.Copy(contentBuff, offset, buffer, 0, buffer.Length); + return; + } + + // read from the file handle + if (handle == null) throw new ArgumentNullException(nameof(handle)); + handle.Seek(offset, SeekOrigin.Begin); + + this.ioCount++; + var rLen = handle.Read(buffer, 0, buffer.Length); + if (rLen != buffer.Length) throw new IOException("incomplete read: read bytes should be " + buffer.Length); + } + + // --- static cache util function + public static Header LoadHeader(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + var buffer = new byte[HeaderInfoLength]; + stream.Read(buffer, 0, HeaderInfoLength); + return new Header(buffer); + } + public static Header LoadHeaderFromFile(string dbPath) + { + using (var fs = File.OpenRead(dbPath)) return LoadHeader(fs); + } + public static byte[] LoadVectorIndex(Stream stream) + { + stream.Seek(HeaderInfoLength, SeekOrigin.Begin); + int len = VectorIndexRows * VectorIndexCols * SegmentIndexSize; + var buff = new byte[len]; + var rLen = stream.Read(buff, 0, buff.Length); + if (rLen != len) throw new IOException("incomplete read: read bytes should be " + len); + return buff; + } + public static byte[] LoadVectorIndexFromFile(string dbPath) + { + using (var fs = File.OpenRead(dbPath)) return LoadVectorIndex(fs); + } + public static byte[] LoadContent(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + using (var ms = new MemoryStream()) + { + stream.CopyTo(ms); + return ms.ToArray(); + } + } + + public static byte[] LoadContentFromFile(string dbPath) + { + using (var fs = File.OpenRead(dbPath)) return LoadContent(fs); + } + + public static int GetInt2(byte[] b, int offset) + { + return ( + (b[offset++] & 0x000000FF) | + (b[offset] & 0x0000FF00) + ); + } + public static int GetInt(byte[] b, int offset) + { + return ( + ((b[offset++]) & 0x000000FF) | + ((b[offset++] << 8) & 0x0000FF00) | + ((b[offset++] << 16) & 0x00FF0000) | + (int)((b[offset] << 24) & 0xFF000000) + ); + } + public static long GetIntLong(byte[] b, int offset) + { + return ( + ((b[offset++] & 0x000000FFL)) | + ((b[offset++] << 8) & 0x0000FF00L) | + ((b[offset++] << 16) & 0x00FF0000L) | + ((b[offset] << 24) & 0xFF000000L) + ); + } + + /* long int to ip string */ + public static string Long2ip(long ip) + { + return string.Join(".", (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, (ip) & 0xFF); + } + + public static byte[] shiftIndex = { 24, 16, 8, 0 }; + + /* check the specified ip address */ + public static long checkIP(String ip) + { + String[] + ps = ip.Split('.'); + if (ps.Length != 4) throw new Exception("invalid ip address `" + ip + "`"); + + long ipDst = 0; + for (int i = 0; i < ps.Length; i++) + { + int val = Convert.ToInt32(ps[i]); + if (val > 255) + { + throw new Exception("ip part `" + ps[i] + "` should be less then 256"); + } + ipDst |= ((long)val << shiftIndex[i]); + } + + return ipDst & 0xFFFFFFFFL; + } + } +} diff --git a/binding/csharp/ReadMe.md b/binding/csharp/ReadMe.md index 8755f6e..f44eeb0 100644 --- a/binding/csharp/ReadMe.md +++ b/binding/csharp/ReadMe.md @@ -2,6 +2,171 @@ # 使用方式 +### 引入文件 +将 IP2Region\xdb\Header.cs 和 IP2Region\xdb\Searcher.cs 复制到项目文件中即可。 + +兼容框架:netstandard2.0、netcoreapp3.1、net5.0和net6.0。其余框架请自行测试。 + +### 完全基于文件的查询 +```csharp +// 1、创建 searcher 对象 +String dbPath = "ip2region.xdb file path"; +Searcher searcher = null; +try +{ + searcher = Searcher.NewWithFileOnly(dbPath); +} +catch (IOException e) +{ + Console.WriteLine("failed to create searcher with `{0}`: {1}", dbPath, e); + return; +} + +// 2、查询 +String ip = "1.2.3.4"; +try +{ + String region = searcher.Search(ip); + Console.WriteLine("{{region: {0}, ioCount: {1}}}\n", region, searcher.IOCount); +} +catch (Exception e) +{ + Console.WriteLine("failed to search({0}): {1}\n", ip, e); +} + +// 3、备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。 +``` + +### 缓存 `VectorIndex` 索引 +```csharp +String dbPath = "ip2region.xdb file path"; + +// 1、从 dbPath 中预先加载 VectorIndex 缓存,并且把这个得到的数据作为全局变量,后续反复使用。 +byte[] vIndex; +try +{ + vIndex = Searcher.LoadVectorIndexFromFile(dbPath); +} +catch (Exception e) +{ + Console.WriteLine("failed to load vector index from `{0}`: {1}", dbPath, e); + return; +} + +// 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。 +Searcher searcher; +try +{ + searcher = Searcher.NewWithVectorIndex(dbPath, vIndex); +} +catch (Exception e) +{ + Console.WriteLine("failed to create vectorIndex cached searcher with `{0}`: {1}", dbPath, e); + return; +} + +// 3、查询 +String ip = "1.2.3.4"; +try +{ + String region = searcher.Search(ip); + Console.WriteLine("{{region: {0}, ioCount: {1}}}", region, searcher.IOCount); +} +catch (Exception e) +{ + Console.WriteLine("failed to search({0}): {1}", ip, e); +} + +// 备注:每个线程需要单独创建一个独立的 Searcher 对象,但是都共享全局的制度 vIndex 缓存。 +``` + +### 缓存整个 `xdb` 数据 + +我们也可以预先加载整个 ip2region.xdb 的数据到内存,然后基于这个数据创建查询对象来实现完全基于文件的查询,类似之前的 memory search。 + +```csharp +String dbPath = "ip2region.xdb file path"; + +// 1、从 dbPath 加载整个 xdb 到内存。 +byte[] cBuff; +try +{ + cBuff = Searcher.LoadContentFromFile(dbPath); +} +catch (Exception e) +{ + Console.WriteLine("failed to load content from `{0}`: {1}", dbPath, e); + return; +} + +// 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。 +Searcher searcher; +try +{ + searcher = Searcher.NewWithBuffer(cBuff); +} +catch (Exception e) +{ + Console.WriteLine("failed to create content cached searcher: {0}", e); + return; +} + +// 3、查询 +String ip = "1.2.3.4"; +try { + String region = searcher.Search(ip); + Console.WriteLine("{{region: {0}, ioCount: {1}}}", region, searcher.IOCount); +} +catch (Exception e) +{ + Console.WriteLine("failed to search({0}): {1}", ip, e); +} + +// 备注:并发使用,用整个 xdb 数据缓存创建的查询对象可以安全的用于并发,也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。 +``` + +# 编译测试程序 +使用 Visual Stuido 打开 IP2Region 解决方案,按“F6”生成测试程序。然后会在 `IP2Region.SearchTest\bin\Debug` 目录下得到一个 `IP2Region.SearchTest.exe` 测试程序。 + # 查询测试 +可以通过 `IP2Region.SearchTest.exe search` 命令来测试查询: +```bash +IP2Region.SearchTest.exe search +IP2Region.SearchTest.exe search [command options] +options: + --db string ip2region binary xdb file path + --cache-policy string cache policy: file/vectorIndex/content +``` + +例如:使用默认的 data/ip2region.xdb 文件进行查询测试: +```bash +IP2Region.SearchTest.exe search --db=../../../../../data/ip2region.xdb +ip2region xdb searcher test program, cachePolicy: vectorIndex +type 'quit' to exit +ip2region>> 1.2.3.4 +{region: 美国|0|华盛顿|0|谷歌, ioCount: 7, took: 0 ms} +ip2region>> +``` + +输入 ip 即可进行查询测试,也可以分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同缓存实现的查询效果。 # bench 测试 + +可以通过 `IP2Region.SearchTest.exe bench` 命令来进行 bench 测试,一方面确保 `xdb` 文件没有错误,一方面可以评估查询性能: +```bash +IP2Region.SearchTest.exe bench +IP2Region.SearchTest.exe bench [command options] +options: + --db string ip2region binary xdb file path + --src string source ip text file path + --cache-policy string cache policy: file/vectorIndex/content +``` + +例如:通过默认的 data/ip2region.xdb 和 data/ip.merge.txt 文件进行 bench 测试: +```bash +IP2Region.SearchTest.exe bench --db=../../../../../data/ip2region.xdb --src=../../../../../data/ip.merge.txt +Bench finished, {cachePolicy: vectorIndex, total: 3417955, took: 00:00:48.0082981, cost: 0 ms/op} +``` + +可以通过分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同缓存实现的效果。 +@Note: 注意 bench 使用的 src 文件要是生成对应 xdb 文件相同的源文件。