第17章 进程间通信
1.剪贴板
剪贴板实际上是系统维护管理的一块内存区域。如果某个程序已经打开了剪贴板,则其他应用程序将不能修改剪贴板,知道前者调用了CloseClipboard函数。并且,只有在调用了EmptyClipboard函数之后,打开剪贴板的当前窗口才拥有剪贴板。
数据发送:
void CClipboardDlg::OnBtnSend()
{
if(OpenClipboard())
{
CString str;
HANDLE hClip; //内存对象句柄;内存是会移动的(操作系统移动)只能用句柄标识
char *pBuf;
EmptyClipboard();//清空剪贴板并释放剪贴板中数据的句柄,然后将剪贴板的所有权分配给当前打开剪贴板的窗口
GetDlgItemText(IDC_EDIT_SEND,str);
//分配一个内存对象
hClip = GlobalAlloc(GMEM_MOVEABLE, str.GetLength()+1);
//对一个内存地址加锁并返回内存地址
pBuf = (char *) GlobalLock(hClip); //将内存对象句柄转换为指针
//将数据拷贝到内存中
strcpy(pBuf,str);
GlobalUnlock(hClip);
SetClipboardData(CF_TEXT,hClip); //
CloseClipboard();//记住要关闭剪贴板
}
}
注:一般情况下在编程的时候,给应用程序分配的内存都是可以移动的或者是可以丢弃的,这样能使有限的内存资源充分利用,所以,在某一个时候我们分配的那块内存的地址是不确定的,因为他是可以移动的,所以得先锁定那块内存块,这样应用程序才能存取这块内存。使用GlobalLock的目的是为了保证内存管理时真的是用内存而不是“虚拟内存的磁盘镜像”,否则效率会降低。
数据接收:
void CClipboardDlg::OnBtnRecv()
{
if(OpenClipboard())
{//因为在接收端只需从剪贴板中得到数据,而不用向剪贴板中写入数据,所以不要调用EmptyClipboard;
if(IsClipboardFormatAvailable(CF_TEXT))//检查剪贴板中是否有想要的特定格式的数据
{
HANDLE hClip;
hClip = GetClipboardData(CF_TEXT);
char *pBuf;
pBuf = (char*)GlobalLock(hClip);
GlobalUnlock(hClip);//将内存解锁
SetDlgItemText(IDC_EDIT_RECV,pBuf);
CloseClipboard();
}
}
}
2.匿名管道
匿名管道是一个未命名的单向管道,通常用来在一个父进程和一个子进程之间传输数据。匿名管道只能实现本地机器上两个进程间的通信,而不能实现跨网络的通信。因为匿名管道只能在父子进程之间进行通信,子进程如果想要获得匿名管道的句柄,只能从父进程继承而来。当一个子进程从其父进程继承了匿名管道的句柄之后,这两个进程就可以通过该句柄进行通信了。
父进程的实现:
定义两个HANDLE变量:m_hRead, m_hWrite
1.创建匿名管道
void CParentView::OnPipeCreate()
{
SECURITY_ATTRIBUTES sa;//定义安全属性结构体
sa.bInheritHandle = TRUE; //子进程可以继承父进程创建的匿名管道的读写句柄
sa.lpSecurityDescriptor = NULL;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
if(!CreatePipe(&m_hRead,&m_hWrite,&sa,0))
{
MessageBox ("创建匿名管道失败!");
return ;
}
//匿名管道创建成功则启动子进程并将其读、写句柄传递给子进程
STARTUPINFO sui;//用来指定新进程主窗口如何出现的结构体
PROCESS_INFORMATION pi;//进程信息结构体
ZeroMemory(&sui,sizeof(STARTUPINFO));//结构体所有成员置为0,防止未设值的属性拥有随机值
sui.cb = sizeof(STARTUPINFO);//指定结构体大小
sui.dwFlags = STARTF_USESTDHANDLES;
sui.hStdInput = m_hRead;//将标准读取句柄设置为管道读取句柄
sui.hStdOutput =m_hWrite;//标准写入句柄设置为管道写入句柄
sui.hStdError = GetStdHandle(STD_ERROR_HANDLE);
//通过GetStdHandle返回一个父进程的标准错误句柄
if(!CreateProcess("..\\Child\\Debug\\Child.exe", //启动子进程,并将子进程的标准输入输出句柄设置为匿名管道的读、写句柄
NULL,//传递命令行参数
NULL,//进程安全属性
NULL,//线程安全属性
TRUE,// handle inheritance flag
0, //创建标记
NULL,//环境块
NULL,//当前路径,NULL让子进程与父进程有相同路径
&sui,//指定新进程主窗口如何出现
&pi))//用来接收关于新的进程的标识信息
{//如果创建子进程失败
CloseHandle (m_hRead);
CloseHandle (m_hWrite);
m_hRead=NULL;
m_hWrite=NULL;
MessageBox ("创建子进程失败!");
return;
}///...endof if
else
{
/*在创建一个新进程时,系统会为该进程建立一个进程内核对象和一个线程内核对象,而内核对象都有一个使用计数,系统会将这两个对象的使用计数赋初值为1.在CreateProcess函数返回之前会在其内部打开这两个对象,保存某些句柄值,这样这两个内核对象的使用计数就都变成2.*/
CloseHandle (pi.hProcess); //关闭所返回的子进程句柄
CloseHandle (pi.hThread); //关闭子进程中主线程句柄
}
}
注:为了让子进程从众多继承的句柄中区分出管道的读写句柄,就必须将子进程的特殊句柄设置为管道的读写句柄。这里将子进程的标准输入输出句柄分别设置为管道的读、写句柄,这样在子进程中,只要得到了标准输入和标准输出句柄,就相当于得到了这个管道的读写句柄。
2.读取数据:
void CParentView::OnPipeRead()
{
char buf[100];
DWORD dwRead;
if(!ReadFile(m_hRead,buf,100,&dwRead,NULL))
{
MessageBox ("读取数据失败!");
return ;
}
else MessageBox (buf);
}
3.写入数据:
void CParentView::OnPipeWrite()
{
char buf[]="http://blog.csdn.net/teshorse";
DWORD dwWrite;
if(!WriteFile(m_hWrite,buf,strlen(buf)+1,
&dwWrite,NULL))
{
MessageBox ("写入数据失败");
return ;
}
}
对于管道的读取和写入实际上是通过调用ReadFile和WriteFile这两个函数完成的。
子进程的实现:
定义两个HANDLE变量:hRead, hWrite
1.获得管道的读取和写入句柄
void CChildView::OnInitialUpdate() //当窗口成功调用之后第一个创造的函数
{
CView::OnInitialUpdate();
//获取子进程的标准输入输出句柄.
m_hRead =GetStdHandle(STD_INPUT_HANDLE);
m_hWrite = GetStdHandle(STD_OUTPUT_HANDLE);
}
2.读取数据
void CChildView::OnPipeRead()
{
char buf[100];
DWORD dwRead;
if(!ReadFile(m_hRead,buf,100,&dwRead,NULL))
{
MessageBox ("读取数据失败!");
return ;
}
else MessageBox (buf);
}
3.写入数据
void CChildView::OnPipeWrite()
{
char buf[]="匿名管道测试程序";
DWORD dwWrite;
if(!WriteFile(m_hWrite,buf,strlen(buf)+1,
&dwWrite,NULL))
{
MessageBox ("写入数据失败");
return ;
}
}
注:匿名管道只能在父子进程之间通信。两个进程如果想要具有父子关系,必须由父进程通过调用CreateProcess函数去启动子进程。因为匿名管道没有名称,所有只能在父进程中调用CreateProcess函数创建子进程时将管道的读、写句柄传递给子进程。
利用匿名管道也可以实现在同一个进程内读取和写入数据。
3.命名管道
基础知识:
①命名管道通过网络来完成进程间的通信,它屏蔽了底层的网络协议细节。
②命名管道充分利用了Windows内建的安全机制,可以指定用户权限。所以区别于Sockets编写网络应用,使用命名管道无需编写验证用户身份的代码。
③命名管道实际上是建立了一个CS通信体系,并在其中可靠地传输数据。命名管道是围绕Windows文件系统设计的一种机制,采用“命名管道文件系统”接口,因此,客户机和服务器可利用标准Win32文件系统函数来进行数据的收发。
④命名管道服务器和客户机的区别在于:服务器是唯一一个有权创建命名管道的进程,也只有它才能接受管道客户机的连接请求。而客户机只能同一个现成的命名管道服务器建立连接。命名管道服务器只能在WindowsNT、2000等系统上创建。
⑤命名管道提供了两种基本通信模式:字节模式和消息模式。在消息模式下通过一系列不连续的数据单位进行数据发送。
⑥对同一个命名管道的实例来说,在某一时刻,它只能和一个客户端进行通信。
实现过程:在服务器端调用CreateNamePipe创建命名管道之后,调用ConnectNamedPipe函数让服务器端进程等待客户端进程连接到该命名管道的实例上。在客户端首先调用WwaitNamePipe函数判断当前是否有可以利用的命名管道实例,如果有,就调用CreateFile函数打开该命名管道的实例,并建立一个连接。
4.邮槽
邮槽是基于广播通信体系设计出来的,它采用无连接的不可靠的数据传输。邮槽是一种单向通信机制,创建邮槽的服务器进程读取数据,打开邮槽的客户机进程写入数据。邮槽适用于开发一对多的广播通信系统。