分享

Windows命名管道与应用

 凌蓝苑 2015-04-14

一 关于命名管道

概述

管道(Pipe)实际是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机。命名管道(Named Pipes)是在管道服务器和一台或多台管道客户机之间进行单向或双向通信的一种命名的管道。一个命名管道的所有实例共享同一个管道名,但是每一个实例均拥有独立的缓存与句柄,并且为客户——服务通信提供有一个分离的管道。实例的使用保证了多个管道客户能够在同一时间使用同一个命名管道。

Microsoft Windows NT、Windows 2000、Windows 95以及Windows 98均提供对命名管道的支持(不包括Windows CE),但只有Windows NT和Windows 2000才支持服务器端的命名管道技术。命名管道可以在同一台计算机的不同进程之间,或在跨越一个网络的不同计算机的不同进程之间进行有连接的可靠数据通信,如果连接中断,连接双方都能立即收到连接断开的信息。命令管道是围绕Windows文件系统而设计的一种机制,采用的是命名管道文件系统(Named Pipe File System, NPFS)接口。对数据的收发也采用文件读写函数ReadFile()和WriteFile()来完成。在设计上,由于命名管道也利用了微软网络提供者(MSNP)重定向器,因此无需涉及底层的通信协议细节。命名管道还充分利用了Windows NT及Windows 2000内建的安全特性,通信的安全性相对较好。

命名规范

每一个命名管道都有一个唯一的名字以区分于存在于系统的命名对象列表中的其他命名管道。管道服务器在调用CreateNamedPipe()函数创建命名管道的一个或多个实例时为其指定了名称。对于管道客户机,则是在调用CreateFile()或CallNamedPipe()函数以连接一个命名管道实例时对管道名进行指定。命名管道的命名规范与邮槽有些类似,对其标识也是采用的UNC格式:\\ServerName\pipe\PipeName    
其中,第一部分\\ServerName指定了服务器的名字,命名管道服务即在此服务器创建,其字串部分可表示为一个小数点(表示本机)、星号(当前网络字段)、域名或是一个真正的服务管道名字长度可以达到256字符;第二部分\Pipe与邮槽的\Mailslot一样是一个不可变化的硬编码字串,以指出该文件是从属于NPFS;第三部分PipeName则使应用程序可以唯一定义及标识一个命名管道的名字,而且可以设置多级目录。

命名管道打开模式

管道服务器端在调用CreateNamedPipe时可以dwOpenMode 通过指定管道的access,overlap以及write-through模式,相应的命名管道客户端在使用CreateFile可以指定相应的模式。具体详见:http://msdn.microsoft.com/en-us/library/windows/desktop/aa365596(v=vs.85).aspx

命名管道的类型及读,等待模式

命名管道提供了两种基本的通信模式:字节模式和消息模式。可在CreateNamePipe()创建命名管道时对dwPipeMode 参数分别用PIPE_TYPE_BYTE和PIPE_TYPE_MESSAGE标志进行设定。在字节模式中,信息以连续字节流的形式在客户与服务器之间流动。这也就意味着,对于客户机应用和服务器应用,在任何一个特定的时间段内,都无法准确知道有多少字节从管道中读出或写入。在这种通信模式中,一方在向管道写入某个数量的字节后,并不能保证管道另一方能读出等量的字节。对于消息模式,客户机和服务器则是通过一系列不连续的数据包进行数据的收发。从管道发出的每一条消息都必须作为一条完整的消息读入。

管道客户端和服务器均可以通过SetNamedPipeHandleState 改变管道句柄的读模式(PIPE_READMODE_MESSAGE, PIPE_READMODE_BYTE ) 及 等待模式(PIPE_NOWAIT, PIPE_WAIT)。

二 命名管道的使用

概要

管道服务器首次调用CreateNamedPipe()函数时,使用nMaxInstance参数指定了能同时存在的管道实例的最大数目。服务器可以重复调用CreateNamedPipe()函数去创建管道新的实例,直至达到设定的最大实例数。下面给出CreateNamedPipe()的函数原型:

HANDLE CreateNamedPipe(            
LPCTSTR lpName, // 指向管道名称的指针              
DWORD dwOpenMode, // 管道打开模式              
DWORD dwPipeMode, // 管道模式              
DWORD nMaxInstances, // 最大实例数              
DWORD nOutBufferSize, // 输出缓存大小              
DWORD nInBufferSize, // 输入缓存大小              
DWORD nDefaultTimeOut, // 超时设置              
LPSECURITY_ATTRIBUTES lpSecurityAttributes // 安全属性指针              
);

