4人参与 • 2026-10-10 • Windows
windows下用c++做管道编程,主要分匿名管道和命名管道两条路,选哪个取决于你的场景:父子进程间传数据用匿名管道,两个独立进程通信用命名管道。下面直接给可上手的实战要点和关键代码。
管道(pipe)可以理解成一个特殊的"内存文件":一端往里写,另一端往外读,数据先进先出(fifo)。它是 windows 上最经典的进程间通信(ipc)手段之一,读写操作和文件一样用 readfile / writefile,学习成本极低。
windows 提供两类管道:
| 命名管道 named pipe | 匿名管道 anonymous pipe | |
|---|---|---|
| 有没有名字 | 有,\\.\pipe\xxx | 没有 |
| 谁能用 | 任意进程,甚至跨机器 | 只能父子进程(靠句柄继承) |
| 方向 | 双向(也可配置成单向) | 单向,双向要两根 |
| 数据模式 | 字节模式 / 消息模式可选 | 只有字节模式 |
| 创建 api | createnamedpipe + connectnamedpipe | createpipe |
这篇文章的主角是命名管道。
先看最经典的入门例子:服务端创建管道等待连接,客户端连上后发消息,服务端收到后回复(回声)。为了中文控制台输出不乱码,示例里有两行 _setmode(_fileno(...), _o_u16text) 设置,后面代码沿用不再解释;所有示例默认在 vs 的 unicode 配置下编译(新建项目的默认值)。
// server.cpp — 最简命名管道服务端
#include <windows.h>
#include <iostream>
#include <string>
#include <io.h> // _setmode
#include <fcntl.h> // _o_u16text
const wchar_t* pipe_name = l"\\\\.\\pipe\\mylearningpipe";
int main()
{
_setmode(_fileno(stdout), _o_u16text); // 控制台中文设置, 见上文说明
// 1. 创建管道: 双向 + 消息模式
handle hpipe = createnamedpipe(
pipe_name,
pipe_access_duplex, // 双向读写
pipe_type_message | pipe_readmode_message, // 消息模式(见第五节)
1, // 最多 1 个客户端
4096, 4096, 0, null);
// 2. 阻塞等待客户端连接
connectnamedpipe(hpipe, null);
// 3. 收发循环: 读一条 -> 回一条, 直到收到 "quit"
wchar_t buf[1024];
dword n;
while (readfile(hpipe, buf, sizeof(buf) - sizeof(wchar_t), &n, null) && n > 0)
{
buf[n / sizeof(wchar_t)] = l'\0';
std::wstring msg(buf);
std::wcout << l"[收到] " << msg << std::endl;
if (msg == l"quit") break;
std::wstring reply = l"服务端已收到: " + msg;
writefile(hpipe, reply.c_str(),
(dword)((reply.size() + 1) * sizeof(wchar_t)), &n, null);
}
// 4. 收尾
disconnectnamedpipe(hpipe);
closehandle(hpipe);
}// client.cpp — 最简命名管道客户端
#include <windows.h>
#include <iostream>
#include <string>
#include <io.h> // _setmode
#include <fcntl.h> // _o_u16text
const wchar_t* pipe_name = l"\\\\.\\pipe\\mylearningpipe";
int main()
{
_setmode(_fileno(stdout), _o_u16text); // 控制台中文设置, 见上文说明
_setmode(_fileno(stdin), _o_u16text);
// 1. 等服务端的管道可用(最多 5 秒), 否则 createfile 会直接失败
if (!waitnamedpipe(pipe_name, 5000))
{
std::wcout << l"连接失败: 请先启动服务端 (错误码 " << getlasterror() << l")" << std::endl;
return 1;
}
// 2. 打开管道 = 建立连接(和打开文件写法一模一样)
handle hpipe = createfile(pipe_name, generic_read | generic_write,
0, null, open_existing, 0, null);
// 3. 客户端读模式也要改成消息模式, 和服务端保持一致
dword mode = pipe_readmode_message;
setnamedpipehandlestate(hpipe, &mode, null, null);
// 4. 发一条 -> 收一条回复
std::wstring line = l"你好, 管道";
dword n;
writefile(hpipe, line.c_str(), (dword)((line.size() + 1) * sizeof(wchar_t)), &n, null);
wchar_t buf[1024];
readfile(hpipe, buf, sizeof(buf) - sizeof(wchar_t), &n, null);
buf[n / sizeof(wchar_t)] = l'\0';
std::wcout << l"[回复] " << buf << std::endl;
closehandle(hpipe);
}waitnamedpipe 就是干这个的)。setnamedpipehandlestate 把读模式设为 pipe_readmode_message,否则默认按字节模式读,可能读到"半条消息"。connectnamedpipe 返回 false 不一定是失败——如果客户端抢在调用之前连上了,错误码是 error_pipe_connected,照样算连接成功。排错速查:客户端
createfile报错码 2(error_file_not_found)→ 服务端没启动或名字写错;报错码 231(error_pipe_busy)→ 管道实例全被占满,等一会重试。
上一节的服务器服务完一个客户端就退出了。真实场景(比如进度广播)里,服务器要能一直接客。关键就两行代码:客户端断开后调用 disconnectnamedpipe,然后用同一个句柄再次 connectnamedpipe。
// multiclientserver.cpp — 循环接受多个客户端(串行版)
#include <windows.h>
#include <iostream>
#include <string>
#include <io.h> // _setmode
#include <fcntl.h> // _o_u16text
const wchar_t* pipe_name = l"\\\\.\\pipe\\mylearningpipe";
int main()
{
_setmode(_fileno(stdout), _o_u16text); // 控制台中文设置, 见上文说明
// nmaxinstances 限制的是"同一时刻"最多有多少个客户端连着这个管道。
// 串行版同一时刻只服务 1 个客户端, 填 1 也够用;
// 这里先写成 pipe_unlimited_instances, 为后面的并发版做铺垫。
handle hpipe = createnamedpipe(
pipe_name,
pipe_access_duplex,
pipe_type_message | pipe_readmode_message | pipe_wait,
pipe_unlimited_instances,
4096, 4096, 0, null);
if (hpipe == invalid_handle_value)
return 1;
std::wcout << l"服务端启动, 循环等待客户端..." << std::endl;
for (;;)
{
// ---- 接受下一个客户端 ----
if (!connectnamedpipe(hpipe, null)
&& getlasterror() != error_pipe_connected)
{
std::wcerr << l"连接出错: " << getlasterror() << std::endl;
continue;
}
std::wcout << l"客户端已连接!" << std::endl;
// ---- 服务当前客户端, 直到它断开 ----
wchar_t buf[1024];
dword n;
for (;;)
{
bool ok = readfile(hpipe, buf, sizeof(buf) - sizeof(wchar_t), &n, null);
if (!ok || n == 0)
break; // 客户端断开连接
buf[n / sizeof(wchar_t)] = l'\0';
std::wstring msg(buf);
std::wcout << l"[收到] " << msg << std::endl;
if (msg == l"quit")
break;
std::wstring reply = l"服务端已收到: " + msg;
writefile(hpipe, reply.c_str(),
(dword)((reply.size() + 1) * sizeof(wchar_t)), &n, null);
}
// ---- 关键: 断开当前客户端, 准备服务下一个 ----
// disconnectnamedpipe 不会销毁管道, 之后可以再次 connectnamedpipe
flushfilebuffers(hpipe);
disconnectnamedpipe(hpipe);
std::wcout << l"客户端已断开, 继续等待下一个..." << std::endl;
}
closehandle(hpipe); // 正常跑不到这里, 可自行加退出信号
}串行版的局限很明显:服务 a 的时候,b 想连也连不上(要排队)。如果想让多个客户端同时连着(比如进度广播给多个 ui),需要多实例并发版。
// 并发版骨架: 每接受一个客户端, 就立刻再创建一个实例等下一个
dword winapi clientthread(lpvoid param)
{
handle hpipe = (handle)param;
// ... 收发消息, 和串行版内层循环一样 ...
flushfilebuffers(hpipe);
disconnectnamedpipe(hpipe);
closehandle(hpipe);
return 0;
}
int main()
{
bool first = true; // 第一个实例必须带 file_flag_first_pipe_instance
for (;;)
{
handle hpipe = createnamedpipe(
pipe_name,
pipe_access_duplex | (first ? file_flag_first_pipe_instance : 0),
pipe_type_message | pipe_readmode_message | pipe_wait,
pipe_unlimited_instances, // 允许多个客户端同时在线
4096, 4096, 0, null);
first = false;
if (hpipe == invalid_handle_value) break;
if (!connectnamedpipe(hpipe, null)
&& getlasterror() != error_pipe_connected)
{
closehandle(hpipe);
continue;
}
handle hthread = createthread(null, 0, clientthread, hpipe, 0, null);
closehandle(hthread); // 不等待线程结束, 立刻回去接下一个客户
}
}两个必须知道的细节:
file_flag_first_pipe_instance:同名管道创建多个实例时,第一个实例必须带这个标志;后续实例再带它反而会失败(error_access_denied)。上面的 first 布尔量就是这个用途。nmaxinstances(即 pipe_unlimited_instances 那个参数)限制的是同名管道实例总数——每个实例同一时刻服务一个客户端,所以它也就是"同时在线客户端数"的上限。串行版只有一个实例,填 1 足够;并发版按需填,pipe_unlimited_instances 表示不限(上限 255)。第七节的进度广播服务器就是并发版的完整落地。
这是命名管道最容易让人糊涂的地方。管道在创建时由服务端指定类型,客户端必须配合。
// ============ 消息模式 (pipe_type_message) ============ // 客户端写两条消息: writefile(hpipe, l"hello", 6 * 2, &n, null); // 第 1 条消息 writefile(hpipe, l"world", 6 * 2, &n, null); // 第 2 条消息 // 服务端读两次, 每次恰好读回一条完整消息: readfile(hpipe, buf1, 1024, &n, null); // -> "hello" readfile(hpipe, buf2, 1024, &n, null); // -> "world" // ============ 字节模式 (pipe_type_byte) ============ // 同样的两次写, 服务端读一次的结果是"不确定"的: readfile(hpipe, buf, 1024, &n, null); // buf 可能是 "helloworld"(全读走了), 也可能是 "hello"(只读到一半), // 甚至更少 —— 字节流没有消息边界, 一切都看缓冲区的心情
| 字节模式 pipe_type_byte | 消息模式 pipe_type_message | |
|---|---|---|
| 数据边界 | 无,连续字节流 | 有,每次 writefile 是一条消息 |
一次 readfile 读到 | 任意数量字节,可能跨消息 | 恰好一条完整消息 |
| 客户端读模式设置 | 默认就是字节模式,不用设 | 必须 setnamedpipehandlestate |
| 大消息 | 天然支持,循环读即可 | 缓冲区不够时报 error_more_data,剩余部分要接着读 |
| 适合场景 | 文件内容、大数据流 | 控制指令、状态/进度上报 |
顺带一提:匿名管道只有字节模式,没有消息模式。
匿名管道没有名字,所以别的进程找不到它——只能靠"句柄继承"传给子进程。典型用法:父进程创建管道,把写端塞给子进程当标准输出,从而捕获子进程的输出(命令行里 cmd1 | cmd2 的竖线就是这个原理)。
// 父进程: 用匿名管道抓取子进程的输出
#include <windows.h>
#include <iostream>
int main()
{
handle hread, hwrite;
security_attributes sa = { sizeof(sa), null, true }; // true = 句柄可被子进程继承
createpipe(&hread, &hwrite, &sa, 0); // 匿名管道: 单向, 字节模式
startupinfo si = { sizeof(si) };
si.dwflags = startf_usestdhandles;
si.hstdoutput = hwrite; // 子进程往 stdout 写, 实际是写进管道
si.hstderror = hwrite;
process_information pi;
createprocess(null, (lpwstr)l"child.exe", null, null,
true /* 允许继承句柄 */, 0, null, null, &si, &pi);
closehandle(hwrite); // 关键! 父进程必须关掉自己这份写端,
// 否则子进程退出后, 读端永远等不到"文件结尾"
char buf[256];
dword n;
while (readfile(hread, buf, sizeof(buf) - 1, &n, null) && n > 0)
{
buf[n] = 0;
std::cout << buf; // 打印子进程的输出
}
closehandle(hread);
waitforsingleobject(pi.hprocess, infinite);
}| 匿名管道 | 命名管道 | |
|---|---|---|
| 创建 | createpipe,一对句柄(读端/写端) | createnamedpipe,一个双向句柄 |
| 谁能连 | 只有继承到句柄的父子进程 | 任何知道名字的进程,甚至跨机器 |
| 方向 | 单向,双向要两根管道 | 双向 |
| 数据模式 | 只有字节模式 | 字节 / 消息可选 |
| 典型用途 | 重定向子进程 stdout、父传子数据 | 任意两个独立进程通信、进度广播 |
选型直觉:父子进程之间传数据、抓子进程输出 → 匿名管道;两个独立进程、多客户端、跨机器 → 命名管道。
比如手机和电脑之间能用管道通信吗。
能,但有条件。
| 通信双方 | 能否直接用命名管道 | 说明 |
|---|---|---|
| 同一台电脑的两个进程 | ✅ 可以 | \\.\pipe\xxx,管道最经典的用法 |
| 局域网内 windows ↔ windows | ✅ 可以 | 管道名改成 \\对方ip\pipe\xxx,代码只改一行 |
| 电脑 ↔ 手机(android/ios) | ❌ 不行 | 手机不支持 smb 命名管道协议 |
| 手机 ↔ 手机 | ❌ 不行 | 同上,一般走 tcp 或系统级框架 |
原因在于:windows 命名管道的底层依赖 smb 协议(ipc$ 管道共享)。只要是支持 smb 的设备(windows 全系、部分 linux/samba 环境),跨机器用命名管道毫无压力;而手机系统不实现这套协议,所以跨平台传输的通用做法是 tcp socket。
实际项目里最常见的架构是分层组合:
手机 (tcp 客户端) pc (接收方)
┌──────────────┐ tcp(9527端口) ┌─────────────────────────────┐
│ 发送文件数据 │ ───────────────► │ 传输引擎(收文件、算进度) │
│ 每块报告进度 │ │ │ 命名管道广播进度 │
└──────────────┘ │ ▼ │
│ ui进程1 / ui进程2 / 悬浮窗... │
└─────────────────────────────┘设备之间走 tcp,本机进程之间走管道。
回到第六节的问题。以"手机 ↔ 电脑传文件、电脑上显示进度"为例,分三种情况给代码。
接收方的传输引擎在 192.168.1.100 上创建管道;另一台电脑(或接收方本机)的 ui 进程订阅进度。代码和第三节几乎一样,只有管道名不同:
// 订阅端(ui 进程): 管道名从 \\.\pipe\xxx 换成 \\主机名或ip\pipe\xxx
#include <windows.h>
#include <iostream>
#include <io.h> // _setmode
#include <fcntl.h> // _o_u16text
const wchar_t* pipe_name = l"\\\\192.168.1.100\\pipe\\filetransferprogress";
int main()
{
_setmode(_fileno(stdout), _o_u16text); // 控制台中文设置, 见第三节说明
waitnamedpipe(pipe_name, 5000);
handle hpipe = createfile(pipe_name, generic_read, 0, null, open_existing, 0, null);
dword mode = pipe_readmode_message;
setnamedpipehandlestate(hpipe, &mode, null, null);
wchar_t buf[256];
dword n;
while (readfile(hpipe, buf, sizeof(buf) - sizeof(wchar_t), &n, null) && n > 0)
{
buf[n / sizeof(wchar_t)] = l'\0';
std::wcout << l"\r" << buf << std::flush; // buf = "进度 40%" 之类
}
closehandle(hpipe);
}跨机器用命名管道的前提(都默认满足,不行就查这三点):
这是"传输引擎 → 本机/局域网内多个 ui 进程"的标准做法,把第四节的并发骨架落到实处:一个引擎边收文件边算进度,广播给所有订阅的客户端。
// progressserver.cpp — 文件传输进度广播服务器
// 场景: 传输引擎一边接收文件数据, 一边把进度广播给所有订阅的 ui 客户端
#include <windows.h>
#include <vector>
#include <string>
#include <algorithm>
const wchar_t* pipe_name = l"\\\\.\\pipe\\filetransferprogress";
critical_section g_cs; // 保护客户端列表
std::vector<handle> g_clients; // 所有已连接客户端的句柄
// 向所有订阅者广播一条消息(消息模式, 一次一条)
void broadcast(const std::wstring& msg)
{
// 先复制一份句柄列表, 在锁外发送, 避免阻塞 i/o 长时间占着锁
entercriticalsection(&g_cs);
std::vector<handle> snapshot = g_clients;
leavecriticalsection(&g_cs);
for (handle h : snapshot)
{
dword n = 0;
if (!writefile(h, msg.c_str(), (dword)((msg.size() + 1) * sizeof(wchar_t)), &n, null))
{
// 写失败: 客户端已断开, 先从列表移除, 句柄交给 clientthread 清理
entercriticalsection(&g_cs);
auto it = std::find(g_clients.begin(), g_clients.end(), h);
if (it != g_clients.end())
g_clients.erase(it);
leavecriticalsection(&g_cs);
}
}
}
// 模拟传输引擎的进度产生线程(真实项目里数据来自 tcp 接收循环)
dword winapi progressthread(lpvoid)
{
for (int pct = 0; pct <= 100; pct += 10)
{
broadcast(l"进度 " + std::to_wstring(pct) + l"%");
sleep(500);
}
broadcast(l"传输完成");
return 0;
}
// 每个客户端一个线程: 用 peeknamedpipe 轮询指令, 顺带检测客户端断开
dword winapi clientthread(lpvoid param)
{
handle hpipe = (handle)param;
wchar_t buf[256];
dword n;
for (;;)
{
// peeknamedpipe 只查看不取走数据, 从不阻塞 —— 这是重点!
// 如果这里用阻塞的 readfile, 会卡住 broadcast 线程对同一句柄的 writefile
dword avail = 0;
bool peeked = peeknamedpipe(hpipe, buf, sizeof(buf), null, &avail, null);
if (!peeked && getlasterror() != error_insufficient_buffer)
break; // 客户端断开(error_insufficient_buffer = 有数据但 peek 缓冲区装不下)
if (peeked && avail > 0)
{
// 有客户端发来的指令, 读出来处理
if (!readfile(hpipe, buf, sizeof(buf) - sizeof(wchar_t), &n, null) || n == 0)
break;
buf[n / sizeof(wchar_t)] = l'\0';
// 处理 "pause" / "cancel" 等指令
}
else
{
sleep(100); // 没有指令, 歇一会儿再查
}
}
// 客户端断开: 从订阅列表移除并清理
entercriticalsection(&g_cs);
auto it = std::find(g_clients.begin(), g_clients.end(), hpipe);
if (it != g_clients.end())
g_clients.erase(it);
leavecriticalsection(&g_cs);
flushfilebuffers(hpipe);
disconnectnamedpipe(hpipe);
closehandle(hpipe);
return 0;
}
int main()
{
initializecriticalsection(&g_cs);
// 启动"传输引擎"的进度模拟
createthread(null, 0, progressthread, null, 0, null);
// 接受循环: 每连上一个客户端就开一个线程, 同时创建下一个实例等新客户
bool first = true;
for (;;)
{
handle hpipe = createnamedpipe(
pipe_name,
pipe_access_duplex | (first ? file_flag_first_pipe_instance : 0),
pipe_type_message | pipe_readmode_message | pipe_wait,
pipe_unlimited_instances,
4096, 4096, 0, null);
first = false;
if (hpipe == invalid_handle_value) break;
if (!connectnamedpipe(hpipe, null)
&& getlasterror() != error_pipe_connected)
{
closehandle(hpipe);
continue;
}
entercriticalsection(&g_cs);
g_clients.push_back(hpipe);
leavecriticalsection(&g_cs);
handle hthread = createthread(null, 0, clientthread, hpipe, 0, null);
closehandle(hthread);
}
}这里有一个很容易踩的坑:不能像第四节那样在 clientthread 里用阻塞的 readfile 等待客户端指令。对同一个同步句柄,一个线程的阻塞读会卡住另一个线程对同一句柄的写——broadcast 的 writefile 会一直挂起,整个广播瘫痪(本文代码实测踩过)。所以这里改用 peeknamedpipe 轮询,它只查看不取走数据、从不阻塞。同理,broadcast 里也不要在临界区内做阻塞 i/o,先复制句柄列表再在锁外发送。
订阅端直接用 7.1 的代码(本机订阅用 \\.\pipe\filetransferprogress,远程机器订阅用 \\192.168.1.100\pipe\filetransferprogress)。开两三个订阅端,就能看到进度同时推给所有窗口——这就是"一个服务器多客户端"的实际价值。
手机不支持 smb 命名管道,所以设备之间这段换成 tcp:手机每发完一块数据,就向电脑上报一行进度。pc 端接收:
// pc 端: tcp 进度接收服务(手机端用 java/kotlin/swift 开 socket 连过来)
// 协议约定: 每发送完一块文件数据, 发一行 "百分比\n", 如 "45\n"
#include <winsock2.h>
#include <iostream>
#include <cstdlib> // atoi
#pragma comment(lib, "ws2_32.lib")
int main()
{
wsadata wsa;
wsastartup(makeword(2, 2), &wsa);
socket s = socket(af_inet, sock_stream, ipproto_tcp);
sockaddr_in addr{};
addr.sin_family = af_inet;
addr.sin_addr.s_addr = inaddr_any; // 监听所有网卡
addr.sin_port = htons(9527);
bind(s, (sockaddr*)&addr, sizeof(addr));
listen(s, 5);
socket c = accept(s, null, null); // 手机连上了
char line[64];
while (recv(c, line, sizeof(line) - 1, 0) > 0)
{
int pct = atoi(line); // 简单起见, 一行一个百分比
std::cout << "进度: " << pct << "%\n";
}
}手机端任何语言都能写:socket("192.168.1.100", 9527),连上后按协议发进度行即可。注意 tcp 也是字节流,和第五节说的一样:工程代码要处理粘包/分包(按行读或加长度头),上面的例子只是演示思路。
| 通信需求 | 推荐方式 |
|---|---|
| 本机两个独立进程传指令/进度 | 命名管道(消息模式) |
| 本机父子进程、抓子进程输出 | 匿名管道 |
| 局域网内 windows ↔ windows | 命名管道(远程管道名) |
| 手机 ↔ 电脑 / 跨平台 | tcp socket |
| 传输文件内容本身 | 字节流(管道字节模式或 tcp) |
到此这篇关于c++ windows 管道编程实战的文章就介绍到这了,更多相关c++ windows 管道编程内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论