一、开篇
程序间通信的主要目的是实现多台计算机(也可以是同一台)中应用程序之间的数据共享与信息交换。在不同的计算机系统中,它们之间要通过网络之间的协议才能实现数据共享与信息交换;在同一台计算机系统中,它们之间只需一定的通道就能实现数据共享与信息交换。在不同计算机系统和同一计算机系统的程序通信中,既有很多相同之处,也有各自的特点。程序间通信都要靠一定的通道(pipe)来实现,其中的通道多种多样,各俱特色。
为了充分认识和掌握程序间通信及其相应的实现技术,本文对各种通信方法进行讨论。包括每种方法的原理和实现等。
二、各种通信途径及实现
首先,程序间相互独立,它的运行环境不为别的进程所改变。
I.[socket]
SOCKET编程是一种典型的会话编程方式,类似于老师家访,敲门----有人开门----进去----交流----出门。它适用于client/server通信方式,也适用于点对点通信方式。
下面分别介绍服务端和客户端的具体任务。
这里介绍TCP的过程。
(1)服务端
服务端首先创建一个套接字,使用socket()调用;然后使用bind()将该套接字与本地IP和某一端口相关联(该端口可以是空闲的也可以是非空闲的,具体的可以参阅笔者的《对端口截听的实现》这篇文章)。使用listen()让套接字等候进入连接,然后用accept()使套接字作好接受客户连接的准备。当连接请求到来后,被阻塞服务进程的accept()函数生成一个新的套接字与客户套接字建立连接,并向客户返回接收信号。用read()来读入数据,用write()来向发送进程写回一些数据,如确认信息或回显信息。
(2)客户端
客户进程也是先创建一个套接字,用socket()调用,然后客户向服务进程发出连接请求,通过调用connect()可以建立一个端的连接,连接成功后用write()向服务端发送数据,用read()读取服务端返回的数据。
这种方式隐含了在建立client/server间通信的非对称性。client/server模型工作时要求有一套为客户机和服务器所共识的惯例来保证服务器能够被提供(或被接受),这一套惯例包括了一套协议,它必须在通信的两头都被实现。根据不同的实际情况,协议可能是对称的或是非对称的。在对称的协议中,每一方都有可能扮演主从角色;在非对称协议中,每一放则是从机。例如终端仿真TELNET是对称协议,HTTP是非对称协议。无论具体的协议是对称的还是非对称的,当服务被提供时必然存在客户进程和服务进程。
这种通信方式适用于单个计算机系统,也适用于多个计算机系统。
●
WSADATA wsaData;
WORD version = MAKEWORD(2, 0);
int ret = WSAStartup(version, &wsaData);
if(ret != 0)
TRACE("Initilize Error!\n");
m_hSocket = socket(AF_INET, SOCK_STREAM,0);
m_addr.sin_family = AF_INET;
m_addr.sin_addr.S_un.S_addr = INADDR_ANY;
m_addr.sin_port = htons(m_nPort);int ret = 0;
int error = 0;
ret = bind(m_hSocket, (LPSOCKADDR)&m_addr, sizeof(m_addr));
if(ret == SOCKET_ERROR){
TRACE("Bind Error: %d \n", (error = WSAGetLastError()));
return ;
}
ret = listen(m_hSocket, 2);
if(ret == SOCKET_ERROR){
TRACE("Listen Error: %d \n", (error = WSAGetLastError()));
return ;
}
SOCKET s = accept(m_hSocket, NULL, NULL);
if(s == SOCKET_ERROR){
TRACE("Accept Error: %d \n", (error = WSAGetLastError()));
return ;
}char buff[256];
ret = recv(s, buff, 256, 0);
if(ret == 0 || ret == SOCKET_ERROR ){
TRACE("Recv data error: %d\n", WSAGetLastError());
return ;
}
char buf[]="hello";
send(s, buf, str.GetLength(), 0);II.[file]
这种方法很原始,使用率也很少,而且占用磁盘空间。利用文件作为程序间通信的通道,程序A向一文件写入数据,程序B通过读取这个文件中的数据从而与程序A进行通信,同时这一过程也可以是反向的。这种方法使用的前提是运行程序的用户需要具有对磁盘读/写的权限。
|A.WriteFile------>fileA<------B.ReadFile(check every <time>)
|A.ReadFile(check every <time>)----->fileB<-----B.WriteFile ●
//A
int buff=0;
ofstream Table;
Table.open("c:\\temp.txt");
Table<<buff<<endl;
Table.close();//B
int buff=1;
for(;;)
{
ifstream Table;
Table.open("c:\\temp.txt");
Table>>buff;
Table.close();
if(buff==0){
printf("Operation code equal 0");
//...
return;
}
sleep(10);
}
这种方法适用于单个计算机系统也适用于多个计算机系统。用这种方法在多个计算机系统间进行控制和数据交换,可以将这个文件放与一计算机上,对方建立空连接,COPY此文件到本地,进行读取。当然也还有别的很多方法。
III.[signal]
这种方法仅用于进程控制,不能进行数据交换。例如在发生非法内存访问、执行无效指令、某些按键(如CTRL+C,DEL等)等都会产生一个信号,其他程序调用有关的系统调用或用户自定义的处理过程进行处理。
信息处理的系统调用是signal,它的原型是void ( *signal( int sig, void (__cdecl *func) ( int sig [, int subcode ] )) ) ( int sig );这个函数可以让一个进程选择很多方法中的一种去处理操作系统返回的中断讯号。
根据类似的思路,我们也可以在多个程序间利用“信号”进行程序间的通信。
这种方法的关键是产生信号。可以通过keybd_evnet()产生信号,这个函数能够起到模拟键盘输出的作用,它的函数原型是VOID keybd_event(BYTE bVk,BYTE bScan,DWORD dwFlags,DWORD dwExtraInfo);vVk是一个虚拟键码,bScan是扫描码,对应相应的键。
这种方法适用于单个计算机系统。
IV.[pipe]
管道(PIPE)是一种简单的进程间通信(IPC)机制,分为无名管道和有名管道两种。命名管道可以在同一台机器的不同进程间以及不同机器上的不同进程间进行双向通信(使用UNC命名规范)。管道的最大好处在于:它可以象对普通文件一样进行操作,它的操作表示符HANDLE,也就是说,它可以使用ReadFile,WriteFile函数进行与底层实现无关的读写操作,用户根本就不必了解网络间/进程间通信的具体细节。
无名管道实际上是内存中的一个临时存储区,由系统安全控制,并且独立于创建它的进程的内存区。管道对数据采用先进先出的方式管理,并严格按顺序操作,管道不能被搜索,管道中的信息只能读一次。无名管道只是在父子进程之间或者一个进程的两个子进程之间进行通信的。它是单向的。无名管道其实是通过用给了一个指定名字的有名管道来实现的。
有名管道的操作和无名管道类似。
管道的创建用createpipe()函数,用readfile()、writefile()对其进行操作。
●
SECURITY_ATTRIBUTES a;
HANDLE hReadPipe,hWritePipe;
CreatePipe(&hReadPipe,&hWritePipe,&a,0);
unsigned long lBytesRead;
char Buff[1024];
int ret;
while(1)
{
ret=PeekNamedPipe(hReadPipe,Buff,1024,&lBytesRead,0,0);
if(lBytesRead)
{
ret=ReadFile(hReadPipe,Buff,lBytesRead,&lBytesRead,0);
if(!ret) break;
}
else
{
//lBytesRead=...;
if(lBytesRead<=0) break;
ret=WriteFile(hWritePipe,Buff,lBytesRead,&lBytesRead,0);
if(!ret) break;
}
}V.[MQ]
消息通信方式以消息缓冲区为中间介质,通信双方的发送和接收操作均以消息为单位。在存储器中,消息缓冲区被组织成队列,通常称为消息队列(MQ)。消息队列是独立于生成它的进程的一段存储区,任何具有正确访问权限的进程都可以访问消息队列,它非常适用于在进程间交换短消息。
其中,每条消息有类型编号分类。消息队列在创建后将一直存在,直到使用相关的调用删除它为止。
VI.[共享储存段(SM)]
共享储存段通信(shared memory)允许多个进程在外部通信协议或同步/互斥机制的支持下使用同一个内存段作为中间介质进行通信,它是一种很有效的数据通信方式。
在进行通信之前,先创建一个共享内存段,然后进行映射和分离操作。同时可以根据需要改变共享内存段的存取权限以及其他的一些特征。
☆对于下面的集中通信方式,就要提到“中间件(Middleware)”。
中间件是一类软件,它对应用程序隐含了实际网络和通信协议的细节。高级编程接口帮助开发者在不同的环境中创建应用程序,而不需要对将使用的网络和通信协议有更多的了解。
正常情况下,中间件在使用不同网络通信协议的客户机/服务器环境。它可以对客户机/服务器应用隐藏协议,从而使开发人员集中精力于改进应用程序,而不是开发通信接口。
中间件产品隐藏了前端应用程序和后端应用程序的区别。中间件层包括通用应用程序和流行应用程序的应用程序编程接口(API)之间的翻译功能。例如,Microsoft的开放式数据库连接(ODBC)标准提供后端数据库系统操作的通用功能。前端应用程序写入ODBC,并利用它的功能。ODBC 隐藏了不同厂商的SQL实现的区别。Microsoft以一组Microsoft Windows驱动程序的形式提供ODBC,以提供对Microsoft Access、Microsoft Excel、Microsoft SQL Server、FoxPro、Btrieve,dBASE,Borland Paradox、IBM DB2、DECRdb和Oracle等格式产生的数据的访问。ODBC是为了使Windows成为客户访问后端数据库的标准而设计的。
□用户需要访问许多不同的后端服务器上的服务。
□后端服务器可以使用不同的操作系统,并需要不同的通信协议。
□后端数据库服务器有不兼容的SQL命令集,它使用户和程序员很难从一个系统转到另一个系统。
□限制用户只能使用某种特定的访问后端服务的应用程序现在已经不现实。用户需要从不同的应用程序来访问服务。
□新的模型是为了使用户在多厂商环境使用多种协议在任何前端来访问任何后端服务。
在多协议、多厂商环境,通常一个程序员需要编写应用程序,来与每个协议和支持系统进行工作。使用中间件,程序员只需简单地编写到中间件的接口,而由中间件处理所有多协议、多厂商问题。有三种类型的中间件:远程过程调用(RPC)、会晤(conversations)和消息传递系统。它们都能很好地隐藏通信过程,以及与它们进行操作的系统差异。
VII.[RPC]
Remote Procedure Call(PRC)
一个远程过程调用是在网络上一个计算机系统对另一计算机系统的请求。RPC保持了中间件在不同网络平台和通信协议上工作的性质。基本上,一个PRC就是一台计算机向另一台计算机发出的直接请求。它是一种请求/回答的过程,这时请求放等待一个回答,这意味着PRC通常是在面向连接界面上发生的一种实时呼叫。
RPC机制强制通信的两个应用程序必须同时处于运行状态。做远程调用时,两者必须先建立连接,而且通讯链路质量对它的效果影响很大。
它的工作方式是:当一个应用程序A需要与远程的另一个应用程序B交换信息或要求B提供协助时,A将在本地产生一个请求,通过通讯链路,同志B接收信息或提供相应的服务,B完成相关处理后将确认信息或结果返回给A。
PRC的优点是应用程序采用调用/返回方式通讯,拥有很高的潜在效率,但需要应用程序间的紧密耦合,通讯线路必须在通信期间一直保持良好的状态,而且必须进行大量的底层通讯的编程工作。
VIII.[conversations]
会晤(conversations)是逻辑连接的两个或多个系统之间的连续会话,同RPC不同,在分布式环境,会晤可能会重叠执行。出于这个原因,对需要在多个地方必须完全同步地完成,修改分布式数据库的工作,会晤是非常必要和有益的。IBM的高级程序对程序通信(APPC)实现了会晤。用于实现会晤的OSI标准也已出现。Covia Technologies(Rosemont,Illinois)的通信集成器(CI,Communication Integrator)是具有会晤的中间件的另一个例子、它可以在大型计算机、中型机和台式机上运行。
IX.[MQSeries消息队列]
为了简化应用程序间的通信,使得通信具有较高的可靠性,又保证实现的简单性,中间件技术是我们的首选,IBM公司的MQSeries就是基于这种技术的产品。MQSeries是一种独立的通信软件,应用程序只需将任务提交给该软件,就可以由该软件自动去完成信息的传递工作。
例如:
应用程序A
|
|
放入|消息
|
|
∨
MQSeries接口----------------
∧ |
| |
| |唤醒
读取|消息(于Q1中) |
| |
| |
应用程序B(无论本地还是远程)<-----
应用程序间的消息传递是通过队列来实现的,是间接的,所以即使B没能正常运行时,A仍然能正常运行,而且,消息还可以唤醒B。
可见,MQSeries的优点是可以确保信息是永久的、可恢复的,确保信息传递的安全性,确保信息的成功发送且只发送一次,同时,还可以使各应用程序可以独立的正常运行。
无论是共有的还是私有的队列,都是用MQOpenQueue()打开,MQOpenQueue函数用来打开一个用来发送消息或是读取消息的队列。它的原型是HRESULT APIENTRY MQOpenQueue( LPCWSTR lpwcsFormatName, DWORD dwAccess, DWORD dwShareMode, LPQUEUEHANDLE phQueue );其中,lpwcsFormatName是用户指定的指向队列的format name字符串,这个format name可以是public,private,或者是direct format。dwAccess是由用户指定的应用程序使用队列的方法,可以是peek,send,receive。当队列打开以后这个设置就不能被更改。dwAccess有三种模式:MQ_PEEK_ACCESS/MQ_SEND_ACCESS/MQ_RECEIVE_ACCESS。dwShareMode是由用户指定的队列的共享模式,分为:MQ_DENY_NONE和MQ_DENY_RECEIVE_SHARE,MQ_DENY_NONE是默认值,允许任何人使用,如果dwAccess设置成了MQ_SEND_ACCESS,那么在此就必须设置成MQ_DENY_NONE;MQ_DENY_RECEIVE_SHARE限制了从此队列读取消息的进程。如果这个队列已经被别的进程打开用来接收消息,那么将返回MQ_ERROR_SHARING_VIOLATION,MQ_DENY_RECEIVE_SHARE仅适用于当dwAccess设置成MQ_RECEIVE_ACCESS或MQ_PEEK_ACCESS的时候。phQueue是输出的已打开的队列的句柄。
然后可以用MQSendMessage()/MQReceiveMessage()等函数进行具体操作,在此不多熬述。
三、结束语
技术是一点一滴积累起来的,对任何知识点的系统性掌握很有必要也很有用。
各种通信的实现技术都具有自己的特点和使用范围,管道、消息队列、共享内存等技术最适用于同一计算机系统内部的进程间通信,以保证高效率。而远程过程调用、Socket会话编程、MQSeries则最适用于远程的应用程序之间通信,可以简化通信的编程,当然也保证通信的可靠性。尤其是MQSeries,它是一个比较完善的中间件产品,为许多的信息系统所选用。
对各种通信技术的合理搭配使用,常常能起到事半功倍的作用。