优雅地关闭命名管道并处理流

时间:2017-07-25 15:58:43

标签: c# named-pipes

我有一个双向命名管道。我不确定如何优雅地关闭它,但是,一旦我完成它 - 如果我从客户端关闭连接,服务器端在尝试处理StreamReader时抛出异常和StreamWriter我正在使用。我现在正在捕捉它,但这对我来说似乎是一个重要的工作。

服务器端代码:

Thread pipeServer = new Thread(ServerThread);
pipeServer.Start();

private void ServerThread(object data)
{
    int threadId = Thread.CurrentThread.ManagedThreadId;
    log.Debug("Spawned thread " + threadId);

    PipeSecurity ps = new PipeSecurity();
    SecurityIdentifier sid = new SecurityIdentifier(WellKnownSidType.WorldSid, null);
    ps.AddAccessRule(new PipeAccessRule(sid, PipeAccessRights.ReadWrite, System.Security.AccessControl.AccessControlType.Allow));
    ps.AddAccessRule(new PipeAccessRule(WindowsIdentity.GetCurrent().Owner, PipeAccessRights.FullControl, System.Security.AccessControl.AccessControlType.Allow));
    log.Debug("Pipe security settings set [Thread " + threadId + "]");

    NamedPipeServerStream pipeServer =
        new NamedPipeServerStream("RDPCommunicationPipe", PipeDirection.InOut, numThreads, PipeTransmissionMode.Message, PipeOptions.None, 0x1000, 0x1000, ps);

    log.Debug("Pipe Servers created");

    // Wait for a client to connect
    log.Info("Pipe created on thread " + threadId + ". Listening for client connection.");
    pipeServer.WaitForConnection();
    log.Debug("Pipe server connection established [Thread " + threadId + "]");

    Thread nextServer = new Thread(ServerThread);
    nextServer.Start();

    try
    {
        // Read the request from the client. Once the client has
        // written to the pipe its security token will be available.

        using (StreamReader sr = new StreamReader(pipeServer))
        {
            using (StreamWriter sw = new StreamWriter(pipeServer) { AutoFlush = true })
            {
                // Verify our identity to the connected client using a
                // string that the client anticipates.

                sw.WriteLine("I am the one true server!");

                log.Debug("[Thread " + threadId + "]" + sr.ReadLine());

                log.Info(string.Format("Client connected on thread {0}. Client ID: {1}", threadId, pipeServer.GetImpersonationUserName()));
                while (!sr.EndOfStream)
                {
                    log.Debug("[Thread " + threadId + "]" + sr.ReadLine());
                }
            }
        }
    }
    // Catch the IOException that is raised if the pipe is broken
    // or disconnected.
    catch (IOException e)
    {
        log.Error("ERROR: " + e);
    }
}

客户端代码:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Starting...");
        var client = new NamedPipeClientStream(".", "RDPCommunicationPipe", PipeDirection.InOut);
        client.Connect();
        Console.WriteLine("Pipe connected successfully");

        using (StreamReader sr = new StreamReader(client))
        {
            using (StreamWriter sw = new StreamWriter(client) { AutoFlush = true })
            {
                string temp;
                do
                {
                    temp = sr.ReadLine();
                    Console.WriteLine(temp);
                } while (temp.Trim() != "I am the one true server!");

                sw.WriteLine("Message received and understood");
                while (!string.IsNullOrEmpty(temp = Console.ReadLine()))
                {
                    sw.WriteLine(temp);
                }
            }
        }
        client.Close();
    }
}

它完美运行,直到我在客户端应用程序中的空行上输入,终止它,关闭客户端。然后服务器应用程序在它到达StreamWriter System.IO.IOException: Pipe is broken.块的末尾时抛出using。如何正确处理我的流处理程序?

(基于找到的问题的代码herehere。)

1 个答案:

答案 0 :(得分:3)

  

我现在正在捕捉它,但这对我来说似乎是一个重要的工作。

恕我直言,如果你想成为一个好邻居并处置你拥有的StreamWriter对象并仍然投入最少的努力,它就会和你一样好。

那就是说,在我看来,在这种特殊情况下,也可以将Dispose()的电话注释掉 - 或者在你的情况下,不要使用using声明 - 以及包含另一个注释,解释在你的代码执行顺序的那一点,你知道所有调用将要做的是抛出一个异常,所以没有必要去做。

当然,如果您只是不打扰处理StreamWriter,那么您将需要明确处置您的管道流。您可能还想使用具有StreamWriter参数的leaveOpen构造函数,并为该参数传递true,以记录您没有StreamWriter的意图拥有管道流对象。

无论哪种方式,你最终都会在终结器队列中留下对象,因为异常会绕过对GC.SuppressFinalize()的调用,当然也不会打扰调用Dispose()。 。只要你没有处理大量的场景(即很多这些对象),这可能就好了。但它肯定不理想。

不幸的是,命名管道本身没有提供套接字所做的“优雅闭包”的语义。也就是说,端点指示它们完成写入的唯一方法是断开连接(用于服务器管道)或关闭(用于服务器或客户端管道)。这两个选项都没有使管道可供读取,因此在管道上实现正常关闭需要在应用程序协议本身内进行握手,而不是依赖于I / O对象。

除了这种不便(我承认,与你的问题没有直接关系),PipeStream.Flush()的实现检查管道是否可写。 即使它无意写任何东西!这是我发现的最后一部分真的很烦人,当然直接导致你问的问题。对于我来说,.NET Framework中的代码在这些异常导致更多麻烦而不是好的情况下抛出异常似乎是不合理的。

所有这一切,你还有其他选择:

  1. NamedPipeServerStreamNamedPipeClientStream类型进行子类化,并覆盖Flush()方法,以便它确实无效。或者更确切地说,如果你能做到这一点会很好。但这些类型是sealed,所以你不能。
  2. 替代子类化这些类型,您可以将它们包装在您自己的Stream实现中。这更麻烦,特别是因为您可能想要覆盖所有异步成员,至少如果您打算在任何I / O性能感兴趣的情况下使用这些对象。
  3. 使用单独的单向管道进行读写。在此实现中,您可以关闭StreamWriter本身作为关闭连接的方式,这将导致事物的正确顺序(即,在管道关闭之前发生刷新)。这也解决了优雅的闭包问题,因为每个连接有两个管道,你可以拥有与套接字相同的基本“半封闭”语义。当然,这个选项因识别哪一对管道连接彼此相互挑战而变得非常复杂。
  4. 这两个(即第二个和第三个,即实际可能的那些)都有一些明显的缺点。由于需要重复所有代码,因此必须拥有自己的Stream类是一件痛苦的事。并且管道对象数量增加一倍似乎是解决异常的一种极端方式(但它可能是支持优雅闭包语义的可接受且理想的实现,具有消除{{1}的抛出异常问题的快乐副作用})。

    请注意,在高容量场景中(但是,为什么使用管道?),抛出和捕获高频异常可能是一个问题(它们很昂贵)。因此,在这种情况下,这两个备选方案中的一个或另一个可能更适合捕获异常并且不必费心关闭/处置您的StreamWriter.Dispose()(两者都会增加会影响高效率的低效率)卷场景)。