进程间通信 - 命名管道实现
引子
好,到這里呢,就需要介紹實現進程間通信的第四種方式了,
也就是通過命名管道來實現,前面介紹的那三種方式呢,都是有缺陷或者說局限性太強,
而這里介紹的命名管道相對來說,在這方面就做得好很多了,
比如,剪貼板的話只能實現本機上進程之間的通信,
而郵槽的話雖然是可以實現跨網絡之間的進程的通信,
但麻煩的是郵槽的服務端只能接收數據,郵槽的客戶端只能發送數據,太悲劇了,
而對于匿名管道的話,其也只能實現本機上進程之間的通信,
你要是能夠實現本機進程間的通信也就算了,
關鍵是它還只用來實現本地的父子進程之間的通信,也太局限了吧?
而這里介紹的這個命名管道的話,就和他們有些不同了,在功能上也就顯得強大很多了,
至少其可以實現跨網絡之間的進程的通信,同時其客戶端既可以接收數據也可以發送數據,
服務端也是既可以接收數據,又可以發送數據的。
?????????????
?????????????
命名管道概述
命名管道是通過網絡來完成進程之間的通信的,命名管道依賴于底層網絡接口,
其中包括有 DNS 服務,TCP/IP 協議等等機制,但是其屏蔽了底層的網絡協議細節,
對于匿名管道而言,其只能實現在父進程和子進程之間進行通信,而對于命名管道而言,
其不僅可以在本地機器上實現兩個進程之間的通信,還可以跨越網絡實現兩個進程之間的通信。
命名管道使用了 Windows 安全機制,因而命名管道的服務端可以控制哪些客戶有權與其建立連接,
而哪些客戶端是不能夠與這個命名管道建立連接的。
利用命名管道機制實現不同機器上的進程之間相互進行通信時,
可以將命名管道作為一種網絡編程方案時,也就是看做是?Socket?就可以了,
它實際上是建立了一個客戶機/服務器通信體系,并在其中可靠的傳輸數據。
命名管道的通信是以連接的方式來進行的,
服務器創建一個命名管道對象,然后在此對象上等待連接請求,
一旦客戶連接過來,則兩者都可以通過命名管道讀或者寫數據。
????????????????
命名管道提供了兩種通信模式:字節模式和消息模式。
在字節模式下,數據以一個連續的字節流的形式在客戶機和服務器之間流動,
而在消息模式下,客戶機和服務器則通過一系列的不連續的數據單位,進行數據的收發,
每次在管道上發出一個消息后,它必須作為一個完整的消息讀入。
???????????????
????????????
命名管道使用流程
服務端:
服務端進程調用?CreateNamedPipe?函數來創建一個有名稱的命名管道,
在創建命名管道的時候必須指定一個本地的命名管道名稱(不然就不叫命名管道了),
Windows?允許同一個本地的命名管道名稱有多個命名管道實例,
所以,服務器進程在調用?CreateNamedPipe?函數時必須指定最大允許的實例數(0 -255),
如果?CreateNamedPipe?函數成功返回后,服務器進程得到一個指向一個命名管道實例的句柄,
然后,服務器進程就可以調用?ConnectNamedPipe?來等待客戶的連接請求,
這個?ConnectNamedPipe?既支持同步形式,又支持異步形式,
若服務器進程以同步形式調用?ConnectNamedPipe?函數,
(同步方式也就是如果沒有得到客戶端的連接請求,則會一直等到)
那么,當該函數返回時,客戶端與服務器之間的命名管道連接也就已經建立起來了。
在已經建立了連接的命名管道實例中,
服務端進程就會得到一個指向該管道實例的句柄,這個句柄稱之為服務端句柄。
同時,服務端進程可以調用?DisconnectNamedPipe?函數,
將一個管道實例與當前建立連接的客戶端進程斷開,從而可以重新連接到新的客戶進程。
當然在服務端也是可以調用?CloseHandle?來關閉一個已經建立連接的命名管道實例。
客戶端
客戶端進程調用?CreateFile?函數連接到一個正在等待連接的命名管道上,
在這里客戶端需要指定將要連接的命名管道的名稱,
當?CreateFile?成功返回后,客戶進程就得到了一個指向已經建立連接的命名管道實例的句柄,
到這里,服務器進程的?ConnectNamedPipe?也就完成了其建立連接的任務。
客戶端進程除了調用?CreateFile?函數來建立管道連接以外,
還可以調用?WaitNamedPipe?函數來測試指定名稱的管道實例是否可用。
在已經建立了連接的命名管道實例中,客戶端進程就會得到一個指向該管道實例的句柄,
這個句柄稱之為客戶端句柄。
在客戶端可以調用?CloseHandle?來關閉一個已經建立連接的命名管道實例。
??????????
?????????????
服務端創建命名管道
HANDLE? WINAPI?? CreateNamedPipe( ?? __in??? LPCTSTR lpName, ? __in??? DWORD dwOpenMode, ? __in??? DWORD dwPipeMode, ? __in??? DWORD nMaxInstances, ?? __in??? DWORD nOutBufferSize, ? __in??? DWORD nInBufferSize, ?? __in??? DWORD nDefaultTimeOut, ? __in??? LPSECURITY_ATTRIBUTES lpSecurityAttributes ?????????????? );該函數用來創建一個命名管道的實例,并返回這個命名管道的句柄,
一個命名管道的服務器進程使用該函數創建命名管道的第一個實例,
并建立它的基本屬性,或者創建一個現有的命名管道的新實例。
如果需要創建一個命名管道的多個實例,就需要多次調用 CreateNamedPipe 函數。
參數?lpName?為一個字符串,其格式必須為?\\.\pipe\pipeName?,其中圓點?”.”?表示的是本地機器,
如果想要與遠程的服務器建立連接,那么這個圓點位置處應指定這個遠程服務器的名稱,
而其中的?“pipe”?這個是個固定的字符串,也就是說不能進行改變的,
最后的?“pipename”?則代表的是我將要創建的命名管道的名稱了。
參數?dwOpenMode?用來指定管道的訪問方式,重疊方式,寫直通方式,還有管道句柄的安全訪問方式。
同一個命名管道的每一個實例都必須具有相同的類型。
如果該參數設置為 0 ,則默認將使用字節類型方式,即通過這個參數可指定創建的是字節模式還是消息流模式。
對于管道句柄的讀取方式來說,同一個管道的不同實例可以指定不同的讀取方式。
如果該值為 0 ,則默認將使用字節讀方式。
而對于管道句柄的等待方式,則同一個管道的不同實例可以取不同的等待方式。
如果該值設置為 0 ,則默認為阻塞方式。
命名管道的訪問方式如下表所列:
| 值 | 解釋 |
| PIPE_ACCESS_DUPLEX | 雙向模式。 服務器進程和客戶端進程都可以從管道讀取數據和向管道中寫入數據。 |
| PIPE_ACCESS_INBOUND | 服務器端就只能讀取數據,而客戶端就只能向管道中寫入數據。 |
| PIPE_ACCESS_OUTBOUND | 服務器端就只能寫入數據,而客戶端就只能從管道中讀取數據。 |
命名管道的寫直通方式和重疊方式:
| 值 | 解釋 |
| FILE_FLAG_WRITE_THROUGH | 寫直通方式(可以簡單的看做是同步操作)。 該方式只影響對字節類型管道的寫入操作。 且只有當客戶端與服務端進程位于不同的計算機上時才有效。 該方式只有等到欲寫入命名管道的數據通過網絡傳送出去, 并且放置到了遠程計算機的管道緩沖區以后, 寫數據的函數才會成功返回。 |
| FILE_FLAG_OVERLAPPED | 重疊模式(可以簡單的看做是異步操作)。 實現前臺線程執行其他操作,而耗時操作可在后臺進行。 |
命名管道的安全訪問方式:
| 值 | 解釋 |
| WRITE_DAC | 調用者對命名管道的任意訪問控制列表都可以進行寫入。 |
| WRITE_OWNER | 調用者對命名管道的所有者可以進行寫入訪問。 |
| ACCESS_SYSTEM_SECURITY | 調用者對命名管道的安全訪問控制列表都可以寫入。 |
參數?dwPipeMode?用來指定管道句柄的類型,讀取和等待方式。
命名管道句柄的類型:
| 值 | 解釋 |
| PIPE_TYPE_BYTE | 數據以字節流的形式寫入管道。 該方式不能在?PIPE_READMODE_MESSAGE?讀方式下使用。 |
| PIPE_TYPE_MESSAGE | 數據以消息流的形式寫入管道。 |
命名管道句柄的讀取方式:
| 值 | 解釋 |
| PIPE_READMODE_BYTE | 以字節流的方式從管道中發讀取數據。 |
| PIPE_READMODE_MESSAGE | 以消息流的方式從管道讀取數據。 該方式只有在?PIPE_TYPE_MESSAGE?類型下才可以使用。 |
命名管道句柄的等待方式:
| 值 | 解釋 |
| PIPE_WAIT | 允許阻塞方式也就是同步方式。 ReadFile,WriteFile,ConnectNamedPipe?函數, 必須等到讀取到數據或寫入新數據或有一個客戶連接來才能返回。 |
| PIPE_NOWAIT | 允許非阻塞方式也就是異步方式。 ReadFile,WriteFile,ConnectNamedPipe?函數總是立即返回。 |
參數?nMaxInstance?指定命名管道能夠創建的實例的最大數目。
該參數的取值可以從?0 –?255 ,這里說的最大實例數目是指對同一個命名管道最多能創建的實例數目,
如果希望同時連接 5 個客戶端,那么則必須調用 5 次?CreateNamedPipe?函數創建 5 個命名管道實例,
然后才能同時接收到 5 個客戶端連接請求的到來,
對于同一個命名管道的實例來說,在某一個時刻,它只能和一個客戶端進行通信。
參數?nOutBufferSize?用來指定將要為輸出緩沖區所保留的字節數。
參數?nInBufferSize?用來指定將要為輸入緩沖區所保留的字節數。
參數?nDefaultTimeOut?用來指定默認的超時值,以毫秒為單位,同一個管道的不同實例必須指定同樣的超時值。
參數?lpSecurityAttributes?用來設置該命名管道的安全性,
一般設置為?NULL ,也就是采用?Windows?提供的默認安全性。
?????????
???????????
服務端等待客戶端連接請求
BOOL WINAPI ConnectNamedPipe( ????????? __in??? HANDLE hNamedPipe, ????????? __in??? LPOVERLAPPED lpOverlapped ????????? );該函數的作用是讓服務器等待客戶端的連接請求的到來。
參數??hNamedPipe?指向一個命名管道實例的服務器的句柄。
該句柄由?CreateNamedPipe?函數返回。
參數?lpOverlapped?指向一個?OVERLAPPED?結構的指針,
如果?hNamedPipe?所標識的命名管道是用?FILE_FLAG_OVERLAPPED ,
(也就是重疊模式或者說異步方式)標記打開的,則這個參數不能為?NULL?,
必須是一個有效的指向一個?OVERLAPPED?結構的指針,否則該函數可能會錯誤的執行。
??????????????
????????????????
客戶端連接命名管道
BOOL? WINAPI? WaitNamedPipe( ??????? __in??? LPCTSTR lpNamedPipeName, ???????? __in??? DWORD nTimeOut ????????? );客戶端在連接服務端程序創建的命名管道之前,
首先應該判斷一下,是否有可以利用的命名管道,
通過調用該函數可以用來實現這一點,該函數會一直等到,
直到等待的時間間隔已過,或者指定的命名管道的實例可以用來連接了,
也就是說該管道的服務器進程有正在等待被連接的的?ConnectNamedPipe?操作。
參數?lpNamedPipeName?用來指定命名管道的名稱,
這個名稱必須包括創建該命名管道的服務器進程所在的機器的名稱,
格式為:\\.\pipe\pipeName?,如果是在同一個機器上編寫的命名管道的服務器端程序和客戶端程序,
則當指定這個名稱時,在開始的兩個反斜杠后可以設置一個圓點來表示服務器進程在本地機器上運行,
如果是跨網絡通信,則在這個圓點位置處應該設置為服務器端所在的主機的名稱。
參數?nTimeOut?用來指定超時間隔。
| 值 | 解釋 |
| NMPWAIT_USE_DEFAULT_WAIT | 超時間隔即為服務器端創建該命名管道時指定的超時間隔。 |
| NMPWAIT_USE_DEFAULT_WAIT | 一直等待,直到出現一個可用的命名管道的實例。 |
???????????
???????????????
示例:命名管道實現進程間通信
服務端實現:(簡單 Console 程序)
項目結構:
NamedPipeServer.h
#ifndef NAMED_PIPE_SERVER_H #define NAMED_PIPE_SERVER_H ? #include <Windows.h> #include <iostream> ? using namespace std; ? //服務端用來保存創建的命名管道句柄 HANDLE hNamedPipe; ? const char * pStr = "Zachary"; const char * pPipeName = "\\\\.\\pipe\\ZacharyPipe"; ? //創建命名管道 void CreateNamedPipeInServer(); ? //從命名管道中讀取數據 void NamedPipeReadInServer(); ? //往命名管道中寫入數據 void NamedPipeWriteInServer(); ? #endif NamedPipeServer.cpp #include "NamedPipeServer.h" ? int main(int argc, char * argv) { CreateNamedPipeInServer(); ? //在服務端往管道中寫入數據 NamedPipeWriteInServer(); ? //接收客戶端發來的數據 NamedPipeReadInServer(); ? system("pause"); } ? ? void CreateNamedPipeInServer() { HANDLE hEvent; OVERLAPPED ovlpd; ? //首先需要創建命名管道 //這里創建的是雙向模式且使用重疊模式的命名管道 hNamedPipe = CreateNamedPipe(pPipeName, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, 0, 1, 1024, 1024, 0, NULL); ? if(INVALID_HANDLE_VALUE == hNamedPipe) { hNamedPipe = NULL; cout<<"創建命名管道失敗 ..."<<endl<<endl; return; } ? //添加事件以等待客戶端連接命名管道 //該事件為手動重置事件,且初始化狀態為無信號狀態 hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if(!hEvent) { cout<<"創建事件失敗 ..."<<endl<<endl; return; } ? memset(&ovlpd, 0, sizeof(OVERLAPPED)); ? //將手動重置事件傳遞給 ovlap 參數 ovlpd.hEvent = hEvent; ? //等待客戶端連接 if(!ConnectNamedPipe(hNamedPipe, &ovlpd)) { if(ERROR_IO_PENDING != GetLastError()) { CloseHandle(hNamedPipe); CloseHandle(hEvent); ? cout<<"等待客戶端連接失敗 ..."<<endl<<endl; return; } } ? //等待事件 hEvent 失敗 if(WAIT_FAILED == WaitForSingleObject(hEvent, INFINITE)) { CloseHandle(hNamedPipe); CloseHandle(hEvent); ? cout<<"等待對象失敗 ..."<<endl<<endl; return; } ? CloseHandle(hEvent); } ? ? void NamedPipeReadInServer() { char * pReadBuf; DWORD dwRead; ? pReadBuf = new char[strlen(pStr) + 1]; memset(pReadBuf, 0, strlen(pStr) + 1); ? //從命名管道中讀取數據 if(!ReadFile(hNamedPipe, pReadBuf, strlen(pStr), &dwRead, NULL)) { delete []pReadBuf; ? cout<<"讀取數據失敗 ..."<<endl<<endl; return; } cout<<"讀取數據成功: "<<pReadBuf<<endl<<endl; } ? ? void NamedPipeWriteInServer() { DWORD dwWrite; ? //向命名管道中寫入數據 if(!WriteFile(hNamedPipe, pStr, strlen(pStr), &dwWrite, NULL)) { cout<<"寫入數據失敗 ..."<<endl<<endl; return; } cout<<"寫入數據成功: "<<pStr<<endl<<endl; }客戶端實現:(簡單 Console 程序)
項目結構:
NamedPipeClient.h
#ifndef NAMED_PIPE_CLIENT_H #define NAMED_PIPE_CLIENT_H ? #include <Windows.h> #include <iostream> ? using namespace std; ? //用來保存在客戶端通過 CreateFile 打開的命名管道句柄 HANDLE hNamedPipe; ? const char * pStr = "Zachary"; const char * pPipeName = "\\\\.\\pipe\\ZacharyPipe"; ? //打開命名管道 void OpenNamedPipeInClient(); ? //客戶端從命名管道中讀取數據 void NamedPipeReadInClient(); ? //客戶端往命名管道中寫入數據 void NamedPipeWriteInClient(); ? #endif ??????????????NamedPipeClient.cpp
#include "NamedPipeClient.h" ? int main(int argc, char * argv) { OpenNamedPipeInClient(); ? //接收服務端發來的數據 NamedPipeReadInClient(); ? //往命名管道中寫入數據 NamedPipeWriteInClient(); ? system("pause"); } ? ? void OpenNamedPipeInClient() { //等待連接命名管道 if(!WaitNamedPipe(pPipeName, NMPWAIT_WAIT_FOREVER)) { cout<<"命名管道實例不存在 ..."<<endl<<endl; return; } ? //打開命名管道 hNamedPipe = CreateFile(pPipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if(INVALID_HANDLE_VALUE == hNamedPipe) { cout<<"打開命名管道失敗 ..."<<endl<<endl; return; } } ? ? void NamedPipeReadInClient() { char * pReadBuf; DWORD dwRead; ? pReadBuf = new char[strlen(pStr) + 1]; memset(pReadBuf, 0, strlen(pStr) + 1); ? //從命名管道中讀取數據 if(!ReadFile(hNamedPipe, pReadBuf, strlen(pStr), &dwRead, NULL)) { delete []pReadBuf; ? cout<<"讀取數據失敗 ..."<<endl<<endl; return; } cout<<"讀取數據成功: "<<pReadBuf<<endl<<endl; } ? ? void NamedPipeWriteInClient() { DWORD dwWrite; ? //向命名管道中寫入數據 if(!WriteFile(hNamedPipe, pStr, strlen(pStr), &dwWrite, NULL)) { cout<<"寫入數據失敗 ..."<<endl<<endl; return; } cout<<"寫入數據成功: "<<pStr<<endl<<endl; }效果展示:
首先啟動服務端進程(可以看到服務端進程正在等待客戶端進程來連接命名管道):
然后啟動客戶端進程,可以看到客戶端進程已經讀取到了來自服務端進程發送到命名管道中的數據,
同時客戶端進程也成功將數據寫入到了命名管道中,從而這些數據可以被服務端進程獲取到:
此時再來看服務端進程,可以發現服務端進程已經結束了等待,也就是已經成功和客戶端進程建立了連接,
同時,服務端進程也成功將數據寫入到了命名管道中,并且也成功獲取到了客戶端寫入到命名管道中的數據。
??????????????????????????
?????????????
結束語
對于命名管道來說的話,簡單理解的話,其實是可以將其看做是一種?Socket?的,
而對于命名管道也就是那幾個?API?在使用,對于一些不常用的?API?,
感興趣的也可以從?MSDN?中獲取到這部分信息。
對于進程間的通信的話,其實也就可以利用介紹的這四種方式來實現了,
第一種是利用剪貼板實現本機進程間的通信。
第二種是利用郵槽實現本機或跨網絡進程間的通信。
第三種是利用匿名管道實現本機父子進程之間的通信。
第四種是利用命名管道實現本機或跨網絡進程間的通信。
然后的話,我還打算介紹一種比較偏門的實現進程間通信的手段,
當然,這要到下一篇博文中才會作出介紹。
最后的話,就是在前面的一篇博文中有一位朋友說可以利用?WCF?來實現進程之間的通信,
這個呢理論上是可以實現的,但是本人也沒有做過這方面的?Demo?,
所以估計得看以后有時間的話,也可以拿過來寫寫文章的。
總結
以上是生活随笔為你收集整理的进程间通信 - 命名管道实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows 服务(附服务开发辅助工具
- 下一篇: 进程间通信 - 邮槽实现