17.3命名管道
17.3.1基础知识
//匿名管道只能在本地机器的父子进程之间通信,而命名管道不仅可以在本地机器上实现两个进程间的通信,还可以跨网络实现两个进程间的通信。
//将命名管道作为网络编程方案时,它实际上建立了客户机/服务器通信体系,并在其中可靠地传输数据。命名管道服务器和客户机的区别在于:服务器是唯一一个有权创建命名管道的进程,也只有它才能接收管道客户机的连接请求,而客户机只能同一个现成的命名管道服务器建立连接。
//命名管道提供了两种基本通信模式:字节模式和消息模式。如果程序中像创建一个命名管道,需要调用CreateNamedPipe函数。
HANDLE CreateNamedPipe(
LPCTSTR lpName, // pointer to pipe name
DWORD dwOpenMode, // pipe open mode
DWORD dwPipeMode, // pipe-specific modes
DWORD nMaxInstances, // maximum number of instances
DWORD nOutBufferSize, // 输出缓冲区所保留的字节数
DWORD nInBufferSize, // 输入缓冲区所保留的字节数
DWORD nDefaultTimeOut, // 默认的超时值
LPSECURITY_ATTRIBUTES lpSecurityAttributes //命名管道的安全描述符
);
//第一个参数的字符串格式必须是:\.\pipe\pipename,该字符串开始是两个反斜杠,其后的原点表示是本地机器;接下来pipe是固定字符串;最后是所创建管道的名称
//第二个参数指定管道的访问方式、重叠方式、写直通方式,还有管道句柄的安全访问方式。
//第三个参数指定管道句柄的类型、读取和等待方式。
//第四个参数指定管道能创建的实例的最大数目
17.3.2服务器端程序
//新建一个单文档MFC应用程序,名称为:NamedPipeSrv。为该工程添加一个名为命名管道的子菜单,然后为他们添加三个菜单项以及视类的命令消息响应函数。
//接下来为视类增加一个句柄变量,用来保存创建的管道实例的句柄:
private:
HANDLE hPipe;
//然后再视类的构造函数中将其初始化为NULL。
//最后在视类的析构函数中判断该句柄是否有值,则调用CloseHandle关闭该句柄
CNamedPipeSrvView::~CNamedPipeSrvView()
{
if(hPipe)
{
CloseHandle(hPipe);
}
}
//1、创建命名管道
//在OnPipeCreate函数中就可以调用CreateNamedPipe函数创建命名管道了
void CNamedPipeSrvView::OnPipeCreate()
{
// TODO: Add your command handler code here
//创建匿名管道
hPipe=CreateNamedPipe("\\\\.\\pipe\\MyPipe",PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED,0,1,1024,1024,0,NULL);
//C语言编程时,若要输入两个反斜杠,得输入四个反斜杠,第二个参数采用双向模式,即服务器端进程和客户端进程都可以通过管道读取和写入数据;第三个参数允许重叠方式。
if(INVALID_HANDLE_VALUE==hPipe)
{
MessageBox("创建命名管道失败!");
hPipe=NULL;
return;
}
//创建匿名的人工重置事件对象
HANDLE hEvent;
hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
//注意第二个参数为TRUE
if(hEvent)
{
MessageBox("创建事件对象失败!");
CloseHandle(hPipe);
hPipe=NULL;
return;
}
OVERLAPPED ovlap;
ZeroMemory(&ovlap,sizeof(OVERLAPPED));
ovlap.hEvent=hEvent;
//等待客户端请求的到来
if(!ConnectNamedPipe(hPipe,&ovlap))
{
if(ERROR_IO_PENDING!=GetLastError())
{
MessageBox("等待客户端连接失败!");
CloseHandle(hPipe);
CloseHandle(hEvent);
hPipe=NULL;
return;
}
}
if(WAIT_FAILED==WaitForSingleObject(hEvent,INFINITE))
//当事件对象有信号时候,表明已经有一个客户端连接上命名管道实例上面了
{
MessageBox("等待事件对象失败!");
CloseHandle(hPipe);
CloseHandle(hEvent);
hPipe=NULL;
return;
}
CloseHandle(hEvent);
}
//2、读取数据,余匿名管道读取数据一样。
void CNamedPipeSrvView::OnPipeRead()
{
// TODO: Add your command handler code here
char buf[100];
DWORD dwRead;
if(!ReadFile(hPipe,buf,100,&dwRead,NULL))
{
MessageBox("读取数据失败!");
return;
}
MessageBox(buf);
}
//3、写入数据与匿名管道一样
void CNamedPipeSrvView::OnPipeWrite()
{
// TODO: Add your command handler code here
char buf[]="vc++";
//保存实际写入的字节数
DWORD dwWrite;
if(!WriteFile(hPipe,buf,strlen(buf)+1,&dwWrite,NULL))
{
MessageBox("写入数据失败!");
return;
}
}
//运行程序,服务器端程序就完成了
17.3.3客户端程序
新建一个MFC应用程序,工程名为NamedPipedClt,并保持和NamedPipedSrv工程目录平级,同时将该工程加入到服务器程序所在的工作区。然后为该工程添加一个子菜单,菜单名称为命名管道,然后为这个子菜单添加三个菜单项并且添加相应的视类的消息响应函数。
接下来为视类增加一个句柄变量,来保存命名管道的句柄
private:
HANDLE hPipe;
同样地,在视类的构造函数中将其初始化为NULL,然后在视类的析构函数中进行判断,然后调用CloseHandle函数关闭该句柄。
1.连接命名管道
客户端在连接命名管道之前首先会判断一下是否有可利用的命名管道,这个通过WaitNamedPipe函数实现
BOOL WaitNamedPipe(
LPCTSTR lpNamedPipeName, // 命名管道的名称
DWORD nTimeOut // 指定超时间隔
);
如果当前命名管道的实例可用,那么客户端就可以调用CreateFile函数打开这个命名管道,与服务器进程进行通信了
void CNamedPipeCltView::OnPipeConnect()
{
// TODO: Add your command handler code here
//判断是否有可利用的命名管道
if(!WaitNamedPipe("\\\\.\\pipe\\MyPipe",NMPWAIT_WAIT_FOREVER))
{
MessageBox("当前没有可利用的命名管道实例!");
return;
}
//打开可用的命名管道,并与服务器端进程进行通信
hPipe=CreateFile("\\\\.\\pipe\\MyPipe",GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
//第五个参数指定创建标记,即打开现有的管道,第六个参数指定文件属性
if(INVALID_HANDLE_VALUE==hPipe)
{
MessageBox("打开命名管道失败!");
hPipe=NULL;
return;
}
}
2.读取数据可以直接复制上面服务器端的使用的代码
void CNamedPipeSrvView::OnPipeRead()
{
// TODO: Add your command handler code here
char buf[100];
DWORD dwRead;
if(!ReadFile(hPipe,buf,100,&dwRead,NULL))
{
MessageBox("读取数据失败!");
return;
}
MessageBox(buf);
}
3.写入数据
void CNamedPipeSrvView::OnPipeWrite()
{
// TODO: Add your command handler code here
char buf[]="vc++";
//保存实际写入的字节数
DWORD dwWrite;
if(!WriteFile(hPipe,buf,strlen(buf)+1,&dwWrite,NULL))
{
MessageBox("写入数据失败!");
return;
}
}
同样地先运行服务器端创建管道,客户端连接管道;服务器端写入数据,客户端读取数据,或者客户端写入数据,服务器端读取数据。运行结果如图。
17.4邮槽
邮槽是基于广播通信体系设计出来的,采用无连接的不可靠的数据传输。邮槽是一种单向通信机制,创建邮槽的服务器进程读取数据,打开邮槽的客户机进程写入数据。在程序中可以通过调用CreateMailslot函数创建一个邮槽。
HANDLE CreateMailslot(
LPCTSTR lpName,//指定邮槽的名称,\\.\mailslot\[path]name,原点表示本地主机,最后的字符串就是邮槽的名称。
DWORD nMaxMessageSize, //写到邮槽单一消息的最大尺寸
DWORD lReadTimeout, //指定读取操作的超时时间间隔
LPSECURITY_ATTRIBUTES lpSecurityAttributes
//安全描述符指针
);
7.4.1服务器端程序
新建一个单文档应用程序,取名MailsoltSrv,同时为该工程添加一个子菜单,取消Pop-up选项,使其成为一个可相应的菜单项;将其名称改为接收数据,ID为IDM_MAILSLOT_RECV;并添加相应的消息响应函数,由视类接收消息响应。
void CMailslotSrvView::OnMailslotRecv()
{
// TODO: Add your command handler code here
HANDLE hMailslot;
//创建邮槽
hMailslot=CreateMailslot("\\\\.\\mailslot\\MyMailslot",0,MAILSLOT_WAIT_FOREVER,NULL);
if(INVALID_HANDLE_VALUE==hMailslot)
{
MessageBox("创建邮槽失败!");
return;
}
char buf[100];
DWORD dwRead;
if(!ReadFile(hMailslot,buf,100,&dwRead,NULL))
{
MessageBox("读取数据失败!");
CloseHandle(hMailslot);
return;
}
MessageBox(buf);
CloseHandle(hMailslot);
}
17.4.2客户端程序
新建一个单文档应用程序,取名MailsoltClt,同时为该工程添加一个子菜单,取消Pop-up选项,使其成为一个可相应的菜单项;将其名称改为发送数据,ID为IDM_MAILSLOT_SEND;并添加相应的消息响应函数,由视类接收消息响应。
void CMailslotCltView::OnMailslotSend()
{
// TODO: Add your command handler code here
HANDLE hMailslot;
//打开邮箱
hMailslot=CreateFile("\\\\.\\mailslot\\MyMailslot",GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if(INVALID_HANDLE_VALUE==hMailslot)
{
MessageBox("打开邮箱失败!");
return;
}
char buf[]="vc++";
DWORD dwWrite;
//想邮槽写入数据
if(!WriteFile(hMailslot,buf,strlen(buf)+1,&dwWrite,NULL))
{
MessageBox("写入数据失败!");
CloseHandle(hMailslot);
return;
}
CloseHandle(hMailslot);
}
如果邮件服务器与客户端进程不在一台机器上,应该将服务器运行所在的机器的主机名替换成原点。然后运行,单击服务器端接收数据,接着单击客户端发送数据,运行结果如图。