C# StreamWriter可以';当其缠绕管道处于';你没抽干吗?

C# StreamWriter可以';当其缠绕管道处于';你没抽干吗?,c#,pipe,C#,Pipe,我有一个简单的服务器客户端应用程序,使用命名管道。我在服务器中使用StreamWriter,在客户端中使用StreamReader。只要客户端进程不从管道中读取(也就是说,不从StreamReader中读取,包装管道),StreamWriter就不会被释放。我想知道为什么 详情如下: 这是服务器: using System; using System.IO; using System.IO.Pipes; class PipeServer { static void Main()

我有一个简单的服务器客户端应用程序,使用命名管道。我在服务器中使用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);
                }
            }
        }
    }
}
注意
线程睡眠(100000):我添加它是为了确保只要客户机进程处于睡眠状态,服务器就不会在服务器中处理StreamWriter,并且服务器不会执行控制台中的
WriteLine(“StreamWriter现在已关闭”)。为什么?

编辑:

我删掉了先前的信息,我想这可能是无关紧要的。 我还想补充一点——感谢Scott在评论中的发言——我观察到这种行为是以相反的方式发生的:如果服务器写入,然后睡眠,客户端(尝试)使用其StreamReader读取——直到服务器唤醒,读取才会发生

第二次编辑:

我在第一次编辑中谈到的行为的另一种方式与此无关,这是一个
flush
问题。 我试着对它进行了更多的试验,得出的结论是斯科特是对的——如果不排水,管道就无法处理。那为什么呢?这似乎与
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 (StreamReader sr = new StreamReader(pipeClient))
{
    string temp = sr.ReadLine();
    Console.WriteLine("blah"); // prints while server sleeps

    Console.WriteLine("Received from server: {0}", temp); // prints while server is sleeping
    Thread.Sleep(10000);

    temp = sr.ReadLine();
    Console.WriteLine("Received from server: {0}", temp);
}
在客户端程序中,使用
块现在如下所示:

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 (StreamReader sr = new StreamReader(pipeClient))
{
    string temp = sr.ReadLine();
    Console.WriteLine("blah"); // prints while server sleeps

    Console.WriteLine("Received from server: {0}", temp); // prints while server is sleeping
    Thread.Sleep(10000);

    temp = sr.ReadLine();
    Console.WriteLine("Received from server: {0}", temp);
}

因此,问题在于Windows命名管道的工作方式。调用时,可以指定输出缓冲区大小。现在,文档中有这样的内容:

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 (StreamReader sr = new StreamReader(pipeClient))
{
    string temp = sr.ReadLine();
    Console.WriteLine("blah"); // prints while server sleeps

    Console.WriteLine("Received from server: {0}", temp); // prints while server is sleeping
    Thread.Sleep(10000);

    temp = sr.ReadLine();
    Console.WriteLine("Received from server: {0}", temp);
}
输入和输出缓冲区大小是建议的。为命名管道的每一端保留的实际缓冲区大小可以是系统默认值、系统最小值或最大值,也可以是向上舍入到下一个分配边界的指定大小

这里的问题是NamedPipeServerStream构造函数将0作为默认输出大小传递(我们可以使用源代码验证这一点,或者在我的情况下只是启动)。根据注释,您可能会假设这将创建一个“默认”缓冲区大小,但事实并非如此,它实际上创建了一个0字节的输出缓冲区。这意味着,除非有人正在读取缓冲区,否则任何对管道的写入都会阻塞。还可以使用属性验证大小

您看到的行为是因为当您处理StreamWriter时,它会在管道流上调用Write(因为它缓冲了内容)。写入调用阻塞,因此Dispose永远不会返回

为了验证这一点,我们将使用WinDBG(因为VS只显示sw.Dispose调用上的阻塞线程)

在WinDBG下运行服务器应用程序。当它阻塞时,暂停调试器(例如按CTRL+Break)并发出以下命令:

加载SOS调试器模块:
。加载SOS clr

转储所有具有交错托管和非托管堆栈帧的正在运行的堆栈(可能需要运行两次,因为sos.dll中有一个哑错误):
!eestack

您应该看到流程中的第一个堆栈看起来是这样的(为简洁起见进行了大量编辑):

所以我们可以看到,我们被困在NtWriteFile中,这是因为它无法将字符串写入大小为0的缓冲区

要轻松解决此问题,请使用NamedPipeServerStream构造函数之一指定显式输出缓冲区大小,例如。例如,除了合理的输出缓冲区大小外,这将完成默认构造函数将执行的所有操作:

new NamedPipeServerStream("testpipe", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.None, 0, 4096)

如果将服务器更改为
newnamedpipeserverstream(“testpipe”,PipeDirection.Out)
,将客户端更改为
newnamedpipeclientstream(“.”,“testpipe”,PipeDirection.In)
,行为是否会发生任何变化?另外,如果你放置一个
线程,会发生什么
读取后,但仍在客户端上使用
中,流在服务器上是否仍然保持打开状态,直到它退出使用状态?@ScottChamberlain,完全没有更改,更改
睡眠
的位置是我所期望的:服务器进程继续,打印
“StreamWriter现在已关闭”
并返回,当客户端睡眠时。在缓冲区耗尽之前,它可能不允许您关闭。如果您执行
服务器写入->客户端读取->服务器写入->服务器处置->客户端读取-/code>,会发生什么情况?您的期望是什么?它工作得很好,不是吗?我确实发现了另一件事:我尝试了
服务器写入->服务器长时间睡眠->客户端读取-/code>——但客户端直到服务器醒来才读取。我想它会挂起,直到客户端第二次读取,这表明它会阻止dispose,直到缓冲区为空。老实说,你超出了我的专业知识范围,我不知道是什么原因造成的,也不知道如何预防。我可能在你的解释中遗漏了一些东西,但是:为什么写调用只阻止Dispose?你说它连接到零大小的缓冲区。那么,为什么其他任何写调用都不会阻塞呢?另一件我不明白的事情是,为什么服务器在Dispose中脱离阻塞会受到客户端脱离睡眠的影响?顺便说一句,我必须掌握WinDBG的窍门,因为我还不太熟悉它。同时,我很高兴得到更多的澄清。因此阻塞只发生在Dispose中,因为StreamWriter在将内容写入底层文件句柄(操作系统最终都是一个命名管道)之前会将内容缓冲到一定数量,除非达到缓冲限制,否则不会调用Write。如果您调用Stream::Write,那么它应该被阻塞,但您当然不会,您可以将它包装在StreamWriter中