如果在已定义超时值变为零以前,有一个实例管道可以使用,则创建成功并返回管道句柄,以此侦听来自客户机的连接请求。另一方面,客户机通过函数WaitNamedPipe()使服务器进程等待来自客户的实例连接。如果在超时值变为零以前,有一个管道可供连接使用,则函数将成功返回,并通过调用CreateFile()或CallNamedPipe()来呼叫对服务器的连接。此时服务器将接受客户的连接请求,成功建立连接,服务器调用的等待客户机建立连接的ConnectNamedPipe()函数也将成功返回。

从调用时序上看,首先是客户机通过WaitNamedPipe()使服务器的CreateFile()在限时时间内创建实例成功,然后双方通过ConnectNamedPipe()和CreateFile()成功连接,在返回用以通信的文件句柄后,客户、服务双方即可进行通信。

在建立了连接后,客户机与服务器即可通过ReadFile()和WriteFile()并利用得到的管道句柄,以文件读写的形式彼此间进行信息交换。 当客户与服务器的通信结束,或是由于某种原因一方需要断开时,由客户机调用CloseFile()函数关闭打开的管道句柄,服务器随即调用DisconnectNamedPipe()函数。当然,服务器也可以通过单方面调用DisconnectNamedPipe()来终止连接。在终止连接后调用函数CloseHandle()来关闭此管道。

应用实例一(多线程命名管道服务器)

概要: 主线程创建一个命名管道实例,并等待命名管道客户端建立连接,当命名管道客户端建立连接时,管道服务器创建一个线程为客户端服务。

管道服务器代码概要:

首先获取主线程的真正的句柄值

dwRet = ::DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &hMainThread, 0, FALSE, DUPLICATE_SAME_ACCESS);

创建退出线程,并将主线程真正的句柄作为参数:

HANDLE hExitThread = reinterpret_cast<HANDLE>(_beginthreadex(NULL, uiDefStackSize, ExitThread, hMainThread, 0, NULL) );

主循环中先通过SleepEx将主循环至于Alertable状态,这样在退出线程中就可以通过QueueUserAPC通知主线程退出,创建命名管道实例,并通过CreateNamedPipe阻塞等待客户端连接:

while(true)

{

dwRet = SleepEx(1, TRUE);

if( dwRet == WAIT_IO_COMPLETION )

{

break;

}

hPipe = CreateNamedPipe(g_namedpipe_name,

PIPE_ACCESS_DUPLEX,

PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE |PIPE_WAIT,

PIPE_UNLIMITED_INSTANCES,

BUFSIZ,

BUFSIZ,

0,

NULL);

if( hPipe == INVALID_HANDLE_VALUE )

{

printf(" create named pipe failed, error = %d\n", GetLastError() );

break;

}

g_hPipe = hPipe;

// Wait for the client to connect; if it succeeds,

// the function returns a nonzero value. If the function

// returns zero, GetLastError returns ERROR_PIPE_CONNECTED.

bConnected = ConnectNamedPipe(hPipe, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);

if( bConnected )

{

// Create a thread for this client.

hThread = reinterpret_cast<HANDLE>( _beginthreadex(NULL, uiDefStackSize, WorkerThread, (LPVOID)hPipe, 0, NULL ) );

if (hThread == NULL)

{

printf("CreateThread failed, error = %d\n", GetLastError() );

return 0;

}

else

{

//detached thread

CloseHandle(hThread);

}

}

else

{

// The client could not connect, so close the pipe.

CloseHandle(hPipe);

}

}

在退出线程中的循环:

while(true)

{

std::getline(std::cin, name);

if ( name == "quit" )

{

///@note: first we should cancel the synchronous io(connectnamedpipe) function

CancelSynchronousIo(hMainThread);

///@note: and then we should notify the main thread to exit.

QueueUserAPC( APCProc, hMainThread, NULL );

break;

}

else

continue;

}

在用户输入quit后,首先CancelSynchronousIo取消所有的阻塞IO操作,然后QueueUserAPC通知主线程退出。

应用实例二(命名管道通过Event实现异步IO)

详见http://msdn.microsoft.com/en-us/library/windows/desktop/aa365603(v=vs.85).aspx

应用实例三(命名管道通过完成端口实现异步IO)

概要: 管道服务器首先创建完成端口对象,然后创建多个Pipe实例,将这些实例与完成端口关联,并通过调用ConnectNamedPipe ReadFile,WriteFile等接口,传递OVERLAPPED非空对象参数提交异步请求,在主线程循环中通过GetQueuedCompletionStatus获取提交的异步请求的结果。

管道服务器代码概要:

创建完成端口:

::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 1);

创建退出线程,退出线程收到用户输入的‘quit’后,通过PostQueuedCompletionStatus通知完成端口线程池退出,因完成端口线程池数量为1,所以只需要调用一次:

HANDLE hExitThread = reinterpret_cast<HANDLE>(_beginthreadex(NULL, uiDefStackSize, ExitThread, NULL, 0, NULL) );

