发信人: bigluo()
整理人: fishy(2000-04-22 10:54:11), 站内信件
|
【 在 needmoney (急需用钱) 的大作中提到: 】
: 也就是说,必须在一个进程里发消息给另一个进程,使那个进程能响应这个消息
: ,并做相应的处理?谢谢
【Win32 进程通讯的观念与技术】
这篇文章谈的是 Interprocess Communication (IPC),我将与你分享跨进程通讯 的各项技术与资料交换的方法。
为什麽需要 Inter-process Communication?
显而易见的,没有一个视窗应用程序可以包办全部的工作。为了避免资料重覆输 入的时间浪费与人为错误,各应用程序间的资料会有互相交换的需求。首先的压 力将来自於使用者,甚至於很可能是你自己。先不说别的,在写这篇文章时,我 就曾剪贴原来以 Delphi 撰写的程序到文书编辑软件,同时,也利用抓图软件帮 我拍下执行画面,最後,这些文章与范例程序得用压缩程序压起来,然後E-mail 寄给杂志编辑。
使用 IPC 在某些情况下是不得不然的决定,有时候程序必须跨过机器边界让另一 部机器内的程序明白该怎麽合作来共同完成工作,这同时也暗示我们可能面临不 同的操作系统的问题。
此外,IPC有助於系统的安全与稳定。由於Win32各个进程彼此独立的特性,一个 进程死掉了,其他的进程还可以继续跑下去,对於某些稳定性要求很高的系统而 言, 值得以额外的负担(Overhead)交换系统的强固性(robustness)。嗯! 我的意 思是说,因为系统对稳定性的需求要求较高,值得拆开来做甚至额外的备援系统 ,既然工作拆成两个以上,此时必然需要IPC。
关於IPC,一般人可能会对其有「执行效率缓慢」的印象,这当然不能说是错误的 ,但绝不是公平的评语。这麽说吧:一个主管亲自去做一件事,往往会比先说明 再授权下属去做来得快,这是单一工作时的情况;然而如果管理者同时有好几件 事在手上,托付别人去做才能使得整个公司的效能提高。换句话说,如果能善用 IPC,整体的系统效能不仅不会下降,反而可能因为充分利用整个运算群的能力 而有提升。
我们的第一个 IPC 例子
每个图形介面的视窗应用程序都接受并处理讯息(Message),因此,使用讯息伫列 通知其他的进程是脑中很自然会浮现的第一个想法;换句话说,两进程间彼此互 相以 SendMessage() 或 PostMessage() 传送讯息通知对方。
即然要互送讯息,就需要一个彼此都认得的讯息编号。於是,除了 Windows 标准 的讯息编号之外,我们还需要额外定义一个(一些)讯息。
进程通讯间用来约定讯息编号常用的方法是呼叫RegisterWindowMessage() API函 数。这个函数只有一个字串型别的引数,Windows系统会检查我们传入的讯息名称 并传回一个安全不重覆的讯息编号,假如传入的讯息名称早已经登记有案,则系 统传回的是稍早传给那个进程的相同编号。
换句话说,两支程序只要彼此都用相同的讯息名称呼叫RegisterWindowMessage( ) 注册讯息,系统便会都给两者一个相同的自订讯息编号。
接下来要送出讯息了,可是,要送给谁呢?嗯,我在这 使用的方法是:第一次先 用广播的,每一个视窗程序都会收到通知,讯息的短叁数(wParam)中写明发讯视 窗的 Handle 值,如果是同志,它自然明白这个讯息代表了什麽,并且也使用Se ndMessage() 回送约定的讯息表示收到。同样的,讯息的短叁数注明自己的Hand le。於是,茫茫人海的小俩口终於得知对方的下落,以後就不再需要公开寻人可 以透过Handle值直接与对方联络了。
除了讯息编号,讯息的wParam,lParam长短叁数也可以用来进一步约定通讯的细 节。事情进展得似乎十分顺利,现在我们知道合作对象,也确信它明白我们的讯 息代表什麽。虽然简单,但是这种暗通款曲的方式是系统默许的。不过,我们还 需要再多解决一个问题。
由於SendMessage 只有 wParam,lParam 两个 DWORD 型别的长短叁数,携带的资 料量十分有限。很显然的,我们需要能够一次传送更多资料的方法。Windows 也 的确提供了许多交换资料的机制,我在这篇文章中将会一一说明,其中最简便的 方法是使用 WM_COPYDATA 讯息,作法如下
将资料内容指定到COPYDATASTRUCT这个资料结构中。
必须使用SendMessage()送出 WM_COPYDATA讯息,讯息的短叁数是发讯端视窗的H andle值,长叁数的内容则是指向COPYDATASTRUCT的指标。
受讯端进程收到讯息时,以长叁数提供的线索依址取回资料。
小俩口书信往返时系统是居中牵线的红娘。就在发讯视窗送出WM_COPYDATA讯息, 受讯视窗取得内容之间,系统在背後默默接管记忆体管理的琐事。有关WM_COPYD ATA的使用有一点需要提醒读者的,收讯端应该视这块记忆体是唯读的,如果後来 程序处理需要这些资料,应该要先将之拷贝出来。
多亏有了这项特殊的性质,使得WM_COPYDATA与讯息沟通模型成为 Win32 平台上 少数同时支援 16-bit与32-bit应用程序的IPC机制。你可以在WM_COPYDATA目录找 到范例程序TwinApp的完整原始程序。
IPC基本概念的讨论
总结来说,上述的例子是两个进程彼此利用RegisterWindowMessage()注册所得的 编号对送讯息,并且利用讯息的长短叁数进一步协定通讯的内容与细节,对於资 料量比较大的资料则使用WM_COPYDATA。
眼尖的读者在检视TwinApp时也许会察觉到一些DDE的影子。当然,比起DDE来说, TwinApp范例程序的讯息沟通模型实在阳春,缺点也不少。不过我的用意本来就不 在於一开始就写一个大型程序出来吓唬人;相反的,我打算提供一个简单的例子 ,并且从这个例子支解出有关进程通讯的几个重要的观念与特性,这些特性并不 是TwinApp所独有的,对於其他IPC机制的讨论也有相同的价值,等我们扣紧了对 IPC的感觉,再陆续讨论其他 Win32 平台所支援的IPC机制。
话说内行的看门道,外行的看热闹。或许我算不得顶尖高手,但至少应该比看热 闹的多看出一些东西来吧! :p 观察TwinApp这个例子 --
进程之间彼此有共同的通讯协定
通讯的仅限於单机,稍候讨论的IPC有些则是可以跨过机器边界甚至网域.
Process在进程通讯中的角色扮演
一般来说,叁与IPC的进程可以归类成Client与Server两类,所谓的Server指的是 提供服务的进程;Client指的是使用或向Server要求服务的进程。
真实的世界中,人的角色扮演是随情境而变的。我们会是别人的子女,但也同时 是别人的爸妈; 即使同样是夫妻,居家生活与外出场合的行为表现也有差异。界 定某一程序是Client与Server的角色端视当时的情况而定并非绝对的。举例来说 ,文书处理软件可能向试算表要求库存统计资料,此时试算表扮演的是Server的 角色,但在试算表向库存管理系统索取统计资料的场合,试算表则是Client。
以我们的第一个例子TwinApp来说,彼此既接收讯息,同时也主动发出讯息。既可 以是Client也可以是Server,没有明显的主从之别,对於这样的情况,有一个专 有名词叫「对等模式」(Peer-to-peer model) 。
同步与非同步的讨论
TwinApp使用SendMessage()送出讯息,程序会暂停在SendMessage()那行等待讯息 处理结束返回後再继续下一列程序,这样的情况属於同步处理。同步(Synchrono us)与非同步(Asynchronous)在IPC中是一个非常重要的论题,有必要先对这两个 名词先做说明:
假设程序A呼叫程序B时,若是A先暂停一直等到程序B结束返回後再继续程序A的下 一动作,我们称其为同步(Synchronous);另一种情况是 -- 如果A呼叫B之後,不 等B执行完,就直接进行A的下一动作,则是所谓的不同步。
以提款机为例,我们会先插入卡片,输入密码,键入金额,然後是内部安全与帐 务查核,最後收回卡片及金额,列印交易明细,一动接一动按步就班;同样是提 款这件事,某位老板可以交待会计小姐去提款,交待完之後他就迳自去忙别的事 ,等到会计小姐提款回来,再向老板回报,这样的程序是所谓的非同步。
如果进一步观察提款这个例子:会计小姐什麽时候出门什麽时候回来是算不得准 的,假定这位老板除了会计小姐之外,另外还交办旁人其他工作,可以预见的, 不一定那一件工作会先做完。由於执行的次序无法预估,采用非同步方式设计的 进程通讯将会多出许多协调与事件处理的工作,使得彼此之间总互相期待点什麽 。
叁与通讯的进程个数,讯息资料的流向
在TwinApp中,简单的只有两个端点。但在实际应用的场合,Server通常得同时应 付好几个Client的要求,如何妥善照顾到每一个Client同时要兼顾系统执行的效 能,是门很大的学问。
当进程对进程搭起通讯的鹊挢时,这座挢是单行道或者是双向通行,同样也值得 列入评估要素。不过有一点需要注意的: 不论选择单工或双工的IPC机制,并不构 成我们建立双向沟通无可跨越的天堑,话说山不转路转,盖两座单向的挢一样可 以有双向通行的效果,不过就先天本质的特性来说,某些IPC机制确实比较容易作 出双工的效果,当然也有天生大嘴巴适合用来广播的,例如本文稍後叙述的Mail Slot。
资料的可视性与安全性
交换的资料在进程之间当然必须是可见的,TwinApp是用WM_COPYDATA交出资料。 IPC有些技术是可以让进程共同存取资料的,稍候我们在 Shared memory 时将有 讨论.
是否需要有视窗或者纯Console Application也能应用.
TwinAPP是以SendMesasage()送出讯息,这表示需要有视窗才行得通。如果你设计 的是纯Console Mode 的应用程序,那麽,选用不需要视窗Handle也行得通的IPC 机制(例如pipe)会比较适合。
关於执行效能的讨论
许多人耽心IPC的执行效能,的确,先不说别的,光是启动另一个Process本身就 比启动一个Thread 的Overhead要高上很多。如果涉及协调的问题,建立一个Mut ex的时间也比Critical section慢上不知多少倍。遗憾的是我们却也别无选择, 因为Critical section在Multi-Thread中固然简单好用,但是不能用在跨越进程 边界的场合。
但是要说 IPC 一定使得系统效能降低,未免也太过悲观了;平视与俯看的视野是 不同的。这年头大家都将Client/Server挂在嘴边,充分运用合理分配整个公司的 运算资源才能提高整体的效能,我想 IPC 在这 自有其应用的价值与效益。
另外一个导致IPC执行效率不彰的元凶来自不良的设计,着名的例子是所谓的Bus y-loop 一个什麽也不做只有一进程序不断地期待的回圈。以稍早的老板与会计小 姐为例,如果老板交办事情之後却将全部的事都停下来,来回踱步只为专心等着 会计小姐回来,时间没有花在刀口上的结果当然效率不彰。找出效率的瓶颈设法 调校是件长期奋战的工作,如同管理是持续不断的合理化。
此处还有一个迷思也有待澄清,同步与非同步对於执行效能的影响是视情况而定 的,并不能说非同步一定会比同步快,抽样样本很小或资料量偏小时,同步往往 比非同步快。比较公允的说法应该是:同时有好几件工作要处理时,整体来说「 非同步」往往快一些。以刚才的提款的事情为例,老板亲自去提款未必比小姐慢 ,但是如果老板同时有好几件工作要处理时,非同步的好处就很明显了。
Win32支援的IPC相关技术
上述的讨论与其说是针对TwinApp的观察,不如说是针对IPC的综合讨论。观念的 说明之後是技术层次的讨论。接下来陆续介绍的是Win32 API支援的各项IPC机制 --
Clipboard COM Dynamic Data Exchange (DDE) File Mapping Mails lots Pipes RPC Windows Sockets WM_COPYDATA 剪贴簿(Clipboard)
人,其实是最佳的 IPC 机制,十分的聪明也十分的有弹性。
剪贴簿几乎是专为人类而设的标准资料交换中心。它最大的特色除了使用者导向 之外,任何应用程序都允许改写其内容,同时它是可以跨越机器边界,交换的范 围不仅限於单机内的各个进程。
由於它是纯使用者导向,使用剪贴簿的程序有一项传统是值得遵守的:如果不是 基於使用者的操作,程序不应该主动去异动剪贴簿的内容;同样的道理,我们也 不应该假设剪贴簿中有我们程序想要的资料,哪怕是不久前才刚放进去的,因为 ,使用者可能已经清除或改变其内容了。
剪贴簿几乎可以容纳任何的资料,除了标准支援的CF_TEXT、CF_BITMAP...等资料 格式,我们可以自行注册登记其他格式的资料。但由於它的使用者导向,也由於 任何程序都可以改写其内容,除非使用者愿意,不然坦白来说不太适合进程间的 资料交换。这也使得应用设计IPC时,剪贴簿成为每支应用程序都标准支援但却也 都适可而止的IPC机制。我们应该再多看看其他的资料交换方法。
File Mapping
在早期MS-DOS时代还没有现在这麽多 IPC机制可供利用时,使用磁碟档案来交换 资料可说是一般应用程序的唯一选择。时至今日,档案不仅没有从IPC领域中消失 ,反而是更加发扬光大了,然而观念上早已不纯粹界定在档案系统的实体档案。 的确,资料位於何处的份际如今是越来越模糊了,虚拟的记忆体实际上是档案, 虚拟的档案结果是记忆体。
Win32 API 中有一个好玩的东西叫做File-mapping;基本的观念是开启一个档案 并将之对映到某一块记忆体,有趣的是,虽然程序是针对这块记忆体操作,实际 上改变的却是档案。
更好玩的是你不必真的在硬碟 开一个实体档案,而是使用分页置换档(paging f ile)的一块空间权充当作档案。这个虚拟的档案空间(或者你要说是记忆体)可以 为进程间共享,通常我们管它一个特别的名字叫 Share-memory,共享记忆体。
由於它的确不是真正的档案,进程间不仅省去特定磁碟目录档案等约定,也毋须 在意谁是最後走的要负责删除档案,当然啦,即使当机不会留下一些垃圾档案。 彼此分享的是正好是同一块记忆体,资料一旦写入,这项改变也立即反应到别的 进程。
使用ShareMemory的大致步骤如下所述
呼叫CreateFileMapping() API函数建立File-mapping核心物件.
CreateFileMapping()函数的第一个引数原本应该是CreateFile()开档所得的档案 物件Handle,若是传入$FFFFFFFF则是以分页置换档(paging file)的一部划作共 享记忆体。函数的最後一个引数是这块区域的叁考名称,进程间彼此将根据此一 相同的识别名称叁考同一块共享记忆体。
FHandle := CreateFileMapping(
$FFFFFFFF, // Shared memory File,Handle 传入 $FFFFFFFF
nil, // 不设安全属性
PAGE_READWRITE, // 存取模式设定为可读写以便进程交换资料
0, // 使用 paging file 时一般将之设为零
Size, // 共享记忆体的大小
pchar(name)); // 其他的进程将以此名称叁考到这块共享记忆体
由於各个进程各有其逻辑定址空间,在正式存取这块共享记忆体之前,我们得将 其全部或部分映射回进程本身的位址空间中。呼叫MapViewOfFile()的用意即是在 此,该函数将传回mapped view 「视野」的起头(就是指标啦),接下来的就是用 这个指标存取记忆体了。
FFileView := MapViewOfFile(
Fhandle, // File-mapping object 的 Handle 值
FILE_MAP_ALL_ACCESS, // 设为 FILE_MAP_ALL_ACCESS 开放存取
0, // 模式以便顺利存取共享记忆体
0,
Size); // 预备映射回来的 byte 数
最後,别忘了使用UnmapViewOfFile()归还指标并呼叫CloseHandle()释放File-m apping核心物件。
碍於篇辐,完整的程序码请读者叁阅ShareMem目录的 DemoSMem专案。另外,为了 方便使用,这些CreateFileMapping(),MapViewOfFile()等函数已经包装进TShar edMem这个类别。
Mutex
Shared memory的示范专案DemoSMem留下诸多悬疑待解,或许你也正有相同的疑问 :既然两个进程都利用这块记忆体,那我们怎麽知道什麽时候资料改变了?此外 ,如何防止进程同时读写资料?
的确,进程通讯既是两个以上的个体,协调是必然存在的负担,要避免两个进程 同时使用关键资源,Mutex(互斥器)的使用是你必备的技术。
从字面上解释,互斥意思是同一时间唯一;换句话说,同一时间最多只许握有Mu tex的执行绪(Thread)有权使用关键资源,其他的执行绪若要使用只有等待。嗯! 在Mutex与Event这两节我将暂时改口为执行绪,事实上这才是真正的CPU排程单 位,由於每个进程至少有个Thread(主执行绪),这样的称呼应该是与本文进程通 讯的主旨不相违背的。
就像是注册讯息,共享记忆体一样(甚至稍後的Event,MailSlot,Pipe都是),在 我们取得核心物件的Handle前,都是以「名称」叁考的,产生一个Mutex的API函 数是:CreateMutex(), 以下范例采自本文所附的ChienIPC程序单元
constructor TMutex.Create(const name: string);
begin
FHandle := CreateMutex(
Nil, // 安全防护属性, 暂时传入nil采用预设值
False, // 执行绪是否一开始就握有 mutex 的所有权
pchar(name)); // Mutex核心物件的名称
if FHandle = 0 then Abort;
end;
好极了,现在我们有了一个Mutex,该怎麽使用呢? 我用一个情节来说明:如果一 群人在一起开会,每个人桌子前面各摆着一支麦克风,为了让大家听清楚彼此说 什麽,这些麦克风暂时都是关的,规定只有主席可以透过中央控制系统开启回路 。要说话的得先举手表示:「我要我要」,如果没有别人举手也没人正在发言, 主席便打开开关将发言权交给他,然後这个人的手放下开始讲话。此时若是其他 人也要讲话,根据规则得先举手,在别人讲完交出发言权前只有继续举手等待的 份。当然,排队的人,可以选择手一直举着;或者他只打算等三分钟,手酸了就 放下来。
执行绪要求拥有Mutex的方法是呼叫WaitForSingleObject()(我要我要,举手等待 ),此时程序将暂停(Blocking)在这列。倘若此时正好没有别的执行绪拥有Mutex (没人讲话),系统会短暂的将Mutex设为Signaled(激发状态),使得WaitForSin gleObject()正常返回,同时,系统也会将这个Mutex的所有权交给这个执行绪, 然後程序继续执行,握有Mutex所有权者开始使用关键资源,并尽快在事後以Rel easeMutex()交出Mutex拥有权。
关於程序实入这部分请您叁阅DemoSMem范例程序的读取与写入程序,同样的,有 关Mutex的API函数也已包装进TMutex类别方便你的使用。
Event
讨论过进程之间以Mutex协调避让的技术之後,Shared memory的示范专案DemoSM em尚留下一个悬疑待解:既然两个进程都利用这块记忆体,那我们怎麽知道什麽 时候资料被改变了呢? 以一个回圈定期不断去抓资料回来比对不仅程序写起来累 人,执行效率也很低落。
当然,回到一开始提出的方法,写入资料的进程用讯息一一个别通知其他合作夥 伴是可以行得通,不过,事情该有更好的解决之道才是。Win32的核心物件中有一 种叫Event(事件)物件,方便我们在某一事件发生时设定其状态以便叁与通讯的进 程注意到某一件重要事情的发生。
产生一个Event物件的方法是呼叫CreateEvent() API函数:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset, // flag for manual-reset event
BOOL bInitialState,// flag for initial state
LPCTSTR lpName // address of event-object name
);
同样的,最後一个引数是执行绪在取得Event Handle前叁考同一Event物件的识别 名称,如果相同名称的Event物件稍早已经产生而且叁用次数尚未归零消灭,并不 会多产生一个Event物件,系统只单纯的将其叁用次数加一,执行绪彼此得以叁考 到同一个物件。第三个引数用来设定Event物件的初值是否为Signaled(激发状态 ) 。第二个引数用来设定事件的激发状态是手动或自动;所谓手动与自动的分别 在於事件的状态变成Signaled(激发状态)时,要由系统自动帮我们重设回非激发 状态,或者由程序自行以ResetEvent()将事件设成非激发状态。
观察DemoSMem的作法是这样的:当某一个进程修改了Shared memory的内容时,该 进程以SetEvent() API 函数将Event物件的状态设为Signaled(激发状态),叁与 进程通讯的各支程序在开跑之初,除了以相同的识别名称建立(叁用)Event物件之 外,还特别分派另一个Thread专司侦测特定Event物件激发状态的任务,一旦物件 激发了,表示一定某一个进程修改了Shared memory的资料,此时我们知道该是重 新读取资料内容的时候了。
呼! 终於将Shared memory的范例程序DemoSMem讲完了,下图是它执行的画面,彼 此看来是亳无关联,但是经由共同分享的记忆体与Mutex,Event两种同步协调技 术,彼此正在密切交换意见。
图: DemoSMem执行情形
MailSlot
执行DemoSMem时如果让你有广播的感觉,接下来要说的MailSlot会让你更有广播 的感觉,而且它是可以跨越机器边界向网路广播的。从字面上看来,这像是与寄 信有关的通讯机制,实际上它的行为也的确与其名称相符合。MailSlot就像是你 的信箱,只要知道地址,任何人都可以寄信给你,不过,只有你才可以打开信箱 读信。
MailSlot是一种由系统维护的虚拟档案,建立并拥有Mailslot的进程扮演Server .的角色,其他的进程包含MailSlot Server本身的进程均可以开启MailSlot写入 讯息,不过,只有MailSlot Server可以读取资料的内容。这是个单一Server多个 Client的机制,同时,资料只允许由Client对Server单向传送。
我想你可能也习惯了,要产生一个MailSlot物件大概也需要一个识别名称吧! :p 说不定连CreateMailSlot()函数名称都猜得一字不差。不过,这次的名称可不像 先前那样可以随便高兴取什麽就取什麽的,它具有以下的固定格式:
\\ServerName\mailslot\[path]name
我第一次看到时心想: 天哪! 这该怎麽填呀? 边举例边说明会比较容易懂
\\.\mailslot\MyMailSlotName MailSlot的识别名称一定从「\\」双倒斜线开始 。接下来的是机器的名称或组群网域的名称,这 的「.」句号代表的是进程所在 的那部机器。再来是「\mailslot」,对於MailSlot,一定是这个单字照抄就是了 。最後则是你自订的MailSlot名字。先前提到MailSlot实际上是特殊的虚拟档案 ,所以,要当它是档名应该也是说得通的。
的确,援引我们对於档案系统的概念,MailSlot的识别名称就像路径档名一样, 可以经过适当的阶层加分类管理,例如: \\.\mailslot\Account\Note。最後再看 一个例子: \\*\mailslot\MyMailSlotName,其中「*」指的是群组内的所有机器 。
说得够多了,让我们动手做做看吧! 首先是建立MailSlot Server的例子,取自本 文所附的ChienIPC这个程序单元
procedure TMailSlotServer.Open;
var
ASlotName: AnsiString;
begin
if FActive then Exit;
// 构成 Mailslot 识别名称
ASlotName := '\\' + FServerName + '\mailslot\' + FSlotName;
FHandle := CreateMailslot(
pchar(ASlotName), // MailSlot 识别名称
0, // 讯息长度的最大值,设为零表示不限
MAILSLOT_WAIT_FOREVER, // read time-out
nil); // 安全属性,先暂时采用预设值
if FHandle = INVALID_HANDLE_VALUE then
FActive := False
else
begin
FActive := True;
FWaitThread.Resume;
end;
end;
再强调一次,只有MailSlolt Server才可以读取资料,读取的方法是先以GetMai lslotInfo()侦测讯息的长度与数量,然後以回圈逐一配置记忆体并以ReadFile( )读出资料(别忘了MailSlot也是档案),以下是一则范例:
procedure TMailSlotServer.ReadFromMailSlot;
var
NextSize: DWORD;
MessageCount: DWORD;
Result: BOOL;
Buffer: pchar;
begin
if FHandle = INVALID_HANDLE_VALUE then Exit;
// 侦测 MailSlot 中是否有资料
Result := GetMailslotInfo(Fhandle, nil,
NextSize, @MessageCount, nil);
if not Result or (NextSize = MAILSLOT_NO_MESSAGE) then
Exit;
// 如果还有资料 (MessageCount <> 0),逐一读出资料
while Result and (MessageCount <> 0) do
begin
// 资料的长度
Buffer := AllocMem(NextSize + 1);
try
// 读出资料
FileRead(Fhandle, Buffer^, NextSize);
if Assigned(FOnDataAvailable) then
FOnDataAvailable(Self, StrPas(Buffer));
finally
FreeMem(Buffer, NextSize + 1);
end;
// 继续看看 MailSlot 中还有没有资料
Result := GetMailslotInfo(Fhandle, nil,
NextSize, @MessageCount, nil);
end;
end;
至於MailSlot的Client程序则没有什麽好说的,就当是档案迳行开启与写入即可 :
procedure TMailSlotClient.Open;
var
ASlotName: string;
begin
if FActive then Exit;
// MailSlot 的识别名称
ASlotName := '\\' + FServerName + '\mailslot\' + FSlotName;
// 开启 MailSlot(档案)
FHandle := CreateFile(pchar(ASlotName),
GENERIC_WRITE, // Client 端对於 MailSlot 只能写入
FILE_SHARE_READ, // 设定为可供分享读取
Nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
FActive := FHandle <> INVALID_HANDLE_VALUE;
end;
function TMailSlotClient.WriteIntoMailSlot(
const Data: string): integer;
begin
Result := 0;
if FHandle = INVALID_HANDLE_VALUE then Exit;
Result := FileWrite(Fhandle, Data[1], Length(Data));
end;
稍早提到MailSlot适合於跨越机器边界的网路广播, 可是我也说明了只有MailSl ot Server才可以读取资料,那要怎麽广播啊?答案在於MailSlot的名称。别的机 器如果也用相同的名称建立MailSlot Server,一旦任一个Client对某一个MailS lot(也是经由名称来叁考)送出讯息,这份讯息会游向网路节点上各个指定同名的 MailSlot,这样子就达成广播的效果。至於讯息是怎麽流来流去的,就留给系统 与网路底层去伤脑筋了,程序只管以档案写入资料的方式送出资料即可。
使用MailSlot时很可能你会遇到讯息重覆的问题;也就是说,虽然MailSlot Cli ent端只写了一个讯息,但相同的讯息MailSlot Server却可能收到两份。原因是 这样的:由於Win32多重通讯协定的缘故,MailSlot在广播时,并不知道到底该采 用哪一条路径,於是便各种可能的通路都传了一份。情况有点像在发布台风警报 ,我们在电视,广播与网路都同时会晓得有台风要来的消息。解决的方法是在资 料开头处加上一些控制用的编号代码,Server据以判断是否是相同的资料。
像MailSlot这样的通讯机制可以应用在哪些场合呢? 着名的例子是WinPopup,刚 才我也写了一支阳春的, 次图是MyWinpop.exe 执行的情况。由於MailSlot广播的 特性,十分适合网管时用来知会使用者重要的讯息,此外,MIS系统也可以用它适 时的报告异常状况,各使用者如果在「开始┃启动」中都放置这支小程序,彼此 便可以之交换讯息,当讯号进来时,也会立即显示讯息的内容。
当然,你还可以想得到其他的应用。像我就觉得它很适合用来作为程序除错工具 ,不仅可以将程序执行的过程与情况记录下来,而且程序在网路上各节点的执行 状况也将源源而来,这是一般的测试方法所不容易达成的效果。
Pipe
看过广播式的 MailSlot後,Pipe则是点对点的通讯机制,资料允许单向或双向於 管子连接的两端移动。pipe可分为Anonymous pipe与Named pipe 两种,Anonymo us pipe的资料只能单向流动,而且仅限於单机内使用,但却是进程重导其标准输 出(Standard Output)成为另一进程之标准输入的方法;Named pipe 就如同先前 讨论的各项IPC机制,由於有一个识别名称,其他的进程很容易可以依照名称找过 来,通讯范围不限於单机,同时,资料允许双向流通。
DDE
如同本文第一个TwinApp这个例子,DDE也是建立在讯息通讯这个基础上的,不过 它的协定内容显然严谨很多。
DDE是由Client端以WM_DDE_INITIATE广播讯息起拉开通话的序幕,Server端受理 後以WM_DDE_ACK回应,连通後则是一连串Server与Client间彼此互送WM_DDE_DAT A、WM_DDE_REQUEST、WM_DDE_ACK等讯息。实际的资料并不是真的经由讯息传递, 而是提供线索彼此利用Atoms(由Windows系统提供的字串对照表)寻求Applicatio n(应用程序), Topic(主题)与Data(资料)等三个项目。最後,以WM_DDE_TERMINA TE讯息结束对话。
进程间建立DDE连接时,当Server端的资料改变时,依资料交换的频繁与Client的 主动程度,其通道的形态可分为:
Cold Link:来要才有;Client端得主动要求传送资料,如果没有来要,即使Ser ver的资料已经改变很多了,Server对Client也置之不理。
Hot Link:有变就给;当资料改变时,Server端将主动通知Client改变的内容。
Warm Link:更新通知;当资料改变时,Server端只对Client端告知资料改变的消 息,真正的资料要等Client提出要求才会送出。
由於DDE讯息通讯牵涉的实作细节颇多,为了使用方便起见,微软也提供DDE管理 函式库(The DDE Management Library, 简称 DDEML), 使用上的最大差别在於使 用DDEML的程序是用Callback函数处理DDE交易(Transaction) 。另外,三大项目 的Application改口叫做(Service name)服务。
时至今日,讨论DDE的文献已不在少数,的确,DDE的使用应该是容易许多了,几 乎没有一个Windows程序开发工具不提供一些元件或类别让程序员更方便制作DDE Server或Client程序。当然,如果你的需求只是在进程间通知某些消息,自行设 计一套讯息通讯协定倒也简单得以完成任务,我想本文的第一个例子TwinApp是一 个不错的提示。
其他的IPC技术
EXE通常呼叫DLL的输出函数(exports function),某些情况下DLL也会使用EXE 事 先预备的回呼(Callback)函数。函数呼叫这个观念与想法如果移植到进程通讯中 会发生什麽事呢? 我的意思是说,让一个进程呼叫另一个进程的函数。Ya! 这就 是所谓的 RPC,进程之间属於函数呼叫层级的合作。可以想见的,由於进程各有 其定址空间,如同OLE,要达到 RPC确实需要额外标准的介面加以定义。
有关IPC的技术与观念我们已经介绍得不少了,不论是讯息交换,剪贴簿,Share d memory,DDE,MailSlot,Pipe等等,几乎都是资料的交换或者Client与Serve r「要求-回应」,叁与通讯的行讯必须对於交换的资料有一定程度的了解与处理 能力。换句话说,在我们以DDE向试算表软件要求传回资料後,这份资料到底代表 什麽得自己解释;同样的,如果要传入资料到试算表软件,即使透过现成元件的 帮忙,仍然必须对试算表软件有基本的认识。
话说回来了,只有试算表自己最清楚资料代表什麽,不是吗?那麽,由它来处理 资料应该才是适当的人选,强以外部程序去操作总有外行人指导内行人的遗憾。 利用OLE技术将应用程序整合在一起工作确实是比较合理的作法,如果COM物件可 以像电子IC一样安插进我们的程序与我们的程序一同工作,那这种我们称之为OL E Control(ActiveX),距离拉大到网路上,DCOM这个名词你一定听说过.
想想看,终於我们可以用甲公司的统计图表元件,然後用乙公司的元件将图表传 真出去,这样窗景真是美好。窗子确实只提供局部的风景,但是加装了望远镜的 窗子可是一个天文台,加了风铃的窗子所提供的就不只是风景了,还有悦耳的声 音。
不论是RPC或OLE,我想这都是属於本文应该讨论但肯定是来不及讨论的,这两个 主题甚至以单篇文章来谈都不怎麽够用。事实上,有些地方(例如DDE这一节)我也 没有提到技术方面的实作细节,碍於篇辐(这篇文章已经太长了) 日後我们会在本 专栏继续以专文介绍RPC等主题。关於以Winsock作为IPC通讯机制这部分,本专栏 的前一篇文章「走! 让我们上BBS聊天去」才刚说明过,在此就不再重覆了。
应用IPC到你的程序中
各项IPC的技术往往以各种方式组合在一起。例如本文提供的DemoSMem范例程序就 同时用了ShareMem交换资料,同步机制则采用Mutex与Event。情况并不如想像中 的复杂:既是进程通讯,那必然是两个以上进程之间的事,既是分开的,中间一 定有介面存在,定义这个介面的具体内容就是所谓的协定,留意资料交换的位置 与方式,需要协调避让的采用合适的同步控制加以处理。这些重点把握住了,应 该心 就已经有数了。
面对各式各样的技术时,如果你正考虑应用IPC到你的程序中,首先得正视自己的 需求,不妨提出类似以下的问题问问自己,最好将之写下来
是否真的需要跨进程处理,成效何在?
技术实作的难易程度与所需付出的成本
资料的流向是单向或双向,需不需反馈(feedback)的控制查核
这些工作只在单机完成,或者需要连上网路,范围只在公司内部区域网路或者是 广域网路
叁与通讯的进程最多与平均的数量是多少?
只在一种作业环境,或者可能同时要满足不同的作业平台
执行效能( performance)是不是关键需求.
应用程序使用 GUI 介面或者 console mode
接下来开始比较各项IPC的特性,哪些是与你列出的需求相符合的,有没有哪些限 制是你必须要排除而避免使用的,各项IPC经过与先前写下的需求交叉评比的结果 ,积分高的自然是脱颖而出。最後,事情如果能简单解决是最好,开发时程缩短 成本自然降低,而且日後维护容易。
结语
技术是不断推陈出新的,当各式各样的IPC机制提出时,回顾进程之所以开始通讯 合作的初衷是有必要的,唯有回到最初原始的简单需求,才能看出技术演进过程 的缘由与其修正的价值,不断的变易之中我们可以粹化出一些不变的原则与观念 ,而这些原则应该是与最初的需求互相吻合的。
以IPC这麽大的题目只写一篇文章是件很痛苦的事,我不清楚有多少读者会期待以 一篇文章能将IPC的技术细节讲清楚,不过已尽我所能的交待来龙去脉。观念与说 理太占篇幅,大部分的技术细节是隐藏在范例的原始程序中,这些程序日後如有 修改或加强,您可以在我的网站(http://www.chih.com)找到更新後的版本,对於 本文如有任何意见或评论,也欢迎您E-mail与我联络, 来信请寄[email protected] inet.net 或[email protected]。
用Delphi实现程序间的数据传递
在实际应用中,我们经常需要多个程序相互配合来完成某些特定功能。例如两个 应用程序间的同步、互斥;应用程序在起第二份实例时的参数自动传递…。要实 现这些功能,就必须能实现程序间的数据传递。
有些特殊的高级技术可在不同的程序间传递数据,如剪贴板、动态数据交换以及 OLE自动化,但有条件限制并且相对较复杂。这里,我介绍三种有效的底层技术, 希望对编程爱好者有所帮助。
利用WM_COPYDATA消息
使用该消息涉及一个TcopyDataStruct结构类型的指针。该结构中有三个成员:
dwData 是一个32位的附加参数
cbData 表示要传递的数据区的大小
lpData 表示要传递的数据区的指针
下面举个例子。该例子由两个程序构成,分别为SendData和GetData。
SendData程序向GetData程序发送消息,并传递edit1中的字符串;GetData在收到 消息后,把SendData发送的字符串接受下来,并显示在相应的edit1中。
SendData程序:
……
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject);
var
ds: TCopyDataStruct;
hd: THandle;
begin
ds.cbData := Length(Edit1.Text) + 1;
GetMem (ds.lpData,ds.cbData); //为传递的数据区分配内存
StrCopy (ds.lpData,PChar(Edit1.Text));
Hd:=FindWindow(nil,'Form2'); // 获得接受窗口的句柄
if Hd <> 0 then
SendMessage (Hd, WM_COPYDATA, Handle,
Cardinal(@ds)) // 发送WM_COPYDATA消息
else
ShowMessage ('目标窗口没找到!');
FreeMem (ds.lpData); //释放资源
end;
GetData程序:
TForm2 = class(TForm)
Edit1: TEdit;
private
{ Private declarations }
public
procedure Mymessage(var t:TWmCopyData);message WM_COPYDATA;
{ Public declarations }
end;
var
Form2: TForm2;
implementation
procedure TForm2.Mymessage(var t:TWmCopyData);
begin
Edit1.text:=StrPas(t.CopyDataStruct^.lpData);//接受数据并显示。
end;
使用这种方法是WIN32应用程序进行交互的最简单的方法。
使用全局原子
Win32系统中,为了实现信息共享,系统维护了一张全局原子表。每个原子中存放 了一些共享数据。关于对原子的操作,有一组专门的API函数:
GlobalAddAtom 在表中增加全局原子
GlobalDeleteAtom 在表中删除全局原子
GlobalFindAtom 在表中搜索全局原子
GlobalGetAtomName 从表中获取全局原子
笔者用这种方法实现了避免程序二次启动,但把第二次启动所带的参数传到第一 个实例中以进行相应的处理的程序。基本处理如下:在工程文件中:
program Pvdde;
uses
Forms,shellapi,Windows,dialogs,dde in 'dde.pas' {Form1};
{$R *.RES}
begin
if GlobalFindAtom(PChar('PDDE_IS_RUNNING')) = 0 then
//避免二次启动
begin
K:=GlobalAddAtom(PChar('PDDE_IS_RUNNING'));
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end
else begin
//传递二次启动时的参数到第一个实例
H := FindWindow(PChar('TForm1'), PChar('资料保密 严禁外传'));
if ParamCount > 0 then
begin
L := GlobalAddAtom(PChar(ParamStr(1)));
if H<>0 then
SendMessage(H, WM_MYMESSAGE, 0, L);
{ 传递原子句柄 }
GlobalDeleteAtom(L); { 使用后释放 }
end;
Application.Terminate;
end;
end.
在相应的窗口单元dde.pas增加对自定义消息WM_MYMESSAGE的处理:
procedure TForm1.MyMessage(var T:TMessage);
{对 WM_MYMESSAGE消息进行处理 }
var
P:Array [0..255] of char;
begin
GlobalGetAtomName(T.LParam, P,255); { 接受数据到p数组中 }
。。。
end;
使用存储映象文件
这种方法相对较复杂一些。
当Win95与Winows Nt向内存中装载文件时,使用了特殊的全局内存区。在该区域 内,应用程序的虚拟内存地址和文件中的相应位置一一对应。由于所有进程共享 了一个用于存储映象文件的全局内存区域,因而当两个进程装载相同模块(应用 程序或DLL文件)时,它们实际可以在内存中共享其执行代码。
笔者通过调用一个带有特殊参数的CreateFileMapping函数,来间接达到程序间共 享内存的目的。下面简要解释一下该函数。
HANDLE CreateFileMapping(
HANDLE hFile, //文件句柄
LPSECURITY_ATTRIBUTES lpFileMappingAttributes, // 可选安全属性
DWORD flProtect, // 映象文件保护方式
DWORD dwMaximumSizeHigh, // 映象文件区域的底值
DWORD dwMaximumSizeLow, // 映象文件区域的顶值
LPCTSTR lpName // 映象文件的名字
);
如果hFile是0xFFFFFFFF,在调用程序中必须指定dwMaximumSizeHigh 和dwMaxim umSizeLow参数的值以确定映象文件的大小。通过这样的参数指定,该函数就创建 了一个由操作系统页文件支持的特殊逻辑映象文件,而不是由实际操作系统的文 件支持的逻辑映象文件。这个逻辑映象文件可以通过复制、继承或者按名字来达 到共享。至于其它参数的详细说明,请参看在线帮助。
在建立了映象文件之后,我们可以通过调用另外一个API函数MapViewOfFile来访 问它的内存,该函数会返回一个指向共享内存块的特定指针。
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, // 映象文件句柄
DWORD dwDesiredAccess, // 访问方式
DWORD dwFileOffsetHigh, // 映象文件区域的底值
DWORD dwFileOffsetLow, // 映象文件区域的顶值
DWORD dwNumberOfBytesToMap // 映射字节数
);
如果 dwNumberOfBytesToMap 是0,映射整个文件。以下举例说明:
private
hMapFile: THandle;
MapFilePointer: Pointer;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.FormCreate(Sender: TObject);
begin
hMapFile := CreateFileMapping (
$FFFFFFFF, // 特殊内存映射句柄
nil, page_ReadWrite, 0,10000,
'DdhDemoMappedFile'); // 文件名
if hMapFile <> 0 then
MapFilePointer := MapViewOfFile (
hMapFile, // 上面映象文件的句柄
File_Map_All_Access,
0, 0, 0) // 访问整个映象文件
else
ShowMessage ('hMapFile = 0');
if MapFilePointer = nil then
ShowMessage ('MapFilePointer = nil');
end;
procedure TForm1.BtnWriteClick(Sender: TObject);
begin
StrCopy (PChar (MapFilePointer),
PChar (EditWrite.Text));//把内容写入共享内存
end;
procedure TForm1.BtnReadClick(Sender: TObject);
var
S: string;
begin
S := PChar (MapFilePointer);//从共享内存读出内容
EditRead.Text := S;
end;
用这种方法,不但可以在不同的程序之间共享数据,还可以在同一程序的不同实 例间共享数据。为了及时通知其它进程共享数据的变化,可以自定义一条用户消 息,通过发消息来实现,这里不再赘述。
利用以上三种方法均可以有效地实现数据传递、共享,所有的例子程序均在Delp hi 3.0,4.0下调试通过。如需源码,写信至[email protected]索
-- ※ 来源:.月光软件站 http://www.moon-soft.com.[FROM: 210.75.48.180]
|
|