引子
好,到這裏呢,就需要介紹實現進程間通信的第四種方式了,
也就是通過命名管道來實現,前面介紹的那三種方式呢,都是有缺陷或者說侷限性太強,
而這裏介紹的命名管道相對來說,在這方面就做得好很多了,
比如,剪貼板的話只能實現本機上進程之間的通信,
而郵槽的話雖然是可以實現跨網絡之間的進程的通信,
但麻煩的是郵槽的服務端只能接收數據,郵槽的客戶端只能發送數據,太悲劇了,
而對於匿名管道的話,其也只能實現本機上進程之間的通信,
你要是能夠實現本機進程間的通信也就算了,
關鍵是它還只用來實現本地的父子進程之間的通信,也太侷限了吧?
而這裏介紹的這個命名管道的話,就和他們有些不同了,在功能上也就顯得強大很多了,
至少其可以實現跨網絡之間的進程的通信,同時其客戶端既可以接收數據也可以發送數據,
服務端也是既可以接收數據,又可以發送數據的。
命名管道概述
命名管道是通過網絡來完成進程之間的通信的,命名管道依賴於底層網絡接口,
其中包括有 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 ,
所以估計得看以後有時間的話,也可以拿過來寫寫文章的。
版權所有,歡迎轉載,但轉載請註明: 轉載自 Zachary.XiaoZhen - 夢想的天空