|
第一种是关于类型的关键字,主要包括host,net,port, 例如 host 210.27.48.2,指明
210.27.48.2是一台主机,net 202.0.0.0 指明 202.0.0.0是一个网络地址,port 23
指明端口号是23。如果没有指定类型,缺省的类型是host.
第二种是确定传输方向的关键字,主要包括src , dst ,dst or src, dst and src
,这些关键字指明了传输的方向。举例说明,src 210.27.48.2 ,指明ip包中源地址是210.27.48.2 , dst net
202.0.0.0 指明目的网络地址是202.0.0.0 。如果没有指明方向关键字,则缺省是src or dst关键字。
第三种是协议的关键字,主要包括fddi,ip,arp,rarp,tcp,udp等类型。Fddi指明是在FDDI(分布式光纤数据接口网络)上的特定
的网络协议,实际上它是"ether"的别名,fddi和ether具有类似的源地址和目的地址,所以可以将fddi协议包当作ether的包进行处理和
分析。其他的几个关键字就是指明了监听的包的协议内容。如果没有指定任何协议,则tcpdump将会监听所有协议的信息包。
除了这三种类型的关键字之外,其他重要的关键字如下:gateway,
broadcast,less,greater,还有三种逻辑运算,取非运算是 'not ' '! ',
与运算是'and','&&';或运算 是'or'
,'││';这些关键字可以组合起来构成强大的组合条件来满足人们的需要,下面举几个例子来说明。
普通情况下,直接启动tcpdump将监视第一个网络界面上所有流过的数据包。
# tcpdump
tcpdump: listening on fxp0
11:58:47.873028 202.102.245.40.netbios-ns > 202.102.245.127.netbios-ns: udp 50
11:58:47.974331 0:10:7b:8:3a:56 > 1:80:c2:0:0:0 802.1d ui/C len=43
0000 0000 0080 0000 1007 cf08 0900 0000
0e80 0000 902b 4695 0980 8701 0014 0002
000f 0000 902b 4695 0008 00
11:58:48.373134 0:0:e8:5b:6d:85 > Broadcast sap e0 ui/C len=97
ffff 0060 0004 ffff ffff ffff ffff ffff
0452 ffff ffff 0000 e85b 6d85 4008 0002
0640 4d41 5354 4552 5f57 4542 0000 0000
0000 00
使用-i参数指定tcpdump监听的网络界面,这在计算机具有多个网络界面时非常有用,
使用-c参数指定要监听的数据包数量,
使用-w参数指定将监听到的数据包写入文件中保存
A想要截获所有210.27.48.1 的主机收到的和发出的所有的数据包:
#tcpdump host 210.27.48.1
B想要截获主机210.27.48.1 和主机210.27.48.2 或210.27.48.3的通信,使用命令:(在命令行中适用 括号时,一定要
#tcpdump host 210.27.48.1 and (210.27.48.2 or 210.27.48.3 )
C如果想要获取主机210.27.48.1除了和主机210.27.48.2之外所有主机通信的ip包,使用命令:
#tcpdump ip host 210.27.48.1 and ! 210.27.48.2
D如果想要获取主机210.27.48.1接收或发出的telnet包,使用如下命令:
#tcpdump tcp port 23 host 210.27.48.1
E 对本机的udp 123 端口进行监视 123 为ntp的服务端口
# tcpdump udp port 123
F 系统将只对名为hostname的主机的通信数据包进行监视。主机名可以是本地主机,也可以是网络上的任何一台计算机。下面的命令可以读取主机hostname发送的所有数据:
#tcpdump -i eth0 src host hostname
G 下面的命令可以监视所有送到主机hostname的数据包:
#tcpdump -i eth0 dst host hostname
H 我们还可以监视通过指定网关的数据包:
#tcpdump -i eth0 gateway Gatewayname
I 如果你还想监视编址到指定端口的TCP或UDP数据包,那么执行以下命令:
#tcpdump -i eth0 host hostname and port 80
J 如果想要获取主机210.27.48.1除了和主机210.27.48.2之外所有主机通信的ip包
,使用命令:
#tcpdump ip host 210.27.48.1 and ! 210.27.48.2
K 想要截获主机210.27.48.1 和主机210.27.48.2 或210.27.48.3的通信,使用命令
:(在命令行中适用 括号时,一定要
#tcpdump host 210.27.48.1 and (210.27.48.2 or 210.27.48.3 )
L 如果想要获取主机210.27.48.1除了和主机210.27.48.2之外所有主机通信的ip包,使用命令:
#tcpdump ip host 210.27.48.1 and ! 210.27.48.2
M 如果想要获取主机210.27.48.1接收或发出的telnet包,使用如下命令:
#tcpdump tcp port 23 host 210.27.48.1
第三种是协议的关键字,主要包括fddi,ip ,arp,rarp,tcp,udp等类型
除了这三种类型的关键字之外,其他重要的关键字如下:gateway, broadcast,less,
greater,还有三种逻辑运算,取非运算是 'not ' '! ', 与运算是'and','&&';或运算 是'o
r' ,'||';
第二种是确定传输方向的关键字,主要包括src , dst ,dst or src, dst and src ,
如果我们只需要列出送到80端口的数据包,用dst port;如果我们只希望看到返回80端口的数据包,用src port。
#tcpdump –i eth0 host hostname and dst port 80 目的端口是80
或者
#tcpdump –i eth0 host hostname and src port 80 源端口是80 一般是提供http的服务的主机
如果条件很多的话 要在条件之前加and 或 or 或 not
#tcpdump -i eth0 host ! 211.161.223.70 and ! 211.161.223.71 and dst port 80
如果在ethernet 使用混杂模式 系统的日志将会记录
May 7 20:03:46 localhost kernel: eth0: Promiscuous mode enabled.
May 7 20:03:46 localhost kernel: device eth0 entered promiscuous mode
May 7 20:03:57 localhost kernel: device eth0 left promiscuous mode
tcpdump对截获的数据并没有进行彻底解码,数据包内的大部分内容是使用十六进制的形式直接打印输出的。显然这不利于分析网络故障,通常的解决办法是
先使用带-w参数的tcpdump
截获数据并保存到文件中,然后再使用其他程序进行解码分析。当然也应该定义过滤规则,以避免捕获的数据包填满整个硬盘。
评论摘要:
1,
其实LZ 只说明了过滤语句的用法,没有将很重要另外一个参数说明,也就是说,如果这个参数不设置正确,会导致包数据的丢失!
它就是-s 参数,snaplen, 也就是数据包的截取长度,仔细看man就会明白的!默认截取长度为60个字节,但一般ethernet MTU都是1500字节。所以,要抓取大于60字节的包时,使用默认参数就会导致包数据丢失!
只要使用-s 0就可以按包长,截取数据!
2,
各位有兴趣,我可以多说一点。
LZ所说的这个格式有一个业界标准,称作bpf (Berkeley Packet
Filter)包过滤语言。现在有很多抓包工具支持这个标准。包扩ethereal的capture filter,
注意不是displayfilter, 因为ethereal的display filter 使用了近似于C/C++表达式的另一套自己的表达方式。
其实,如果各位使用过libpcap的,就知道,所有的libpcap都需要snaplen
这个参数设定才能抓到整个的包数据。那如果不熟悉C/C++怎么办?,没关系那也可以结合tcpdump -xls 0
命令行加管道导向到Awk的程序来自己解析IP数据包并分析应用层的数据。如果用tcpdump+Perl的话,更可以经过简单的包数据重构,方便地组成
NetPacket模块能自动识别的包数据,让现成的NetPacket模块自动解析和分析底层的IP和TCP/UDP层数据包。而你则只要懂得gawk
/nawk或Perl就可以简单的自己编程来解析非标的应用层的数据了。注意,这里我并没有使用perl
的libpcap兼容模块,而是直接用tcpdump替代了。这样对Perl模块的要求降得比较低了,如果遇到非标的协议,就是不会C/C++,
没装抓包的perl模块,也可以通过自己编程来解析数剧包了!
3,
还好的是可以通过hex value来查包中的汉字。我是先在linux上边tcpdump出完整的package data,然后呢,将文件down到windows上来用ethereal来做查看的。
我不是很理解你所说的 '要么在服务器端装支持相应编码的代理,然后dump数据。
你说的编码的代理,我觉得这个与整个网页的charset是有关系的。可能是不同的charset,gb2312,unicode或是gbk之类的。很难理解你说的编码的代理是什么东东?
我想ethereal就是没有去分析每个包应该用什么来charset来做显示,所以都以ascii来显示了。所以汉字就显示不出来。
要想ethereal知道使用什么样的charset显示,那么就需要联系其他包里的charset设置来做显示,看来ethereal目前还没这么做,就只是简单的显示ascii。
就象大家更熟悉的const一样,volatile是一个类型修饰符(type specifier)。它是被设计用来修饰被不同线程访问和修改的变量。如果没有volatile,基本上会导致这样的结果:要么无法编写多线程程序,要么编译器失去大量优化的机会。 讲讲volatile的作用 volatile的本意是“易变的” 由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。比如: static int i=0; int main(void) { while (1) if (i) dosomething(); } /* Interrupt service routine. */ void ISR_2(void) { i=1; } 程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。 一般说来,volatile用在如下的几个地方: 1、中断服务程序中修改的供其它程序检测的变量需要加volatile; 2、多任务环境下各任务间共享的标志应该加volatile; 3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义; 另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。 推荐一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子: 1). 并行设备的硬件寄存器(如:状态寄存器) 2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables) 3). 多线程应用中被几个任务共享的变量
原文:目录 o 一个COM对象和它的VTable o GUID o QueryInterface(),AddRef(),和Release() o 一个IClassFactory对象 o 打包成dll o 我们的C++/C包含文件 o 定义文件(DEF) o 安装DLL,注册对象 o 一个C语言的例子 o 一个C++的例子 o 修改代码 o 下一步?
介绍 有大量例子说明如何使用和创建COM/OLE/ActiveX控件。但是这些例子典型的使用MFC,.NET,C#,WTL,至少是ATL,因为这些框架预制了一些样板文件。不信的是,这些框架也隐藏了许多底层细节,所以你从未真正的学习COM组件,而只是学习了如何使用一个顶层的框架。 如果试图使用C,而抛弃MFC,WTL,.NET,ATL,C#甚至是任何C++,那么你会发现很少有这方面的例子和资料。这个就是本篇文章所要说明的第一个系列。 在使用标准Win32控件如Static,Edit,Listbox,Combobox等时,你得到一个控件句柄(HWND)然后向它传递消息(通过SendMessage)为了操纵它。同样,控件也给你传递消息(比如把消息放到你的消息对列,你通过GetMessage去获得)当它想通知你一些事件或者给你一些数据。 这与OLE/COM对象不同。你不必传递消息。取而代之的是,COM对象给你一些函数指针你可以调用它们去操纵对象。例如,一个IE浏览器对象会给你一个函数指针使得你可以调用它来加载网页。一个Office对象会你一个函数指针你可以调用它来加载文档。如果一个COM对象需要通知你一些事件或向你传递数据,那么你可能需要写一些特定的函数,并向COM对象提供函数指针,使得它们可以调用这些函数当它们需要的时候。换句话来说,你需要创建你自己的COM对象。大多数在C方面的争议在于定义你自己的COM对象。为了完成这个,你需要知道关于COM对象的细节--就是大多数框架隐藏掉的,但是这里我们会阐述。 总结一下,你调用COM对象的函数来操纵它,它调用你的函数来通知你某些事件或者给你传递数据或与你的程序交互。这个机制调用DLL中的函数类似,DLL也可以调用你的函数--callback函数。但是与DLL不同的是,你不能使用LoadLibrary()和GetProcAddress()来获得COM中的函数,我们马上会发现,你会使用不同的操作系统函数得到一个对象的指针,然后使用那个对象来得到函数的指针。 一个COM对象和它的VTable 在我们学习如何使用COM对象,我们首先需要明白什么是COM对象,最好的方法是我们创建一个自己的COM对象。 在我们作之前,让我们看一下C struct结构,作为一个C程序员,你应该很熟悉struct,这里有个例子定义了一个简单的结构(IExample)包含两个成员 struct IExample { DWORD count; char buffer[80]; } 使用typedef来简化 typedef struct { DWORD count; char buffer[80]; } IExample; 这里有个例子分配结构空间,初始化成员: IExample *example; example = (IExample *)GlobalAlloc(GMEM_FIXED, sizeof (IExample)); example->count = 1; example->buffer[0] = 0; 现在我们为结构添加函数指针 long SetString(char *str) { return (0); } long GetString(char *buffer, long length) { return (0); } typedef long SetStringPtr(char *); typedef long GetStringPtr(char *, long); typedef struct { SetStringPtr *SetString; GetStringPtr *GetString; DWORD count; char buffer[80]; } IExample; 初始化 example->SetString = SetString; example->GetString = GetString; 现在让我们把上面的指针放在一个数组里面,我们添加一个成员lpVtbl。 typedef struct { IExampleVtbl *lpVtbl; DWORD count; char buffer[80]; } IExample; 这里有个例子初始化IExample,包括IExampleVtbl: // Since the contents of IExample_Vtbl will never change, we'll // just declare it static and initialize it that way. It can // be reused for lots of instances of IExample. static const IExampleVtbl IExample_Vtbl = {SetString, GetString};
IExample * example;
// Create (allocate) a IExample struct. example = (IExample *)GlobalAlloc(GMEM_FIXED, sizeof(IExample));
// Initialize the IExample (ie, store a pointer to // IExample_Vtbl in it). example->lpVtbl = &IExample_Vtbl; example->count = 1; example->buffer[0] = 0; 调用我们的函数: char buffer[80]; example=>lpVtbl->SetString("Some text"); example->lpVtbl->GetString(buffer, sizeof (buffer)); 接下来让我们重新完善SetString和GetString typedef long SetStringPtr(IExample *, char *); typedef long GetStringPtr(IExample *, char *, long);
long SetString(IExample *this, char * str) { DWORD i;
// Let's copy the passed str to IExample's buffer i = lstrlen(str); if (i > 79) i = 79; CopyMemory(this->buffer, str, i); this->buffer[i] = 0;
return(0); }
long GetString(IExample *this, char *buffer, long length) { DWORD i;
// Let's copy IExample's buffer to the passed buffer i = lstrlen(this->buffer); --length; if (i > length) i = length; CopyMemory(buffer, this->buffer, i); buffer[i] = 0;
return(0); } 我们会这样子调用: example->lpVtbl->SetString(example, "Some text"); example->lpVtbl->GetString(example, buffer, sizeof (buffer)); 如果你曾经使用过C++,你会发现这个好像很熟悉,是的上面所做的,是我们在使用C创建一个C++的类。IExample结构是一个C++类(不继承任何其他类)。一个C++类就是一个结构第一个元素是一个数组的指针--这个数组指向了所有类内部的函数。每个函数的第一个参数通常是类指针本身。 简单来说,一个COM对象只是一个C++类。到了这里你可能会认为"哇!IExample现在是一个COM对象了?这个也太简单了"。等一下,IExample接近了,但是不止这些。并不是这么容易。 首先,让我们介绍一些COM技术的理论。你看到上面的数组指针了--指向IExampleVtbl结构?COM文档中称它为接口或者VTable。 COM对象的其中一个要求是VTable的前三个成员必须是QueryInterface,AddRef,和Release。我们必须要实现这些函数。微软已经规定要传递给它们的参数,它们的返回值以及调用约定。我们使用#include来包含一些头文件。我们这么定义IExampleVtbl结构: #include <windows.h> #include <objbase.h> #include <INITGUID.H> typedef HRESULT STDMETHODCALLTYPE QueryInterfacePtr(IExample *, REFIID, void **); typedef ULONG STDMETHODCALLTYPE AddRefPtr(IExample *); typedef ULONG STDMETHODCALLTYPE ReleasePtr(IExample *); typedef struct { QueryInterfactPtr *QueryInterface; AddRefPtr *AddRef; ReleasePtr *Release; SetStringPtr *SetString; GetStringPtr *GetString; } IExampleVtbl; 让我们先看看QueryInterface。首先函数必须返回HRESULT类型,其实这就是一个long。然后它规定了STDMETHODCALLTYPE调用规约。这意味着参数通过Stack传递而非寄存器,同样也规定了谁将清理堆栈当函数返回时。实际上,对一个COM对象来说,我们应该保证每个函数被声明为STDMETHODCALLTYPE,并返回long(HRESULT)类型。第一个参数是对象的指针。我们不是要讲IExample转换成COM对象么?是的,所以我们传递此参数(记住传递给函数的第一个参数必须是我们用来调用函数的结构体指针,COM就是基于这种设计的) 后来的内容,我们会看一下什么是REFID,并看看QueryInterface的第三个参数。目前请注意AddRef和Release同样也传递了结构体指针。 好了,现在我们对SetString和GetString添加STDMETHODCALLTYPE类型: typedef HRESULT STDMETHODCALLTYPE SetStringPtr(IExample *, char *); typedef HRESULT STDMETHODCALLTYPE GetStringPtr(IExample *, char *, long);
HRESULT STDMETHODCALLTYPE SetString(IExample *this, char * str) { ...
return(0); }
HRESULT STDMETHODCALLTYPE GetString(IExample *this, char *buffer, long value) { ...
return(0); } 总结一下,一个COM对象基本上来说就是一个C++类。一个C++类就是一个结构体总是以VTable *作为其第一个参数。VTable的前三个成员总是QueryInterface,AddRef和Release。VTable其他的成员就看自己怎么设计的了。传递给这些函数的第一个参数是这个对象的指针。 o. GUID 让我们继续我们实现IExample为COM对象的旅程。我们还没有真正实现QueryInterface,AddRef和Release。在我们实现之前,我们必须先来说说GUID(Globally Universal Identifier)。它是一个16字节的数组,每个都表示唯一的序列。一个GUID不能与世界上任何其他某个GUID相同。每个GUID被唯一创建。 我们如何创建者16字节的序列呢?可以使用微软的GUIDGEN.exe,它与你的编译器捆绑着,或者你可以通过SDK而得到它。 你一旦运行了GUIDGEN,就会自动产生一个新的GUID。 // {0B5B3D8E-574C-4fa3-9010-25B8E4CE24C2} DEFINE_GUID(<<name>>, 0xb5b3d8e, 0x574c, 0x4fa3, 0x90, 0x10, 0x25, 0xb8, 0xe4, 0xce, 0x24, 0xc2); DEFINE_GUID是一个宏,编译器会编译为16字节的数组 但是有一件事情我们必须做,就是把<<name>>替换为我们想使用的C变量名,我们称之为CLSID_IExample。 // {0B5B3D8E-574C-4fa3-9010-25B8E4CE24C2} DEFINE_GUID(CLSID_IExample, 0xb5b3d8e, 0x574c, 0x4fa3, 0x90, 0x10, 0x25, 0xb8, 0xe4, 0xce, 0x24, 0xc2); 现在我们已经定义了IExample使用的GUID,接着我们需要定义IExample的VTable("interface"),IExampleVtbl的GUID // {74666CAC-C2B1-4fa8-A049-97F3214802F0} DEFINE_GUID(IID_IExample, 0x74666cac, 0xc2b1, 0x4fa8, 0xa0, 0x49, 0x97, 0xf3, 0x21, 0x48, 0x2, 0xf0); 总结一下,每个COM对象都有一个GUID,它是一个16字节的数组不同于其他任何GUID。GUID可以通过GUIDGEN.exe生成。一个COM对象的VTable(interface)也有一个GUID。 o. QueryInterface(),AddRef(),和Release() 如果我们希望允许其他程序得到我们创建的IExample结构,为了的是程序可以调用我们的函数。 除了我们自己的COM对象之外,有许多其他的COM组件已经在一个PC上被安装(我们之后会介绍如何安装)。不同的计算机有不同COM组件被安装。程序如何从其他的COM对象中区别出我们的IExample COM对象,从而确定它是否被安装? 还记得每个COM对象都有唯一的GUID么,我们的IExample也一样,IExample的VTable也一样。我们需要作的就是告诉程序开发者IExample和它的VTable的GUID。典型的方式是,通过一个包含了两个GUID的宏的.H文件,其他程序知道了GUID后,作什么呢? 现在该QueryInterface函数登场了。其他程序把IExample的VTable的GUID传递给我们的QueryInterface函数,我们将会检查来确保它是IExample VTable的GUID,如果是的话,我们会返回一些东西让程序知道它真的拥有IExample对象。如果错误的GUID被传递,我们会返回错误并且让它知道它没有IExample对象。所以,所有其他COM对象会返回错误如果他们的QueryInterface被传递了IExample VTable的GUID,只有我们的QueryInterface才会返回正确的结果。 QueryInterface的第二个参数是我们需要检查的GUID,第三个参数是句柄,如果GUID和IExample VTable的GUID相匹配,则会被返回对象指针,否则返回NULL。QueryInterface返回long型的NOERROR(被定义为0)如果GUID匹配,否则返回非0的错误(E_NOINTERFACE)。让我们看一下IExample的QueryInterface: HRESULT STDMETHODCALLTYPE QueryInterface(IExample *this, REFIID vTableGuid, void **ppv) { // Check if the GUID matches IExample // VTable's GUID. Remember that we gave the // C variable name IID_IExample to our // VTable GUID. We can use an OLE function called // IsEqualIID to do the comparison for us. if (!IsEqualIID(riid, &IID_IExample)) { // We don't recognize the GUID passed // to us. Let the caller know this, // by clearing his handle, // and returning E_NOINTERFACE. *ppv = 0; return(E_NOINTERFACE); }
// It's a match! // First, we fill in his handle with // the same object pointer he passed us. That's // our IExample we created/initialized, // and he obtained from us. *ppv = this; // Now we call our own AddRef function, // passing the IExample. this->lpVtbl->AddRef(this); // Let him know he indeed has a IExample. return(NOERROR); } 接下来让我们讨论AddRef和Release函数,你已经注意到了我们在QueryInterface函数内调用了AddRef函数。 我们为别的程序分配了IExample的空间,它只是得到它的地址。所以当其他程序使用完他们的时候,我们应该负责释放空间。我们如何知道什么时候该释放呢? 我们采用引用计数的方法。如果你回过头去看看IExample的定义,你会发现它有一个DWORD类型的名为count的成员变量。我们将要利用此成员。当我们创建一个IExample的时候,我们将其初始化为0,然后每当AddRef被调用的时候,我们就增加1,每当Release被调用的时候,就减少1。 所以,当我们传递IExample到QueryInterface,我们会调用AddRef来增加count的值,当其他程序使用完毕的时候,它们会调用Release来减少count的值,如果为0的话,我们会释放IExample的空间。 关于COM的另一条重要规则:如果你得到了其他人创建的COM对象,你必须调用Release函数当你使用完它的时候。 下面是我们的AddRef和Release函数: ULONG STDMETHODCALLTYPE AddRef(IExample *this) { // Increment the reference count (count member). ++this->count; // We're supposed to return the updated count. return(this->count); } ULONG STDMETHODCALLTYPE Release(IExample *this) { // Decrement the reference count. --this->count; // If it's now zero, we can free IExample. if (this->count == 0) { GlobalFree(this); return(0); } // We're supposed to return the updated count. return(this->count); } 还有一件我们需要作的事情,微软定义了一个IUnknown的COM对象,一个IUnknown像IExample一样,除了它的VTable只包含QueryInterface,AddRef和Release函数。微软为其创建了一个特殊的GUID,但是你知道是什么么?我们的IExample也可以伪装成一个IUnknown对象,毕竟它也有QueryInterface,AddRef和Release函数。如果只是关心这三个函数,那么根本没有必要知道它是不是一个真正的IExample对象。我们要修改我们的代码为的是当传递给我们一个IExample或IUnknown的GUID都返回成功。微软提供了IID_IUnknown来表示IUnknown的GUID。 // Check if the GUID matches IExample's GUID or IUnknown's GUID. if (!IsEqualIID(vTableGuid, &IID_IExample) && !IsEqualIID(vTableGuid, &IID_IUnknown)) 总结一下,我们自己的COM对象来说,我们为其他程序分配空间,我们就要负责释放它。我们在AddRef和Release中使用引用计数来达到这个目的。我们的QueryInterface允许其他程序验证他们想要的对象,也让我们增加了引用计数。 现在IExample是一个真正的COM对象了么?是的,太棒了,不算难,我们做到了! 错,我们仍然需要把这些捆绑而使得其他程序可以使用(例如,一个动态链接库),写一个安装程序并解释其他程序如何得到我们创建的IExample。
o. 一个IClassFactory对象 现在我们需要看一下如何得到IExample对象,最后我们要写一些代码来实现它。微软已经设计一个标准的方法。这需要在我们的DLL文件里包含第二个COM对象--IClassFactory,它有一些特殊的函数,且定义了自己的GUID,通过IID_IClassFactory引用。 IClassFactory的VTable有5个特殊函数,QueryInterface,AddRef,Release,CreateInstance和LockServer。注意到了IClassFactory有它自己的QueryInterface,AddRef和Release函数,就像我们的IExample对象一样。(这里为了避免名字冲突,我们将IClassFactory的函数加上class前缀,classQueryInterface)。 真正重要的函数是CreateInstance。当程序无论何时需要我们创建一个IExample对象,并初始化返回它的时候,就必须调用我们的IClassFactory的CreateInstance函数。实际上,如果需要IExample多个对象,则可以调用CreateInstance多次。这就是程序如何得到我们的IExample对象,但是你可能会问“程序如何得到IClassFactory对象呢?”,我们稍候会解释,现在我们简单的实现IClassFactory的五个函数,生成其虚函数表。 生成需函数表很容易,不同于IExample的虚函数表,我们没有必要定义我们自己的IClassFactory的虚函数表。微软已经为我们定义了IClassFactoryVtbl结构。我们需要作的就是声明我们的VTable并且填充我们自己实现的5个函数。让我们创建一个静态的VTable,并命名为IClassFactory_Vtbl: static const IClassFactoryVtbl IClassFactory_Vtbl = {classQueryInterface, classAddRef, classRelease, classCreateInstance, classLockServer); 同样的,创建一个IClassFactory对象也很容易,因为微软已经定义了此结构。我们仅仅需要一个对象,让我们声明一个静态的IClassFactory对象并命名为MyClassFactoryObj,并用上面的VTable来初始化: static IClassFactory MyIClassFactoryObj = {&IClassFactory_Vtbl}; 现在我们需要实现这5个函数。我们的classAddRef和ClassRelease是没有意义的,因为我们没有真正分配IClassFactory的空间(我们只是简单的声明为静态对象),也不需要释放空间。所以classAddRef总是返回1,表明总有一个IClassFactory对象。classRelease也一样,我们不需要任何引用计数。 ULONG STDMETHODCALLTYPE classAddRef(IClassFactory *this) { return (1); } ULONG STDMETHODCALLTYPE classRlease(IClassFactory *this) { return (1); } 现在来看看QueryInterface函数,它需要检查传入的GUID是否是一个IUnknown的GUID或者一个IClassFactory的GUID。我们作同样的事情如同在IExample的QueryInterface函数中一样。 HRESULT STDMETHODCALLTYPE classQueryInterface(IClassFactory *this, REFIID factoryGuid, void **ppv) { // Check if the GUID matches an IClassFactory or IUnknown GUID. if (!IsEqualIID(factoryGuid, &IID_IUnknown) && !IsEqualIID(factoryGuid, &IID_IClassFactory)) { // It doesn't. Clear his handle, and return E_NOINTERFACE. *ppv = 0; return(E_NOINTERFACE); } // It's a match! // First, we fill in his handle with the same object pointer he passed us. // That's our IClassFactory (MyIClassFactoryObj) he obtained from us. *ppv = this; // Call our IClassFactory's AddRef, passing the IClassFactory. this->lpVtbl->AddRef(this); // Let him know he indeed has an IClassFactory. return(NOERROR); } IClassFactory的LockServer函数现在只是一个桩函数: HRESULT STDMETHODCALLTYPE classLockServer(IClassFactory *this, BOOL flock) { return(NOERROR); } CreateInstance被定义成: HRESULT STDMETHODCALLTYPE classCreateInstance(IClassFactory *, IUnknown *, REFIID, void **); 通常的,CreateInstance的第一个参数是指向IClassFactory对象(MyIClassFactoryObj)的指针。 当我们使用聚合的时候才需要使用第二个参数。这里我们不涉及它。如果它非空,那么说明希望我们支持聚合,这里我们不这么做,而直接返回错误。 第三个参数是IExample的VTable的GUID(如果希望我们分配,初始化,返回一个IExample对象) 第四个参数是我们返回我们创建的IExample对象句柄。 让我们来看一下CreateInstance函数(被命名为classCreateInstance): HRESULT STDMETHODCALLTYPE classCreateInstance(IClassFactory *this, IUnknown *punkOuter, REFIID vTableGuid, void **ppv) { HRESULT hr; struct IExample *thisobj; // Assume an error by clearing caller's handle. *ppv = 0; // We don't support aggregation in IExample. if (punkOuter) hr = CLASS_E_NOAGGREGATION; else { // Create our IExample object, and initialize it. if (!(thisobj = GlobalAlloc(GMEM_FIXED, sizeof(struct IExample)))) hr = E_OUTOFMEMORY; else { // Store IExample's VTable. We declared it // as a static variable IExample_Vtbl. thisobj->lpVtbl = &IExample_Vtbl; // Increment reference count so we // can call Release() below and it will // deallocate only if there // is an error with QueryInterface(). thisobj->count = 1; // Fill in the caller's handle // with a pointer to the IExample we just // allocated above. We'll let IExample's // QueryInterface do that, because // it also checks the GUID the caller // passed, and also increments the // reference count (to 2) if all goes well. hr = IExample_Vtbl.QueryInterface(thisobj, vTableGuid, ppv); // Decrement reference count. // NOTE: If there was an error in QueryInterface() // then Release() will be decrementing // the count back to 0 and will free the // IExample for us. One error that may // occur is that the caller is asking for // some sort of object that we don't // support (ie, it's a GUID we don't recognize). IExample_Vtbl.Release(thisobj); } } return(hr); }
o. 打包成dll 为了让其他程序可以得到我们的IClassFactory(并且调用CreateInstance函数得到IExample对象),我们会将上面的代码打包成一个DLL文件。这篇文档不想说如何创建一个DLL文件,如果你不熟悉的话,那么你应该首先阅读一下相关方面的资料。 我们已经完成了IExample和IClassFactory的所有代码,我们现在需要做的就是变成DLL代码。 还有许多要做,微软已经规定了我们必须在DLL中添加DllGetClassObject函数,以及它的参数,该怎么做,及返回值。一个程序通过调用DllGetClassObject来得到我们IClassFactory对象的指针(实际上,后面我们会看到,程序会调用一个名为CoGetClassObject的OLE函数,那个函数会调用我们的DllGetClassObject.)所以,程序如何得到IClassFactory对象--通过调用DllGetClassObject。我们的DllGetClassObject函数必须完成这个工作,它这样子被定义: HRESULT PASCAL DllGetClassObject(REFCLSID objGuid, REFIID factoryGuid, void **factoryHandle); 第一个参数是IExample的GUID(而不是VTable的GUID)。我们需要检查此来确保调用者明确的调用DLL的DllGetClassObject函数。注意到每个COM的DLL都有一个DllGetClassObject函数,所以我们需要GUID来从其他COM的DLL中区分出DllGetClassObject函数。 第二个参数是IClassFactory的GUID。 第三个参数是调用者希望我们返回一个IClassFactory对象指针(如果传递了正确的IExample的GUID) HRESULT PASCAL DllGetClassObject(REFCLSID objGuid, REFIID factoryGuid, void **factoryHandle) { HRESULT hr; // Check that the caller is passing // our IExample GUID. That's the COM // object our DLL implements. if (IsEqualCLSID(objGuid, &CLSID_IExample)) { // Fill in the caller's handle // with a pointer to our IClassFactory object. // We'll let our IClassFactory's // QueryInterface do that, because it also // checks the IClassFactory GUID and does other book-keeping. hr = classQueryInterface(&MyIClassFactoryObj, factoryGuid, factoryHandle); } else { // We don't understand this GUID. // It's obviously not for our DLL. // Let the caller know this by // clearing his handle and returning // CLASS_E_CLASSNOTAVAILABLE. *factoryHandle = 0; hr = CLASS_E_CLASSNOTAVAILABLE; } return(hr); } 大多数的工作已经完成了,还有一件事情。程序不会加载我们的DLL,而是操作系统代替程序加载当程序调用CoGetDllClassObject(CoGetClassObject加载我们的DLL,通过LoadLibrary函数加载,GetProcAddress函数得到DllGetClassObject函数的地址,然后调用它)。不幸的是,微软并没有设计出程序如何告知操作系统已经使用完了我们DLL并且可以卸载它(FreeLibrary)的方法。所以我们必须告诉操作系统让它知道什么时候可以安全的卸载DLL。我们必须提供一个名为DllCanUnloadNow函数,当它返回S_OK的时候就可以安全的卸载我们的DLL,否则返回S_FALSE。 我们如何知道什么时候是可以安全卸载的呢? 我们还需要引用计数器。每当我们分配一个对象的空间,我们就要增加引用计数,每次程序调用Release函数,我们释放对象,减少引用计数。只有当计数为0时,我们才告诉OS我们的DLL可以被安全卸载,因为我们可以确信没有其他程序在使用我们的任何对象。所以我们声明一个静态的OutstandingObjects来维护这个引用计数(当DLL被加载的时候,这个值为0)。 我们在哪里增加此引用计数呢?在我们的IClassFactory的CreateInstance函数中,当我们调用GlobalAlloc分配空间并所有事情都顺利地完成后。我们在那个函数中添加一行,在Release的后面: static DWORD OutstandingObjects = 0; HRESULT STDMETHODCALLTYPE classCreateInstance(IClassFactory *this, IUnknown *punkOuter, REFIID vTableGuid, void **ppv) { ... IExampleVtbl.Release(thisobj); // Increment our count of outstanding objects if all // went well. if (!hr) InterlockedIncrement(&OutstandingObjects); } } return(hr); } 哪里最合适减少引用计数呢?在我们的IExample的Release函数中,在GlobalFree函数后面: InterlockedDecrement(&OutstandingObjects); 微软规定应该提供一种方法允许程序在内存中锁住我们的DLL。为了那个目的,我们调用IClassFactory的LockServer函数,如果我们希望增加一个锁,则传递参数1,传递0则意味着减少DLL的锁。我们需要第二个静态的DWORD引用计数变量LockCount(DLL加载时初始化为0): static DWORD LockCount = 0; HRESULT STDMETHODCALLTYPE classLockServer(IClassFactory *this, BOOL flock) { if (flock) InterlockedIncrement(&LockCount); else InterlockedDecrement(&LockCount); return(NOERROR); } 现在我们可以实现DllCanUnloadNow函数了: HRESULT PASCAL DllCanUnloadNow(void) { // If someone has retrieved pointers to any of our objects, and // not yet Release()'ed them, then we return S_FALSE to indicate // not to unload this DLL. Also, if someone has us locked, return // S_FALSE return((OutstandingObjects | LockCount) ? S_FALSE : S_OK); }
o 我们的C++/C包含文件 早先我们提过,为了让程序使用我们的IExample动态链接库文件,我们需要提供IExample和IExample的VTable的GUID。我们把GUID宏放在.H文件里,我们可以给别人使用,也可以给自己的DLL使用。在这个.H文件里,我们还需要包含IExampleVtbl和IExample的结构,这样程序员可以通过IExample来调用我们的函数。 到现在为止我们是这么定义的: typedef HRESULT STDMETHODCALLTYPE QueryInterfacePtr(IExample *, REFIID, void **); typedef ULONG STDMETHODCALLTYPE AddRefPtr(IExample *); typedef ULONG STDMETHODCALLTYPE ReleasePtr(IExample *); typedef HRESULT STDMETHODCALLTYPE SetStringPtr(IExample *, char *); typedef HRESULT STDMETHODCALLTYPE GetStringPtr(IExample *, char *, long); typedef struct { QueryInterfacePtr *QueryInterface; AddRefPtr *AddRef; ReleasePtr *Release; SetStringPtr *SetString; GetStringPtr *GetString; } IExampleVtbl; typedef struct { IExampleVtbl *lpVtbl; DWORD count; char buffer[80]; } IExample; 这样子定义有个问题,我们不希望让其他程序知道count和buffer成员,我们希望隐藏它们。外部程序应该不能直接访问我们结构的数据成员。它们只需要知道lpVtbl来调用我们的函数即可,我们希望我们的IExample是这么定义的: typedef struct { IExampleVtbl *lpVtbl; } IExample; 另外,尽管typedef可以使函数定义变得简单,但是如果函数过多的话,会显得有些冗长还可能容易犯错。 最后的问题是上面的定义是C定义,它使得C++程序要使用我们的COM对象变得不容易。毕竟尽管我们用C实现的IExample,但是IExample是一个C++类。对C++程序来说定义一个C++类比C结构要容易得多。 代替上述的定义,微软提供了一个宏使得我们可以定义我们的VTable和对象对C和C++都支持,而且隐藏了其他数据成员。要使用此宏,首先必须要定义对象的符号INTERFACE,在此之前我们必须undef此符号避免编译警告。接着,我们使用DECLARE_INTERFACE_宏,在宏内,我们列举了IExample的函数,它看起来应该是这样: #undef INTERFACE #define INTERFACE IExample DECLARE_INTERFACE_ (INTERFACE, IUnknown) { STDMETHOD (QueryInterface) (THIS_ REFIID, void **) PURE; STDMETHOD_ (ULONG, AddRef) (THIS) PURE; STDMETHOD_ (ULONG, Release) (THIS) PURE; STDMETHOD (SetString) (THIS_ char *) PURE; STDMETHOD (GetString) (THIS_ char *, DWORD) PURE; }; 看上去可能有些奇怪。 当定义一个函数,STDMETHOD表示函数返回HRESULT。我们的QueryInterface,SetString和GetString都返回HRESULT。AddRef和Release不是,他们返回ULONG,这就是为什么我们用STDMETHOD_(有个下划线)。接着我们把函数名放在括号中,如果函数不返回HRESULT,我们需要在函数名前指出其返回类型,然后逗号。函数名之后是()内的是参数。THIS表示我们对象的指针(IExample),如果函数只有此一个参数,就只需要在()内加入THIS,如同AddRef和Release函数一样。其他函数有额外的参数,我们必须使用THIS_(有个下划线)。我们列出余下的参数,注意到了,在THIS_和其他参数之间没有逗号,但是其他参数之间有逗号。最后我们加上PURE和分号。 虽然宏很奇怪,但是它是定义COM对象的方法为了让C编译器和C++编译器都可以识别。 “但是IExample结构的定义去哪里了?”。这个宏很奇怪,它可以让C编译器自动产生IExample结构的定义,它仅仅包含一个lpVtbl成员。通过这种方法定义我们的VTable,我们自动的得到合适的IExample结构。 把GUID添加到此.H文件中,我们创建IExample.h文件。 但是你知道IExample还有2个数据成员。所以我们打算定义一个IExample变种在我们的DLL文件中,我们定义其为MyRealIExample,它是IExample的真正定义: typedef struct { IExampleVtbl *lpVtbl; DWORD count; char buffer[80]; } MyRealIExample; 在IClassFactory的CreateInstance函数中,我们分配MyRealIExample结构: if (!(thisobj = GlobalAlloc(GMEM_FIXED, sizeof(struct MyRealIExample)))) 程序不需要知道我们实际给它的对象还多了额外的数据,毕竟,这些结构都含有相同的lpVtbl指针。现在我们的DLL函数可以通过将IExample转换为MyRealIExample类型而得到额外的数据成员。
o 定义文件(DEF) 我们需要DEF文件来导出DllCanUnloadNow和DllGetClassObject函数。微软编译器希望他们被定义为PRIVATE: LIBRARY IExample EXPORTS DllCanUnloadNow PRIVATE DllGetClassObject PRIVATE
o 安装DLL,注册对象 我们已经完成了生成IExample.dll的所有事情,我们可以开始编译IExample.dll了。 这不是我们的最后步骤,在其他程序使用我们的IExample之前,我们需要作两件事情: 1. 安装DLL文件使得程序运行时可以被找到 2. 注册我们的DLL为COM对象 我们需要创建一个安装程序使得可以将IExample.dll拷贝至一个合适的地方。例如,我们会在Program Files下创建一个IExample目录,然后拷贝DLL到此目录下(当然了,我们的安装程序应该做版本检查,如果有我们DLL的早期版本已经被安装,我们不会覆盖) 接着我们要注册此DLL,这包括创建几个注册表健值。 首先我们在HKEY_LOCAL_MACHINE\Software\Classes\CLSID下创建一个键值,名称必须为IExample的GUID,且必须为text格式。 在GUID键值下,创建字符串InprocServer32,默认值为DLL的安装全路径。添加ThreadingModel字符串,如果我们没有必要约束程序必须以单线程的方式调用我们的DLL函数,则我们可以指定其值为“both”。在我们的IExample函数中没有使用全局数据结构,所以我们是线程安全的。 在运行安装程序之后,IExample.dll被注册为COM组件,其他程序可以使用它了。
|