我们既可以用非标准的进程间通信技术,如Windows消息、内存映射和内存共享等,也可以用标准的通信技术。微软标准进程间通信技术的发展过程如下所述。 (1)进程间通信初期 自从有Windows操作系统后,剪贴板(Clipboard)首先解决了不同程序间的通信问题(由剪贴板作为数据交换中心,进行复制、粘贴的操作)。但是剪贴板传递的都是“死”数据,应用程序开发者得自行编写、解析数据格式的代码。于是动态数据交换(Dynamic Data Exchange,DDE)的通信协定应运而生,它可以让应用程序之间自动获取彼此的最新数据。但是,解决彼此之间的数据格式转换仍然是程序员沉重的负担。对象的链接和嵌入(Object Linking and Embedded,OLE)的诞生把原来应用程序的数据交换提高到“对象交换”,这样程序间不但获得数据,而且也可以获得彼此的对象,并且可以直接使用彼此数据内容。 (2)OLE(对象链接与嵌入) 1991年制定的OLE1.0规范主要解决多个应用程序之间的通信和消息传递问题,微软希望第三方开发商能够遵守这个规范,以便在当时的Windows平台上的应用程序能够相互协调工作,更大地提高工作效率。然而事与愿违,只有很少的软件开发商支持它。为此,微软于1993年发布了新的规范——OLE2.0,它在原有的基础上完善并增强了以下各方面的性能:①OLE自动化,一个程序有计划地控制另一个程序的能力;②OLE控件,小型的组件程序,可嵌入到另外的程序,提供自己的专有功能;③OLE文档,完善了早期的混合文档功能,不仅支持简单的链接和嵌入,还支持在位激活、拖放等功能。 (3)ActiveX战略 同OLE1.0相比,OLE2.0得到了很多软件厂商的支持。许多程序设计人员编写了大量的实现OLE自动化服务器功能的组件(不一定是EXE文件),这些组件一般不求功能齐全、强大,而是可以实现专门的功能,被其他程序编程控制,由此承袭OLE的名字,称为OLE控件。它们在文件名中的扩展名一般为ocx(OLE Control Extension)。微软刚刚赢得广大软件厂商的支持,使OLE技术深入人心,然而天不如人愿,国际互联网的超速发展让比尔·盖茨始料未及。加上早期的OLE1.0不得人心,导致后来的人们总把在Word中插入一个图形当作OLE技术的全部,各类资料在介绍新OLE技术时命名也不统一,造成很大的混乱。针对这些情况,微软在1996年重新制订了一个关于OLE的规 范——OLE 96。这个规范扩展了 OLE控件的能力,并贯彻微软的Internet战略使它更易于在网络环境中使用,还考虑命名混淆的问题,重新给OLE控件贴上一个标签——ActiveX控件。不仅如此,以前的什么OLE文档也相应称为ActiveX 文档了。总之,为了满足Internet战略,微软把OLE换成了ActiveX,企图使人们重新看待新的OLE——ActiveX,把它看成网络上的解决软件组件问题的标准。 (4)OLE/ActiveX与COM/DCOM比较 OLE/ActiveX名称比COM/DCOM更为我们熟悉,其实OLE和ActiveX是商业名称,它们的发展过程为OLE→ActiveX(网络OLE)。COM和DCOM是纯技术名词,它们是OLE/ActiveX基础,其发展过程为COM→DCOM(网络COM),其中COM(Component Object Model,组件对象模式)是在OLE2.0中建立的规范。OLE/ActiveX不仅可以实现进程之间的通信,而且可以创建进程,它们是“类厂”组件对象。 3.2.2 应用程序与进程 应用程序和进程在概念上是有一定区别的,前者是静态的程序代码,而后者是动态的实体。只有应用程序加载到系统中后才能成为一个进程。Windows进程分为独立进程和共享进程两种。一般情况下,人们把独立运行的程序称为进程,其实这只是独立进程。在实际情况下也常常遇到另一种情况,即一个应用程序可能启动多个进程,一个进程空间可以运行多个程序,这就是共享进程。例如,同一个应用程序重复运行就启动了多个进程;而在一个进程中调用其他程序,或者通过程序挂钩,这就使同一个进程空间里运行了多个程序。这里着重讨论独立进程之间的通信。 3.2.3 进程之间通信的类型 根据不同的标准,进程之间通信类型有不同的划分方法。实际中也有多种划分方法,这里只给出几种划分方法如下所述。 (1)低级通信和高级通信 · 低级通信:只能传递状态和整数值(控制信息),包括进程互斥和同步所采用的信号量和管程机制。其第一个缺点为传送信息量小、效率低,每次通信传递的信息量固定,若传递较多信息则需要进行多次通信;第二个缺点是编程复杂,直接实现通信的细节,容易出错。 · 高级通信:能够传送任意数量的数据,包括共享存储区、管道、消息等。 (2)直接通信和间接通信 l 直接通信:信息直接传递给接收方,如管道,在发送时,指定接收方的地址或标识,也可以指定多个接收方或广播式地址;在接收时,允许接收来自任意发送方的消息,并在读出消息的同时获取发送方的地址。 l 间接通信:借助于收发双方进程之外的共享数据结构作为通信中转,如剪贴板。通常接收和发送方的数目可以是任意的。 (3)本地通信和远程通信 l 本地通信方式:这种通信又称之为同机通信,它是在同一台计算机上的程序之间进行的,也就是说客户进程和服务进程位于同一台计算机上。 l 远程通信方式:这种通信又称之为网间的进程通信,要解决的是不同主机进程间的相互通信问题(可把同机进程通信看作是其中的特例)。在这种通信中,首先要解决的是网络间的进程标识问题。同一主机上,不同进程可用进程号(process ID)唯一标识。但在网络环境下,各主机独立分配的进程号不能唯一标识该进程。例如,主机A赋予某个进程号5,在B机中也可以存在5号进程,因此,“5号进程”这句话就没有意义了。其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同。因此,网间的进程通信还要解决多重协议的识别问题。 3.3 使用自定义消息通信 Windows程序与其他类型程序的区别就是使用消息,例如键盘或鼠标消息等,在DOS系统下的程序没有定义消息。在Windows操作系统中,消息不但可以用于进程内的通信,也可以用于进程间的通信。这里重点介绍进程间的消息通信。 3.3.1 通过自定义消息实现进程间通信的方法 消息分为两种,即系统消息和用户(程序设计者)自定义消息。系统消息定义从0到0x3FF,可以使用0x400到0x7FFF定义自己的消息。Windows把0x400定义为WM_USER。如果想定义自己的一个消息,可以在WM_USER上加上一个值。 还有一种自定义窗口消息的方法是用RegisterWindowsMessage()函数来注册这个消息。与在WM_USER上加上某个数相比,它的好处是不必考虑所表示的消息标识符是否超出工程的允许范围。 要想用消息实现进程间通信,则需要在这两个程序中定义或注册相同的消息,才能保证数据通信顺利进行。用户自定义消息的方法如下: l #define WM_COMM WM_USER+100 const UINT wm_nRegMsg=RegisterWindowMessage("reg_data"); 有了这两种定义的消息后,可以用如下的方法来发送消息。 pWnd->SendMessage(WM_COMM,NULL,(LPARAM)uMsg); pWnd->SendMessage(wm_nRegMsg,NULL,(LPARAM)uMsg); 其中pWnd为接收这个消息的窗口句柄,uMsg为要通过消息发送的数据,是长整型。这两个消息的发送可以分别在一个发送函数里实现,其具体的做法见以后的实例。通过实验可知,这两种自定义消息的发送效果是一样的。 在接收消息的程序中,除与发送消息的程序需要定义相同的消息外,还需要定义相应的消息映射和消息映射函数,消息的映射方法如下: ON_MESSAGE(WM_COMM,OnUserReceiveMsg) ON_REGISTERED_MESSAGE(wm_nRegMsg,OnRegReceiveMsg) 与以上消息映射对应的函数定义如下: void CDataRecvDlg::OnUserReceiveMsg(WPARAM wParam,LPARAM lParam) { // 增加用户自定义程序代码 … } //-------------------------------------------------------------------- void CDataRecvDlg::OnRegReceiveMsg(WPARAM wParam,LPARAM lParam) { // 增加用户自定义程序代码 … } 其中OnUserReceiveMsg()函数为WM_COMM消息的映射函数,OnRegReceiveMsg()函数为wm_nRegMsg消息的映射函数。可以看出,这两种消息的映射函数形式是一样的。 3.3.2 通过自定义消息实现进程间通信的实例 为说明以自定义消息实现进程之间的通信,作者用VC++ 编写了这样的程序。有两个对话框程序,其中一个为发送程序,另一个为接收程序。在这两个程序中分别定义了两个消息WM_COMM和wm_nRegMsg,在CDataSendDlg类中增加了用于发送数据的两个函数,即void CDataSendDlg::OnSendUsermsg()和void CDataSendDlg::OnSendRegmsg()。它们的源代码如下: void CDataSendDlg::OnSendUsermsg() { UpdateData(); // 更新数据 CWnd *pWnd=CWnd::FindWindow(NULL,_T("DataRecv")); // 查找DataRecv进程 if(pWnd==NULL){ AfxMessageBox(TEXT("Unable to find DataRecv.")); return; } UINT uMsg; uMsg=atoi(m_strUserMsg); pWnd->SendMessage(WM_COMM,NULL,(LPARAM)uMsg); // 发送. } //-------------------------------------------------------------------- void CDataSendDlg::OnSendRegmsg() { UpdateData(); // 更新数据 CWnd *pWnd=CWnd::FindWindow(NULL,_T("DataRecv")); // 查找DataRecv进程 if(pWnd==NULL){ AfxMessageBox("Unable to find DataRecv."); return; } UINT uMsg; uMsg=atoi(m_strRegMsg); pWnd->SendMessage(wm_nRegMsg,NULL,(LPARAM)uMsg); // 发送 } 在接收数据的程序中要做三件事:①定义自定义消息;②定义消息映射表;③定义消息映射函数。自定义消息的方法如前面所述。在CDataRecvDlg类中增加了两个用于接收数据的函数,即void CDataRecvDlg::OnUserReceiveMsg()和void CDataRecvDlg:: OnRegReceiveMsg()。消息映射表如下: BEGIN_MESSAGE_MAP(CDataRecvDlg, CDialog) //{{AFX_MSG_MAP(CDataRecvDlg) ON_MESSAGE(WM_COMM,OnUserReceiveMsg) ON_REGISTERED_MESSAGE(wm_nRegMsg,OnRegReceiveMsg) //}}AFX_MSG_MAP END_MESSAGE_MAP() 消息映射表中的映射函数名的格式是一样的。其实它们可以用同一个函数,为了说明方便,这里把它们的名字取为不一样。接收数据程序中的消息映射函数的源代码如下: void CDataRecvDlg::OnUserReceiveMsg(WPARAM wParam,LPARAM lParam) { m_strUserMsg.Format("%d\n",int(lParam)); UpdateData(FALSE); // 更新数据 } //-------------------------------------------------------------------- void CDataRecvDlg::OnRegReceiveMsg(WPARAM wParam,LPARAM lParam) { m_strRegMsg.Format("%d\n",int(lParam)); UpdateData(FALSE); // 更新数据 } 从上面的实例中可以看出,以自定义消息来进行进程之间的通信存在一定的局限性,即所发送的数据只能是长整型,而对于字符串,则不能进行通信。要进行字符串或大批量的数据的传输,则需要采用其他的通信方法。 3.4 使用WM_COPYDATA消息通信 对于少量数据可以用WM_COPYDATA方便地实现通信。由于SendMessage()是阻塞的,只有接收方响应了消息,SendMessage()才能返回,否则一直阻塞。所以,对于大量数据来说,用SendMessage()就容易造成窗口假死。 3.4.1 通过WM_COPYDATA消息实现进程间通信的方法 在Win32中,WM_COPYDATA消息主要目的是允许在进程间传递只读数据。SDK文档推荐用户使用SendMessage()函数,接收方在数据复制完成前不返回,这样发送方就不可能删除和修改数据。这个函数的原型如下: SendMessage(WM_COPYDATA,wParam,lParam) 其中wParam设置为包含数据的窗口句柄,lParam指向一个COPYDATASTRUCT的结构,其定义为: typedef struct tagCOPYDATASTRUCT{ DWORD dwData; DWORD cbData; PVOID lpData; }COPYDATASTRUCT; 其中dwData为自定义数据, cbData为数据大小, lpData为指向数据的指针。需要注意的是,WM_COPYDATA消息保证发送的数据从原进程复制到目标进程。但是,WM_COPYDATA消息不能发送HDC、HBITMAP之类的东西,它们对于目标进程来说是无效的。目标进程得到这些数据不能在原进程作任何事情,因为它们属于不同的进程。 与其他进程通信方法一样,要实现进程间的数据通信,在发送数据的程序中,首先要找到接收数据进程的窗口句柄pWnd,可以用CWnd::FindWindow(NULL,_ T("DataRecv"))函数来得到,其中字符串"DataRecv"为接收数据的程序名。然后用SendMessage()函数发送数据,其具体的做法见后面的实例。 在接收数据的程序中,首先在消息映射表中增加WM_COPYDATA消息映射,然后定义消息映射函数,其函数的格式为: BOOL CDataRecvDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct) { // 增加用户自定义程序代码 … } 3.4.2 通过WM_COPYDATA消息实现进程间通信的实例 与前面所说的自定义消息不一样,WM_COPYDATA消息是Win32提供的消息。与自定义消息相比较,WM_COPYDATA消息可以传递一个较大的数据块。这里仍然用两个对话框程序来实现WM_COPYDATA消息的通信。 以下分别给出发送数据程序的发送函数和接收数据程序的接收函数。在发送数据的对话框类CDataSendDlg中,用MFC ClassWizard工具或者手工的方法增加函数void CDataSendDlg::OnSendCopydata(),其具体代码如下: void CDataSendDlg::OnSendCopydata() { UpdateData(); // 更新数据 CWnd *pWnd=CWnd::FindWindow(NULL,_T("DataRecv")); // 查找DataRecv进程 if(pWnd==NULL){ AfxMessageBox("Unable to find DataRecv."); return; } COPYDATASTRUCT cpd; // 给COPYDATASTRUCT结构赋值 cpd.dwData = 0; cpd.cbData = m_strCopyData.GetLength(); cpd.lpData = (void*)m_strCopyData.GetBuffer(cpd.cbData); pWnd->SendMessage(WM_COPYDATA,NULL,(LPARAM)&cpd); // 发送 } 在用MFC AppWizard(exe)创建接收数据的对话框程序后,生成对话框类CDataRecvDlg。在这个类中,首先要定义接收WM_COPYDATA消息的映射,可以用ClassWizard工具来增加,也可以手动增加,但手动增加需要修改三个地方:①在消息映射表中增加ON_WM_COPYDATA();②增加成员函数BOOL CDataRecvDlg::OnCopyData();③在CDataRecvDlg类中增加WM_COPYDATA消息映射函数的定义。 WM_COPYDATA消息的映射如下: BEGIN_MESSAGE_MAP(CDataRecvDlg, CDialog) //{{AFX_MSG_MAP(CDataRecvDlg) ON_WM_COPYDATA() //}}AFX_MSG_MAP END_MESSAGE_MAP() CDataRecvDlg::OnCopyData()函数的定义如下: BOOL CDataRecvDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct) { m_strCopyData=(LPSTR)pCopyDataStruct->lpData; // 获得实际长度的字符串 m_strCopyData=m_strCopyData.Left(pCopyDataStruct->cbData); // 更新数据 UpdateData(FALSE); return CDialog::OnCopyData(pWnd, pCopyDataStruct); } 其中m_strCopyData为接收到的字符串,pCopyDataStruct为COPYDATASTRUCT结构指针。注意由pCopyDataStruct直接得到的m_strCopyData字符串长度可能不是实际发送的字符串长度,需要用发送字符串时所给定的字符串长度来进一步确定,其长度由pCopyDataStruct ->cbData来得到。
VC 进程间通信二 2010-05-07 10:35
VC 进程间通信三 2010-05-07 10:36
3.8.3 使用DDE通信的实例 由上面的介绍可知,可以编写基于消息DDE应用程序,也可以编写应用DDEML的应用程序。对于前者,实现的方法较复杂,这里不做介绍。这里介绍一个应用DDEML编写的DDE通信实例。 为了便于管理,这里把这个程序封装成一个CMyDde类,下面介绍这个类。CMyDde类头文件如下: // DDE.h: 定义CMyDde类 // #ifndef _DDE_H_INCLUDED #define _DDE_H_INCLUDED #include <ddeml.h> class CMyDde { public: CMyDde(); ~CMyDde(); // 静态回调成员函数 static HDDEDATA CALLBACK DdeCallback(UINT iType,UINT iFmt, HCONV hConv,HSZ hsz1,HSZ hsz2, HDDEDATA hData,DWORD dwData1,DWORD data2); void DdeCall(UINT iType, LPCSTR szSvr,LPCSTR szTopic,LPCSTR szAtom); void DdeServer(CString strReply); void DdeClient(CString strRequest);
CString GetReply() { return m_strReply;} CString GetRequest() { return m_strRequest;} private: static CMyDde* fakeThis; DWORD idInst; CString AppName; CString m_strReply; CString m_strRequest; }; #endif 其中包含了ddeml.h头文件,DdeCallback()为static回调函数。之所以使用static,是因为DdeInitialize()函数的需要,否则编译会出错。 对于服务程序,使用类中的DdeServer()函数。在这个函数中用DdeInitialize()调用回调函数DdeCallback(),注册服务名MyDDEService,以便客户程序与服务程序取得联系。在DdeInitialize()中设置事务过滤,例如以下的DdeServer()函数中,在DdeInitialize()中设置CBF_FAIL_POKES,表示XTYP_ POKES事件将被过滤掉。DdeServer()函数的代码 如下: void CMyDde::DdeServer(CString strReply) { m_strReply=strReply; fakeThis=this; // 建立DDE DdeInitialize(&idInst,DdeCallback,APPCLASS_STANDARD| CBF_FAIL_ADVISES| CBF_FAIL_POKES| CBF_SKIP_REGISTRATIONS| CBF_SKIP_UNREGISTRATIONS,0L); // 注册服务名MyDDEService,使该程序作为DDE服务器 AppName="MyDDEService"; HSZ hszService=DdeCreateStringHandle(idInst,AppName,0); DdeNameService(idInst,hszService,NULL,DNS_REGISTER); } 回调函数(Callback function)大量用于Windows的系统服务,通过它,程序员可以安装设备驱动程序和消息过滤系统,以控制Windows的有效使用。以下是DDE服务程序的回调函数源代码: HDDEDATA CALLBACK CMyDde::DdeCallback(UINT iType, UINT iFmt,HCONV hConv, HSZ hsz1, // Topic. HSZ hsz2, // atom. HDDEDATA hData,DWORD dwData1,DWORD data2) { char szBuffer[100]; switch(iType) { // 建立交易连接 case XTYP_CONNECT: // 获得应用名 DdeQueryString(fakeThis->idInst,hsz2, szBuffer,sizeof(szBuffer),0);
// 如果此应用不能被此服务器支持,返回NULL if(strcmp(szBuffer,fakeThis->AppName)) return NULL; // 获得topic名 DdeQueryString(fakeThis->idInst,hsz1, szBuffer,sizeof(szBuffer),0);
// 如果连接成功,返回1 return (HDDEDATA)1;
case XTYP_REQUEST: // 获得topic名 DdeQueryString(fakeThis->idInst,hsz1, szBuffer,sizeof(szBuffer),0);
if(strcmp(szBuffer,"query")==0) { // 获得Item 名 DdeQueryString(fakeThis->idInst,hsz2, szBuffer,sizeof(szBuffer),0);
strcpy(szBuffer,fakeThis->m_strReply); return DdeCreateDataHandle(fakeThis->idInst, (LPBYTE)szBuffer,sizeof(szBuffer),0,hsz2,CF_TEXT,0); } break;
case XTYP_EXECUTE: // 获得topic名 DdeQueryString(fakeThis->idInst,hsz1, szBuffer,sizeof(szBuffer),0); if(strcmp(szBuffer,"data")==0) { // 获得数据 DdeGetData(hData, (LPBYTE)szBuffer, 40L, 0L); fakeThis->m_strRequest=szBuffer; return (HDDEDATA)1; } break; }
return NULL; } 其中只使用了三个选项,即XTYP_CONNECT、XTYP_REQUEST和XTYP_ EXECUTE,还有其他的一些选项,见微软的MSDN说明。XTYP_CONNECT响应于客户程序使用的DdeConnect()函数。XTYP_REQUEST和XTYP_EXECUTE分别响应于客户程序中使用DdeClientTransaction()函数的XTYP_REQUEST和XTYP_ EXECUTE选项。在服务程序中,对于XTYP_REQUEST选项,可以用DdeCreateDataHandle函数向客户程序发送数据,而XTYP_EXECUTE则不能。而对于XTYP_EXECUTE选项,可以用DdeGetData()函数从客户获取数据,而XTYP_REQUEST则不能。 在服务程序中用DdeQueryString()函数从客户程序中获得Topics名和Items名,先得到Topics名,然后得到Items名。在本实例中XTYP_REQUEST选项的Topics名是“query”,Items名为“1”,而XTYP_EXECUTE选项的Topics名是“data”,Items名为“1”,但Items名都没有被利用。 以下是用于客户程序的主函数,也需要用DdeInitialize()函数初始化,并设置过滤类型。其中使用了类型调用函数DdeCall()。DdeClient()函数的代码如下: void CMyDde::DdeClient(CString strRequest) { m_strRequest=strRequest; idInst=0; DdeInitialize(&idInst,NULL,APPCLASS_STANDARD| CBF_FAIL_ADVISES| CBF_FAIL_POKES| CBF_SKIP_REGISTRATIONS| CBF_SKIP_UNREGISTRATIONS,0L); DdeCall(XTYP_EXECUTE,TEXT("MyDDEService"),TEXT("data"),TEXT("1")); DdeCall(XTYP_REQUEST,TEXT("MyDDEService"),TEXT("query"),TEXT("1")); } 在类型调用的DdeCall()函数中,首先获得Service名、Topics名和Items名的字符串句柄,然后用DdeConnect()函数与服务程序连接。如果连接成功,就可以用DdeClientTransaction() 函数和用XTYP_REQUEST和XTYP_EXECUTE类型向服务程序发送数据。其中,对于XTYP_REQUEST,可以用DdeGetData()函数从服务程序获得数据。最后用DdeDisconnect()函数断开与服务程序的连接,并且用DdeFreeStringHandle()函数释放Service名、Topics名和Items名的字符串句柄。DdeCall()函数的源代码如下: void CMyDde::DdeCall(UINT iType,LPCSTR szSvr,LPCSTR szTopic,LPCSTR szItem) { HSZ hszServName = DdeCreateStringHandle(idInst,szSvr,CP_WINANSI); HSZ hszTopic = DdeCreateStringHandle(idInst,szTopic,CP_WINANSI); HSZ hszItem = DdeCreateStringHandle(idInst,szItem,CP_WINANSI); HCONV hConv= DdeConnect(idInst,hszServName,hszTopic,NULL);
HDDEDATA hData; DWORD dwResult; char szBuffer[100]; DWORD dwLength;
switch(iType) { case XTYP_REQUEST:
// 向服务器发送请求 hData = DdeClientTransaction(NULL,0,hConv, hszItem, CF_TEXT, iType, 5000, &dwResult);
// 从服务器取得返回值 dwLength = DdeGetData(hData, (LPBYTE)szBuffer,sizeof(szBuffer), 0); if (dwLength > 0) m_strReply=szBuffer; break; case XTYP_EXECUTE: strcpy(szBuffer,m_strRequest);
// 向服务器发送执行命令 hData = DdeClientTransaction((LPBYTE)szBuffer, sizeof(szBuffer), hConv, hszItem, CF_TEXT, iType, 5000, &dwResult); break; }
DdeDisconnect(hConv); DdeFreeStringHandle(idInst,hszServName); DdeFreeStringHandle(idInst,hszTopic); DdeFreeStringHandle(idInst,hszItem); } 小结 本章首先介绍了一些有关进程间通信的背景知识,便于读者弄清一些名词和概念,对进程间的通信有更深入的了解。 本章介绍了简单的本地进程之间的通信技术,它们也是较为实用的通信技术。还有另外一些简单的通信技术没有介绍,例如,使用一个临时文件、使用Windows注册表等,这些技术较容易,可以自己研究。一些高级的进程间通信技术将在以后章节介绍。 还把自定义消息、WM_COPYDATA消息、内存读写函数、FileMapping和剪贴板通信技术用一个服务程序和一个客户程序实现,如图3.1和图3.2所示。其中的数据发送类型如这两张图所示,并且发送字符串的方法可以用来发送较大的数据。 |
VC 进程间通信
最新推荐文章于 2018-03-13 13:32:33 发布