VC++深入詳解(16):進程通訊

來源:互聯網
上載者:User

我們只介紹幾種簡單的處理序間通訊機制:剪下板、匿名管道、具名管道、匿名管道和油槽。
平時大家都用過剪下板,比如選中記事本上的一段文字,然後Ctrl+C複製到剪下板上,然後在word中按下Ctrl+V,將其複製。這其實完成了兩個進程之間的通訊:從記事本到word。實際上,剪下板是系統維護管理的一段記憶體地區,當在一個進程中複製資料時,是將資料複製到這個記憶體地區,而在另一個進程中粘貼資料時,是從這個記憶體地區中取出資料,然後顯示在視窗上。
下面我們寫一個樣本程式來實現剪下板的功能。
首先是程式的外觀:一個對話方塊程式,上面有兩個編輯框,分別用來輸入發送到剪下板的資料和顯示從剪下板接收的資料。然後再增加2個按鈕,用來控制發送和接收資料。

我們對發送資料按鈕進行響應:

void CCH_16_ClipboardDlg::OnBntSend() {// TODO: Add your control notification handler code here//開啟剪下板if(!OpenClipboard()){MessageBox("開啟剪下板失敗");return ;}//擷取剪下板控制權,釋放剪下板中之前的資料if(!EmptyClipboard()){MessageBox("進入剪下板失敗");return ;}//擷取編輯框中的文字CString str;GetDlgItemText(IDC_EDIT_SEND,str);//記憶體對象,從全域堆分配HGLOBAL  hClip;hClip = GlobalAlloc(GMEM_MOVEABLE,//sizeof(str)+1);//大小//擷取記憶體對象的指標char* pBuf;pBuf = (char*)GlobalLock(hClip);//完成資料的拷貝strcpy(pBuf,str);//減少引用計數,解鎖GlobalUnlock(hClip);//向剪貼簿中放入資料SetClipboardData(CF_TEXT,//文字格式設定 hClip);//記憶體對象//關閉剪下板,使其他進程可以擷取剪下板CloseClipboard();}

運行程式,在發送剪下板上輸入一些文字,然後可以開啟一個記事本程式,選擇“粘貼”,之前輸入的文字就粘貼到記事本上了。這就實現了我們的進程,用記事本進程的通訊。
總體上說,這段程式分為以下幾步:
1.開啟剪下板
2.進入剪下板
3.擷取編輯框中的資料
4.將資料放到剪下板中
5.關閉剪下板

但是在將資料放到剪下板中的SetClipboardData函數值得詳細討論一下:
HANDLE SetClipboardData(UINT uFormat, HANDLE hMem );
uFormat指明了放入的資料的格式,我們既可以選擇一個已經註冊過的自己的格式,也可以選擇標準的格式。這裡選擇的是標準的文字格式設定。
hMem是具有指定格式的控制代碼:如果這個參數為NULL,指示調用視窗直到對剪下板資料有請求時才提供指定剪下板格式的資料。如果視窗採用延遲交換技術,則該視窗必須處理WM_RENDERFORMAT和WM_RENDERALLFORMATS 訊息。
這個設定是為了提高效率:當我們把資料複製到剪下板上時,這些資料都要佔據記憶體空間。實際上,我們經常會複製了一堆資料,卻並沒有使用它們;然後又複製一段資料。這樣第一次複製的資料就浪費了。Windows提供了這樣一種機制:先提供一個空剪下板,當一個進程需要粘貼已複製的資料時,會發送
WM_RENDERFORMAT訊息,而在訊息響應函數中,完成之前就應該完成的複製操作。
我們這裡並沒有使用這個複雜的機制,只是在其中填入一個記憶體對象。這個記憶體對象必須由GlobalAlloc分配,且分配標記必須為GMEM_MOVEABLE。
GlobalAlloc從堆上分配指定數目的位元。GMEM_MOVEABLE表明這個記憶體對象在堆中是可以移動的。因為在必要的某些時候,Windows可以移動記憶體,從而更好的支援系統記憶體管理。正因為是可移動的,所以不能直接用指標訪問,比較麻煩,可使用函數 GlobalLock 將該控制代碼轉換為一個指標。此時拿到的,才是實體記憶體的地址。有了指標之後,我們使用strcyp將編輯框中資料拷貝到這個記憶體對象中。拷貝完成後,使用GlobalUnlock對其解鎖。

下面我們看看資料接收功能的實現:

void CCH_16_ClipboardDlg::OnBtnRecv() {// TODO: Add your control notification handler code hereif(!OpenClipboard()){MessageBox("無法開啟剪下板");return ;}//判斷剪下板中的資料是否是指定格式if(!IsClipboardFormatAvailable(CF_TEXT)){MessageBox("剪下板資料格式錯誤");return ;}//擷取剪下板記憶體對象HANDLE hClip;hClip = GetClipboardData(CF_TEXT);//擷取記憶體對象指標char* pBuf = (char*)GlobalLock(hClip);GlobalUnlock(hClip);//設定文字SetDlgItemText(IDC_EDIT_RECV,pBuf);//關閉剪下板,使其他進程可以擷取剪下板CloseClipboard();}

此時,在編輯框輸入輸入後,點擊發送,然後點擊接收,就能收到之前發送的資料了。
程式的流程大概是這樣:
1.開啟剪下板
2.判斷剪下板中的文字格式是否與我們預期的格式相符
3.如果相符,接收資料,儲存到記憶體對象中
4.擷取記憶體對象的指標
5.將資料顯示出來

這樣,就實現了從剪下板到進程的通訊。

下面我們看匿名管道。它是一種未命名的單向管道,用來在父進程和子進程之間傳輸資料。匿名管道只能實現本地機器上兩個進程間的通訊,不能實現跨網路通訊。
首先建立一個單文檔應用程式,給我們的視類增加兩個成員變數:

private:HANDLE hWrite;HANDLE hRead;

在建構函式中,將它們都設為NULL,在解構函式中,如果之前沒有釋放它們,則釋放他們:

CCH_17_ParentView::CCH_17_ParentView(){// TODO: add construction code herehRead = NULL;hWrite = NULL;}CCH_17_ParentView::~CCH_17_ParentView(){if(hRead){CloseHandle(hRead);}if(hWrite){CloseHandle(hWrite);}}

給菜單增加一個“匿名管道”菜單,增加3個功能表項目:IDM_PIPE_CREATE“建立匿名管道”、IDM_PIPE_READ“發送資料”、IDM_PIPE_WRITE“接收資料”。

void CCH_17_ParentView::OnPipeCreate() {// TODO: Add your command handler code here//定義安全屬性結構體SECURITY_ATTRIBUTES sa;//可以被繼承sa.bInheritHandle = TRUE;//預設安全描述sa.lpSecurityDescriptor = NULL;sa.nLength = sizeof(SECURITY_ATTRIBUTES);//建立匿名管道if(CreatePipe(&hRead,//讀控制代碼  &hWrite,//寫控制代碼  &sa,//安全屬性  0))//預設大小{MessageBox("建立匿名管道失敗");return ;}//新建立的進程的主視窗資訊STARTUPINFO sui;ZeroMemory(&sui,sizeof(STARTUPINFO));sui.cb = sizeof(STARTUPINFO);sui.dwFlags = STARTF_USESTDHANDLES;sui.hStdInput = hRead;sui.hStdOutput = hWrite;//擷取標準輸入控制代碼sui.hStdError = GetStdHandle(STD_ERROR_HANDLE);//新建立的進程和主線程資訊,由函數填寫PROCESS_INFORMATION pi;//建立子進程if(!CreateProcess("..\\CH_17_Child\\Debug\\CH_17_Child.exe",//子進程名 NULL,//命令列參數為空白 NULL,//新建立的進程使用預設安全層級 NULL,//新建立的主線程使用預設安全層級 TRUE,//子進程可以繼承父進程的控制代碼 0,//無特殊建立標記 NULL,//新進程使用調用進程的環境塊 NULL,//子進程和父進程具有相同的當前路徑 &sui,//啟動資訊 &pi))//新建立的進程和主線程資訊{CloseHandle(hWrite);hWrite = NULL;CloseHandle(hRead);hRead = NULL;MessageBox("建立子進程失敗");return ;}else{//關閉控制代碼CloseHandle(pi.hProcess);CloseHandle(pi.hThread);}}

程式其實不長,只是CreateProcess函數的參數很多。我們重點看一下最後兩個:
STARTUPINFO 類型的sui用來指定新進程的主視窗將如何顯示,它的成員較多:

typedef struct _STARTUPINFO {     DWORD   cb;     LPTSTR  lpReserved;     LPTSTR  lpDesktop;     LPTSTR  lpTitle;     DWORD   dwX;     DWORD   dwY;     DWORD   dwXSize;     DWORD   dwYSize;     DWORD   dwXCountChars;     DWORD   dwYCountChars;     DWORD   dwFillAttribute;     DWORD   dwFlags;     WORD    wShowWindow;     WORD    cbReserved2;     LPBYTE  lpReserved2;     HANDLE  hStdInput;     HANDLE  hStdOutput;     HANDLE  hStdError; } STARTUPINFO, *LPSTARTUPINFO; 

面對這種結構體,我們應該先看看其中有沒有特殊的成員。比如其中的dwFlags。這個成員決定了整個結構體中的哪些成員被使用。(由於不是全都有用,所以使用之前應該先對結構體清零。)我們這裡使用的是STARTF_USESTDHANDLES,則只用設定進程的標準輸入、輸出、錯誤控制代碼即可。將子進程的輸入輸出控制代碼設定為管道的讀寫控制代碼。
當子進程啟動時,他會繼承父進程的所有可繼承的已開啟的控制代碼,但是子進程並不知道哪個控制代碼用來讀管道,那個控制代碼用來寫管道,因為在建立子進程中並沒有指定。所以在這裡需要設定。而標準錯誤控制代碼,可以通過函數GetStdHandle獲得。雖然這個程式中並沒有使用這個控制代碼,但是我們必須將它填寫好。

PROCESS_INFORMATION類型的pi是供CreateProcess函數填寫新建立的進程和線程的資訊的結構體,它的成員如下:

typedef struct _PROCESS_INFORMATION {     HANDLE hProcess;     HANDLE hThread;     DWORD dwProcessId;     DWORD dwThreadId; } PROCESS_INFORMATION; 

前兩個是進程和線程的控制代碼,後兩個是進程和線程的全域識別碼。有了它,我們才可以在後面釋放控制代碼。

再看看讀寫匿名管道的程式:

void CCH_17_ParentView::OnPipeRead() {// TODO: Add your command handler code herechar buf[200];DWORD dwRead;if(!ReadFile(hRead,//讀控制代碼buf,//緩衝區100,//將要讀取的大小&dwRead,//實際讀取的大小NULL))//非重疊{MessageBox("讀取資料失敗");return ;}MessageBox(buf);}void CCH_17_ParentView::OnPipeWrite() {// TODO: Add your command handler code herechar buf[] = "父進程發送的資料";DWORD dwWrite;if(!WriteFile(hWrite,//控制代碼  buf,//緩衝區  strlen(buf)+1,//將要讀取餓大小  &dwWrite,//實際大小  NULL))//非重疊{MessageBox("寫入資料失敗");return ;}}

使用的是之前就是用過的ReadFile和WriteFile實現的,這裡就不再多說了。

下面看看子進程的編寫。首先將子進程放在與父進程平級的目錄下。同樣的,為其增加兩個成員變數用來標識管道的讀寫控制代碼,並在建構函式中初始化它們,在解構函式中釋放他們。設定為單文檔應用程式,為其增加一個菜單“匿名管道”,2個功能表項目IDM_PIPE_READ“讀取資料”,IDM_PIPE_WRITE“寫入資料”。
首先,需要擷取子進程的標準輸入和輸出控制代碼,我們可以在虛函數OnInitialUpdate中完成,這個函數是在視窗建立後第一個調用的函數。

void CCH_17_ChildView::OnInitialUpdate() {CView::OnInitialUpdate();// TODO: Add your specialized code here and/or call the base classhRead = GetStdHandle(STD_INPUT_HANDLE);hWrite = GetStdHandle(STD_OUTPUT_HANDLE);}

輸入輸出函數都比較簡單:

void CCH_17_ChildView::OnPipeWrite() {// TODO: Add your command handler code herechar buf[] = "匿名管道用戶端資料";DWORD dwWrite;if(!WriteFile(hWrite,buf,strlen(buf)+1,&dwWrite,NULL)){MessageBox("寫入資料失敗");return ;}}

void CCH_17_ChildView::OnInitialUpdate() {CView::OnInitialUpdate();// TODO: Add your specialized code here and/or call the base classhRead = GetStdHandle(STD_INPUT_HANDLE);hWrite = GetStdHandle(STD_OUTPUT_HANDLE);}

在啟動程式時,我們應該啟動父進程,然後利用父進程的“建立匿名管道”功能表項目啟動子進程。然後就能實現一個發送,另一個接收的功能了。

下面我們看具名管道。具名管道通過網路來完成進程間的通訊,但又屏蔽了網路通訊協定的細節,是得我們再不瞭解網路通訊協定的情況下也可以實現進程通訊。匿名管道可以完成父子進程間的通訊,具名管道不僅可以在本機上實現兩個進程間的通訊,還可以跨網路實現兩個進程間的通訊。
因為是跨網路的通訊,自然也離不開用戶端/伺服器通訊體系。具名管道的伺服器建立具名管道,而用戶端用來串連已存在的具名管道。具名管道採用了Windows的“具名管道檔案系統”介面,因此,客戶機和伺服器可利用標準的Win32檔案系統函數(ReadFile和WriteFile)進行資料的收發。

我們先編寫伺服器端程式。建立一個單文檔應用程式,增加一個菜單“具名管道”,增加3個功能表項目:
IDM_PIPE_CREATE“建立管道”,IDM_PIPE_READ“讀取資料”,IDM_PIPE_WRITE“寫入資料”。
先看建立管道的訊息響應函數:

void CCH_17_NamedPipeSrvView::OnPipeCreate() {// TODO: Add your command handler code herehPipe = CreateNamedPipe("\\\\.\\pipe\\MyPipe",//管道名稱PIPE_ACCESS_DUPLEX |//雙向管道FILE_FLAG_OVERLAPPED,//重疊模式0,//管道採用位元組類型1,//最多隻能建立一個執行個體1024,//輸出緩衝區的保留位元組數1024,//輸入緩衝區的保留位元組數0,//預設逾時值NULL);//預設安全屬性if(INVALID_HANDLE_VALUE == hPipe){MessageBox("建立具名管道失敗");hPipe = NULL;return ;}//建立匿名事件對象HANDLE hEvent;hEvent = CreateEvent(NULL,//預設安全屬性 TRUE,//人工重設 FALSE,//建立線程沒有獲得該對象 NULL);//沒有名字if(!hEvent){MessageBox("建立事件對象失敗");CloseHandle(hPipe);hPipe = NULL;return ;}//OVERLAPPED結構OVERLAPPED ovlap;ZeroMemory(&ovlap,sizeof(OVERLAPPED));ovlap.hEvent = hEvent;//等待用戶端串連if(!ConnectNamedPipe(hPipe,&ovlap)){//對於重疊模式,還需要判斷是等待處理還是真的失敗了if(ERROR_IO_PENDING != GetLastError()){MessageBox("等待用戶端串連失敗");CloseHandle(hPipe);CloseHandle(hEvent);hPipe = NULL;return ;}}//等待事件對象if(WAIT_FAILED == WaitForSingleObject(hEvent,INFINITE)){MessageBox("等待對象失敗");CloseHandle(hPipe);CloseHandle(hEvent);hPipe = NULL;return ;}}

總體上說,這個函數做了兩件事情:1.建立具名管道:CreateNamedPipe。2.等待用戶端請求的到來:ConnectNamedPipe。

在CreateNamedPipe中,管道名稱是有預設格式的:\\.\pipe\pipename 其中pipe使用大小寫無所謂,如在C語言中,如果想在雙引號內輸出1個\,就需要輸出兩個,所以斜杠比較多。這裡採用的雙向模式+重疊模式,其他的參數就不提了。
因為在CreateNamedPipe採用了重疊模式,所以在ConnectNamedPipe的第二個參數中,必須要填入一個OVERLAPPED的結構體地址,且結構體中必須包含一個手工重設的事件對象。所以我們的程式中,先建立了事件對象,然後定義了OVERLAPPED結構體,最後調用了ConnectNamedPipe函數。如果ConnectNamedPipe失敗,則返回0。但是,這裡需要判斷失敗的原因,看看是真的失敗了,還是只是暫時沒有處理,會在稍後處理。

讀寫函數相對比較簡單:

void CCH_17_NamedPipeSrvView::OnPipeRead() {// TODO: Add your command handler code herechar buf[100];DWORD dwRead;if(!ReadFile(hPipe,buf,100,&dwRead,NULL)){MessageBox("讀取資料失敗");return ;}MessageBox(buf);}void CCH_17_NamedPipeSrvView::OnPipeWrite() {// TODO: Add your command handler code herechar buf[] = "具名管道伺服器資料";DWORD dwWrite;if(!WriteFile(hPipe,buf,strlen(buf)+1,&dwWrite,NULL)){MessageBox("伺服器寫入資料失敗");return ;}}

我們再看用戶端程式。在工作區中增加一個單文檔引用程式。為其增加一個控制代碼hPipe。在建構函式中初始化為NULL,在解構函式中刪除它。給菜單增加3個功能表項目分別為“串連管道”IDM_PIPE_CONNECT、“讀取資料”IDM_PIPE_READ、“寫入資料”IDM_PIPE_WRITE。並增加訊息響應函數。

void CCH_17_NamedPipeCltView::OnPipeConnect() {// TODO: Add your command handler code hereif(!WaitNamedPipe("\\\\.\\pipe\\MyPipe",NMPWAIT_WAIT_FOREVER)){MessageBox("當前沒有可利用的具名管道");return ;}//開啟具名管道hPipe = CreateFile("\\\\.\\pipe\\MyPipe",//管道名稱GENERIC_READ |//可讀GENERIC_WRITE ,//可寫0,//不可共用NULL,//預設安全屬性OPEN_EXISTING,//開啟現有管道FILE_ATTRIBUTE_NORMAL,//普通檔案屬性NULL);if(INVALID_HANDLE_VALUE == hPipe){MessageBox("開啟具名管道失敗");hPipe = NULL;return ;}}void CCH_17_NamedPipeCltView::OnPipeRead() {// TODO: Add your command handler code herechar buf[100];DWORD dwRead;if(!ReadFile(hPipe,buf,100,&dwRead,NULL)){MessageBox("讀取資料失敗");return ;}MessageBox(buf);}void CCH_17_NamedPipeCltView::OnPipeWrite() {// TODO: Add your command handler code herechar buf[] = "具名管道用戶端資料";DWORD dwWrite;if(!WriteFile(hPipe,buf,strlen(buf)+1,&dwWrite,NULL)){MessageBox("伺服器寫入資料失敗");return ;}}

下面我們看最後一種處理序間通訊機制——郵槽。郵槽是基於廣播體系設計的,採用不需連線的不可靠的資料轉送。郵槽是一種單項通訊機制,建立郵槽的伺服器處理序讀取資料,開啟郵槽的客戶機進程寫入資料。為保證郵槽在各種Windows平台下都能正常工作,我們在傳輸訊息時,應將訊息限制在424個位元組一下。
因此,在伺服器端,我們可以增加一個功能表項目,對其響應:

void CCH_17_MailslotSrvView::OnMailslotRecv() {// TODO: Add your command handler code hereHANDLE hMaileslot;hMaileslot = CreateMailslot("\\\\.\\mailslot\\MyMailslot",//郵槽名0,//任意長度的訊息大小MAILSLOT_WAIT_FOREVER,//一直等待NULL);//預設安全屬性if(INVALID_HANDLE_VALUE == hMaileslot){MessageBox("建立油槽失敗");return ;}char buf[200];DWORD dwRead;if(!ReadFile(hMaileslot,buf,200,&dwRead,NULL)){MessageBox("讀取資料失敗");CloseHandle(hMaileslot);return ;}MessageBox(buf);CloseHandle(hMaileslot);}

注意,伺服器端只接收資料。
而在用戶端再加一個功能表項目,添加響應:

void CCH_17_MailslotCltView::OnMailslotSend() {// TODO: Add your command handler code hereHANDLE hMailslot;hMailslot = CreateFile("\\\\.\\mailslot\\MyMailslot",//郵槽名   GENERIC_WRITE,//寫資料   FILE_SHARE_WRITE,//寫共用   NULL,//預設安全屬性   OPEN_EXISTING,//開啟已存在的郵槽   FILE_ATTRIBUTE_NORMAL,//常規屬性   NULL);//if(INVALID_HANDLE_VALUE == hMailslot){MessageBox("開啟郵槽失敗");return ;}char buf[] = "用戶端發送資料";DWORD dwWrite;if(!WriteFile(hMailslot,buf,sizeof(buf),&dwWrite,NULL)){MessageBox("寫入資料失敗");CloseHandle(hMailslot);return ;}CloseHandle(hMailslot);}

啟動兩個進程,先點擊伺服器的接收資料,然後點擊用戶端的發送資料。伺服器就能接收到用戶端發送的資料了。如果想實現雙向通訊,那麼需要給伺服器增加發送資料的機制,而在用戶端增加接收資料的機制。

小結一下,這四種通訊方式:剪下板、匿名管道只能在本機使用;而具名管道和油槽都能在跨網路通訊;此外,油槽能實現一對多的通訊方式,但是資料量較小。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.