轉自:http://www.rosoo.net/a/201110/15096.html
在Windows下各個任務是以不同的進程來完成的,當一個進程啓動後,操作系統爲其分配了4GB的私有地址空間,由於位於同一個進程中的線程共享同一個 地址空間,所以線程間的通信很簡單,就像兩個人如果在同一個房間裏說話的話就比較容易,只要動動嘴皮子就OK了, 但是如果在兩個國家裏就比較麻煩,必須藉助於一些其他的手段,比如打電話等. 以下介紹四種進程通信方式,雖然是在windows下的環境但是在其他的操作系統裏也遵循着同樣的原理,不信的話可以把大學裏的操作系統教材拿出來看看, 它們分別是剪貼板、 匿名管道、命名管道和郵槽。
1. 剪貼板(clipboard)
其實這個東西我們每天操作電腦的時候都在接觸,我們經常實用ctrl+c和ctrl+v就是基於了剪貼板的方式來實現了兩個進程間的通信, 就拿我現在來說吧,我在寫這篇文章的時候是在notepad下寫的, 一會我要把這篇文章裏的所有文字都粘貼到csdn的網頁上, 這裏就是兩個進程,一個是notepad進程和一個IE進程進行通信, 它們要傳輸的數據格式是TEXT,當然你也可以把這些內容拷貝到word、Excel、PowerPoint甚至是另一個notepad上面(你要清楚再 啓動一個notepad,這個跟前一個notepad是兩個進程,雖然它們長得很像),這就說明剪貼板是所有程序都可以訪問的,如果你對多線程編程比較了
解的話, 你就會明白一個數據一旦要被很多線程訪問,如果這些線程中有一些需要求改這個數據,就要對這個數據加鎖來保證數據的正確性了,剪貼板也是一樣的,當我把這 段文字ctrl+c時,它就要先對系統中的剪貼板加鎖,然後把內容放進去,再釋放鎖,如果你明白了以上的一些道理,那麼請你繼續往下看,如果還沒太明白那 也請你繼續往下看, 也許你對文字的理解能力已經落後於對代碼的理解了.
BOOL OpenClipboard()
windows提供的一個API函數,作用是打開剪貼板,如果程序打開了剪貼板,則其他程序經不能修改剪貼板(道理上面講了),直到 CloseClipboard(), 在windows中所有帶有Open這個單詞的函數都會有一個與之對應的帶有Close這個單詞的函數, 而且你在open之後一定不要忘記close,你可以自己試試看,只調用OpenClipboard()而不去執行CloseClipboard()會有 什麼效果,至今我還沒有發現例外的情況,如果你發現了請你告訴我.
HANDLE SetClipboardData(UINT uFormat, HANDLE hMem)
它的作用是將hMem所“代表”的內存中的內容以uFormat的格式放到剪貼板上,詳細的參數說明去查MSDN吧,這裏你可能有一些疑問,hMem是個 句柄而內存是用指針來訪問的,你說的沒錯,所以我用了“代表”這個詞而沒有用“指向”,在windows裏很多資源都會有一個HANDLE以它來標識各個 資源一遍於操作系統的管理,內存也一樣,我們一般動態開闢(用new, malloc)的heap都不會被操作系統任意移動,因爲它是一個進程的私有空間,而如果你開闢全局Heap數據的話,操作系統很可能會移動它,如果這個 時候你已然使用指針的話,那麼操作系統一旦移動了一塊全局Heap數據就要修改到所有指向這塊內存的指針,這顯然不現實,而這個時候如果你已然使用你的指
針來管理那塊內存的話,那就出了大麻煩,因爲那塊內存已經被移走了,而如果使用句柄來標識這塊內存的話則會解決這個問題,因爲它只是一個標籤,並沒有實際 的物理意義,就像如果你使用一個人的家庭住址來標識這個人的話就會有麻煩,因爲一旦他搬走了,你就找錯人了, 但是以身份證號就OK了, 詳細的情況可以參考GlobalAlloc這個函數。
BOOL IsClipboardFormatAvailable(UINT uFormat)
這個函數的作用就是要檢查一下剪貼板中的數據是否是uFormat形式的,比如我現打開了mspaint(畫圖板)程序畫了幾筆,然後Ctrl+C,再打 開notepad程序Ctrl+V,你當然知道這不會成功,它就是使用了這個API函數在粘貼前判斷了一下剪貼板中的數據類型是否是我所需要的.
好了我們下面來寫兩個進程來實現它們的通信, 事先說明我寫的只是關鍵代碼並不能直接運行
發送方:
- void Send(char* pSnd)
- {
- if (OpenClipboard())
- {
- HANDLE hClip;
- char *pBuf = NULL;
- EmptyClipboard();
- hClip = GlobalAlloc(GMEM_MOVEABLE, strlen(pSnd) + 1);
- pBuf = (char *)GlobalLock(hClip);
-
- strcpy(pBuf, pSnd);
- GlobalUnloak(hClip);
- SetClipboardData(CF_TEXT, hClip);
- CloseClipboard();
- }
- }
在你的程序中加入以上這段話,它就把pSnd中的內容發到了剪貼板上,相當於你作了Ctrl+C, 不信你可以執行這段程序後,打開一個notepad然後手動Ctrl+v看看是不是很驚奇.
- void Receive()
- {
- if (OpenClipboard())
- {
- if (IsClipboardFormatAvailable(CF_TEXT))
- {
- HANDLE hClip;
- char *pBuf = NULL;
- hClip = GetClipboardData(CF_TEXT);
- pBuf = (char *)GlobalLock(hClip);
- GlobalUnlock(hClip);
- MessageBox(pBuf);
- }
-
- CloseClipboard();
- }
- }
上面這段程序就相當於你執行了Ctrl+V操作,它把剪貼板中的數據取了出來;
剪貼板是系統提供的,所有進程都可以訪問它,它就是一段全局內存區,操作系統中的每個進程就都會像線程訪問共享變量一樣的使用它,很簡單,但是問題很多, 正是因爲所有的進程都可以訪問它,所以如果你的兩個進程間的通信如果使用這種方式的話,第一,通信效率不高;第二,會影響到其他進程的執行, 如果我現在Ctrl+C了一段文字,再執行Ctrl+V的時候卻出現了一些亂七八糟的東西的話那就會很麻煩, 所以可以基於剪貼板來做一個簡單的病毒程序,如果你有興趣的話;
2. 匿名管道(Pipe)
現在大多數都是基於管道通信的,因爲每兩個進程都可以共享一個管道來進行單獨的對話,就象打電話單獨佔用一條線路一樣,而不必擔心像剪貼板一樣會有串音, 匿名管道是一種只能在本地機器上實現兩個進程間通信的管道,它只能用來實現一個父進程和一個子進程之間實現數據傳輸.其實它是非常有用的,我做過一個實際 的項目就是利用匿名管道,項目就是讓我寫一個Ping程序來監測網絡的通信狀況,並且要把統計結果和執行過程顯示在我們的軟件裏, windows有一個自帶的ping程序,而且有執行過程和統計,所以我沒必要再發明一個(重複發明就等於犯罪----程序員要牢記阿),
只是windows的那個Ping程序的執行結果都顯示在了CMD的界面上了,我需要把它提取出來顯示在我們的軟件界面上,於是我就利用了匿名管道實現了 這個程序, 當我們的軟件要啓動Ping任務時,我就先CreatePipe創建匿名管道,再CreateProcess啓動了windows下面的Ping程序(它 作爲我們軟件的子進程),當然要把管道的讀寫句柄一起傳給子進程,這樣我就可以輕鬆的把Ping的執行結果了寫入到我的Buffer裏了,是不是很 easy。
BOOL CreatePipe(PHANDLE hReadPipe, PHANDLE hWritePipe, LPSECURITY_ATTRIBUTES lpPipeAttributes, DWORD nSize)
這個API函數是有用來創建匿名管道的,它返回管道的讀寫句柄(hReadPipe,hWritePipe), 記住lpPipeAttributes不能爲NULL,因爲這意味着函數的返回句柄不能被子進程所繼承,你要知道匿名管道可是實現父子進程通信的阿,只有 當一個子進程從其父進程中繼承了匿名管道句柄後,這兩個進程纔可以通信,lpPipeAttributes不爲NULL還遠不 夠,LPSECURITY_ATTRIBUTES這個結構體的內容去查MSDN吧,我只告訴你其中的BOOL bInheritHandle這個成員變量要賦值爲TRUE,
這樣才真正實現了子進程可以從父進程中繼承匿名管道.
BOOL CreateProcess(...)
這個系統API函數是用來在你的進程中啓動一個子進程用的,它的參數實在太多了,你還是去查MSDN吧,別怪我太懶惰,我只說幾個關鍵的地方,不想說的太詳細.
下面我就在寫一個程序利用匿名管道來通信
父進程的實現:
- Class CParent
- {
- ....
- private:
- HANDLE m_hWrite;
- HANDLE m_hRead;
- }
- void CParent::onCreatePipe()
- {
- SECURITY_ATTRIBUTES sa;
- sa.bInheritHandle = TRUE;
- sa.lpSecurityDescriptor = NULL;
- sa.nLength = sizeof(SECURITY_ATTRIBUTES);
- if (!CreatePipe(&m_hRead, &m_hWrite, &sa, 0)
- {
- return;
- }
-
- STARTUPINFO sui;
- PROCESS_INFOMATION pi;
- ZeroMemory(&sui, sizeof(STARTUPINFO));
-
- sui.cb = sizeof(STARTUPINFO);
- sui.dwFlags = STARTF_USESTDHANDLES;
-
- sui.hStdInput = m_hRead;
- sui.hstdOutput = m_hWrite;
-
- sui.hStdError = GetStdHandle(STD_ERROR_HANDLE);
-
- if (!CreateProcess("Child.exe", NULL, NULL, NULL, TRUE, 0, NULL, NULL, &sui, &pi))
- {
- CloseHandle(m_hRead);
- CLoseHandle(m_hWrite);
- return;
- }
- else
- {
- CloseHandle(pi.hProcess);
- Closehandle(pi.hThread);
-
- }
-
- }
- void CPraent::OnPiepRead()
- {
- char buf[100];
- DWORD dwRead;
- if (!ReadFile(hRead, buf, 100, &dwRead, NULL))
- {
-
- return;
- }
- Messagebox(buf)
- }
-
- void CParent::onPipeWrite(char *pBuf)
- {
- ASSERT(pBuf != NULL);
- DWORD dwWrite;
- if (!WriteFile(hWrite, pBuf, strlen(pBuf) + 1, &dwWrite, NULL))
- {
- return;
- }
- }
子進程的實現:
- Class Child
- {
- ......
- private:
- HANDLE m_hRead;
- HANDLE m_hWrite;
- }
- void CChild :: CChild()
- {
- m_hRead = GetStdHandle(STD_INPUT_HANDLE);
- m_hWrite = GetStdhandle(STD_OUTPUT_HANDLE);
-
- }
- void CChild::OnReadPipe()
- void CChild::OnWritePipe()
匿名管道由於是匿名的方式所以它不能實現兩個同級的進程進行通信,因爲一個進程創建了一個管道後,另一個線程並不知道如何找到這個管道,所以它只能通過父 進程直接把管道讀寫柄直接傳遞給子進程的方式進行進程通信,至於爲什麼有了命名管道還要保留匿名管道的問題, 我想主要是因爲父子進程通信的方式已然被廣泛的採用,而這種方式無疑要比命名管道消耗的資源更少,效率更高,就像自己自己寫的進程調用了自己寫的一個函數 一樣。
3. 命名管道(Pipe)
命名管道不僅可以在本機上實現兩個進程間的通信,還可以跨網絡實現兩個進程間的通信,就像我現在正使用MSN跟我遠方的同學聊天一樣!其實如果你用過 Socket編寫網絡程序的話,你就會明白所謂的命名管道之間的通信就相當於把計算機低層網絡網絡通信部分給封裝了起來,使用戶使用起來不必瞭解那麼多網 絡通信的知識,總之一句話就是用起來簡單,其實我們在爲別人提供函數庫的時候都應該遵循這個規律,把低層煩瑣,複雜,抽象的都封裝起來,對高層提供統一的 接口.
在Windows2000/NT以後,都可以在創建管道時指定據有訪問權限的用戶使用管道,進一步保證了安全性,而如果你要是自己使用Socket實現這 個功能的話就太麻煩了,當然很多程序員已然會自己實現它,他們的理由很可能是因爲windows都不安全.命名管道實現進程間的通信也跟網絡通信一樣是 C/S結構的,服務器進程負責創建命名管道及接受客戶機的連接請求,就象socket中Server部分要實現bind、linstening和
accept一樣, 而客戶端只負責連接,對應於socket中的connect一樣.
命名管道提供了兩種基本通信模式:字節模式和消息模式,在字節模式下,數據以一個連續的字節流的形式在server於client之間流動,而消息模式 下,客戶機和服務器則通過一系列不連續的數據單位進行數據收發,每次管道上發出了一條消息後,它必須作爲一條完整的消息讀入,是不是很像TCP和UDP.
HANDLE CreateNamePipe(....)
創建命名管道的API, 我依然不想解釋它的具體參數含義,我只解釋它的第一個參數LPCTSTR lpName,它的字符串格式是"\\\\.\\pipe\\pipename"
爲什麼這麼多\, 其實一共就4個,可你看到有8個是因爲C/C++中字符串中如果包含一個'\'就必須"\\"才能表達它的意思,你還記得嗎?它的實際格式是"\\.
\pipe\pipename",它的'.'表示的是本機地址,如果是要與遠程服務器連接,就在這個'.'處指定服務器的名稱,接下來的pipe是固定的 不要改,pipename就是你要命名的管道名字.
BOOL ConnectNamedPipe(HANDLE hNamePipe, LPOVERLAPPED lpOverlapped)
初看這個函數的名字你一定認爲這個是客戶端用來連接服務器管道的,事物的表面總是欺騙我們,恰恰相反它是服務器用來等待遠程連接的,類似於socket中的listen.
BOOL WaitNamedPipe(LPCTSTR lpNamedPipeName, DWORD nTimeOut)
有了上面那個函數的教訓,如果我問題這個函數是作什麼的你一定不會立即回答,是的,它是在客戶端來判斷是否有可以利用的命名管道的,每個客戶端最開始都應該使用它判斷一些,就像socket中的connect要判斷一下server是否已經啓動了.
下面是服務器代碼:
- class CNamePipeServer
- {
- ...
- private:
- HANDLE m_hPipe;
- }
-
- void CNamePipeServer::NamePipeCreated()
- {
- m_hPipe = CreateNamedPipe("\\\\.\\pipe\\MyPipe",
-
- PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
- 0, 1, 1024, 1024, 0, NULL);
- if (INVALID_HANDLE_VALUE == m_hPipe)
- {
- return;
- }
- HANDLE hEvent;
- hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
- if (INVALID_HANDLE_VALUE == hEvent)
- {
- return;
- }
-
- OVERLAPPED ovlap;
- ZeroMemory(&ovlap, sizeof(OVERLAPPED));
- ovlap。hEvent = hEvent;
-
- if (!ConnectNamePipe(hPipe, &ovlap))
- {
-
- if (ERROR_IO_PENDING != GetLastError())
- {
- ....
- return;
- }
- }
-
- if (WAIT_FAILED == WaitForSingleObject(hEvent, INFINTE))
- {
- ...
- return;
- }
- CloseHandle(hEvent);
- }
- void CNamePipeServer::OnReadPipe()
- void CNamePipeServer::OnWritePipe()
命名管道讀寫的方式與匿名管道的相同, 不再冗述。
客戶端實現:
- clase CNamePipeClient
- {
- ...
- private:
- HANDLE m_hPipe;
- }
- void CNamePipeClient::OnPipeConnect()
- {
- if (!WaitNamedPipe("\\\\.\\pipe\\MyPipe", NMPWAIT_WAIT_FOREVER))
- {
- return;
- }
-
- m_hPipe = CreateFile("\\\\.\\pipe\\MyPipe", GENERIC_READ | GENERIC_WRITE, 0, NULL
- , OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
- if (INVALID_HANDLE_VALUE == m_hPipe)
- {
- reutrn;
- }
- }
- void CNamePipeClient::OnReadPipe()
- void CNamePipeClient::OnWritePipe()
同上.
命名管道我沒有在實際中使用過,所以對它的一些特點理解的並不是很透徹,不能爲大家提供更多的建議了.
4. 郵槽(Mailslot)
郵槽是基於廣播通信設計出來的,採用不可靠的數據傳輸,它是一種單向通信機制,創建郵槽的服務器進程讀取數據,打開郵槽的客戶端進程寫入數據,據說郵槽廣泛的應用於網絡會議系統.
服務器進程
- void MailslotRecv()
- {
- HANDLE hMailslot;
-
- hMailslot = Createmailslot("\\\\.\\mailsolt\\MyMailslot", 0, MAILSLOT_WAIT_FOREVER, NULL);
- if (INVALID_HANDLE_VALUE
-
- == hMailslot)
- {
- return;
- }
-
- char buf[100]
- DWORD dwRead;
-
- if (!ReadFile(hMailslot, buf, 100, &dwRead, NULL))
- {
- ...
- return;
- }
- MessageBox(buf);
- CloseHandle(hMailslot);
- }
客戶端進程:
- void MailslotSnd(char *pBuf)
- {
- ASERRT(pBuf != NULL);
- HANDLE hMailslot;
-
- hMailslot = CreateFile("\\\\.\\mailslot\\MyMailslot", ENERIC_READ | GENERIC_WRITE
- , 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
- if (INVALID_HANDLE_VALUE == hMailslot)
- {
- return;
- }
-
- DWORD dwWrite;
- if (!WriteFile(hMailslot, pBuf, strlen(pBuf) + 1, &dwWrite, NULL))
- {
- ....
- return;
- }
- CloseHandle(hMailslot);
- }
郵槽的使用是不是更簡單, 我同樣也沒有在實際的項目中使用過它,依然不作過多的評價.
|