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