zkemkeeper是中控指纹签到机的sdk,最近因为有读取考勤日志的需要,因此要实现连指纹机获取日志与php进行通信,却没想到是个蛋疼的过程.
Reflector反编译
先尝试反编译sdk下的一坨dll,然后发现这特么不是.net的dll,目测是C或者C++编译的COM组件,于是老老实实进行注册:
1 | copy .\*.dll %windir%\System32\ |
2 | regsvr32 %windir%\System32\zkemkeeper.dll |
我就不吐槽官方的包把system32写成小写了…
然后看看Demo…有C#和VB.net两种,于是考虑把Demo里引用的Interop.zkemkeeper.dll给拿来用吧,反编译了下,是COM组件,但喜闻乐见的发现这dll是pinvoke的.P/Invoke的全称是Platform Invoke (平台调用) 它实际上是一种函数调用机制,通过P/Invoke可以调用非托管DLL中的函数 .于是里面没有啥对我们有用的代码,核心函数全在那些c++的dll里.不过姑且还算COM组件,还是能试试在php下调用吧:
谷歌了下网上的php调用.net的dll教程.基本上都是dll自己写的或者是有源码的,但是我手头只有一个孤零零的P/Invoke的dll,怎么办呢?
由于用reflector看了下发现assembly中没有ComVisible项,于是用dll注入给加上这一项.
这里需要先给reflector安装Reflexil插件.重启reflector后,先选择需要的dll,然后打开Tools->Reflexil v1.5,在Custom attributes选项卡中右击Create New,在Attributes选项卡中,Constructor找到mscorlib->CommonLanguageRuntimeLibrary->System.Runtime.InteropServices->ComVisibleAttribute,选择.ctor,如图所示:
同理,在Constructor arguments选项卡中,Type找到mscorlib->CommonLanguageRuntimeLibrary->Boolean并选择,Specification选Default,Arg.type选Boolean,Argument手动输入True并确定.
最后在类上右击,找到Reflexil->Save as,就可以导出注入过的dll了.
强命名签名文件
木有项目文件,只有一个dll,怎么做强命名签名?还是有办法的:
为没有源码的DLL文件添加强名称:
1 | sn -k Interop.zkemkeeper.snk //创建一个新的随机密钥对 |
3 | ildasm Interop.zkemkeeper.dll /out=Interop.zkemkeeper.il //反编译目标程序集 |
5 | ilasm Interop.zkemkeeper.il /dll /resource=Interop.zkemkeeper.res /key=Interop.zkemkeeper.snk /optimize |
ok,这个时候的dll就是带强签名的了,真不知道微软这样搞强签名有什么意义-.-
注册.netdll并添加到全局程序集缓存
以管理员身份运行vs的命令提示符工具:(一般在Visual Studio Tools中有个Visual Studio 命令提示)
1 | sn - v Interop.zkemkeeper.dll |
3 | regasm Interop.zkemkeeper.dll |
5 | gacutil /I Interop.zkemkeeper.dll |
如图所示就代表成功了:
获取ProgID
刚才那个dll还留着,我们可以使用以下命令生成个reg文件:
1 | regasm /regfile Interop.zkemkeeper.dll |
这步只是为了看我们之前注册组件的时候在注册表的位置.
打开regedit,找到reg文件里记录的{xxxx..}这样的键,展开,找到ProgID,字符串键的值就是php调用COM的时候需要的类名了.
php调用.net dll:
首先php需要开启com_dotnet,在php.ini中开启插件:
2 | extension=php_com_dotnet.dll |
然后php中便可调用了,方法类似如下:
1 | $r = new Com( "刚才获得的ProgID" ); |
–悲剧的分割线–
当然,以上是理想状态下,实际情况是,我在前面都成功的情况下,最后调用仍然报错了:
0x80040154 没有注册类
…
(╯‵□′)╯︵┻━┻
谷歌了半天,最后觉得仍然是zkemkeeper的问题.因为官方包你的Demo已经能正常运行,说明类是注册了的,而这个错误在中控的论坛上都sdk没注册成功的情况下报的,折腾了很久,仍然未找到原因.(如果有谁知道请务必告诉我_(:з」∠)_ )
于是迫不得已只能用另一种方法了,那就是http监听:
使用WinForm监听http
先引用Interop.zkemkeeper.dll,然后写个方法GetData.test()按照Demo的事例抓取需要的数据(此处略);
winform使用端口监听还是蛮方便的,一个后台线程就能搞定:
2 | private HttpListener _listerner = new HttpListener(); |
4 | private void StartBackWork( ) |
6 | if (!_listerner.IsListening) |
7 | using (_listerner = new HttpListener()) |
9 | _listerner.AuthenticationSchemes = AuthenticationSchemes.Anonymous; //指定身份验证 Anonymous匿名访问 |
12 | _listerner.Prefixes.Add(textBox1.Text.Trim()); |
14 | catch (ArgumentException) |
16 | MessageBox.Show( "地址不合法!" , "错误" , MessageBoxButtons.OK, MessageBoxIcon.Error); |
23 | catch (HttpListenerException e) |
25 | MessageBox.Show( "启动失败:\n" + e.Message, "警告" , MessageBoxButtons.OK, MessageBoxIcon.Warning); |
28 | string [] ipstr = richTextBox1.Text.Split( ',' ); |
29 | foreach ( string s in ipstr) |
33 | MessageBox.Show( "IP不能为空" , "警告" , MessageBoxButtons.OK, MessageBoxIcon.Warning); |
40 | richTextBox1.ReadOnly = true ; |
41 | textBox1.ReadOnly = true ; |
42 | MessageBox.Show( "监听启动成功" , "提示" , MessageBoxButtons.OK, MessageBoxIcon.Information); |
46 | //没有请求则GetContext处于阻塞状态 |
47 | HttpListenerContext ctx = _listerner.GetContext(); |
48 | ctx.Response.StatusCode = 200; //设置返回的http状态代码 |
49 | string name = ctx.Request.QueryString[ "name" ]; |
53 | Console.WriteLine(name); |
58 | using (StreamWriter writer = new StreamWriter(ctx.Response.OutputStream)) |
60 | //writer.Write("aaa"); |
62 | writer.Write(GetData.test(ipstr)); |
当然,为了不卡UI,我们要新建一个Thead来运行它:
1 | if (!_listerner.IsListening) |
3 | _t = new Thread(StartBackWork); |
这样,当这个程序运行着的时候,就能监听指定的本地http地址(如localhost),如果其他机器或者php访问请求,就可以触发事件,该程序调用那几个COM组件去考勤机上取数据,最终返回给http请求方.
参考文章:
PHP调用C#dll类库方法
关于程序集生成失败 — 引用的程序集没有强名称的解决办法
Making Non Strongly Named .NET DLL’s COM visible with a wrapper DLL
C# net winform wpf 发送post数据和xml到网页
C#实现一个简单的HTTP服务器