1. 程式人生 > >程序間通訊詳解

程序間通訊詳解

 

引子

前面的一篇博文介紹了程序之間通訊的一種最為簡單的方式,

也就是在本地程序之間通過剪貼簿來實現程序間通訊,而剪貼簿自有其缺陷,

很顯然的是,剪貼簿只能在本地機器上實現,

無法實現本地程序與遠端伺服器上的程序之間的通訊,

那麼有沒有辦法實現本地程序和遠端程序的通訊呢?

辦法自然是有的,要是實在搞不出,

我拿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 程式)

專案結構:

image

訊息以及成員函式和成員變數的宣告:

// 實現
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 程式)

專案結構:

image

訊息以及成員函式和成員變數的宣告:

// 實現
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);
    }
}

效果展示:

首先啟動服務端程序並單擊建立按鈕:

image

然後啟動客戶端程序,並在客戶端程式文字框中輸入資料,然後單擊發送按鈕:

image

然後回到服務端程式中,並且單擊接收按鈕:

image

從上面的截圖中可以看出,通過郵槽確實實現了從客戶端程序向服務端程序傳送資料。

當然上面的Demo中的服務端和客戶端都是在本地機器上實現的,

如果想要實現本地程序和遠端程序通訊的話,

只需在客戶端呼叫CreateFile開啟郵槽時,將下面截圖中標記的圓點置換為遠端伺服器的名稱即可以實現了。

image

結束語

對於郵槽呢,其實還是蠻簡單的,

在服務端的話,也就只需要在服務端呼叫CreateMailslot建立一個郵槽,

然後再在服務端呼叫ReadFile來等待讀取資料即可以了,

而在客戶端的話,也就只需要呼叫CreateFile來開啟一個已經在服務端建立好的郵槽,

然後再呼叫WriteFile往這個郵槽中寫入資料就可以了。

也就是說,對於郵槽的話,也就那麼點東西需要介紹,

但是通過前面的介紹我們也很容易知道,對於通過利用郵槽來實現本地程序和遠端程序的通訊還是有缺陷的,

缺陷就是對於郵槽來說,服務端只能接收來自客戶端的資料,而不能給客戶端傳送資料,

而客戶端的話,則只能給服務端傳送資料,而不能接收服務端傳送過來的資料(事實上,服務端也傳送不了)。

如果要實現客戶端可以傳送資料給服務端,同時也能接收來自服務端的資料,

而服務端也可以傳送資料給客戶端,並且服務端也可以接收到來自客戶端的資料的話,

那需要利用另外的程序間通訊的手段了,對於這點,留到下一篇博文介紹。