Windows进程间的通信-命名管道

本文详细介绍了Windows进程间的通信机制——命名管道,包括服务端与客户端的创建与连接,以及相关的辅助函数。命名管道可用于同一台电脑或不同电脑间的进程通信,支持单向或双向通信,并可以通过多种方式实现并发操作。文章还提供了简单的服务端和客户端示例代码,以及如何查看当前系统中的命名管道。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

命名管道是一种命名的可以在管道服务器进程与管道客户进程进行单向或双向通信的一种机制。命名管道即可以是基于消息的也可以是基于字节流的。命名管道既可以实现同一台电脑上的进程间的通信,也可以实现基于网络的不同电脑上的进程间的通信,当然同一个进程即可以是服务端也可以是客户端。一个命名管道的多个实例可以共享同一个管道名称,但每个实例具有独立的缓冲区和句柄,因此每个实例可以同时与不同管道客户端进行通信而不互相影响,但是同一时刻,同一个实例只能与一个客户端进行通信。

命名管道服务端

服务端通过CreateNamePipe创建命名管道,以下是其函数声明

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个字符。
  • dwOpenMode表示打开模式,该参数必须设置如下三个互斥标志中的一个:PIPE_ACCESS_DUPLEX(管道既可以读又可以写),PIPE_ACCESS_INBOUND(管道只能读不能写),PIPE_ACCESS_OUTBOUND(管道只能写不能读)。该参数还可以包含如下标志中的一个或多个:FILE_FLAG_FIRST_PIPE_INSTANCE (表示只有第一个管道实例可以创建成功,即使nMaxInstances参数设置为多个),FILE_FLAG_WRITE_THROUGH(此标志只对基于网络的字节流模式的管道起作用,当设置此标志时,当程序向管道写数据时,只有当所有数据写入到远端缓存区时才返回),FILE_FLAG_OVERLAPPED (是否启用重叠IO又叫异步IO,当此标志设置时,程序可以同一个线程并发访问管道的多个实例,也可以在一个管道实例上同时执行读和写操作)。多个标志用”|”分割。
  • dwPipeMode用来设置管道模式,该参数必须包含如下互斥标志中的一个:PIPE_TYPE_BYTE (表示管道是基于字节流的),PIPE_TYPE_MESSAGE (表示管道是基于消息的)。该参数还可以设置如下两组互斥标志中的一个或多个:PIPE_READMODE_BYTE ,PIPE_READMODE_MESSAGE ,表示管道读取模式是基于消息还是基于字节流的,PIPE_READMODE_MESSAGE只能与PIPE_TYPE_MESSAGE一起使用; PIPE_WAIT,PIPE_NOWAIT,表示管道读取,写入与等待连接是是否阻塞,PIPE_NOWAIT表示不阻塞,因为实现管道异步有更好的方法(重叠IO),所以这个标志总是设置为PIPE_WAIT。
  • nMaxInstances表示最多可以创建同名管道的多少个实例,不能超过255,也可以设置为PIPE_UNLIMITED_INSTANCES,由系统根据自身资源来决定最多能创建多少个实例。一般有三种方式实现同名管道的多个实例的并发操作:1、为每一个连接上的实例创建一个线程执行读写,2、采用重叠结构来实现,3、采用完成端口来实现。
  • nOutBufferSize输入缓冲区的大小
  • nInBufferSize输入缓冲区的大小
  • nDefaultTimeOut客户端连接时的默认超时时间,只有客户端调用WaitNamePipe函数并将nTimeOut参数设置为NMPWAIT_USE_DEFAULT_WAIT起作用,当设置为0时,表示为50ms
  • lpSecurityAttributes设置基于ACL访问控制属性。

    返回值为管道实例句柄。

    服务端通过ConnectNamedPipe函数等待客户端连接,函数声明如下:

BOOL WINAPI ConnectNamedPipe(
  _In_        HANDLE       hNamedPipe,
  _Inout_opt_ LPOVERLAPPED lpOverlapped
);
  • hNamePipe管道实例句柄
  • lpOverlapped 重叠IO结构,当打开模式设置为FILE_FLAG_OVERLAPPED 有效。

    如果成功返回值为True,如果启用重叠IO结构当返回为FALSE,但是调用GetLastError返回值为ERROR_IO_PENDING也表示成功。

通过ReadFile,ReadFileEx函数来读取管道,通过WriteFile,WirteFileEx函数来写入管道。

以下为一个不使用重叠结构的单实例的管道服务端代码:

#include <windows.h>
#include <tchar.h>
#include <iostream>
using namespace std;

LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe");
const int nBufferSize = 1024;
int main()
{
    // 创建命名管道
    HANDLE hPipe = CreateNamedPipe(
        lpszPipename, PIPE_ACCESS_DUPLEX,
        PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
        PIPE_UNLIMITED_INSTANCES, nBufferSize, nBufferSize, 0, NULL);
    if (hPipe == INVALID_HANDLE_VALUE)
    {
        cout << "CreateNamedPipe failed:" << GetLastError() << endl;
        return -1;
    }

    // 等待客户端连接
    BOOL bRet = ConnectNamedPipe(hPipe, NULL);
    if (!bRet)
    {
        cout << "ConnectNamedPipe failed:" << GetLastError() << endl;
        CloseHandle(hPipe);
        return -2;
    }

    // 读取管道
    DWORD dwBytesRead = 0;
    TCHAR *pszRecvBuf = new TCHAR[nBufferSize];
    bRet = ReadFile(hPipe, 
        pszRecvBuf, // 接收缓存区
        nBufferSize * sizeof(TCHAR), // 接收缓冲区大小
        &dwBytesRead,  // 读取到的字节数 
        NULL);         // 不用重叠结构
    if (!bRet || dwBytesRead == 0)
    {
        cout << "ReadFile failed:" << GetLastError() << endl;
        CloseHandle(hPipe);
        return -3;
    }
    delete [] pszRecvBuf;

    DWORD dwBytesWirte = 0;
    TCHAR szWriteBuf[] = L"OK";
    // 写入管道
    bRet = WriteFile(hPipe, 
        szWriteBuf,  // 写入数据缓冲区     
        sizeof(szWriteBuf), // 要写入的字节数 
        &dwBytesWirte,   // 写入的数据
        NULL); // 不用重叠结构

    if (!bRet || dwBytesWirte == 0)
    {
        cout << "WriteFile failed:" << GetLastError() << endl;
        CloseHandle(hPipe);
        return -4;
    }

    FlushFileBuffers(hPipe);    // 保证客户端完全读取管道中的内容
    DisconnectNamedPipe(hPipe); // 断开管道连接
    CloseHandle(hPipe);         // 关闭管道

    return 0;
}

命名管道客户端

客户端使用CreateFile函数来创建管道连接,如果管道是在本地机器上管道名称与服务端管道名称一致,如果是在远端机器上管道名称需要设置为\server\pipe\pipename(server为远端IP地址)。
客户端使用WaitNamedPipe函数检测服务端管道是否有实例可以连接,函数声明如下

BOOL WINAPI WaitNamedPipe(
  _In_ LPCTSTR lpNamedPipeName, // 管道名称
  _In_ DWORD   nTimeOut 
);
  • lpNamedPipeName管道名称
  • nTimeOut检测超时时间,当设置为NMPWAIT_USE_DEFAULT_WAIT 是,则会使用服务端管道创建时的设置默认超时时间(nDefaultTimeOut参数),当设置为NMPWAIT_WAIT_FOREVER 表示永久等待,知道服务端有可用实例为止,这里需要注意如果服务端管道尚未创建(也就是说至少存在一个实例),该函数会立即返回,而不管nTimeOut设置。

客户端通过SetNamedPipeHandleState函数设置管道句柄的模式,函数声明如下:

BOOL WINAPI SetNamedPipeHandleState(
  _In_     HANDLE  hNamedPipe,
  _In_opt_ LPDWORD lpMode,
  _In_opt_ LPDWORD lpMaxCollectionCount,
  _In_opt_ LPDWORD lpCollectDataTimeout
);

  • hNamedPipe表示管道句柄
  • lpMode管道的模式可以设置为PIPE_TYPE_BYTE (表示管道是基于字节流的),PIPE_TYPE_MESSAGE (表示管道是基于消息的),要与服务端保持一致
  • lpMaxCollectionCount,只有服务端在远程机器上起作用,表示客户端缓冲区最多积攒多少个字节,就必须向外发送一次,一般都设置为NULL
  • lpCollectDataTimeout,只有服务端在远程机器上才起作用,表示向远端机器穿输信息时,必须在多长时间内完成,一般都设置为NULL

客户端也通过ReadFile,ReadFileEx函数来读取管道,通过WriteFile,WirteFileEx函数来写入管道。

以下为一个不使用重叠结构的客户端管道例子:

#include <windows.h>
#include <tchar.h>
#include <iostream>
using namespace std;

LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe");
const int nBufferSize = 1024;

