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 文件相同的源文件。