C++进程通信之命名管道
命名管道定义
一个命名管道是一个命名的,单向或双面管道的管道服务器和一个或多个管道客户端之间的通信。命名管道的所有实例共享相同的管道名称,但每个实例都有自己的缓冲区和句柄,并为客户端/服务器通信提供单独的管道。实例的使用使多个管道客户端能够同时使用同一个命名管道。
这里要理解实例的概念:当我用CreateNamedPipe在服务器端创建一个名为pipeTest的命名管道时,即pipeTest拥有了一个实例。再次重复刚才的操作时,即创建了pipeTest的第二个实例;当用CreateNamedPipe在服务器端创建一个名为pipeTestAnother的命名管道时,则该pipeTestAnother管道拥有了第一个实例。
命名管道的使用步骤
服务器端:
首先,使用CreateNamedPipe创建属于该管道的实例。然后等待客户端实例的连接,服务器端可以使用ConnectNamedPipe进行阻塞同步等待客户端实例的连接,也可以非阻塞,然后执行ReadFile不停读取客户端发送到管道的数据。
客户端:
执行WaitNamedPipe(不是真正的用于连接的函数)来等待管道的出现,存在管道后,执行CreateFile来连接存在的服务器管道,获取对应句柄后,执行WriteFile往管道发送数据。
上面是以最简单的单向的客户端发送数据进管道,服务器端接受管道数据。还有其他的通信选项。包括双通道的Read&Write。Read&Write过程中的同步与异步;按字节流方式/消息的方式写入/读取管道数据。
CreateNamedPipe
HANDLE CreateNamedPipeA( [in] LPCSTR lpName, [in] DWORD dwOpenMode, [in] DWORD dwPipeMode, [in] DWORD nMaxInstances, [in] DWORD nOutBufferSize, [in] DWORD nInBufferSize, [in] DWORD nDefaultTimeOut, [in, LPSECURITY_ATTRIBUTES lpSecurityAttributes );
模式 | 意义 |
PIPE_ACCESS_DUPLEX 0x00000003 |
管道是双向的;服务器和客户端进程都可以读取和写入管道。此模式为服务器提供了对管道的GENERIC_READ和GENERIC_WRITE访问权限。当客户端使用CreateFile函数连接到管道时,它可以指定GENERIC_READ或GENERIC_WRITE或两者 。 |
PIPE_ACCESS_INBOUND 0x00000001 |
管道中的数据流仅从客户端到服务器。此模式为服务器提供了对管道的GENERIC_READ访问权限。客户端在连接到管道时必须指定GENERIC_WRITE访问权限。如果客户端必须通过调用GetNamedPipeInfo或GetNamedPipeHandleState函数读取管道设置,则客户端在连接到管道时必须指定GENERIC_WRITE和FILE_READ_ATTRIBUTES访问权限 |
PIPE_ACCESS_OUTBOUND 0x00000002 |
管道中的数据流仅从服务器流向客户端。此模式为服务器提供了对管道的GENERIC_WRITE访问权限。客户端在连接到管道时必须指定GENERIC_READ访问权限。如果客户端必须通过调用SetNamedPipeHandleState函数更改管道设置,则客户端在连接到管道时必须指定GENERIC_READ和FILE_WRITE_ATTRIBUTES访问权限。 |
模式 | 意义 |
FILE_FLAG_FIRST_PIPE_INSTANCE 0x00080000 |
如果您尝试使用此标志创建管道的多个实例,则第一个实例的创建成功,但下一个实例的创建失败并显示ERROR_ACCESS_DENIED。 |
FILE_FLAG_WRITE_THROUGH 0x80000000 |
直写模式已启用。此模式仅影响字节类型管道上的写入操作,并且仅当客户端和服务器进程位于不同的计算机上时。如果启用此模式,则写入命名管道的函数不会返回,直到写入的数据通过网络传输并位于远程计算机上的管道缓冲区中。如果未启用此模式,系统会通过缓冲数据来提高网络操作的效率,直到累积的字节数达到最少或经过最长时间。 |
FILE_FLAG_OVERLAPPED 0x40000000 |
重叠模式已启用。如果启用此模式,则执行可能需要很长时间才能完成的读取、写入和连接操作的函数可以立即返回。这种模式使启动操作的线程可以执行其他操作,而耗时的操作在后台执行。例如,在重叠模式下,线程可以同时处理管道的多个实例上的输入和输出 (I/O) 操作,或者在同一个管道句柄上同时执行读写操作。如果未启用重叠模式,则对管道句柄执行读取、写入和连接操作的函数在操作完成之前不会返回。该 ReadFileEx和 WriteFileEx函数只能在重叠模式下与管道句柄一起使用。的 ReadFile的, WriteFile的, ConnectNamedPipe和 TransactNamedPipe功能可以执行同步或作为重叠操作。 |
模式 | 意义 |
WRITE_DAC 0x00040000L |
调用者将拥有对命名管道的自由访问控制列表 (ACL) 的写访问权限。 |
WRITE_OWNER 0x00080000L |
调用者将拥有对命名管道所有者的写访问权限。 |
ACCESS_SYSTEM_SECURITY 0x01000000L |
调用者将拥有对命名管道的 SACL 的写访问权限。有关详细信息,请参阅 访问控制列表 (ACL)和 SACL 访问权限。 |
dwPipeMode
管道模式。
如果dwPipeMode指定的值不是 0 或下表中列出的标志,则该函数将失败。
可以指定以下类型模式之一。必须为管道的每个实例指定相同的类型模式。
模式 | 意义 |
PIPE_TYPE_BYTE 0x00000000 |
数据以字节流的形式写入管道。此模式不能与 PIPE_READMODE_MESSAGE 一起使用。管道不区分在不同写操作期间写入的字节。 |
PIPE_TYPE_MESSAGE 0x00000004 |
数据作为消息流写入管道。管道将每次写入操作期间写入的字节视为一个消息单元。当消息未完全读取时,GetLastError函数返回ERROR_MORE_DATA。此模式可与PIPE_READMODE_MESSAGE或PIPE_READMODE_BYTE 一起使用。 |
对于PIPE_TYPE_BYTE模式写入管道,读取端不用每次读取固定大小的数据,可以读取任意字节大小数据。
对于PIPE_TYPE_MESSAGE模式写入管道,读取端用消息模式读取时必须全部读取完,不能只读取部分消息。
具体参考:
https://www.cnblogs.com/duyy/p/3738610.html
可以指定以下读取模式之一。同一管道的不同实例可以指定不同的读取模式。
模式 | 意义 |
---|---|
|
数据作为字节流从管道中读取。此模式可与PIPE_TYPE_MESSAGE或PIPE_TYPE_BYTE 一起使用。 |
|
数据作为消息流从管道中读取。仅当还指定了PIPE_TYPE_MESSAGE 时才能使用此模式。 |
可以指定以下等待模式之一。同一管道的不同实例可以指定不同的等待模式。
模式 | 意义 |
---|---|
|
阻塞模式已启用。当在ReadFile、WriteFile或 ConnectNamedPipe函数中指定管道句柄时 , 直到有数据要读取、所有数据都已写入或客户端已连接时,操作才会完成。使用此模式可能意味着在某些情况下无限期地等待客户端进程执行操作。 |
|
非阻塞模式已启用。在这种模式下,ReadFile、WriteFile和 ConnectNamedPipe总是立即返回。
请注意,为了与 Microsoft LAN Manager 2.0 版兼容,支持非阻塞模式,并且不应使用命名管道实现异步 I/O。有关异步管道 I/O 的更多信息,请参阅 同步和重叠输入和输出。 |
nMaxInstances
可以为此管道创建的最大实例数。管道的第一个实例可以指定这个值;必须为管道的其他实例指定相同的编号。可接受的值在 1 到PIPE_UNLIMITED_INSTANCES (255)的范围内。
如果此参数为PIPE_UNLIMITED_INSTANCES,则可以创建的管道实例数量仅受系统资源可用性的限制。如果nMaxInstances大于PIPE_UNLIMITED_INSTANCES,则返回值为INVALID_HANDLE_VALUE并且GetLastError返回ERROR_INVALID_PARAMETER。
默认超时值(以毫秒为单位),如果 WaitNamedPipe函数指定NMPWAIT_USE_DEFAULT_WAIT。命名管道的每个实例都必须指定相同的值。
零值将导致默认超时为 50 毫秒
lpSecurityAttributes
指向SECURITY_ATTRIBUTES结构的指针,该 结构为新命名管道指定安全描述符并确定子进程是否可以继承返回的句柄
返回值:
如果函数成功,则返回值是命名管道实例的服务器端的句柄。
如果函数失败,则返回值为INVALID_HANDLE_VALUE。要获取扩展错误信息,请调用 GetLastError。
ConnectNamedPipe
BOOL ConnectNamedPipe( [in] HANDLE hNamedPipe, [in, LPOVERLAPPED lpOverlapped );
功能:允许命名管道服务器进程等待客户端进程连接到命名管道的实例。客户端进程通过调用 CreateFile或 CallNamedPipe函数进行连接。
hNamedPipe
命名管道实例的服务器端的句柄。此句柄由CreateNamedPipe函数返回 。
lpOverlapped
指向OVERLAPPED结构的指针 。
如果hNamedPipe是用 FILE_FLAG_OVERLAPPED 打开的,则lpOverlapped参数不能为NULL。它必须指向一个有效的OVERLAPPED结构。如果hNamedPipe使用 FILE_FLAG_OVERLAPPED 打开并且lpOverlapped为NULL,则该函数可能会错误地报告连接操作已完成。
如果hNamedPipe是用 FILE_FLAG_OVERLAPPED 创建的并且lpOverlapped不是NULL,则OVERLAPPED结构应该包含一个手动重置事件对象的句柄(服务器可以使用CreateEvent函数创建它 )。【非阻塞重叠IO,异步等待客户端连接】
如果未使用 FILE_FLAG_OVERLAPPED 打开hNamedPipe,则在连接客户端或发生错误之前,该函数不会返回。如果客户端在调用函数后连接,则成功的同步操作会导致函数返回非零值。【阻塞同步等待客户端连接】
返回值:
如果操作是同步的,则ConnectNamedPipe在操作完成之前不会返回。如果函数成功,则返回值非零。如果函数失败,则返回值为零。要获取扩展错误信息,请调用 GetLastError。
如果操作是异步的,ConnectNamedPipe 会立即返回。如果操作仍处于挂起状态,则返回值为零且GetLastError返回 ERROR_IO_PENDING。(您可以使用HasOverlappedIoCompleted宏来确定操作何时完成。)如果函数失败,则返回值为零,并且 GetLastError返回 ERROR_IO_PENDING 或 ERROR_PIPE_CONNECTED 以外的值。
如果客户端在调用函数之前连接,则函数返回零并且GetLastError返回 ERROR_PIPE_CONNECTED。如果客户端在调用CreateNamedPipe和调用 ConnectNamedPipe之间的时间间隔内进行连接,则会发生这种情况【举例:比如服务器端执行CreateNamePipe后,不立即执行ConnectionNamePipe,客户端随即执行CreateFile,客户端此时可以往里面写数据、然后再执行ConnectionNamePipe时函数返回零并且GetLastError返回 ERROR_PIPE_CONNECTED)】 。
WaitNamedPipe
BOOL WaitNamedPipeA( [in] LPCSTR lpNamedPipeName, [in] DWORD nTimeOut );
nTimeOut:
函数等待命名管道实例可用的毫秒数。您可以使用以下值之一而不是指定毫秒数。
价值 | 意义 |
---|---|
|
超时间隔是服务器进程在CreateNamedPipe函数中指定的默认值 。 |
|
在命名管道的实例可用之前,该函数不会返回。 |
返回值:
如果管道的实例在超时间隔过去之前可用,则返回值非零。
如果管道的实例在超时间隔过去之前不可用,则返回值为零。要获取扩展错误信息,请调用 GetLastError。
客户端同步写入,服务器端同步读取
服务器端读取:
#pragma warning(disable:4786) #include<Windows.h> #include <string> DWORD WINAPI ThreadProc(LPVOID); bool readMessage(HANDLE hTmpNamedPipe, std::string& message) { char recvBuffer[10001] = { 0 }; DWORD nBytesRead = 0; int ret = ReadFile( hTmpNamedPipe, recvBuffer, sizeof(recvBuffer), &nBytesRead, NULL ); if (0 >= ret) { printf("Read from client error: %d.\n", errno); return false; } else { message = static_cast<std::string>(recvBuffer); return true; } } DWORD WINAPI ThreadProc(LPVOID) { HANDLE hNamedPipe = CreateNamedPipe ("\\\\.\\pipe\\pipeTest", PIPE_ACCESS_INBOUND, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE, 3, 0, 0, 1000, // timeout in millseconds NULL); printf("\\\\.\\pipe\\pipeTest... \n"); if (NULL == hNamedPipe || INVALID_HANDLE_VALUE == hNamedPipe) { return NULL; } if (NULL == hNamedPipe) { return 0; } std::string message; while (true) { printf("strat to read from client...\n"); BOOL fConnected = ConnectNamedPipe(hNamedPipe, NULL); if (fConnected) { printf("connect success...\n"); while (readMessage(hNamedPipe, message)) { while (std::string::npos != message.find("|")) { std::string tmpSourceStr = message.substr(0, message.find("|")); if (tmpSourceStr.length() > 0) { printf("push message=%s in queue...\n", message.c_str()); } message = message.substr(message.find("|") + 1); } } } DisconnectNamedPipe(hNamedPipe); } return NULL; } int main() { HANDLE hThread1; DWORD threadId1; printf("another server is running\r\n"); hThread1 = CreateThread(NULL, 0, ThreadProc, 0, 0, &threadId1);//创建线程 while (true) { Sleep(30000); } return 0; }
当客户端没有实例接入时,服务器端在ConnectNamedPipe函数处阻塞等待客户端接入。
客户端写入
#include <iostream> #include <WinSock2.h> using namespace std; DWORD WINAPI ThreadProc(LPVOID); void Execute(); void ConnectANamePipe(); void ReconnectNamePipe(); HANDLE hThread; int main() { DWORD threadId; hThread = CreateThread(NULL, 0, ThreadProc, 0, 0, &threadId);//创建线程 std::cout << "Hello World!\n"; while (1) { Sleep(5000); } } void ConnectANamePipe() { if (!WaitNamedPipe(TEXT("\\\\.\\pipe\\pipeTest"), NMPWAIT_WAIT_FOREVER)) { cout << "conenct namepipe failed" << endl; } DWORD len; HANDLE hClientNamedPipe = CreateFile("\\\\.\\pipe\\pipeTest", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hClientNamedPipe == NULL || hClientNamedPipe == INVALID_HANDLE_VALUE) { printf("Connect to WDS ERROR %d...\n", GetLastError()); } else { while (true) { DWORD nBytesWritten = 0; printf("子线程,pid=%d...\n", GetCurrentThreadId()); char buff[256] = "i am cliect,hello server|"; /* if (WriteFile(hClientNamedPipe, buff, sizeof(buff), &nBytesWritten, &olWrite) == false) { printf("write error,getlasterror=%d...\n", GetLastError()); ReconnectNamePipe(); } else { printf("write success\n"); Sleep(500); }*/ int ret = (WriteFile(hClientNamedPipe, buff, sizeof(buff), &nBytesWritten, NULL)); if (0 == ret) { long int iError = GetLastError(); printf("write to server error=%d", iError); } printf("Write to server ok.\n"); Sleep(1000); } } CloseHandle(hClientNamedPipe); } DWORD WINAPI ThreadProc(LPVOID) { ConnectANamePipe(); return 0; } void ReconnectNamePipe() { CloseHandle(hThread); ConnectANamePipe(); }
客户端接入后,结果如下:
客户端异步写入,服务器端同步读取
// NamePipeClient.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <iostream> #include <WinSock2.h> DWORD WINAPI ThreadProc(LPVOID); void Execute(); void ConnectANamePipe(); void ReconnectNamePipe(); HANDLE hThread; int main() { DWORD threadId; hThread = CreateThread(NULL, 0, ThreadProc, 0, 0, &threadId);//创建线程 std::cout << "Hello World!\n"; while (1) { Sleep(5000); } } void ConnectANamePipe() { DWORD len; HANDLE hClientNamedPipe = CreateFile("\\\\.\\pipe\\pipeTest", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); if (hClientNamedPipe == NULL || hClientNamedPipe == INVALID_HANDLE_VALUE) { printf("Connect to WDS ERROR %d...\n", GetLastError()); } /*HANDLE hClientNamedPipe=CreateFile("\\\\.\\Pipe\\pipeTest",GENERIC_WRITE|GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL); if(hClientNamedPipe==INVALID_HANDLE_VALUE) { printf("error,getlasterror=%d\n",GetLastError()); }*/ //CreateFile连接管道成功后,服务器端ovlap.hEvent即可变为有信号 else { while (true) { OVERLAPPED olWrite; memset(&olWrite, 0, sizeof(olWrite)); olWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);//创建了一个初始状态为FALSE的手动(人工)重置的事件对象,当手动重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程。 DWORD nBytesWritten = 0; printf("子线程,pid=%d...\n", GetCurrentThreadId()); char buff[256] = "i am cliect,hello server|"; /* if (WriteFile(hClientNamedPipe, buff, sizeof(buff), &nBytesWritten, &olWrite) == false) { printf("write error,getlasterror=%d...\n", GetLastError()); ReconnectNamePipe(); } else { printf("write success\n"); Sleep(500); }*/ int ret = (WriteFile(hClientNamedPipe, buff, sizeof(buff), &nBytesWritten, &olWrite)); printf(" Write to server return : %d.\n", ret); if (0 == ret) { long int iError = GetLastError(); if (iError != ERROR_IO_PENDING /*997*/)//997表示这个请求是悬而未决的 { printf("a Write to server Error : %d.\n", GetLastError()); ReconnectNamePipe(); return; } } if (0 == GetOverlappedResult(hClientNamedPipe, &olWrite, &nBytesWritten, true)) { printf("Write to server Error : %d.\n", GetLastError()); ReconnectNamePipe(); return; } else { printf("Write to server ok.\n"); Sleep(1000); } } } CloseHandle(hClientNamedPipe); } DWORD WINAPI ThreadProc(LPVOID) { ConnectANamePipe(); return 0; } void ReconnectNamePipe() { CloseHandle(hThread); ConnectANamePipe(); }
服务器端异步
#pragma warning(disable:4786) #include<Windows.h> #include <string> DWORD WINAPI ThreadProc(LPVOID); bool readMessage(HANDLE hTmpNamedPipe, std::string& message) { char recvBuffer[10001] = { 0 }; DWORD nBytesRead = 0; int ret = ReadFile( hTmpNamedPipe, recvBuffer, sizeof(recvBuffer), &nBytesRead, NULL ); if (0 >= ret) { printf("Read from client error: %d.\n", errno); return false; } else { message = static_cast<std::string>(recvBuffer); return true; } } DWORD WINAPI ThreadProc(LPVOID) { HANDLE hNamedPipe = CreateNamedPipe ("\\\\.\\pipe\\pipeTest", PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE, 3,//pipeTest管道最大实例数 0, 0, 1000, // timeout in millseconds NULL); printf("\\\\.\\pipe\\pipeTest... \n"); if (NULL == hNamedPipe || INVALID_HANDLE_VALUE == hNamedPipe) { return NULL; } printf("waiting for signal\n"); while (true) { HANDLE hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);//初始状态为TRUE的人工重置对象 if (INVALID_HANDLE_VALUE == hEvent) { return NULL; } OVERLAPPED ovlap; ZeroMemory(&ovlap, sizeof(OVERLAPPED)); ovlap.hEvent = hEvent;//将一个事件与重叠I/O绑定 std::string message; BOOL fPendingIO = FALSE; DWORD dwWait; printf("strat to read from client...\n"); BOOL fConnected = ConnectNamedPipe(hNamedPipe, &ovlap); if (fConnected) { printf("ConnectNamedPipe failed with %d.\n", GetLastError()); return 0; } switch (GetLastError()) { // The overlapped connection in progress. case ERROR_IO_PENDING: printf("ConnectNamedPipe status fPendingIO.\n"); fPendingIO = true; break; // Client is already connected, so signal an event. case ERROR_PIPE_CONNECTED: if (SetEvent(hEvent)) break; // If an error occurs during the connect operation... default: { printf("ConnectNamedPipe failed with %d.\n", GetLastError()); return 0; } } /* 阻塞状态下,等待管道另一端的连入,当一个连接到来的时候,ovlap.hEvent会立即变为有信号状态 */ dwWait = WaitForSingleObject(ovlap.hEvent, INFINITE); DWORD dwTransBytes = -1; switch (dwWait) { case 0: if (fPendingIO) { //获取Overlapped结果 if (GetOverlappedResult(hNamedPipe, &ovlap, &dwTransBytes, TRUE) == FALSE) { printf("ConnectNamedPipe failed %d\n", GetLastError()); return -1; } } // 读写完成 case WAIT_IO_COMPLETION: { while (readMessage(hNamedPipe, message)) { while (std::string::npos != message.find("|")) { std::string tmpSourceStr = message.substr(0, message.find("|")); if (tmpSourceStr.length() > 0) { printf("push message=%s in queue...\n", message.c_str()); } message = message.substr(message.find("|") + 1); } } break; } } DisconnectNamedPipe(hNamedPipe); } } int main() { HANDLE hThread1; DWORD threadId1; /*HANDLE hThread2; DWORD threadId2;*/ printf("another server is running\r\n"); hThread1 = CreateThread(NULL, 0, ThreadProc, 0, 0, &threadId1);//创建线程 //hThread2 = CreateThread(NULL, 0, ThreadProc, 0, 0, &threadId2);//创建线程 while (true) { Sleep(30000); } return 0; }
说明:服务器端事件同步使用了HANDLE hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);创建了初始状态为TRUE的人工重置对象。
在使用CreateNamedPipe(FILE_FLAG_OVERLAPPED)时,使用ConnectNamedPipe(h, &op)后,会立即返回(非阻塞、异步的),这个时候一般是返回FALSE,使用GetLastError()会得到ERROR_IO_PENDING,表示这个请求是悬而未决的。我使用一个BOOL fPendingIO标识来记录所有悬而未决的的请求,fPendingIO=TRUE。然后使用WaitForMultipleObject方法等待这个事件。线程就会在此处阻塞。我们通过实际现象也可以看出,当客户端未建立连接时,服务器端在WaitForMultipleObject处等待事件变为有信号状态,继续往下执行。
现在来解释一下为什么开始创建事件时初始态为signaled。按照常理,WaitForMultipleObjects不会被阻塞,因为其中一个事件的状态为signaled。其实不然,它的状态在connectNamedPipe(h, &op)后已经改变了。对以OVERLAPPED关联的事件,当使用OVERLAPPED相关的方法操作后,其状态会可能会改变的,主要基于下面3个原则:1)当实际操作在函数返回前已经完成,事件的状态不会改变。2)当函数返回是,实际的操作没有完成,也即是说这个操作是Pending的,这个时候事件会被设置为nonsignaled.3) 当操作的Pending完成后,事件会被设置为signaled。有了上面的3条原则,OVERLAPPED关联的事件的状态变化就好理解了。当使用connectNamedPipe(h, &op)方法时,函数会立即返回,而实际这个操作并没有进行,而是Pending了,所以,event会由signaled变为nonsignaled,当真正有Client连接时,这个操作才会完成,这个时候,event会由nonsignaled变为signaled。这个时候,WaitForMultipleObject会继续执行下去。对于Pending后的操作,一定要使用GetOverlappedResult方法,判断结果。上面的原则适用ReadFile, WriteFile, ConnectNamedPipe, 和 TransactNamedPipe等函数。
参考:https://www.cnblogs.com/xtfnpgy/p/9285448.html#top