进程间通信的四种方式:
Ø 剪贴板
Ø 匿名管道
Ø 命名管道
Ø 邮槽
1. 剪贴板:
剪贴板其实是系统管理的一个内存区域,当一个程序发生拷贝的时候,将是该内存区域得到填充,使用粘贴的时候是重该区域取出数据,然后显示的对应窗口上。
将指定内容赋值到剪贴板上:
a. 打开剪贴板:OpenClipboard,注意:一旦打开了剪贴版,其它运用程序将无法修改剪贴板,直到调用了CloseClipboard。
b. 清空剪贴板:EmptyClipboard,清空剪切板,并将所有权交付给打开剪贴板的运用程序
c. 为即将拷贝的内容分配内存空间:GlobalAlloc,第一个参数指示分配内存的类型,重要的有两类,GMEM_FIXED:Allocates fixed memory. The return value is a pointer;GMEM_MOVEABLE:Allocates movable memory. In Win32, memory blocks are never moved in physical memory, but they can be moved within the default heap. The return value is a handle to the memory object. To translate the handle into a pointer, use the GlobalLock function. This flag cannot be combined with the GMEM_FIXED flag.
本例中采用GMEM_MOVEABLE,其返回值是一个指向内存对象的句柄。
d. 将句柄转换为指针:GlobalLock,将指定内存块锁定。
The internal data structures for each memory object include a lock count that is initially zero. For movable memory objects, GlobalLock increments the count by one, and the GlobalUnlock function decrements the count by one. For each call that a process makes to GlobalLock for an object, it must eventually call GlobalUnlock. Locked memory will not be moved or discarded, unless the memory object is reallocated by using the GlobalReAlloc function. The memory block of a locked memory object remains locked until its lock count is decremented to zero, at which time it can be moved or discarded. 18:26有翻译
e. 将字符串的内容拷贝到可移动堆中:strcpy
f. 释放内存块锁定:GlobalUnlock
g. 放置数据:SetClipboardData, The SetClipboardData function places data on the clipboard in a specified clipboard format. The window must be the current clipboard owner, and the application must have called the OpenClipboard function.
(本例程序没有采用)SetClipboardData的第一个参数可以是指定的格式或NULL,如果是NULL,则采用的是延迟提交的技术,所谓延迟提交表示的是为了避免下面这种情况:当一个拷贝数据到剪贴板的动作发生时,直到下一个从剪贴板上取出数据的过程中,数据一直占用着内存空间,造成了资源浪费。为了改善这种情况,延迟提交技术采用SetClipboardData调用一个空的内存区块,当下一个从剪贴板取出数据的动作发生时,自动发送一个WM_RENERFORMAT消息,剪贴板的所有者程序再次调用具有实际内存区块参数的SetClipboardData方法,发生实际剪贴动作。第二次调用前不用再调用OpenClipboard方法。
h. 关闭剪贴板:CloseClipboard
实现代码如下:
if(OpenClipboard()) //打开剪贴板
{
CString str;
HANDLE hClip; //剪贴板句柄
char* pBuf;
EmptyClipboard();
GetDlgItemText(IDC_EDIT_SEND,str);
//分配内存的长度一般是字符串的长度加1用来存放空字符,否则系统将自动覆盖掉现有字符串的最后一位用来存放空字符,空字符作为结尾标识
hClip=GlobalAlloc(GMEM_MOVEABLE,str.GetLength()+1);
pBuf=(char*)GlobalLock(hClip); //将句柄转换为指针,返回内存对象地址并加锁,如果GlobalAlloc参数是GMEM_FIXED,则这样不需要这样的转换。 GMEM_FIXED锁计数总为零,该语句将增长Lock数
strcpy(pBuf,str); //为分配好的内存空间填充想赋的值
GlobalUnlock(hClip); //如果GlobalAlloc参数是GMEM_FIXED,则不起作用。该语句将减少Lock数,如果Lock数为0,则指定动态内存区域将可被移动和抛弃
SetClipboardData(CF_TEXT,hClip); //以指定格式存放数据,不完成指定格式转换,不能完成粘贴
CloseClipboard();
}
从剪贴板上提取数据:
具体代码如下:
if(OpenClipboard())
{ GMEM_FIXED
if(IsClipboardFormatAvailable(CF_TEXT)) //指定格式数据在接贴板上是否存在
{
HANDLE hClip=GetClipboardData(CF_TEXT); //从剪贴板上得到数据,且拿到了数据块的句柄
char* pBuf;
pBuf=(char*)GlobalLock(hClip);
GlobalUnlock(hClip);
SetDlgItemText(IDC_EDIT_RECV,pBuf);
}
CloseClipboard();
}
2. what is the pipes
A pipe is a section of shared memory that processes use for communication. The process that creates a pipe is the pipe server. A process that connects to a pipe is a pipe client. One process writes information to the pipe, then the other process reads the information from the pipe. This overview describes how to create, manage, and use pipes.
3. 匿名管道
创建父进程:
a. CreatePipe:其中第三个参数代表安全属性结构体SECURITY_ATTRIBUTES
的指针,在前几章的运用中,都是运用了NULL,代表返回的安全句柄不可以被子进程所继承。但在本运用中,涉及到的是匿名管道。匿名管道就是父子进程之间的通信,所以结构体必须设置相应的值。子进程要想获得匿名管道的读写句柄,只能从父进程继承而来。一旦子进程有了继承而来的读写句柄,就可以和父进程进行通信了。对于机构体SECURITY_ATTRIBUTES
,最重要的是第三个参数
bInheritHandle,表示
Specifies whether the returned handle is inherited when a new process is created. If this member is TRUE, the new process inherits the handle.
b. CreateProcess:如果创建管道成功,则创建子进程,并将管道的读写句柄传递给子进程。
1.MFC单文档程序菜单中增加创建管道,读取数据,写入数据三项
2.View类中增加成员变量
3. CreateProcess具见 00:51:30
倒数第三个参数 [in] Pointer to a STARTUPINFO structure that specifies how the main window for the new process
should appear 指向STARTUPINFO结构体的一个指针,用来指定新的进程它的主窗口如何出现
最后一个参数指向PROCESS_INFORMATION结构体的一个指针的返回值,用来接收新的进程的标识信息
PROCESS_INFORMATION
The PROCESS_INFORMATION structure is filled in by the CreateProcess function with information about a newly created process and its primary thread.
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
} PROCESS_INFORMATION;
Members
hProcess //新创建进程的句柄
A handle to the newly created process. The handle is used to specify the process in all functions that perform operations on the process object.
hThread //新创建进程主线程的句柄
A handle to the primary thread of the newly created process. The handle is used to specify the thread in all functions that perform operations on the thread object.
dwProcessId //全局进程标识符,如果进程结束操作系统可能会将标识分配给其他进程,我们调用标识的时候要确保进程在运行
A global process identifier that can be used to identify a process. The value is valid from the time the process is created until the time the process is terminated.
dwThreadId //全局线程标识符
A global thread identifiers that can be used to identify a thread. The value is valid from the time the thread is created until the time the thread is terminated.
创建匿名管道具体代码:
SECURITY_ATTRIBUTES sa;
//总共就三个参数
sa.bInheritHandle=TRUE; //表示可被子进程所继承
sa.lpSecurityDescriptor=NULL; //安全描述符号一般都设置成NULL,即默认描述符
sa.nLength=sizeof(SECURITY_ATTRIBUTES); //管道长度
if(!CreatePipe(&hRead,&hWrite,&sa,0))
{
MessageBox("创建匿名函数失败!");
return;
}
//管道创建成功后,接着创建子进程,并将读写句柄传递给子进程
STARTUPINFO sui;
PROCESS_INFORMATION pi;
//调用ZeroMemory方法将该结构体中的所有成员都置为0,这是因为这个结构体的成员很多,如果开始的时候没有置为0的话,那它的值是随机的,将这样的结构体传给CreateProcess,可能会影响到执行的结果。
ZeroMemory(&sui,sizeof(STARTUPINFO));
sui.cb=sizeof(STARTUPINFO); //设置结构体的大小
sui.dwFlags=STARTF_USESTDHANDLES; //该标识表示标准输入句柄,标准输出句柄和错误句柄是有用的
sui.hStdInput=hRead; //将子进程的输入句柄设置成父进程的读句柄
sui.hStdOutput=hWrite; //将子进程的输出句柄设置成父进程的写句柄
sui.hStdError=GetStdHandle(STD_ERROR_HANDLE); //得到标准错误句柄,是父进程的错误句柄,该行代码在本程序中没有实际的用途意义
//因为是匿名管道,是没有名称的管道,只有通过CreateProcess由上而下的传递管道操作句柄。
if(!CreateProcess("..//Child//Debug//Child.exe",NULL,NULL,NULL,
TRUE,0,NULL,NULL,&sui,&pi))
{
MessageBox("创建子进程失败!");
CloseHandle(hRead);
CloseHandle(hWrite);
//避免在析构函数中再次关闭,析构函数采用:
//if(hRead) CloseHandle(hRead)
hRead=NULL;
hWrite=NULL;
return;
}
else
{
//创建一个新的进程的时候,系统会创建一个进程内核对象和一个线程内核对象,内核对象都有一个使用基数,初始调用的时候,都设置为1。在CreateProcess返回之前,该函数打开进程和线程的内核对象,,并将进程相关的句柄放置到结构体PROCESS_INFORMATION的hProcess和hThread中,当Process在内部打开这些对象的时候,使得每个对象的使用基数增加到2了。如果在父进程中不需要使用这两个句柄,就将这个句柄进行关闭,使得使用基数减1。当子进程终结的时候,系统会在将使用基数减1,使得子进程的进程内核对象和线程内核对象的使用基数变为0,这样内核对象就可以被释放了。
CloseHandle(pi.hProcess); //关闭子进程的句柄
CloseHandle(pi.hThread); //关闭子进程中主线程的句柄
}
父进程写匿名管道:
char *buf="hello world";
DWORD dwWrite;
if(!WriteFile(hWrite,buf,strlen(buf)+1,&dwWrite,NULL))
{
MessageBox("匿名管道写入数据失败!");
return;
}
父进程读匿名管道:
char buf[100];
DWORD dwRead;
if(!ReadFile(hRead,buf,100,&dwRead,NULL))
{
MessageBox("匿名管道读取数据失败!");
return;
}
MessageBox(buf);
创建子进程程序:
可以将获取父进程的匿名管道的读写句柄操作放在CView类的OnInitialUpdate方法中实现,该方法是在CView完全构造后调用的第一个方法。代码如下:
hRead=GetStdHandle(STD_INPUT_HANDLE);
hWrite=GetStdHandle(STD_OUTPUT_HANDLE);
子进程的读写匿名管道的代码和父进程的一样,这里不再累述。
4. 命名管道:
命名管道是通过网络来完成进程间的通信,它屏蔽了底层的网络协议细节。我们在不了解网络协议的情况下,也可以利用命名管道来实现进程间的通信。而上述的匿名管道只能在本地机器上,且连个父子进程间通信。命名管道也具有匿名管道的功能。
命名管道服务器和客户机的区别在于:服务器是唯一一个有权创建命名管道的进程,也只有它才能接受管道客户机的连接请求。而客户机只能同一个现成的命名管道服务器建立连接。
命名管道提供了两种基本通信模式:字节模式和消息模式。在字节模式中,数据以一个连续的字节流的形式,在客户机和服务器之间流动。而在消息模式中,客户机和服务器则通过一系列不连续的数据单位,进行数据的收发,每次在管道上发出了一条消息后,它必须作为一条完整的消息读入。
命名管道服务器端代码(核心为命名管道的创建与等待客户端的连接):
说明:CreateNamedPipe,创建命名管道,其中第一个参数管道的名称是格式为"//./pipe/pipename", 在VC中使用的时候,因涉及到转义符,作为字符串,应使用".//pipe//pipename",其中pipe不能更改,大小写没有区分
hPipe=CreateNamedPipe(".//pipe//MyPipe",PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,0,1,1024,1024,0,NULL);
if(INVALID_HANDLE_VALUE==hPipe)
{
MessageBox("创建命名管道失败!");
CloseHandle(hPipe);
hPipe=NULL;
return;
}
HANDLE hEvent;
hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
if(!hEvent)
{
MessageBox("创建事件对象失败!");
CloseHandle(hPipe);
hPipe=NULL;
return;
}
OVERLAPPED ovlap;
//这里调用ZeroMemory和上一章的意义是一样的,为了避免ConnectNamedPipe在调用该结构中使用的是一些不可欲知的参数值,防止有影响
ZeroMemory(&ovlap,sizeof(OVERLAPPED));
ovlap.hEvent=hEvent;
说明:等待客户端连接到一个命名管道实例。If hNamedPipe was created with FILE_FLAG_OVERLAPPED and lpOverlapped is not NULL, the OVERLAPPED structure pointed to by lpOverlapped must contain a handle to a manual-reset event object (which the server can create by using the CreateEvent function).这是为什么上面需要申明一个自动的事件的对象。
if(!ConnectNamedPipe(hPipe,&ovlap))
{
if(ERROR_IO_PENDING!=GetLastError())
{
MessageBox("等待客户端的连接失败!");
CloseHandle(hPipe);
CloseHandle(hEvent);
hPipe=NULL;
}
}
//等待事件状态有效,如果当前无效,INFINITE参数表明则一直等待下去
if(WAIT_FAILED==WaitForSingleObject(hEvent,INFINITE))
{
MessageBox("等待对象失败!");
CloseHandle(hPipe);
CloseHandle(hEvent);
hPipe=NULL;
}
CloseHandle(hEvent); //说明已经有客户端连接到了
命名管道的读写和上一章是类似的,这里就省略掉了
命名管道的客户端实现(核心为命名管道的连接):
//WaitNamePipe的参数NMPWAIT_WAIT_FOREVER一直等待下去,直到等待到可用的连接,当然也可以设置超时的时间,但前提是所有的程序里所有的命名管道的超时时间必须一样
if(!WaitNamedPipe(".//pipe//MyPipe",NMPWAIT_WAIT_FOREVER))
{
MessageBox("当前没有可用的命名管道实例");
return;
}
//打开命名管道,建立连接
hPipe=CreateFile("127.0.0.1//pipe//MyPipe",GENERIC_READ|GENERIC_WRITE, 0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if(INVALID_HANDLE_VALUE==hPipe)
{
MessageBox("打开命名管道失败!");
hPipe=NULL;
}
注意:程序的运行方式为先点击服务端的创建命名管道,然后点击客户端的连接管道,再点击服务端的发送数据,再在客户端点击接收数据。
5. 邮槽:
Ø 邮槽是基于广播通信体系设计出来的,它采用无连接的不可靠的数据传输。
Ø 邮槽是一种单向通信机制,创建邮槽的服务器进程读取数据,打开邮槽的客户机进程写入数据。
Ø 为保证邮槽在各种Windows平台下都能够正常工作,我们传输消息的时候,应将消息的长度限制在424字节以下。
服务器端的代码实现:
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);
hMailslot=NULL;
return;
}
MessageBox(buf);
CloseHandle(hMailslot);
客户端的代码实现:
HANDLE hMailslot;
//因为邮槽的客户是负责写入数据,所以访问方式只需要是写,但对于服务器端而言,是读取数据,所以客户端共享方式设置为读FILE_SHARE_READ
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[]="this is test!";
DWORD dwWrite;
if(!WriteFile(hMailslot,buf,strlen(buf)+1,&dwWrite,NULL))
{
MessageBox("写入数据失败");
CloseHandle(hMailslot);
return;
}
CloseHandle(hMailslot);
注意:程序的运行方式为先点击服务器端的接收数据以此创建一个邮槽,然后在点击客户端的发送数据,最终服务器端会接收到数据。