程序間通訊詳解
引子
前面的一篇博文介紹了程序之間通訊的一種最為簡單的方式,
也就是在本地程序之間通過剪貼簿來實現程序間通訊,而剪貼簿自有其缺陷,
很顯然的是,剪貼簿只能在本地機器上實現,
無法實現本地程序與遠端伺服器上的程序之間的通訊,
那麼有沒有辦法實現本地程序和遠端程序的通訊呢?
辦法自然是有的,要是實在搞不出,
我拿Socket來實現本地程序和遠端程序的通訊來實現也是可以的,
但是你想啊,要用Socket來實現本地程序和遠端程序之間的通訊,
那不僅我要在本地程序中加一堆的Socket程式碼,
並且伺服器上的程序中也是需要加一堆的Socket程式碼的,
那不搞死人去,也太麻煩了吧,所以不行不行,得換一種方案。
下面就來介紹一種超級無敵簡單的方案,其可以用來實現本地程序與遠端程序之間的通訊,
那就是通過郵槽來實現。
郵槽定義
郵槽(Mailslot)也稱為郵件槽,其是 Windows 提供的一種用來實現程序間通訊的手段,
其提供的是基於不可靠的,並且是單向資料傳輸的服務。
郵件槽只支援單向資料傳輸,也就是伺服器只能接收資料,而客戶端只能傳送資料,
何為服務端?何為客戶端?
服務端就是建立郵槽的那一端,而客戶端就是已存在的郵件槽的那一端。
還有需要提及的一點是,客戶端在使用郵槽傳送資料的時候只有當資料的長度 < 425 位元組時,
才可以被廣播給多個伺服器,如果訊息的長度 > 425 位元組的話,那麼在這種情形下,
郵槽是不支援廣播通訊的。
郵槽的實現
首先是服務端呼叫CreateMailslot函式,這個函式會將建立郵件槽的請求傳遞給核心的系統服務,
也就是NtCreateMailslot函式,而NtCreateMailslotFile這個函式會到達底層的郵槽驅動程式,
也就是msfs.sys,然後一些建立郵槽的工作就交給郵槽驅動程式來完成了,對於底層驅動,這裡不作介紹,
而在高層,我們也就只需要呼叫CreateMailslot函式就可以實現建立郵槽了。
郵槽的建立
下面我們就來看看這個CreateMailslot函數了:
該函式利用指定的名稱來建立一個郵槽,然後返回所建立的郵槽的控制代碼。
HANDLE WINAPI CreateMailslot(
__in LPCTSTR lpName,
__in DWORD nMaxMessageSize,
__in DWORD lReadTimeout,
__in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
在這裡需要注意的是兩個斜槓後的那個“.”,在這裡使用圓點代表的是本地機器,
引數nMaxMessageSize用來指定可以被寫入到郵槽的單一訊息的最大尺寸,
為了可以傳送任意大小的訊息,需要將該引數設定為0。
引數lReadTimeOut指定讀取操作的超時時間間隔,以毫秒作為單位。
讀取操作在超時之前可以等待一個訊息被寫入到郵槽中,如果將這個值設定為0,那麼若沒有訊息可用的話,該函式將立即返回。
如果將該值設定為MAILSLOT_WAIT_FOREVER,則該函式會一直等待,直到有訊息可用。
引數lpSecurityAttributes一般設定為NULL即可,即採用Windows預設的針對於郵槽的安全性。
示例:郵槽實現程序間通訊
服務端實現:(簡單 MFC 程式)
專案結構:
訊息以及成員函式和成員變數的宣告:
// 實現
protected:
HICON m_hIcon;
// 生成的訊息對映函式
virtual BOOL OnInitDialog();
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
DECLARE_MESSAGE_MAP()
public:
afx_msg void OnBnClickedBtnExit();
afx_msg void OnBnClickedBtnRecv();
afx_msg void OnBnClickedBtnCreate();
//定義一個用來建立執行緒的成員函式
HANDLE CreateRecvThread(LPVOID lpParameter, DWORD threadFlag, LPDWORD lpThreadID);
//控制元件變數:用來接收使用者輸入的資料
CEdit m_RecvEdit;
//成員變數:用來儲存建立的郵件槽控制代碼
HANDLE m_hMailslot;
訊息對映表定義:
//用來定義郵槽傳送和接收的最大資料位元組數
const int maxDataLen = 424;
//用來接收由客戶端傳送過來的資料
char * pStrRecvData;
CMailSlotServerDlg::CMailSlotServerDlg(CWnd* pParent /*=NULL*/)
: CDialogEx(CMailSlotServerDlg::IDD, pParent)
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
m_hMailslot = NULL;
//給用來接收資料的指標變數分配記憶體並清為 0
pStrRecvData = new char[maxDataLen];
memset(pStrRecvData, 0, maxDataLen);
}
void CMailSlotServerDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT_MAILSLOT, m_RecvEdit);
}
BEGIN_MESSAGE_MAP(CMailSlotServerDlg, CDialogEx)
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(ID_BTN_EXIT, &CMailSlotServerDlg::OnBnClickedBtnExit)
ON_BN_CLICKED(ID_BTN_RECV, &CMailSlotServerDlg::OnBnClickedBtnRecv)
ON_BN_CLICKED(ID_BTN_CREATE, &CMailSlotServerDlg::OnBnClickedBtnCreate)
END_MESSAGE_MAP()
訊息處理函式:
//退出按鈕的訊息處理例程
void CMailSlotServerDlg::OnBnClickedBtnExit()
{
CDialogEx::OnOK();
}
//建立按鈕的訊息處理
void CMailSlotServerDlg::OnBnClickedBtnCreate()
{
//建立名為 ZacharyMailSlot 的郵槽
this->m_hMailslot = CreateMailslot(TEXT("\\\\.\\mailslot\\ZacharyMailSlot"), 0,
MAILSLOT_WAIT_FOREVER, NULL);
if(INVALID_HANDLE_VALUE == this->m_hMailslot)
{
MessageBox(TEXT("建立郵槽失敗 ..."), TEXT("提示"), MB_ICONERROR);
return;
}
}
//接收按鈕的訊息處理
void CMailSlotServerDlg::OnBnClickedBtnRecv()
{
CString cStrRecvData;
DWORD dwRead;
//建立接收資料的執行緒,將郵槽控制代碼傳遞給執行緒
CreateRecvThread((LPVOID)this->m_hMailslot, 0, NULL);
cStrRecvData = pStrRecvData;
this->m_RecvEdit.SetWindowText(cStrRecvData);
UpdateData(FALSE);
}
//執行緒處理函式
DWORD WINAPI RecvThreadProc(LPVOID lpPrameter)
{
HANDLE hRecvMailSlot;
DWORD dwRead;
hRecvMailSlot = (HANDLE)lpPrameter;
//利用傳進來的郵槽控制代碼接收收據,並將資料存放到 pStrRecvData 中
if(!ReadFile(hRecvMailSlot, pStrRecvData, maxDataLen, &dwRead, NULL))
{
return NULL;
}
//關閉郵槽
CloseHandle(hRecvMailSlot);
return NULL;
}
HANDLE CMailSlotServerDlg::CreateRecvThread(LPVOID lpParameter, DWORD threadFlag, LPDWORD lpThreadID)
{
//建立一個執行緒
return CreateThread(NULL, 0, RecvThreadProc, lpParameter, threadFlag, lpThreadID);
}
客戶端實現:(簡單 MFC 程式)
專案結構:
訊息以及成員函式和成員變數的宣告:
// 實現
protected:
HICON m_hIcon;
// 生成的訊息對映函式
virtual BOOL OnInitDialog();
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
DECLARE_MESSAGE_MAP()
public:
afx_msg void OnBnClickedBtnExit();
afx_msg void OnBnClickedBtnSend();
CEdit m_SendEdit;
訊息對映表定義:
const int maxDataLen = 424;
CMailSlotClientDlg::CMailSlotClientDlg(CWnd* pParent /*=NULL*/)
: CDialogEx(CMailSlotClientDlg::IDD, pParent)
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
void CMailSlotClientDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT_SEND, m_SendEdit);
}
BEGIN_MESSAGE_MAP(CMailSlotClientDlg, CDialogEx)
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(ID_BTN_EXIT, &CMailSlotClientDlg::OnBnClickedBtnExit)
ON_BN_CLICKED(ID_BTN_SEND, &CMailSlotClientDlg::OnBnClickedBtnSend)
END_MESSAGE_MAP()
訊息處理函式:
//退出按鈕的訊息處理例程
void CMailSlotClientDlg::OnBnClickedBtnExit()
{
CDialogEx::OnOK();
}
//傳送資料的訊息處理例程
void CMailSlotClientDlg::OnBnClickedBtnSend()
{
UpdateData();
if(this->m_SendEdit.GetWindowTextLength() > 0 &&
this->m_SendEdit.GetWindowTextLength() < maxDataLen)
{
HANDLE hSendMailSlot;
CString cStrSendData;
DWORD dwWrite;
char * pSendBuf;
//開啟由服務端建立的郵件槽
hSendMailSlot = CreateFile(TEXT("\\\\.\\mailslot\\ZacharyMailSlot"),
GENERIC_WRITE, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if(INVALID_HANDLE_VALUE == hSendMailSlot)
{
MessageBox(TEXT("開啟郵槽失敗 ..."), TEXT("提示"), MB_ICONERROR);
return;
}
this->m_SendEdit.GetWindowText(cStrSendData);
//需要將 Unicode 字元轉換為 ASCII 字元傳送
pSendBuf = new char[cStrSendData.GetLength() + 1];
memset(pSendBuf, 0, sizeof(cStrSendData.GetLength() + 1));
for(int i=0;i<cStrSendData.GetLength();i++)
{
pSendBuf[i] = cStrSendData.GetAt(i);
}
//通過郵件槽向服務端傳送資料
if(!WriteFile(hSendMailSlot, pSendBuf, cStrSendData.GetLength(), &dwWrite, NULL))
{
MessageBox(TEXT("寫入資料失敗 ..."), TEXT("提示"), MB_ICONERROR);
CloseHandle(hSendMailSlot);
return;
}
MessageBox(TEXT("寫入資料成功 ..."), TEXT("提示"), MB_ICONINFORMATION);
}
}
效果展示:
首先啟動服務端程序並單擊建立按鈕:
然後啟動客戶端程序,並在客戶端程式文字框中輸入資料,然後單擊發送按鈕:
然後回到服務端程式中,並且單擊接收按鈕:
從上面的截圖中可以看出,通過郵槽確實實現了從客戶端程序向服務端程序傳送資料。
當然上面的Demo中的服務端和客戶端都是在本地機器上實現的,
如果想要實現本地程序和遠端程序通訊的話,
只需在客戶端呼叫CreateFile開啟郵槽時,將下面截圖中標記的圓點置換為遠端伺服器的名稱即可以實現了。
結束語
對於郵槽呢,其實還是蠻簡單的,
在服務端的話,也就只需要在服務端呼叫CreateMailslot建立一個郵槽,
然後再在服務端呼叫ReadFile來等待讀取資料即可以了,
而在客戶端的話,也就只需要呼叫CreateFile來開啟一個已經在服務端建立好的郵槽,
然後再呼叫WriteFile往這個郵槽中寫入資料就可以了。
也就是說,對於郵槽的話,也就那麼點東西需要介紹,
但是通過前面的介紹我們也很容易知道,對於通過利用郵槽來實現本地程序和遠端程序的通訊還是有缺陷的,
缺陷就是對於郵槽來說,服務端只能接收來自客戶端的資料,而不能給客戶端傳送資料,
而客戶端的話,則只能給服務端傳送資料,而不能接收服務端傳送過來的資料(事實上,服務端也傳送不了)。
如果要實現客戶端可以傳送資料給服務端,同時也能接收來自服務端的資料,
而服務端也可以傳送資料給客戶端,並且服務端也可以接收到來自客戶端的資料的話,
那需要利用另外的程序間通訊的手段了,對於這點,留到下一篇博文介紹。