最近在做一个asp.net的项目,其中有个功能是根据输入的ip地址得到相对应的省市信息。目前网上有很多相关的服务,但是要么是收费的要么就是使用上有限制(每秒查询次数限制)。经过一番搜索,终于在github上发现了一个开源的准确率99.9%的ip地址定位库ip2region。它目前提供了java,php,c,python,nodejs,golang查询绑定,竟然没有提供C#查询绑定…不过还好,我们有P/Invoke调用

封装生成动态库

首先,下载ip2region源码。创建一个C++的动态库工程,将ip2region.h和ip2region.cpp(对ip2region.c直接改名)文件包含进来,直接编译生成动态库ip2region.dll。

P/Invoke调用

应用P/Invoke Interop Assistant GUI Tool工具,生成对应的C#导出类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class Util
{
/// Return Type: unsigned int
///ip2rObj: ip2region_entry*
///dbFile: char*
[System.Runtime.InteropServices.DllImportAttribute(@"ip2region.dll", EntryPoint = "ip2region_create", CallingConvention = CallingConvention.Cdecl)]
public static extern uint ip2region_create(ref ip2region_entry ip2rObj, string dbFile);
/// Return Type: unsigned int
///ip2rObj: ip2region_entry*
[System.Runtime.InteropServices.DllImportAttribute(@"ip2region.dll", EntryPoint = "ip2region_destroy", CallingConvention = CallingConvention.Cdecl)]
public static extern uint ip2region_destroy(ref ip2region_entry ip2rObj);
/// Return Type: unsigned int
///ip2rObj: ip2region_entry*
///ip: char*
///datablock: datablock_entry*
[System.Runtime.InteropServices.DllImportAttribute(@"ip2region.dll", EntryPoint = "ip2region_btree_search_string", CallingConvention = CallingConvention.Cdecl)]
public static extern uint ip2region_btree_search_string(ref ip2region_entry ip2rObj, string ip, ref datablock_entry datablock);
/// Return Type: unsigned int
///ip2rObj: ip2region_entry*
///ip: char*
///datablock: datablock_entry*
[System.Runtime.InteropServices.DllImportAttribute(@"ip2region.dll", EntryPoint = "ip2region_binary_search_string", CallingConvention = CallingConvention.Cdecl)]
public static extern uint ip2region_binary_search_string(ref ip2region_entry ip2rObj, string ip, ref datablock_entry datablock);
/// Return Type: unsigned int
///ip2rObj: ip2region_entry*
///ip: char*
///datablock: datablock_entry*
[System.Runtime.InteropServices.DllImportAttribute(@"ip2region.dll", EntryPoint = "ip2region_memory_search_string", CallingConvention = CallingConvention.Cdecl)]
public static extern uint ip2region_memory_search_string(ref ip2region_entry ip2rObj, string ip, ref datablock_entry datablock);
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct ip2region_entry
{
/// unsigned int*
public System.IntPtr HeaderSip;
/// unsigned int*
public System.IntPtr HeaderPtr;
/// unsigned int
public uint headerLen;
/// char*
[System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.LPStr)]
public string dbFile;
/// void*
public System.IntPtr dbHandler;
/// char*
[System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.LPStr)]
public string dbBinStr;
/// unsigned int
public uint firstIndexPtr;
/// unsigned int
public uint lastIndexPtr;
/// unsigned int
public uint totalBlocks;
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Ansi)]
public struct datablock_entry
{
/// unsigned int
public uint city_id;
/// char[256]
[System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.ByValTStr, SizeConst = 256)]
public string region;
}

这里需要注意的是,函数的调用方式需设置为CallingConvention = CallingConvention.Cdecl。同时,对于导出函数的名称最好用dependens工具查看一下。因为C++对于有些调用方式为了实现重载会重命名导出函数名。

32位ip2region.dll Cdecl导出函数列表

写代码进行调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ip2region_entry ip2rObj = new ip2region_entry();
datablock_entry datablockObj = new datablock_entry();
uint createRet = Util.ip2region_create(ref ip2rObj, "ip2region.db");
Util.ip2region_btree_search_string(ref ip2rObj, ipStr, ref datablockObj);
///ip2region.db需要强制使用utf-8编码,默认就是在linux下生成的。而windows下是ANSI编码,故需要做一次转换。
string resultStr = Helper.GetOfUtf8(Encoding.Default.GetBytes(datablockObj.region));
public class Helper
{
public static string GetOfUtf8(byte[] strObj)
{
///开始解码
System.Text.Decoder utf8Decoder = System.Text.Encoding.UTF8.GetDecoder();
int charCount = utf8Decoder.GetCharCount(strObj, 0, strObj.Length);
Char[] chars = new Char[charCount];
int charsDecodedCount = utf8Decoder.GetChars(strObj, 0, strObj.Length, chars, 0);
return new string(chars);
}
}

动态库路径限制

