伴随着 IP 位置库 的上线,笔者的“童年梦想”又成真了一个。为了分发这份来之不易的数据库,笔者找到了 ip2region 项目。该项目提供了一种体积小且查询速度极快的离线IP位置数据库文件格式,同时提供了多种语言支持的查询客户端。但 ip2region 项目的作者并未提供除 Java 以外的数据库文件生成代码,笔者打算为该项目移植 .NET 5.0 的数据库文件生成器,并在本文中记录下移植过程。
移植前准备
ip2region 的 Java 版数据库生成器 代码并不复杂,源代码文件只有 8 个。以笔者粗浅的 Java 经验来看,因为 C# 与 Java 大体相似,移植过程中无需对程序的结构和命名进行变更,也无需对处理逻辑进行调整。移植需要做的就是让程序可以编译通过,基本上就算成功。
开始移植
笔者新建了一个名为 IP2RegionDotNetDbMaker 的 .NET 5.0 控制台应用程序,删掉 Program.cs 文件并将所有的 Java 文件复制到项目中。
下一步操作很暴力,就是直接将源代码的后缀从 .java 改为 .cs 。为此,笔者在 LINQPad 中写了一段小代码,来完成这个操作:
var dir = @"D:\coderbusy.com\demo\IP2RegionDotNetDbMaker\IP2RegionDotNetDbMaker"; var javaFiles = Directory.GetFiles(dir, "*.java"); foreach (var javaFile in javaFiles) { var _ = Path.GetDirectoryName(javaFile); var fileName = Path.GetFileNameWithoutExtension(javaFile); var csFile = Path.Combine(_, fileName + ".cs"); File.Move(javaFile, csFile); }
在暴力改名之后的源代码文件里,不出意外的报了很多错误:
需要先把 package 和 import 这两种语句去掉,然后把缺失的命名空间给加上。
var dir = @"D:\coderbusy.com\demo\IP2RegionDotNetDbMaker\IP2RegionDotNetDbMaker"; var files = Directory.GetFiles(dir, "*.cs"); foreach (var file in files) { var lines = File.ReadAllLines(file, Encoding.UTF8); var builder = new StringBuilder(); builder.AppendLine($"using System;{Environment.NewLine}namespace IP2RegionDotNetDbMaker{Environment.NewLine}{{"); foreach (var line in lines) { if (line.StartsWith("package ")) { continue; } if (line.StartsWith("import ")) { continue; } builder.AppendLine(line); } builder.AppendLine("}"); var content = builder.ToString(); File.WriteAllText(file, content, Encoding.UTF8); }
异常声明在 C# 中不支持,可以通过正则将其替换掉:
在替换时,确定开启“使用正则表达式”,查找项为:throws ([\w ,]+)Exception
替换项保持为空。之后,替换掉所有的 @Override
和 final
关键字。
在 C# 中 out 是一个关键字不能被当作类型使用,Java 编程中常用的 System.out.println
方法需要被替换成 Console.WriteLine
,直接全局替换搞定。
重构 DbMakerConfigException 类型:
using System; namespace IP2RegionDotNetDbMaker { /** * configuration exception * * @author chenxin<chenxin619315@gmail.com> */ public class DbMakerConfigException : Exception { public DbMakerConfigException(string info) : base(info) { } } }
在 .NET 5.0 中 Mock 实现 Java 所需的 API
新建 Mock.cs 文件,用于存放 Java API 到 C# API 的Mock 代码。使用扩展方法对 String 类型进行扩展,并实现 Java API 所用的方法:
public static class Extensions { public static Int32 length(this string str) { if (String.IsNullOrWhiteSpace(str)) { return 0; } return str.Length; } public static string trim(this string str) { return str.Trim(); } public static char charAt(this string str, Int32 i) { return str[i]; } public static int indexOf(this string str, string value) { return str.IndexOf(value); } public static int indexOf(this string str, char value, Int32 start) { return str.IndexOf(value, start); } public static string substring(this string str, Int32 startIndex) { return str.Substring(startIndex); } public static string substring(this string str, Int32 startIndex, Int32 endIndex) { return str.Substring(startIndex, endIndex - startIndex); } public static bool equals(this string str1, string str2) { return String.Equals(str1, str2, StringComparison.InvariantCultureIgnoreCase); } public static string[] split(this string str, string separator) { return str.Split(separator); } public static byte[] getBytes(this string str) { return Encoding.UTF8.GetBytes(str); } public static byte[] getBytes(this string str, string encoding) { return Encoding.GetEncoding(encoding).GetBytes(str); } public static bool endsWith(this string str, string value) { return str.EndsWith(value); } }
Mock 实现 StringBuilder 类型:
public class StringBuilder { private readonly System.Text.StringBuilder _builder = new System.Text.StringBuilder(); internal StringBuilder append(object value) { _builder.Append(value); return this; } internal string toString() { return _builder.ToString(); } }
Mock 实现 File 类型:
public class File { public File(string ipSrcFile) { _fileInfo = new FileInfo(ipSrcFile); } private FileInfo _fileInfo; public FileInfo FileInfo => _fileInfo; internal bool exists() { return _fileInfo.Exists; } }
Mock 实现 LinkedList 类型:
public class LinkedList<T> : List<T> { internal T getFirst() { return this[0]; } internal void add(T item) { this.Add(item); } internal T getLast() { return this[this.Count - 1]; } internal IEnumerable<T> iterator() { return this; } }
Mock 实现 HashMap 类型:
public class HashMap<K, V> : Dictionary<K, V> { internal void put(K k, V v) { this[k] = v; } internal bool containsKey(K key) { return this.ContainsKey(key); } internal V get(K k) { if (containsKey(k)) { return this[k]; } return default; } }
Mock 实现 FileReader 类型:
public class FileReader { private File globalRegionFile; private Queue<String> _lines = new Queue<string>(); public FileReader(File file) { this.globalRegionFile = file; using (var fs = file.FileInfo.OpenRead()) { using (var sr = new StreamReader(fs)) { while (!sr.EndOfStream) { var line = sr.ReadLine(); _lines.Enqueue(line); } } } } internal string readLine() { if (_lines.TryDequeue(out var line)) { return line; } return null; } internal void close() { } }
Mock 实现 BufferedReader 类型:
public class BufferedReader { private FileReader fileReader; public BufferedReader(FileReader fileReader) { this.fileReader = fileReader; } internal string readLine() { return fileReader.readLine(); } internal void close() { fileReader.close(); } }
Mock 实现 RandomAccessFile 类型:
public class RandomAccessFile { private string dbFile; private Stream stream; internal void seek(long v) { stream.Seek(v, SeekOrigin.Begin); } internal void write(byte[] vs) { stream.Write(vs); } internal void readFully(byte[] dbBinStr, int v, int length) { stream.Read(dbBinStr, v, length); } private string v; public RandomAccessFile(string dbFile, string v) { this.dbFile = dbFile; this.v = v; this.stream = new FileStream(dbFile, FileMode.OpenOrCreate, FileAccess.ReadWrite); } public long length() { return this.stream.Length; } internal void close() { if (stream == null) { return; } this.stream.Flush(); this.stream.Close(); this.stream = null; } internal long getFilePointer() { return stream.Position; } internal void write(int v) { var bytes = BitConverter.GetBytes(v); stream.Write(bytes); } }
语法与属性修正
经过以上的 Mock 操作,报错部分便仅仅涉及语法和部分属性。
C# 中并不存在“扩展属性”类似的东西,所以 Java 中以“小驼峰”命名的 length
字段调用,据需要改为“大驼峰”方式的 Length
。位运算符 >>>
也需要改为 >>
,同时,还有几个语法错误需要修正。比如:C# 中并不支持 Java 中的 for(Type e:collection)
语法,需要用 foreach
来替代。之后,项目就可以编译通过了。
结果验证
将 data 目录拷贝至 bin 目录,使用以下命令便可启动生成:
dbMaker -src ./data/ip.merge.txt -region ./data/global_region.csv
伴随着大量的控制台输出,笔者似乎找到了黑客帝国的感觉。经过一小会儿的等待,生成已经成功执行。
通过二进制对比,该结果文件仅在行尾的日期存储部分与源文件不同:
通过阅读代码,文件末尾部分的数据是生成的时间戳和一小段声明信息。文件尾的不一致并不会对使用造成影响。这表示,这次移植是成功的。
开源地址
目前,该代码已经上传至 Gitee ,地址是:
https://gitee.com/coderbusy/demo/tree/master/IP2RegionDotNetDbMaker
反向操作最为致命。
可以可以,这动作很骚