NamedPipeServerStream 创建时必须指定管道名和方向
命名管道不是“开箱即用”的通信通道,
NamedPipeServerStream构造函数里漏掉
pipeName或搞错
PipeDirection,服务端直接抛
ArgumentException。Windows 要求管道名全局唯一且符合
\.pipe<em>name</em>格式,但 C# 封装层允许只传
"mypipe"—— 它会自动补前缀,前提是不能含反斜杠或控制字符。
常见错误现象:
IOException: "The pipe name is invalid",往往是因为传了
"\.pipemypipe"(重复前缀)或空字符串;还有人误用
PipeDirection.InOut却在客户端用
NamedPipeClientStream指定
PipeDirection.In,握手失败。 服务端必须用
new NamedPipeServerStream("log_collector", PipeDirection.InOut),名字别带路径分隔符
客户端对应用 new NamedPipeClientStream(".", "log_collector", PipeDirection.InOut),第一个参数是服务器机器名,本地用 "."即可 方向不匹配时,
ConnectAsync()会超时而非立刻报错,调试时容易误判为网络问题
读写必须配对使用 ReadAsync/WriteAsync 且注意缓冲区大小
命名管道底层基于字节流,没有消息边界。你
WriteAsync发 1024 字节,对方
ReadAsync可能一次只收到 512 字节——这不是 bug,是 Windows 管道的正常行为。硬编码固定长度读取(比如总期待 1024 字节)会导致阻塞或数据错位。
典型场景:日志转发服务中,客户端每条日志加 4 字节长度头,服务端必须先读够 4 字节再按长度读正文。若直接
await stream.ReadAsync(buffer, 0, buffer.Length),buffer 剩余部分就永远等不到数据。 务必用
await stream.ReadExactlyAsync(buffer, cancellationToken)(.NET 5+),它会循环读直到填满 buffer 如果用旧版 .NET,自己封装循环读逻辑,别依赖单次
ReadAsync返回值等于 buffer 长度
WriteAsync后建议调用
FlushAsync(),尤其跨进程时,否则对方可能迟迟收不到数据
客户端 ConnectAsync 超时需主动处理,不能只靠 try-catch
ConnectAsync默认无限等待,服务端没启动时,客户端线程就卡死了。这不是设计缺陷,而是 Windows 管道语义:客户端要“等到管道可用”。但实际业务中,你得控制等待时间,否则 UI 冻结或健康检查失败。
错误做法:把
ConnectAsync包进
Task.Run(() => stream.Connect())再用
Wait(timeout)—— 这会浪费线程且无法取消底层 IO。 正确方式:用
await stream.ConnectAsync(timeoutMs, cancellationToken)(.NET 6+),timeoutMs 设为 3000 比较稳妥 .NET 5 及以下只能用
Task.WhenAny(ConnectAsync(), Task.Delay(3000)),但要注意
ConnectAsync未完成时手动
Dispose流对象,避免句柄泄漏 超时后不要重试
ConnectAsync在同一个
NamedPipeClientStream实例上,必须新建实例
多客户端并发时,每个连接必须独立线程/任务处理
NamedPipeServerStream不是线程安全的,也不能复用。一个实例只能服务一个客户端连接。常见误区是启一个 server stream 后反复
WaitForConnectionAsync,却把后续读写丢给同一个 stream 对象 —— 第二个客户端一连,第一个就断了。
真实场景:监控代理要同时接收 10 个采集器的数据,必须为每个连接分配独立的
NamedPipeServerStream和处理任务。 标准模式:server 启动后循环
await server.WaitForConnectionAsync(),每次成功就
Task.Run(() => HandleClientAsync(server))HandleClientAsync 内部要立即创建新 buffer、读写逻辑封闭,别共享任何 stream 级变量 别忘了在 finally 块里
server.Dispose(),否则句柄累积到上限(默认 65536)后整个进程无法创建新管道
最易被忽略的是异常后资源清理 —— 某个客户端连接突然断开,
ReadAsync抛
IOException,如果没在 catch 里 dispose stream,这个句柄就泄露了。Windows 下看不见内存增长,但句柄数爆满时所有新管道都创建失败,排查起来极难定位。