在将工程部署到测试环境中时发现,必须将ip2region.dll放在固定的目录下才行。因为System.Runtime.InteropServices.DllImportAttribute要求路径必须是固定的,不可动态去指定。可是,这样的话就会给维护和部署带来不便,有没有什么办法可以动态的去获得ip2region.dll路径并将它load起来呢?在参考了几篇网文之后找到了一种方法,就是调用windows加载动态库的api(LoadLibrary, GetProcAddress, FreeLibrary)并通过C#的委托来实现调用。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class DllInvoke
{
[DllImport("kernel32.dll")]
private static extern IntPtr LoadLibrary(String path);
[DllImport("kernel32.dll")]
private static extern IntPtr GetProcAddress(IntPtr lib, String funcName);
[DllImport("kernel32.dll")]
private static extern bool FreeLibrary(IntPtr lib);
private IntPtr hLib;
public DllInvoke(String DLLPath)
{
hLib = LoadLibrary(DLLPath);
}
~DllInvoke()
{
FreeLibrary(hLib);
}
///将要执行的函数转换为委托
public Delegate Invoke(String APIName, Type t)
{
IntPtr api = GetProcAddress(hLib, APIName);
return (Delegate)Marshal.GetDelegateForFunctionPointer(api, t);
}
}
public class funcExport
{
private DllInvoke dll;
private const string x86Dll = "ip2region.dll";
private const string x64Dll = "ip2region64.dll";
public bool isValid { get; set; }
public delegate uint ip2RegionCreateD(ref ip2region_entry ip2rObj, string dbFile);
public delegate uint ip2RegionDestroy(ref ip2region_entry ip2rObj);
public delegate uint ip2RegionBtreeSearchString(ref ip2region_entry ip2rObj, string ip, ref datablock_entry datablock);
public funcExport.ip2RegionCreateD createDIns;
public funcExport.ip2RegionDestroy destoryDIns;
public funcExport.ip2RegionBtreeSearchString bSearchDIns;
public funcExport(string dllPath)
{
isValid = true;
var dllFile = System.IO.Path.Combine(dllPath, x64Dll);
dll = new DllInvoke();
if (dll.DllLoad(dllFile))
{
createDIns =
(funcExport.ip2RegionCreateD) dll.Invoke("ip2region_create", typeof (funcExport.ip2RegionCreateD));
destoryDIns =
(funcExport.ip2RegionDestroy) dll.Invoke("ip2region_destroy", typeof (funcExport.ip2RegionDestroy));
bSearchDIns =
(funcExport.ip2RegionBtreeSearchString)
dll.Invoke("ip2region_btree_search_string", typeof (funcExport.ip2RegionBtreeSearchString));
}
else
{
dllFile = System.IO.Path.Combine(dllPath, x86Dll); ;
if (dll.DllLoad(dllFile))
{
createDIns =
(funcExport.ip2RegionCreateD)
dll.Invoke("_ip2region_create@8", typeof (funcExport.ip2RegionCreateD));
destoryDIns =
(funcExport.ip2RegionDestroy)
dll.Invoke("_ip2region_destroy@4", typeof (funcExport.ip2RegionDestroy));
bSearchDIns =
(funcExport.ip2RegionBtreeSearchString)
dll.Invoke("_ip2region_btree_search_string@12",
typeof (funcExport.ip2RegionBtreeSearchString));
}
else
{
isValid = false;
}
}
}
}

细心的网友可能已经发现,在funcExport类中我会进行两次加载尝试,分别是32位和64位ip2region动态库。为什么会有如此操作呢?因为,在我将ASP.NET的工程部署到测试环境中时发现程序不再work了,甚至还抛出了异常…
通过调试发现,原本加载32位的ip2region.dll没有成功,可是为什么在我调试的时候是可以正常load的呢?
原来,我是部署在64位的Win server 2008上,默认的应用程序池是运行在64位模式下的,是不允许调用32位的dll的。

在 IIS 7.x 中,要“启用 32bit 应用程序支持”,需要对“应用程序池”进行配置。将“enable32BitAppOnWin64”设置为“True”。这样便将应用程序池的工作进程设置为 WOW64 模式,而在 WOW64 模式下,工作进程将仅加载 32 位应用程序的 32 位进程。

应用程序池设置启用32bit应用程序支持

而本地调试之所以可以load是因为设置的开发模式是Any CPU并且是设置的IIS express进行调试,那么也就没有IIS application中的限制了。

可是也不能为了这个而让整个网站程序运行在WoW64模式呀,这样肯定会影响性能的。所以就有了以上两次加载尝试,先加载64位的动态库,若失败的话则再加载32位的动态库。这里有一点需要注意的是,由于这种方式不能指定函数调用方式,而.NET的默认调用方式是Stdcall,所以在生成动态库的时候将调用约定设置为stdcall。
设置dll调用约定
至此本篇完。总结如下:

  • .NET调用C/C++动态库时除了用Framework提供的方法DllImportAttribute外还可通过调用kernel32.dll中的(LoadLibrary, GetProcAddress, FreeLibrary)来实现。
  • P/Invoke调用时需注意函数的调用约定一致。
  • 若要64位系统上的IIS支持跑32位的程序或调用32位的动态库,需要启用32bit应用程序支持.