int main()
{
    HANDLE hPipe = INVALID_HANDLE_VALUE;
    while (true)
    {
        // 创建连接管道的句柄
        HANDLE hPipe = CreateFile(lpszPipename,
            GENERIC_READ | GENERIC_WRITE,   // 管道可读可写
            0,                              // 共享模式,0表示不共享
            NULL,                           // 默认的访问控制权限    
            OPEN_EXISTING,                  // 打开已经存在的管道
            0,                              // 默认的文件属性 
            NULL);                          // 不设置临时模板文件,只有创建新文件时有用
        if (hPipe != INVALID_HANDLE_VALUE)
            break;

        // 如果返回错误不是ERROR_PIPE_BUSY
        if (GetLastError() != ERROR_PIPE_BUSY) 
        {
            cout << "open pipe faild:" << GetLastError() << endl;
            return -1;
        }
        WaitNamedPipe(lpszPipename, NMPWAIT_WAIT_FOREVER);
    }

    DWORD dwMode = PIPE_READMODE_MESSAGE;
    BOOL bRet = SetNamedPipeHandleState(hPipe, &dwMode,  NULL,  NULL);  
    if (!bRet)
    {
        cout << "SetNamedPipeHandleState faild:" << GetLastError() << endl;
        return -1;
    }

    // 写入管道
    DWORD dwBytesWirte = 0;
    TCHAR szWriteBuf[] = L"Hello";
    bRet = WriteFile(hPipe,
        szWriteBuf,  // 写入数据缓冲区     
        sizeof(szWriteBuf), // 要写入的字节数 
        &dwBytesWirte,   // 写入的数据
        NULL); // 不用重叠结构

    if (!bRet || dwBytesWirte == 0)
    {
        cout << "WriteFile failed:" << GetLastError() << endl;
        CloseHandle(hPipe);
        return -3;
    }

    // 读取管道
    DWORD dwBytesRead = 0;
    TCHAR *pszRecvBuf = new TCHAR[nBufferSize];
    bRet = ReadFile(hPipe,
        pszRecvBuf, // 接收缓存区
        nBufferSize * sizeof(TCHAR), // 接收缓冲区大小
        &dwBytesRead,  // 读取到的字节数 
        NULL);         // 不用重叠结构
    if (!bRet || dwBytesRead == 0)
    {
        cout << "ReadFile failed:" << GetLastError() << endl;
        CloseHandle(hPipe);
        return -4;
    }
    _tprintf(TEXT("%s\n"), pszRecvBuf);
    delete[] pszRecvBuf;

    CloseHandle(hPipe);
    return 0;
}

为了方便使用windows还提供了两个简化函数来完成一次管道读取:
TransactNamedPipe函数将WriteFile与ReadFile当成是一次客户事务,一次完成写入数据和读取数据,函数声明如下:

BOOL WINAPI TransactNamedPipe(
  _In_        HANDLE       hNamedPipe,  // 管道句柄
  _In_        LPVOID       lpInBuffer,  // 要写入的数据缓存
  _In_        DWORD        nInBufferSize, // 要写入的数据大小,以字节为单位
  _Out_       LPVOID       lpOutBuffer,   // 读取数据缓存
  _In_        DWORD        nOutBufferSize, // 读取数据缓存大小
  _Out_       LPDWORD      lpBytesRead, // 实际读取的字节数
  _Inout_opt_ LPOVERLAPPED lpOverlapped // 重叠IO结构
);

CallNamedPipe将CreateFile,WaitNamedPipe,WriteFile,ReadFile,CloseHandle当成是一次客户事务,一次完成管道打开,写入,读取,和关闭,函数声明如下:

BOOL WINAPI CallNamedPipe(
  _In_  LPCTSTR lpNamedPipeName, // 管道名称
  _In_  LPVOID  lpInBuffer, // 吸入数据缓存
  _In_  DWORD   nInBufferSize, // 写入数据缓存大小
  _Out_ LPVOID  lpOutBuffer, // 读取数据缓存
  _In_  DWORD   nOutBufferSize, // 读取数据缓存大小
  _Out_ LPDWORD lpBytesRead, // 实际读取的字节数
  _In_  DWORD   nTimeOut // 超时设置
);

注意CallNamePipe是同步的,不支持重叠结构。

其他的一些辅助函数

  1. GetNamedPipeClientComputerName:获取客户端机器名称
  2. GetNamedPipeClientProcessId:获取客户进程ID
  3. GetNamedPipeClientSessionId:获取客户进程所属会话
  4. GetNamedPipeHandleState:获取管道状态信息,例如管道是基于消息的还是基于字节流的,管道是阻塞的还是非阻塞的,管道实例的数量
  5. GetNamedPipeInfo:获取管道信息,例如输入输出缓冲区大小,最大实例数量
  6. GetNamedPipeServerProcessId:获取服务端进程ID
  7. GetNamedPipeServerSessionId:获取服务端回话ID

查看当前机器上都有哪些命名管道

windows在SysinternalsSuite工具包中提供了pipelist.exe工具来查看当前电脑上有哪些命名管道:
这里写图片描述
其中Pipe Name列显示管道名称,Instances显示此管道当前存在多少个实例,Max Instances表示此管道最多可以创建多少个实例,-1表示系统根据自身资源来决定,也就是在调用CreateNamedPipe函数中的nMaxInstances设置为PIPE_UNLIMITED_INSTANCES。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值