通知主线程退出:

std::getline(std::cin, name);

if ( name == "quit" )

{

::PostQueuedCompletionStatus(hIocpPort, 0, 0, (LPOVERLAPPED)&exitOverlapped);

break;

}

在主函数中创建一定数量的管道设备对象,并与完成端口进行关联,并通过ConnectNamedPipe提交异步接收客户端连接请求:

for ( DWORD i = 0; i < INSTANCES; i++ )

{

Pipe[i].hPipeInst = CreateNamedPipe(

lpszPipename, // pipe name

PIPE_ACCESS_DUPLEX | // read/write access

FILE_FLAG_OVERLAPPED, // overlapped mode

PIPE_TYPE_MESSAGE | // message-type pipe

PIPE_READMODE_MESSAGE | // message-read mode

PIPE_WAIT, // blocking mode

INSTANCES, // number of instances

BUFSIZE*sizeof(TCHAR), // output buffer size

BUFSIZE*sizeof(TCHAR), // input buffer size

PIPE_TIMEOUT, // client time-out

NULL); // default security attributes

if (Pipe[i].hPipeInst == INVALID_HANDLE_VALUE)

{

printf("CreateNamedPipe failed with %d.\n", GetLastError());

return 0;

}

::CreateIoCompletionPort(Pipe[i].hPipeInst, hIocpPort, i, 0);

// Call the subroutine to connect to the new client

bSuccess = ConnectToNewClient( &Pipe[i] );

if ( bSuccess == FALSE )

{

CloseHandle( Pipe[i].hPipeInst );

}

}

完成端口线程为主线程,通过调用GetQueuedCompletionStatus获取异步请求的结果,根据提交请求时的dwState确定请求的种类,如果为CONNECTING_STATE说明有客户请求,处理该请求并通过ReadFile或WriteFile提交pipe读写请求:

while (1)

{

bSuccess = GetQueuedCompletionStatus( hIocpPort, &dwNumberOfBytes, &ulCompletionKey, (LPOVERLAPPED *)&pOverlapped, INFINITE );

///if accepted exit state, then exit the main thread.

if ( pOverlapped != NULL && pOverlapped->dwState == EXITING_STATE )

{

break;

}

OPRESULT opResult = {0};

opResult.bResult = bSuccess;

opResult.dwBytesTransferred = dwNumberOfBytes;

opResult.ulCompletionKey = ulCompletionKey;

OnOperationComplete(opResult, pOverlapped);

}

 
命名管道客户端

管道客户端首先通过CreateFile传入管道实例名称连接管道服务器,如果服务器忙碌无法建立连接,通过WaitNamedPipe等待超时或直到命名管道有可用的连接。通过WriteFile和ReadFile阻塞的写入和读取管道的内容;

与命名管道服务器建立连接:

while (1)

{

hPipe = CreateFile(

g_namedpipe_name, // pipe name

GENERIC_READ | // read and write access

GENERIC_WRITE,

0, // no sharing

NULL, // default security attributes

OPEN_EXISTING, // opens existing pipe

0, // default attributes

NULL); // no template file

// Break if the pipe handle is valid.

if ( hPipe != INVALID_HANDLE_VALUE )

break;

// Exit if an error other than ERROR_PIPE_BUSY occurs.

if ( GetLastError() != ERROR_PIPE_BUSY )

{

_tprintf( TEXT("Could not open pipe. GLE=%d\n"), GetLastError() );

return -1;

}

// All pipe instances are busy, so wait for 20 seconds.

if ( ! WaitNamedPipe(g_namedpipe_name, 20000) )

{

printf("Could not open pipe: 20 second wait timed out.");

return -1;

}

}

发送数据到管道缓冲区:

fSuccess = WriteFile(

hPipe, // pipe handle

lpvMessage, // message

cbToWrite, // message length

&cbWritten, // bytes written

NULL); // not overlapped

循环读取命名管道缓冲区中的内容:

do

{

// Read from the pipe.

chBuf[BUFSIZ-1] = _T('\0');

fSuccess = ReadFile(

hPipe, // pipe handle

chBuf, // buffer to receive reply

(BUFSIZ - 1)*sizeof(TCHAR), // size of buffer

&cbRead, // number of bytes read

NULL); // not overlapped

if ( ! fSuccess && GetLastError() != ERROR_MORE_DATA )

{

printf("ReadFile error is:%d\n", GetLastError() );

break;

}

_tprintf( TEXT("%s"), chBuf );

} while ( ! fSuccess); // repeat loop if ERROR_MORE_DATA

使用完之后调用CloseHandle关闭和释放管道设备;

End

代码见:http://www.oschina.net/code/snippet_102078_24050

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多