我有一个简单的服务器 - 客户端应用程序,使用命名管道。我在服务器中使用StreamWriter,在客户端中使用StreamReader。只要客户端进程没有从管道读取(即,不从StreamReader读取,包裹管道),StreamWriter就不会被释放。我想了解原因。
以下是详细信息:
这是服务器:
using System;
using System.IO;
using System.IO.Pipes;
class PipeServer
{
static void Main()
{
using (NamedPipeServerStream pipeServer =
new NamedPipeServerStream("testpipe"))
{
Console.Write("Waiting for client connection...");
pipeServer.WaitForConnection();
Console.WriteLine("Client connected.");
try
{
StreamWriter sw = new StreamWriter(pipeServer);
try
{
sw.WriteLine("hello client!");
}
finally
{
sw.Dispose();
}
// Would print only after the client finished sleeping
// and reading from its StreamReader
Console.WriteLine("StreamWriter is now closed");
}
catch (IOException e)
{
Console.WriteLine("ERROR: {0}", e.Message);
}
}
}
}
这是客户:
using System;
using System.IO;
using System.IO.Pipes;
using System.Threading;
class PipeClient
{
static void Main(string[] args)
{
using (NamedPipeClientStream pipeClient =
new NamedPipeClientStream(".", "testpipe"))
{
Console.Write("Attempting to connect to pipe...");
pipeClient.Connect();
Console.WriteLine("Connected to pipe.");
using (StreamReader sr = new StreamReader(pipeClient))
{
Thread.Sleep(100000);
string temp = sr.ReadLine();
if (temp != null)
{
Console.WriteLine("Received from server: {0}", temp);
}
}
}
}
}
注意客户端中的Thread.Sleep(100000);
:我添加了它以确保只要客户端进程处于休眠状态,StreamWriter就不会在服务器中处理,并且服务器将不会执行{{1} }。为什么?
修改
我切断了以前的信息,我认为这可能是无关紧要的。 我还想补充一点 - 感谢Scott在评论中 - 我发现这种行为发生了反过来:如果服务器写入,然后睡眠,客户端(尝试)使用其StreamReader读取 - 读取不是'发生,直到服务器醒来。
第二次编辑:
我在第一次编辑中谈到的其他方式是无关紧要的,这是一个Console.WriteLine("StreamWriter is now closed");
问题。
我试着给它做一些试验,并得出结论斯科特的权利 - 如果没有排水管道就不能处理。那为什么?这似乎与flush
假定它拥有流的事实相矛盾,除非另有说明(见here)。
以下是上述代码的附加细节:
在服务器程序中,StreamWriter
现在看起来像这样:
try-finally
在客户端程序中,try
{
sw.AutoFlush = true;
sw.WriteLine("hello client!");
Thread.Sleep(10000);
sw.WriteLine("hello again, client!");
}
finally
{
sw.Dispose(); // awaits while client is sleeping
}
Console.WriteLine("StreamWriter is now closed");
块现在看起来像这样:
using
答案 0 :(得分:3)
所以问题在于Windows命名管道的工作方式。调用CreateNamedPipe时,您可以指定输出缓冲区大小。现在文档说的是这样的:
输入和输出缓冲区大小是建议性的。为命名管道的每一端保留的实际缓冲区大小是系统默认值,系统最小值或最大值,或指定的大小向上舍入到下一个分配边界。
这里的问题是NamedPipeServerStream构造函数传递0作为默认输出大小(我们可以使用源验证这一点,或者在我的情况下只是启动ILSpy)。您可能会认为这会根据注释创建一个“默认”缓冲区大小,但事实并非如此,它实际上会创建一个0字节的输出缓冲区。这意味着除非有人正在读取缓冲区,否则对管道块进行任何写入。您还可以使用OutBufferSize属性验证大小。
您看到的行为是因为当您处理StreamWriter时,它会在管道流上调用Write(因为它已缓冲了内容)。写调用块,因此Dispose永远不会返回。
要验证这一点,我们将使用WinDBG(因为VS只显示sw.Dispose调用上的阻塞线程)。
在WinDBG下运行服务器应用程序。当它阻塞暂停调试器时(例如按CTRL + Break)并发出以下命令:
加载SOS调试器模块:.loadby sos clr
使用交错的托管和非托管堆栈框架转储所有正在运行的堆栈(由于sos.dll中存在一个愚蠢的错误,可能需要运行两次):!eestack
你应该看到这个过程中的第一个堆栈看起来是这样的(为了简洁而经过大量编辑):
Thread 0
Current frame: ntdll!NtWriteFile+0x14
KERNELBASE!WriteFile+0x76, calling ntdll!NtWriteFile
System.IO.Pipes.PipeStream.WriteCore
System.IO.StreamWriter.Flush(Boolean, Boolean))
System.IO.StreamWriter.Dispose(Boolean))
所以我们可以看到我们陷入了NtWriteFile,这是因为它无法将字符串写入大小为0的缓冲区。
要解决此问题很简单,请使用NamedPipeServerStream构造函数之一指定显式输出缓冲区大小,例如this构造函数。例如,这将执行默认构造函数将执行的所有操作,但具有合理的输出缓冲区大小:
new NamedPipeServerStream("testpipe", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.None, 0, 4096)