背景技术
在Windows 16位环境下(Windows 3.1/3.2),所有Windows应用程序共享单一地址,任何进程都能够对这一空间中的内容(包括属于其他进程的内存)进行读写操作,甚至可存取操作系统本身的数据,这样就可能破坏其他程序的数据段代码。
出于安全的考虑,Windows 32环境下每个进程都有自己的地址空间,一个WIN32进程不能存取另一个进程的内存数据。两个进程可以用具有相同值的指针寻址,但将被映射到不同的物理地址。所读写的只是它们各自的数据,这样就减少了进程之间的相互干扰。比如:在进程A声明了一个指针P,这个指针只能被本进程A使用,在其他进程B中使用P,将会导致致命的问题,Windows禁止其操作。这样实现了对每个进程执行环境进行保护和隔离。这也是操作系统要实现的基本功能。
在WIN32环境下,每个WIN32进程拥有4GB的地址空间,但并不代表它真正拥有4GB的实际物理内存,而只是操作系统利用CPU的内存分配功能提供的虚拟地址空间。在一般情况下,绝大多数虚拟地址并没有物理内存于它对应,在真正可以使用这些地址空间之前,还要由操作系统提供实际的物理内存(这个过程叫“提交”commit)。在不同的情况下,系统提交的物理内存是不同的,可能是RAM,也可能是硬盘模拟的虚拟内存。
实际应用过程中,Windows各个进程之间常常需要交换数据,进行数据通讯。WIN32API提供了许多函数使我们能够方便高效的进行进程间的通讯,通过这些函数我们可以控制不同进程间的数据交换,就如同在WIN16中对本地进程进行读写操作一样。
常见Windows应用IPC技术(IPC:Inter-Process Communication,进程间通信)主要有以下几种:
使用剪贴板:在16位时代常使用的方式,CWnd中提供支持。因为剪贴板使用的场合很多,容易出现冲突的情况,且效率不高。
动态数据交换(DDE):其方式在一块全局内存中手工放置大量的数据,然后使用窗口消息传递内存指针。这是16位WIN时代使用的方式,因为在WIN32下已经没有全局和局部内存了,现在的内存只有一种就是虚存。所以,这种方法在WIN32下无效。
消息管道(匿名管道Anonymous Pipes、命名管道Named Pipes):它需要在程序间建立一条信道,通过该信道可以象自己的应用程序访问一个平面文件一样读写数据。缺陷:服务端必须运行在Windows NT/2000/XP。
邮件槽(Mailslots):广播式通信,在WIN32系统中提供的新方法,可以在不同主机间交换数据,实现了跨网络,但在WIN9X下只支持邮件槽客户。服务端必须运行在Windows NT/2000/XP。
Windows套接字(Windows Socket)、TCP/IP方式:该模式遵守一套通信标准,具备消息管道所有的功能,可让不同操作系统之上的应用程序之间可以互相通信。这种方式用于网络方面比较好,但用于本地进程间的通信,效率不高。
COM/DCOM:通过COM系统的代理存根方式进行进程间数据交换,但只能够表现在对接口函数的调用时传送数据,通过DCOM可以在不同主机间传送数据。
RPC:远程过程调用,调用方法比较复杂。很少使用,因其与UNIX的RPC不兼容。
向另一进程发送WM_COPYDATA消息:传输只读数据可用Win32中的WM_COPYDATA消息。该消息允许在进程间传递只读数据,但不能处理返回,可用共享内存弥补回传数据。SDK文档推荐用户使用SendMessage函数,接受方在数据拷贝完成前不返回,这样发送方就不可能删除和修改数据。
上述实现IPC的方法都是Windows操作系统中提供的通用方法。单从技术角度而言,可选择其一直接使用。而从应用角度讲,不是个完整的解决方案,或多或少地存在这样那样的缺陷和不足。下面对这些问题做出描述说明。
1、未能构建完整的服务端、客户端应用模型,使用不方便
在实际应用过程中,我们需要一个客户机/服务器的计算模型。由服务端提供数据响应服务,客户端发送命令请求。此外,在通信过程中还需要考虑数据格式的传递问题。就像是TCP/IP传输协议。上面提到的共享内存、DDE等方式,都未实现该模型。
邮槽虽然具备了一定的特性,但因为是单项通信,服务端收到信息后,无法向客户端写入数据返回。消息管道已经具备客户机/服务器模型,但有使用限制,服务端必须运行在Windows Server Systems系列的操作系统。
我们需要构建类似消息管道的模型,还要避开Windows操作系统的版本限制。
2、未考虑接口相关的参数序列化、接口通用性和易用性的问题
在消息管道等通信模式中,可以使用变长的字符流来实现通用数据传递,而并未没有给出更利于使用的参数序列化算法和实现。这样,在使用的过程中很麻烦。每次都需要把给定参数的数据组织成字符流,而在服务端需要把该数据流解压成和客户端一致的数据。这些,在标准IPC通信方法中都未实现。
3、未考虑效率和并发调用问题
现有的诸多IPC通信方式,虽以各种方式实现了跨进程通信,但效率和并发问题未考虑。在应用中,经常遇到需要并发调用服务的问题。
在此种情况下,单纯使用共享内存、DDE等模式,不能解决并发调用内存使用冲突问题。而我们知道,消息管道模式存在服务端应用Windows操作系统版本限制问题。
具体实施方式
参考附图1至4,下面将对本发明进行详细描述。
如图1、2中所示,本实施例通过建立SSIPCall工具,提供一种数据格式上实现了应用无关性的通用进程间通信实现方法,该方法包括:
服务端启动服务;
客户端调用服务,对参数打包、序列化,把调用的相关数据信息打包成无类型数据参数的信息包;
服务端处理调用,对收到的信息包解包,提取出打包数据并做处理后,重新打包成无类型数据参数的信息包返回;
客户端对返回信息包进行解包,提取数据后覆盖调用处参数数值;
服务端关闭服务。
该方法具有如下特征:
1)基于SSIPCall的应用程序分为客户端A、服务端B、SSIPCall实现DLL(C)三个部分。
SSIPCall实现DLL即SSIPCall工具。它是一个基本Windows DLL模块。为客户端A和服务端B提供通信机制。客户端A和服务端B实际上是两个进程。
2)客户端A的关键元素包括:
A1:一个通用的数据接口,让SSIPCall可满足不同的业务需求;
A2:参数打包、序列化,把客户端调用相关的数据信息打包成一定长度的信息包;
A3:数据解包、复制,服务端把调用处理完成后,有返回信息给客户端,客户端把这些信息进行解包,A3是A2的反过程。复制,把数据从通用数据接口提取出来后,覆盖客户端的调用处参数数值。
关于A1的通用数据在后面会做详细说明。
3)服务端B的关键元素包括:
B1:参数解包,服务端收到客户端调用信号后,把打包数据提取出来,解包。
B2:服务端解包后的数据格式,目前B2=A1。这样,服务端可以获得和客户端打包前一致的数据。实现了通信调用细节对调用者来说的透明化。
B3:服务端服务程序段拿到B2格式的数据后,对客户端A的调用做处理。并修改B2内的数据。
B4:服务端最终把修改后的B2重新进行打包成中间格式,以便于通信。
此后,客户端A通过步骤A3对返回结果信息进行解包,并把数据复制到调用处。
3)客户端A和服务端B之间的基本流程为:
服务端B启动服务;
客户端A调用服务(通过SSIPCall的FunCall函数);
服务端B服务代码段处理调用,并做处理返回;
客户端A获得返回数据,解包后覆盖调用处参数数值;
服务端B关闭服务后,客户端将在一个时间范围内重试调用,超过时限,调用失败。
4)SSIPCall工具的核心DLL实现文件,提供了进程间的数据传送机制,这对使用者来说是透明的。其中用到了Windows的“共享内存”机制,作为进程间数据传送基础。
各类应用的环境不尽相同,有些内存配置较大,有些配置较小。考虑到能适用于大内存的情况,满足复杂应用;也能以较小的内存满足基本应用。此外,还需要考虑不使应用程序使用内存情况出现比较大的动荡,避免出现内存泄漏。SSIPCall提供了两种内存分配模式,分别是在客户端分配内存和在服务端分配内存。下面分别介绍两种模型。
模型A:由服务端主动共享参数内存(静态分配)(图3):
该模式下,在服务端启动服务窗口后,打开一个内存映射(内存申请相对较大,可允许几个调用并发),并对该内存区域进行分区,每个区域处理一次调用,分多个区域,这样可同时并发处理函数调用,各处理之间使用不同的内存区域,相互无数据信息干扰。
调用基本分为5个步骤:
客户端处理共享内存区域的分配,得到一个未被占用的共享内存分区的索引号;
客户端找到服务窗口,并使用SendMessage发送消息;
服务端窗口过程中,调用回调函数(在窗口过程中,根据索引号获得参数内存,并把指针作为参数,调用回调函数);
服务端回调函数处理,实现数据回填(回调函数内,解析客户端用到的数据内容,执行对相关服务函数、类函数的调用,并把结果回填到客户端内存);
客户端根据索引号,获得数据返回(这里返回信息使用的内存区域和刚才作为参数用的内存区域是同一个块,分配的共享内存区域既作为入口,又作为出口返回信息的载体)。
模型B:客户端每次动态申请共享内存(动态分配)(图4):
该模式下,服务端不负责预先共享内存,每次调用均由调用者在SendMessage前申请一个共享内存,SendMessage把客户端分配的内存名字告诉服务端。服务端调用完成后,根据这个名字把数据信息写回到共享内存。客户端从此共享内存区域获得信息返回。
调用过程也可分为5个步骤:
客户端申请共享内存,作为服务端回写信息用(共享内存的名字为了不重复,使用了GUID,GUID的算法决定了,每次函数调用不可能申请出重复名字的共享内存。这样,在多线程的情况下,每次调用的参数信息之间不会相互干扰);
客户端找到服务窗口,并使用SendMessage发送消息;
服务端窗口过程中,获得客户端数据,并调用回调函数(在窗口过程中,获得参数数据和由共享内存映射的回写指针,调用回调函数);
服务端回调函数处理,实现数据回填(回调函数内,解析参数数据,执行对相关服务函数的调用,并把结果回填到客户端内存,在客户端申请的,名字已传到服务端);
客户端根据申请的共享内存,获得数据返回
(在这种模式下,因为要把客户端申请得到的共享内存名字在SendMessage里传给服务端窗口,这里用到了字符串作为参数。在Windows平台下,用SendMessage实现时只能通过发送WM_COPYDATA消息实现)。
模型A和模型B的比较如下表所示:
作为一个完整的、适应性强的产品SSIPCall来说,模型A和模型B都得到了实现,它们都有各自的特性和专长,并非是替代关系。
比较内容 |
模型A |
模型B |
内存分配方式 |
在服务端分配内存。静态分配。大小和线程数相关。 |
客户端分配内存。动态分配。在每次调用时,由客户端分配,并把客户端的内存名字通过参数通知服务端。 |
线程模式和并发模式 |
支持多线程。线程个数参数配置。并发调用达到线程个数时不能再进行调用,只能等待。 |
支持多线程。线程格式参数配置。并发调用达到线程个数时,把调用提交给当前执行任务最小的线程。 |
模型A使用了静态分配内存,故而整个系统内存分配情况稳定。但在达到并发调用上限时,对性能有一定影响,适合于一般情况下的应用。模型B动态分配内存,保证了并发调用,但因为内存每次都需要重新分配,这牺牲了调用性能。适合于配置较好的计算机系统。
以上两种模型的支持,需要通过条件编译实现,客户端和服务端要配套。
通过建立SSIPCall工具,提供一种数据格式上实现了应用无关性的通用进程间通信实现方法还需要考虑到下面几个问题的解决:
1、效率问题
服务端只有一个窗口的情况下,不能实现并发调用。SendMessage只能等目标窗口的上次调用完成后,才能处理本次调用。这与我们的设计初衷是相违背的。
解决效率问题,使用多线程实现。可让服务进程内的每个服务窗口分属不同的线程。这样,每个窗口在处理的时候,不会相互影响性能。线程是Windows操作系统CPU处理机资源的最小调度单位。
不过需要解决的问题是,因为同一个功能调用可由多个服务窗口来处理。这样就有个调度算法的问题,可以使用一些调度策略:最简单的使用随机策略;根据每个服务窗口的忙、不忙情况来选择。
SSIPCall早期使用随机算法。后来考虑到把SSIPCall移植到ABWOA软件总线中使用,为提高性能,使用了任务处理忙、不忙程度相关的调度算法。
如图5所示,该模式下,需要6步才能完成调用。与图3、图4的单线程实现版本相比,多了选择服务窗口这一步。选择服务窗口使用了一些策略,用于计算每个窗口任务执行数,并从中选择较少的窗口,这样能提高性能和整体系统的吞吐量。
实现记录执行任务信息,用到了下面的结构体(以下信息存放在服务相关共享内存中):
typedef struct_my_ssipc_server_node
//服务线程结点信息
{
HWND hWnd;//当前结点的窗口句柄
long nExecuteCount;//当前窗口的任务记数
}MYSERVERNODE,*LPMYSERVERNODE;
typedef struct_my_ssipc_server_info
//服务线程结点列表
{
long thread_count;
MYSERVERNODE thread_list[SSIPC_MAX_THREAD];
}MYSERVERINFO,*LPMYSERVERINFO;
每次SendMessage前,先获得nExecuteCount最小的服务窗口,然后直接取出hWnd。接着将nExecuteCount加1,表示新增了一个任务,然后SendMessage发送消息。
在服务窗口的窗口过程函数中,需要将此nExecuteCount减1,表示完成了一个任务,也自然减少了一个任务。
2、参数序列化未考虑
两个进程间调用函数,直接是没办法调用的,只能借助于IPC机制。但参数如何传递是问题。还有考虑到通用性,需要提供一个应用无关性序列化实现模型。
一般的函数申明,比如:
int test_fun(int a,int b,int c,int*pIntValue);
在调用的地方:
int retvalue=0;
int iret=test_fun(12,32,3,&retvalue);
这是在同进程内的参数传递方法,调用函数时,C++编译器把这些参数压入堆栈,以便函数执行体可以使用这些数据(图6)。
两个进程间传递数据,无论使用WM_COPYDATA还是使用共享内存,只是将以上16个字节作为整体来处理,传递时,可获得首地址P,然后复制16个字节。把16个字节作为二进制流传递给其他进程。其他进程,把这16个字节,再复原为4个参数,这样就实现了参数传递。因为中间使用二进制流,所以可以自由传递。
如图7中所示,客户端可构造一个通用结构体:
typedef struct_my_ssipc_buff
{
DWORD dwFunctionCode;//函数命令号
long nParamCount;//参数个数(当前最多30个参数)
long len1;LPVOID buff1;
long len2;LPVOID buff2;
long len3;LPVOID buff3;
long len4;LPVOID buff4;
。。。。。。
long len30;LPVOID buff30;
}MYFUNBUFF,*LPMYFUNBUFF;
buff1、buff2、buff3、buff4可分别指向a、b、c的地址,和pIntValue(本身已经是指针),这样以便后面的打包程序序列化。
MYFUNBUFF Mycall;
Mycall.dwFunctionCode=TEST_FUN;//是个常量,用于标识什么功能调用
Mycall.nParamCount=4;//表示本次调用有四个参数
Mycall.buff1=&a;
Mycall.len1=4;
Mycall.buff2=&b;
Mycall.len2=4;
Mycall.buff3=&c;
Mycall.len3=4;
Mycall.buff4=pIntValue;
Mycall.len4=4;
客户端调用时,只需要提供以上参数信息即可。打包过程在SSIPCall内部实现。
SSIPCall序列化、还原实现过程如图8所示。
命令包(IPC二进制流)的格式如图9所示:
CC CC CC CC PP PP PP PP LL LL LL LL XXXXXXXX LL LL LL LLXXXXXXXX
CC表示函数命令号
PP表示参数个数
LL表示每个参数的长度
XXXX表示参数缓存
命令号、参数格式、每个参数的长度都用4个字节表示。
以上数据在打包时把参数信息使用memcpy复制到缓存区域,形成上面的数据模型,从而变成流格式(内部排列按照命令包的格式)。将这些数据传到服务端。服务端根据以上约定的格式,将数据还原到通用结构体MYFUNBUFF中。在服务端,从而得到了如在客户端序列化前的数据格式。此时,在服务端可直接访问MYFUNBUFF中的数据即可。
图9较形象地描绘了这个过程。
因为结构体内使用LPVOID作为参数,可指向任何类型信息,包括整形、浮点等简单类型的数据;也可指向构造数据,如:结构体、公用体。这样就实现了应用数据格式的无关性。
每个缓存的len值必须设置,否则在序列化(打包)时,无法知道参数的数据信息什么时候结束,也就不能把信息完整地复制。如len1表示buff1指向的信息的长度,len2表示buff2指向信息的长度。典型的整形就是4个字节(WIN32环境下)。为了兼顾到各类复杂应用,这里设置了最多可允许30个指针。在最新版本的SSIPCall中已经升级到可支持255个指针。
在使用结构体参数时注意:内部不能使用指针,因为打包程序无法知道其指向内容的长度,也就没办法系列化。解决的办法是使用定长数组。
struct test_struct
{
char buff[255];
int buff_len;//buff的实际内容长度
double c1;
int c2;
long c3;
}
下面的格式作为参数是错误的:
struct test_struct
{
char *buff;
int buff_len;//buff的实际内容长度
double c1;
int c2;
long c3;
}
因为SSIPCall打包程序是个通用的程序,它不知道buff_len就是buff的指针内容长度,也就没办法处理打包问题。
若遇到结构体里需要子结构体数组,可定义形如:
typedef struct_my_ssipc_exchange_node
{
BYTE bUsed;//当前结点是否在用
HWND hWnd;//当前结点的窗口句柄
long buff_len;
BYTE buff[SSIPC_BUFSIZE];
}MYFUNEXCHANGENODE,*LPMYFUNEXCHANGENODE;
typedef struct_my_ssipc_exchange_buff
{
long buff_count;//实际有用的个数
MYFUNEXCHANGENODE buff_list[SSIPC_MAX_THREAD];
}MYFUNEXCHANGEBUFF,*LPMYFUNEXCHANGEBUFF;
以上这些结构体可使用sizeof()函数获得总内存长度,在复制信息时,按照这个长度直接复制到序列化缓存中即可,不需要关心结构体的格式。按照静态定义的结构体,在内容中将呈顺序分布,这样才能用memcpy复制将获得整个结构体的数据。
而在服务端,只要把获得的指针转换为LPMYFUNEXCHANGEBUFF指针即可,然后就可以访问里面的内容。
在结构体里面用到指针的情况下,指针变量所指向的内存地址和结构体内存地址不是顺序分布,指针可能指向另外一块内存,不能使用memcpy复制完全。且单纯的指针传到另外的进程中,将毫无意义。
SSIPCall提供了SSIPC_PackFun()和SSIPC_UnpackFun()来完成打包、解包。实现序列化和信息还原。
3、一个DLL支持多服务
可以根据业务要求,将服务进程内的服务窗口分类,如:为纸币服务调用的窗口、为硬币服务调用的窗口,实现一个DLL内支持多应用。便于代码分类实现。
服务端窗口的命名方法、窗口句柄公开方法
为了让客户端可以显式找到服务窗口,我们对窗口名称需按照一些规则进行命名,或者把这些服务窗口的句柄放在一个与“服务名”相关的共享内存区域。且客户端能找到这些信息,这样客户端才能找到服务窗口,才能发送SendMessage消息。
在一个服务程序中,可能提供多种“服务”。比如:一个现金模块服务程序,可能需要提供纸币的服务接口,也需要提供硬币的服务接口,为了便于在服务端分布实现代码;也让客户端调用时,有个比较清晰的类别区分。我们可对各个服务窗口进行类别区分。但事实上,这些服务都在同一个EXE里实现。
下面是服务相关的设计思路说明:
每种服务相关的服务窗口名字具有相同的前缀
如EPP的服务窗口命名为:
″_ssipc_v1.0_#_win_epp_#0″
″_ssipc_v1.0_#_win_epp_#1″
″_ssipc_v1.0_#_win_epp_#2″
_ssipc_v1.0_是统一的命名前缀,以避免服务窗口名字和其他应用程序名字重复;
_win_epp_是服务名;
#0、#1、#2是每个线程所有的服务窗口,用于实现同服务的并发调用。
属于同一个服务,将拥有下面这些共享信息:
1)“服务信息区”,记录当前服务每个线程的窗口句柄、当前任务
″_ssipc_v1.0_#_win_epp_#server_info″
这块内存区域提供给客户端,以便在调用时选择服务窗口的决策分析。
2)每个服务所属的“参数信息区”,内存分区数=线程数=服务窗口数
″_ssipc_v1.0_#_win_epp_#exchange″
这块内存区域用于让客户端分配获得内存分块,以便传递信息。
若使用第二种内存分配方式,即在客户端内存申请以GUID命名的共享内存区域作为信息返回用时,此内存区域无效。
3)每个服务所属的“访问锁”,控制不会出现一个内存分区被多个线程获得分配:
″_ssipc_v1.0_#_win_epp_#lock″
两个服务之间,以上共享内存区域不同,这样避免了两个服务之间的信息干扰,同时提高了综合调度能力。因为每个服务在分配内存时使用不同的“访问锁”、不同的“参数信息区”和“服务信息区”,系统的并发性就强。
在内部实现SSIPCall.dll中,每种服务对应一个SSIPC对象,每个对象内包括多个线程和服务窗口(一个线程内包含一个服务窗口)。
图10对象1、对象2、对象A、对象B都是CSSIPC类的实例化。SSIPCall.dll被加载后,其内存空间使用加载它的EXE的进程空间。所以,SSIPCall.dll的内存空间就是服务端EXE进程的内存空间。
最后,还需要注意的是,以上列举的仅是本发明的具体实施例。显然,本发明不限于以上实施例,还可以有许多变形,本领域的普通技术人员能从本发明公开的内容直接导出或联想到的所有变形,均应认为是本发明的保护范围。