1、进程间通信方式
进程中的线程共享进程的4G地址空间,因此,线程间的通信很简单。而进程的地址空间属于进程私有,所以进程间通信需要其他机制。其中主要有:剪贴板、匿名管道、有名管道、邮槽。
2、剪贴板
剪贴板发送数据方编程流程:打开剪贴板->清空剪贴板上数据->向剪贴板上放置数据->关闭剪贴板。
CWnd::OpenClipboard(void)用来打开剪贴板,打开成功返回返回TRUE,如果本程序或其它程序已打开剪贴板则返回FALSE。
::EmptyClipboard(void)用来清空剪贴板,释放剪贴板上数据的句柄。打开剪贴板后应调用EmptyClipboard()来获得剪贴板的使用权。
::SetClipboardData(UINT uFormat, HANDLE hMem)用来向剪贴板上放置数据。需要注意的是调用SetClipboardData()的窗口必须是剪贴板的拥有者。函数参数意义:
如果hMem不为NULL,则其标识了一个内存对象,而这个对象必须是利用GlobalAlloc()为其分配内存的,且第一个参数标志应为GMEM_MOVEABLE。
GlobalAlloc()用来从堆中分配内存,对应的释放内存函数为GlobalFree(HGLOBAL hMem)。函数原型:
HGLOBAL GlobalAlloc(UINT uFlags, SIZE_T dwBytes);
如上表所提到的,如果指定CMEM_MOVEABLE标志,则可以利用ClobalLock()将内存对象句柄转换为指针。而指定GMEM_FIXED标志则不用。
GlobalLock(HGLOBAL hMem)用来将指定的全局内存对象加锁,返回该内存对象的指针。已加锁的内存不能被移动或释放,除非调用GloballUnLock()函数来解锁。
::CloseClipboard(void)用来关闭剪贴板。
以下为向剪贴板发送数据的代码:
void CClipboardDlg::OnBnClickedBtnSend()
{
// TODO: 在此添加控件通知处理程序代码
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, (LPSTR)(LPCTSTR)str);
GlobalUnlock(hClip);
SetClipboardData(CF_TEXT, hClip);//向剪贴板放置数据
CloseClipboard();//关闭剪贴板
}
}
剪贴板接收数据方编程流程:打开剪贴板-> 检测剪贴板上数据 ->从剪贴板上获取数据->关闭剪贴板。
IsClipboardFormatAvailable(UINT format)用来检测剪贴板上是否包含参数format指定的特定格式的数据,格式相符返回TRUE,否则返回FALSE。
GetClipboardData()用来从剪贴板中获取指定格式的数据,返回值为数据内存对象的句柄,故可以用GlobalLock()将句柄转换为指针。函数原型:HANDLE GetClipboardData(UINT uFormat);
以下为向剪贴板接收数据的代码:
void CClipboardDlg::OnBnClickedBtnRecv()
{
// TODO: 在此添加控件通知处理程序代码
if(OpenClipboard())//打开剪贴板
{
if(IsClipboardFormatAvailable(CF_TEXT))//检测剪贴板上是否包含指定格式数据
{
HANDLE hClip;
TCHAR *pBuf;
hClip = GetClipboardData(CF_TEXT);//从剪贴板获取指定格式数据
pBuf = (TCHAR*)GlobalLock(hClip);
GlobalUnlock(hClip);
SetDlgItemText(IDC_EDIT_RECV, pBuf);
}
CloseClipboard();//关闭剪贴板
}
}
2、匿名管道
匿名管道是一个未命名的单向管道,通常用来父子进程间通信。CreatePipe()函数用来创建匿名管道:
BOOL CreatePipe(
__out PHANDLE hReadPipe,//获得匿名管道的读取句柄
__out PHANDLE hWritePipe,//获得匿名管道的写入句柄
__in_opt LPSECURITY_ATTRIBUTES lpPipeAttributes,//匿名管道是否可以被子进程继承,这里应该为TRUE
__in DWORD nSize//匿名管道缓冲区大小
);
以下为使用匿名管道进行父子进程通信的例子:父进程先调用CreatePipe()创建一个匿名管道供父子使用,同时获得了该匿名管道的读端句柄和写端句柄。父进程在创建子进程时将子进程的标准输入设置成匿名管道的读端句柄,将子进程的标准输出设置成匿名管道的写端句柄,将子进程的标准出错输出设置成父进程的标准出错输出,这些设置通过CreateProcess创建子进程时设置lpStarupInfo参数指向的STARTUPINFOW结构体来实现:将STARTUPINFOW结构体中的dwFlags设为STARTF_USESTDHANDLES标记,将STARTUPINFOW结构体中的hStdInput赋值为匿名管道的读端句柄、hStdOutput赋值为匿名管道的写端句柄、hStdError赋值为父进程的标准出错句柄。
GetStdHandle(DWORD nStdHandle)可以获得标准输入(参数为STD_INPUT_HANDLE)、标准输出(参数为STD_OUTPUT_HANDLE)、标准出错输出(参数为STD_ERROR_HANDLE)的句柄。
而此时,父子进程就可以通过这个匿名管道来进行通信:父进程向管道写端写数据,则子进程可以从管道读端读出数据,因为子进程的标准输入已被设置成匿名管道的读端句柄;子进程向管道写端写数据,则父进程可以从管道读端读出数据,因为子进程的标准输出已被设置成匿名管道的写句柄。
父进程中代码:
先定义保存匿名管道读、写端的句柄
HANDLE hWrite;//匿名管道读端句柄
HANDLE hRead;//匿名管道写端句柄
再创建匿名管道,获得管道的读、写端句柄,创建子进程,
将子进程的标准输入设置成匿名管道的读端句柄、标准输出设置成匿名管道的写端句柄、标准出错设置成父进程的标准出错
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);//结构体大小
sa.lpSecurityDescriptor = NULL;//使用默认的安全描述符
sa.bInheritHandle = TRUE;//子进程可以继承该匿名管道
if(!CreatePipe(&hRead, &hWrite, &sa, 0))//创建匿名管道
{
MessageBox(_T("创建匿名管道失败!"));
return;
}
STARTUPINFO sui;
sui.cb = sizeof(STARTUPINFO);
sui.dwFlags = STARTF_USESTDHANDLES;
sui.hStdInput = hRead;//子进程的标准输入
sui.hStdOutput = hWrite;//子进程的标准输出
sui.hStdError = GetStdHandle(STD_ERROR_HANDLE);//子进程的标准出错输出
PROCESS_INFORMATION pi;
ZeroMemory(&sui, sizeof(STARTUPINFO));
if(!CreateProcess(_T("..\\Debug\\Child.exe"), NULL, NULL, NULL,//创建子进程
TRUE, 0, NULL, NULL, &sui, &pi))
{
DWORD error;
error = GetLastError();
TCHAR buf[10];
_itow(error, buf, 10);
CloseHandle(hRead);
CloseHandle(hWrite);
hRead = NULL;
hWrite = NULL;
MessageBox(/*_T("创建子进程失败!")*/buf);
return;
}
else
{
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
父进程向管道写端写数据
TCHAR buf[] = _T("www.sunxin.org");
DWORD dwWrite;
if(!WriteFile(hWrite, buf, (_tcslen(buf)+1)*2, &dwWrite, NULL))
{
MessageBox(_T("写入数据失败"));
return;
}
父进程从管道读端中读数据
TCHAR buf[100];
DWORD dwRead;
if(!ReadFile(hRead, buf, 200, &dwRead, NULL))
{
MessageBox(_T("读取数据失败"));
return;
}
MessageBox(buf);
父进程结束或不使用时应关闭打开的管道的读、写句柄
CloseHandle(hRead);
CloseHandle(hWrite);
子进程中代码:
先定义保存匿名管道读、写端的句柄
HANDLE hWrite;//匿名管道读端句柄
HANDLE hRead;//匿名管道写端句柄
再获得匿名管道的读、写端句柄
hRead = GetStdHandle(STD_INPUT_HANDLE);//获得标准输入,即为管道读端
hWrite = GetStdHandle(STD_OUTPUT_HANDLE);//获得标准输出,即为管道写端
子进程向管道写端写数据
TCHAR buf[] = _T("匿名管道测试程序");
DWORD dwWrite;
if(!WriteFile(hWrite, buf, (_tcslen(buf)+1)*2, &dwWrite, NULL))
{
MessageBox(_T("写入数据失败"));
return;
}
子进程从管道读端读取数据
TCHAR buf[100];
DWORD dwRead;
if(!ReadFile(hRead, buf, 200, &dwRead, NULL))
{
MessageBox(_T("读取数据失败"));
return;
}
MessageBox(buf);
子进程结束或不使用时应关闭打开的管道的读、写句柄
CloseHandle(hRead);
CloseHandle(hWrite);
除了利用上面的方法子进程来操作匿名管道外,还可以有以下方法可以获得子进程继承自父进程的句柄,从而直接对匿名管道进行操作:
1、父进程将匿名管道的读、写端句柄值通过命令行参数传递给子进程。
2、父进程调用WaitForInputIdle()等待子进程完成初始化,然后再向子进程的线程创建的窗口发送一条消息。
3、将匿名管道的读、写句柄值加入到父进程的环境块中。
3、命名管道
CreateNamedPipe()用来创建命名管道的实例,成功返回命名管道的句柄,INVALID_HANDLE_VALUE表示失败,同时设置GetLastError。可以多次调用该函数来创建一个命名管道的多个实例。
HANDLE WINAPI CreateNamedPipe(
_In_ LPCTSTR lpName,
_In_ DWORD dwOpenMode,
_In_ DWORD dwPipeMode,
_In_ DWORD nMaxInstances,
_In_ DWORD nOutBufferSize,
_In_ DWORD nInBufferSize,
_In_ DWORD nDefaultTimeOut,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
lpName:
格式必须为\\.\pipe\pipename,最多可达256个字符的长度,不用区分大小写。需要注意的是,在C语言中,如果想要指定一个反斜杠,那么在代码中应为两个反斜杠,如果想要指定两个反斜杠,那么在代码中应为四个反斜杠。如果存在指定名字的一个管道,则创建那个管道的一个新实例。
两个反斜杠后面的原点表示是本地机器,如果想要与远程的服务器建立连接,那么应将原点替换为远程服务器的名称。
dwOpenMode:创建管道的标志,管道的所有实例必须为相同的标志。下述常数组的一个组合
访问方式,下述常数之一(对于管道的所有实例都要一样):
PIPE_ACCESS_DUPLEX 管道是双向的
PIPE_ACCESS_INBOUND 数据从客户端流到服务器端
PIPE_ACCESS_OUTBOUND 数据从服务器端流到客户端
写直通和重叠方式,下述常数的任意组合:
FILE_FLAG_FIRST_PIPE_INSTANCE 只能创建管道的一个实例
FILE_FLAG_WRITE_THROUGH 写直通方式,该方式只对字节类型管道的写入操作有效,且客户端与服务器端必须为不同的机子。如果制定了该标志,那么只有等到欲写入到命名管道的数据通过网络发送了出去,并且放到了对方的管道缓冲区后,写操作才返回;如果没有指定该标志,则缓冲区满或超时的时候,写操作就返回。
FILE_FLAG_OVERLAPPED 重叠方式,允许(但不要求)用这个管道进行异步(重叠式)操作。ReadFileEx和WriteFileEx只能在重叠方式下使用管道。
管道的安全访问方式,下列常数之一:
WRITE_DAC 调用者对命名管道的任意访问控制列表都可以进行写入访问
WRITE_OWNER 调用者对命名管道的所有者可以进行写入访问
ACCESS_ SYSTEM_SECURITY 调用者对命名管道的安全访问控制列表可以进行写入访问
dwPipeMode:下述常数组的一个组合,0则为字节管道类型,字节读取方式和默认的等待方式
管道类型,下列常数之一:
PIPE_TYPE_BYTE 字节类型管道,即数据作为一个连续的字节数据流写入管道
PIPE_TYPE_MESSAGE 消息类型管道,即数据用数据块(名为“消息”或“报文”)的形式写入管道
读取方式,下列常数之一:
PIPE_READMODE_BYTE 数据以单独字节的形式从管道中读出
PIPE_READMODE_MESSAGE 数据以名为“消息”的数据块形式从管道中读出(要求指定PIPE_TYPE_MESSAGE)
等待方式,下列常数之一:
PIPE_WAIT 同步操作在等待的时候挂起线程
PIPE_NOWAIT(不推荐!) 同步操作立即返回。这样可为异步传输提供一种落后的实现方法,已由Win32的重叠式传输机制取代了
nMaxInstances:管道能够创建实例的最大数目,应为1—PIPE_UNLIMITED_INSTANCES之间,如果希望同时能够连接5个客户端,那么必须创建5个管道实例,才能同时接受5个连接请求。
nOutBufferSize:输出缓冲区大小,0为默认值。
nInBufferSize:输入缓冲区大小,0为默认值。
nDefaultTimeOut:超时时间。
lpSecurityAttributes:指定管道的安全描述符和子进程是否可以继承该管道句柄。NULL则为使用默认的安全描述符,且不能继承。
ConnectNamedPipe()用于让服务器等待客户端连接请求的到来,函数原型:
BOOL WINAPI ConnectNamedPipe( _In_ HANDLE hNamedPipe, _Inout_opt_ LPOVERLAPPED lpOverlapped );hNamePipe:命名管道实例句柄。
lpOverlapped:指向一个OVERLAPPED结构的指针,如果命名管道是用FILE_FLAG_OVERLAPPED标志创建的,则改参数不能为NULL,且OVERLAPPED结构体中必须包含人工重置事件对象的句柄。
ConnectNamedPipe执行失败会返回0值,但有一种特殊情况:操作还处于未决状态,随后的某个时间这个操作可能能够完成,在这种情况下GetLastError()会返回ERROR_IO_PENDING 。
服务器端创建命名管道,等待客户端连接的代码示例:
//创建命名管道
hPipe = CreateNamedPipe(_T("\\\\.\\pipe\\MyPipe"), PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
0, 1, 1024, 1024, 0, NULL);
if(INVALID_HANDLE_VALUE == hPipe)
{
MessageBox(_T("创建命名管道失败"));
hPipe = NULL;
return;
}
//创建人工重置事件对象
HANDLE hEvent;
hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if(!hEvent)
{
MessageBox(_T("创建事件对象失败"));
CloseHandle(hPipe);
hPipe = NULL;
return;
}
//等待客户端的请求
OVERLAPPED ovlap;
ZeroMemory(&ovlap, sizeof(OVERLAPPED));
ovlap.hEvent = hEvent;
if(!ConnectNamedPipe(hPipe, &ovlap))
{
if(ERROR_IO_PENDING != GetLastError())
{
MessageBox(_T("等待客户连接失败"));
CloseHandle(hPipe);
CloseHandle(hEvent);
hPipe = NULL;
return;
}
}
//等待事件对象变为有信号状态
if(WAIT_FAILED == WaitForSingleObject(hEvent, INFINITE))
{
MessageBox(_T("等待对象失败"));
CloseHandle(hPipe);
CloseHandle(hEvent);
hPipe = NULL;
return;
}
CloseHandle(hEvent);
服务器端读取管道数据:
TCHAR buf[100];
DWORD dwRead;
if(!ReadFile(hPipe, buf, 200, &dwRead, NULL))
{
MessageBox(_T("读取数据失败"));
return;
}
MessageBox(buf);
服务器端向管道写入数据:
TCHAR buf[] = _T("www.sunxin.org");
DWORD dwWrite;
if(!WriteFile(hPipe, buf, (_tcslen(buf)+1)*2, &dwWrite, NULL))
{
MessageBox(_T("写入数据失败"));
return;
}
最后在程序结束或不使用命名管道的时候应该关闭管道:
CloseHandle(hPipe);
客户端在连接服务器端创建的命名管道之前,应先调用WaitNamedPipe()判断是否有可利用的命名管道。
WaitNamedPipe()函数会等该指定的命名管道实例可以连接了,直到指定的时间已到。
BOOL WINAPI WaitNamedPipe( _In_ LPCTSTR lpNamedPipeName, _In_ DWORD nTimeOut );lpNamedPipeName:命名管道名称,格式为 \\.\pipe\pipename。两个反斜杠后面的原点表示是本地机器,如果想要与远程的服务器建立连接,那么应将原点替换为远程服务器的名称。
nTimeOut:超时时间,取值可以为以下两个值
NMPWAIT_USE_DEFAULT_WAIT:超时间隔就是服务器端创建管道时指定的超时值,同一个管道的所有实例应相同。
NMPWAIT_WAIT_FOREVER:一直等待,直到出现可用的管道实例。
如果命名管道实例可以使用,那么就可以调用CreateFile()打开这个管道,然后与服务器进程进行通信了。
客户端打开命名管道代码示例:
if(!WaitNamedPipe(_T("\\\\.\\pipe\\MyPipe"), NMPWAIT_WAIT_FOREVER))
{
MessageBox(_T("当前没有可利用的命名管道实例"));
return;
}
hPipe = CreateFile(_T("\\\\.\\pipe\\MyPipe"), GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if(INVALID_HANDLE_VALUE == hPipe)
{
MessageBox(_T("打开命名管道失败"));
hPipe = NULL;
return;
}
客户端从管道读取数据:
TCHAR buf[100];
DWORD dwRead;
if(!ReadFile(hPipe, buf, 200, &dwRead, NULL))
{
MessageBox(_T("读取数据失败"));
return;
}
MessageBox(buf);
客户端向管道写入数据:
<span style="font-size:14px;"> TCHAR buf[] = _T("命名管道测试程序");
DWORD dwWrite;
if(!WriteFile(hPipe, buf, (_tcslen(buf)+1)*2, &dwWrite, NULL))
{
MessageBox(_T("写入数据失败"));
return;
}</span>
最后在程序结束或不使用命名管道的时候应该关闭管道:
<span style="font-size:14px;">CloseHandle(hPipe);</span>
这是MSDN上多线程命名管道服务器端和客户端的代码示例:http://msdn.microsoft.com/en-us/library/aa365588%28v=vs.85%29.aspx
http://msdn.microsoft.com/en-us/library/aa365592%28v=vs.85%29.aspx
4、邮槽
邮槽是基于广播通信设计出来的,它采用无连接的、不可靠的数据传输;并且它是一种单向通信机制,创建邮槽的服务器进程读取数据,打开邮槽的客户端进程写入数据;而且,要保证邮槽在各种windows平台下正常工作,那么传输的消息数据大小应该在424字节以下。
CreateMailslot()用来创建邮槽,返回邮槽句柄。函数原型:
HANDLE WINAPI CreateMailslot(
_In_ LPCTSTR lpName,
_In_ DWORD nMaxMessageSize,
_In_ DWORD lReadTimeout,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
lpName:邮槽名称,格式应为\\.\mailslot\pathname,两个反斜杠后应为服务器所在机器名,点则表示为本地主机。
nMaxMessageSize:写入到邮槽的单一消息的最大尺寸,0为不限制消息大小。
lReadTimeout:指定读取操作的超时时间,为0则读取操作立即返回,MAILSLOT_WAIT_FOREVER则为一直等该。
lpSecurityAttributes:SECURITY_ATTRIBUTES结构指针,指定安全描述符及邮槽句柄是否可以被子进程继承,NULL则为使用默认的安全描述符和不能被继承。
以下为邮槽服务器端创建邮槽和接收数据代码:
//创建邮槽
HANDLE hMailslot;
hMailslot = CreateMailslot(_T("\\\\.\\mailslot\\MyMailslot"), 0,
MAILSLOT_WAIT_FOREVER, NULL);
if(INVALID_HANDLE_VALUE == hMailslot)
{
MessageBox(_T("创建邮槽失败"));
return;
}
//接收数据
TCHAR buf[100];
DWORD dwRead;
if(!ReadFile(hMailslot, buf, 200, &dwRead, NULL))
{
MessageBox(_T("读取邮槽失败"));
CloseHandle(hMailslot);
return;
}
MessageBox(buf);
//关闭邮槽句柄
CloseHandle(hMailslot);
以下为邮槽客户端打开邮槽,向邮槽写入数据的实例:
//打开邮槽
HANDLE hMailslot;
hMailslot = CreateFile(_T("\\\\.\\mailslot\\MyMailslot"),GENERIC_WRITE,
FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if(INVALID_HANDLE_VALUE == hMailslot)
{
MessageBox(_T("打开邮槽失败"));
return;
}
//向邮槽写入数据
TCHAR buf[] = _T("http://www.sunxin.org");
DWORD dwWrite;
if(!WriteFile(hMailslot, buf, (_tcslen(buf)+1)*2, &dwWrite, NULL))
{
MessageBox(_T("写入数据失败"));
CloseHandle(hMailslot);
return;
}
//关闭邮槽句柄
CloseHandle(hMailslot);
如果想要在一个程序中利用邮槽实现既能接收数据,又能发送数据,那么可以可以在这个程序中同时实现邮槽的服务器端和客户端。
5、四种进程间通信方法的比较
内容出自《孙鑫VC++深入详解》