要点:
引子
前面的一篇博文介绍了进程之间通信的一种最为简单的方式,
也就是在本地进程之间通过剪贴板来实现进程间通信,而剪贴板自有其缺陷,
很显然的是,剪贴板只能在本地机器上实现,
无法实现本地进程与远程服务器上的进程之间的通信,
那么有没有办法实现本地进程和远程进程的通信呢?
办法自然是有的,要是实在搞不出,
我拿 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
);
参数 lpName 指定了将要创建的邮槽的名称,该名称的格式必须为 \\.\mailslot\MailslotName。
在这里需要注意的是两个斜杠后的那个 “.”,在这里使用圆点代表的是本地机器,
参数 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 往这个邮槽中写入数据就可以了。
也就是说,对于邮槽的话,也就那么点东西需要介绍,
但是通过前面的介绍我们也很容易知道,对于通过利用邮槽来实现本地进程和远程进程的通信还是有缺陷的,
缺陷就是对于邮槽来说,服务端只能接收来自客户端的数据,而不能给客户端发送数据,
而客户端的话,则只能给服务端发送数据,而不能接收服务端发送过来的数据(事实上,服务端也发送不了)。
如果要实现客户端可以发送数据给服务端,同时也能接收来自服务端的数据,
而服务端也可以发送数据给客户端,并且服务端也可以接收到来自客户端的数据的话,
那需要利用另外的进程间通信的手段了,对于这点,留到下一篇博文介绍。