網絡安全編程:通過消息實現進程間的通信
在很多軟件中需要多個進程協同工作,而不是單一的進程進行工作。那么多進程的協同工作就涉及進程間的通信。在Windows下,進程間的通信有多種實現的方法,比如管道、郵槽、剪貼板、內存共享……本文介紹通過消息實現進程間的通信。
通過消息進行進程間的通信,有一定的限制性。Windows下有窗口的應用程序是基于消息驅動進行工作的,那么沒有窗口的程序就不是基于消息驅動來進行工作的。對于非窗口的應用程序是無法通過消息進行進程間通信的。
通過消息實現進程間的通信在這里介紹兩種方法,一種是通過自定義消息進行進程間的通信,另一種是通過使用WM_COPYDATA消息進行進程間的通信。
01 通過自定義消息進行進程通信
消息分為兩種,一種是系統已經定義的消息,另一種是用戶自定義的消息。系統已經定義的消息是從0到0x3ff,用戶自定義的消息可以從0x400開始。系統中提供了一個宏WM_USER,在進行自定義消息時,在WM_USER的基礎上加一個值就可以了。下面來實現一個自定義消息完成進程間通信的程序例子。
1. 實現自定義消息的步驟
通過自定義消息進行進程間通信,只有帶有窗口的進程才能完成基于消息的進程間通信。既然是進程間通信,那么就需要至少編寫兩個程序,一個是接收消息的服務端,另一個是發送消息的客戶端,并且這兩個程序都需要有窗口。
先來介紹程序的功能,在發送消息的客戶端,通過自定義消息給接收消息的服務端發送兩個整型的數值。接收消息的服務端,將接收到的兩個數值進行簡單的加法運算。接收消息的服務端在VC下,使用MFC通過自定義消息來完成進程間的通信需要3個步驟,首先要定義一個消息,其次是添加自定義消息的消息映射,最后是添加消息映射對應的消息處理函數。
首先在服務端和客戶端定義一個消息,具體如下:
- #define WM_UMSG WM_USER + 1
然后是在接收消息的服務端添加消息映射,如下:
- BEGIN_MESSAGE_MAP(CUserWMDlg, CDialog)
- //{{AFX_MSG_MAP(CUserWMDlg)
- ON_WM_SYSCOMMAND()
- ON_WM_PAINT()
- ON_WM_QUERYDRAGICON()
- ON_MESSAGE(WM_UMSG, RevcMsg)
- //}}AFX_MSG_MAP
- END_MESSAGE_MAP()
在這個消息映射中,ON_MESSAGE(WM_UMSG, RevcMsg)是自定義消息的消息映射。
最后在接收消息的服務端添加自定義消息的消息響應函數。根據消息映射可以得知,消息響應函數的函數名為RevcMsg(),定義如下:
- VOID CUserWMDlg::RevcMsg(WPARAM wParam, LPARAM lParam)
- {
- // ….
- }
2. 完成自定義消息通信的代碼
來看兩個程序的窗口界面,如圖1和圖2所示。
圖1 自定義消息服務端(接收端)
圖2 自定義消息客戶端(發送端)
知道了兩個程序的作用以及窗口的界面,那么開始對它們分別進行編碼。首先來看自定義消息服務端的代碼,該部分的代碼比較簡單。消息響應函數代碼如下:
- VOID CUserWMDlg::RevcMsg(WPARAM wParam, LPARAM lParam)
- {
- int nNum1, nNum2, nSum;
- nNum1 = (int)wParam;
- nNum2 = (int)lParam;
- nSum = nNum1 + nNum2;
- CString str;
- str.Format("%d", nSum);
- SetDlgItemText(IDC_EDIT_REVCDATA, str);
- }
在消息響應的函數中有兩個參數,分別是WPARAM類型和LPARAM類型。這兩個參數可以接收兩個4字節的參數。這里代碼中接收了兩個整型數值,進行相加后顯示在了窗口上的編輯框中。
在發送消息端,也需要定義相同的消息類型。這里不再重復介紹,只要把響應的定義復制粘貼即可。主要看發送消息的函數,代碼如下:
- void CUserWMCDlg::OnBtnSend()
- {
- // 在此處添加處理程序代碼
- int nNum1, nNum2;
- nNum1 = GetDlgItemInt(IDC_EDIT_SENDDATA, FALSE, FALSE);
- nNum2 = GetDlgItemInt(IDC_EDIT_SENDDATA2, FALSE, FALSE);
- HWND hWnd = ::FindWindow(NULL, "自定義消息服務端");
- ::SendMessage(hWnd, WM_UMSG, (WPARAM)nNum1, (LPARAM)nNum2);
- }
通過SendMessage()函數完成了發送,同樣也非常簡單。在SendMessage()函數中,通過第3個參數和第4個參數將兩個整型值發送給了目標的窗口。
從自定義消息的例子中可以看出,自定義消息對于進程間的通信只能完成簡單的數值型的傳遞,對于類型復雜的數據的通信就無法完成了。那么,通過消息是否能完成字符串等數據的通信傳遞呢?答案是肯定的。接下來看使用WM_COPYDATA消息完成進程間通信的例子。
02 通過WM_COPYDATA消息進行進程通信
自定義消息傳遞的數據類型過于簡單,而通過WM_COPYDATA消息進行進程間的通信會更加靈活。但是由于SendMessage()函數在發送消息時的阻塞機制,在使用WM_COPYDATA時傳遞的消息也不宜過多。
1. WM_COPYDATA消息介紹
應用程序發送WM_COPYDATA消息可以將數據傳遞給其他應用程序。WM_COPYDATA消息需要使用SendMessage()函數進行發送,而不能使用PostMessage()消息。通過SendMessage()函數發送WM_COPYDATA消息的形式如下:
- SendMessage(
- (HWND) hWnd,
- WM_COPYDATA,
- (WPARAM) wParam,
- (LPARAM) lParam
- );
第1個參數hWnd是接收消息的目標窗口句柄;第2個參數是消息的類型,也就是當前正在介紹的消息WM_COPYDATA;第3個參數是發送消息的窗口句柄;第4個參數是一個COPYDATASTRUCT結構體的指針。
COPYDATASTRUCT結構體的定義如下:
- typedef struct tagCOPYDATASTRUCT {
- ULONG_PTR dwData;
- DWORD cbData;
- PVOID lpData;
- } COPYDATASTRUCT, *PCOPYDATASTRUCT;
其中,dwData是自定義的數據,cbData用來指定lpData指向的數據的大小,lpData是指向數據的指針。
在程序中,發送WM_COPYDATA消息方仍然會通過調用FindWindow()函數來查找目標窗口的句柄,而接收消息方需要響應對WM_COPYDATA消息的處理。WM_COPYDATA不是自定義消息,在編程時不必像自定義消息那樣需要自己定義消息和添加消息映射,這部分工作可以直接通過MFC輔助進行。
MFC添加WM_COPYDATA消息響應的方法如下:
首先在要響應WM_COPYDATA消息的窗口對應的類上單擊鼠標右鍵,在彈出的快捷菜單中選擇“Add Windows Message Handler”,如圖3所示。選擇該菜單項后會出現如圖4所示的添加消息響應函數對話框。
圖3 選擇“Add Windows Message Handler”
圖4 添加消息響應函數對話框
在“New Windows messages/events:”列中找到WM_COPYDATA消息,然后雙擊將它添加到“Existing message/event handlers:”列中。最后單擊“Add Handler”按鈕,MFC就自動生成了WM_COPYDATA的消息映射及消息響應函數。Windows其他常用的消息都可以通過該對話框輔助生成消息映射及消息響應函數。
2. WM_COPYDATA程序界面及介紹
程序同樣分為客戶端程序和服務端程序。首先來看程序運行的效果,如圖5所示。
圖5 WM_COPYDATA的服務端與客戶端界面
WM_COPYDATA的服務端會接收WM_COPYDATA消息,在接收到WM_COPYDATA消息進行處理后同樣會發送一個WM_COPYDATA消息給客戶端進行消息反饋。WM_COPYDATA的客戶端會通過FindWindow()函數來查找WM_COPYDATA的服務端,并發送WM_COPYDATA消息,同樣也會接收服務端發來的WM_COPYDATA消息并進行處理。
3. WM_COPYDATA客戶端程序的實現
我們來完成程序的編碼工作,首先來看WM_COPYDATA客戶端。客戶端的界面中有3個控件,分別是一個按鈕控件、一個編輯框控件和一個列表框控件(為列表框控件定義一個控件變量:CListBox m_ListRec;)。
WM_COPYDATA客戶端的代碼如下:
- void CCopyDataCDlg::OnBtnSend()
- {
- // 在此處添加處理程序代碼
- // 查找接收 WM_COPYDATA 消息的窗口句柄
- HWND hWnd = ::FindWindow(NULL, "COPYDATA 服務端");
- CString strText;
- GetDlgItemText(IDC_EDIT_SENDDATA, strText);
- // 設置 COPYDATASTRUCT 結構體
- COPYDATASTRUCT cds;
- cds.dwData = 0;
- cds.cbData = strText.GetLength() + 1;
- cds.lpData = strText.GetBuffer(cds.cbData);
- // m_hWnd 是 CWnd 類中的一個成員函數
- // 表示該窗口的句柄
- ::SendMessage(hWnd, WM_COPYDATA, (WPARAM)m_hWnd, (LPARAM)&cds);
- }
- BOOL CCopyDataCDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct)
- {
- // 在此處添加處理程序代碼或者調用默認方法
- // 處理服務端發來的 WM_COPYDATA 消息
- CString strText;
- strText.Format("服務端在[%s]接收到該消息", pCopyDataStruct->lpData);
- m_ListRec.AddString(strText);
- return CDialog::OnCopyData(pWnd, pCopyDataStruct);
- }
4. WM_COPYDATA服務端程序的實現
WM_COPYDATA 服務端有兩個控件,分別是一個列表框控件和一個按鈕控件。為列表框控件定義一個控件變量:CListBox m_ListData。
WM_COPYDATA 服務端的代碼如下:
- BOOL CCopyDataSDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct)
- {
- // 在此處添加處理程序代碼或者調用默認方法
- CString strText;
- // 通過發送消息的窗口句柄獲得窗口對應的進程號,即 PID
- DWORD dwPid = 0;
- ::GetWindowThreadProcessId(pWnd->m_hWnd, &dwPid);
- // 格式化字符串并添加至列表框中
- strText.Format("PID=[%d]的進程發來的消息為:%s",
- dwPid, pCopyDataStruct->lpData);
- m_ListData.AddString(strText);
- // 獲取本地時間
- SYSTEMTIME st;
- GetLocalTime(&st);
- CString strTime;
- strTime.Format("%02d:%02d:%02d", st.wHour, st.wMinute, st.wSecond);
- // 將本地時間發送給客戶端程序
- COPYDATASTRUCT cds;
- cds.dwData = 0;
- cds.cbData = strTime.GetLength() + 1;
- cds.lpData = strTime.GetBuffer(cds.cbData);
- // 注意 SendMessage()函數的第 3 個參數為 NULL
- ::SendMessage(pWnd->m_hWnd, WM_COPYDATA, NULL, (LPARAM)&cds);
- return CDialog::OnCopyData(pWnd, pCopyDataStruct);
- }
- void CCopyDataSDlg::OnBtnDelall()
- {
- // 在此處添加處理程序代碼
- // 清空列表框內容
- while ( m_ListData.GetCount() )
- {
- m_ListData.DeleteString(0);
- }
- }
在接收消息的服務端調用GetWindowThreadProcessId()通過發送消息的窗口得到了發送消息的進程PID號,并將接收消息的時間反饋給了發送消息的客戶端。
關于WM_COPYDATA的服務端和客戶端的代碼都有比較詳細的注釋,因此沒有過多解釋。這里需要強調一點,WM_COPYDATA消息需要兩個附加消息,也就是SendMessage()函數的wParam和lParam參數都需要使用。wParam參數表示發送消息的窗口句柄,但是該參數可以省略,還可以通過類型轉換傳遞其他數值型的數據。lParam參數是COPYDATASTRUCT結構體指針類型,不可以省略,否則接收WM_COPYDATA消息的服務端會無法響應。