使用System.IO.Pipelines实现TLS/SSL

13

我注意到了新的System.IO.Pipelines并正在尝试将现有的基于流的代码移植到其中。流的问题是众所周知的,但同时它具有丰富的相关类生态系统。

从这里提供的示例中,有一个小的tcp回声服务器。 https://blogs.msdn.microsoft.com/dotnet/2018/07/09/system-io-pipelines-high-performance-io-in-net/

这里附上一段代码片段:

    private static async Task ProcessLinesAsync(Socket socket)
    {
        Console.WriteLine($"[{socket.RemoteEndPoint}]: connected");

        var pipe = new Pipe();
        Task writing = FillPipeAsync(socket, pipe.Writer);
        Task reading = ReadPipeAsync(socket, pipe.Reader);

        await Task.WhenAll(reading, writing);

        Console.WriteLine($"[{socket.RemoteEndPoint}]: disconnected");
    }

    private static async Task FillPipeAsync(Socket socket, PipeWriter writer)
    {
        const int minimumBufferSize = 512;

        while (true)
        {
            try
            {
                // Request a minimum of 512 bytes from the PipeWriter
                Memory<byte> memory = writer.GetMemory(minimumBufferSize);

                int bytesRead = await socket.ReceiveAsync(memory, SocketFlags.None);
                if (bytesRead == 0)
                {
                    break;
                }

                // Tell the PipeWriter how much was read
                writer.Advance(bytesRead);
            }
            catch
            {
                break;
            }

            // Make the data available to the PipeReader
            FlushResult result = await writer.FlushAsync();

            if (result.IsCompleted)
            {
                break;
            }
        }

        // Signal to the reader that we're done writing
        writer.Complete();
    }

    private static async Task ReadPipeAsync(Socket socket, PipeReader reader)
    {
        while (true)
        {
            ReadResult result = await reader.ReadAsync();

            ReadOnlySequence<byte> buffer = result.Buffer;
            SequencePosition? position = null;

            do
            {
                // Find the EOL
                position = buffer.PositionOf((byte)'\n');

                if (position != null)
                {
                    var line = buffer.Slice(0, position.Value);
                    ProcessLine(socket, line);

                    // This is equivalent to position + 1
                    var next = buffer.GetPosition(1, position.Value);

                    // Skip what we've already processed including \n
                    buffer = buffer.Slice(next);
                }
            }
            while (position != null);

            // We sliced the buffer until no more data could be processed
            // Tell the PipeReader how much we consumed and how much we left to process
            reader.AdvanceTo(buffer.Start, buffer.End);

            if (result.IsCompleted)
            {
                break;
            }
        }

        reader.Complete();
    }

    private static void ProcessLine(Socket socket, in ReadOnlySequence<byte> buffer)
    {
        if (_echo)
        {
            Console.Write($"[{socket.RemoteEndPoint}]: ");
            foreach (var segment in buffer)
            {
                Console.Write(Encoding.UTF8.GetString(segment.Span));
            }
            Console.WriteLine();
        }
    }

使用流时,您可以通过将其包装在SslStream中轻松地向代码添加SSL/TLS。Pipelines如何解决这个问题?


2
乍一看,Kestrel似乎使用流(包括SslStream)来提供其管道。这在Mark Gravell关于Pipes的博客中大致涵盖了[https://blog.marcgravell.com/2018/07/pipe-dreams-part-2.html] - 请参见“泵送管道”部分和明确提到TLS的“这是我之前制作的一个”部分。 - Rup
1
有趣的是,但考虑到支持管道的论点在某种程度上对流式处理持负面态度,这是否不会背道而驰呢? - agnsaft
有几件事需要注意。1. System.IO.Pipelines 只是整体开发的第一步,因此目前还没有可用于充当 System.IO.Pipelines 端点的 API(例如 dotnetty、Kestrel 或 .NET sockets 直接接受数据并将其泵入其中)。尽管计划在未来实现这些功能,但 .NET Core 团队首先需要设计一个清晰的 API,并找到时间来实现它们。 - Tseng
  1. 管道的存在是为了解决接收分块数据并以最高效且易于使用的方式进行解析的特定问题,这样该 API 的用户就不必担心如何汇集连续的缓冲区或粘合块以及背后的内存管理。
  2. 允许后台进程在数据从线路传输时立即处理数据(例如,一旦我们获得第一个字节,就开始解析头部)。
- Tseng
1个回答

9

命名管道是一种网络协议,就像HTTP、FTP和SMTP一样。让我们快速看一下.NET Framework的一些示例:

  • SSL会自动利用HTTP连接,具体取决于基本URI。如果URI以“HTTPS:”开头,则使用SSL。
  • 通过在调用GetResponse()之前将EnableSsl属性设置为true,可以手动利用FTP连接的SSL。
  • SMTP与FTP相同地利用SSL。

但是,如果我们使用不同的网络协议,例如管道呢?我们知道没有类似于“HTTPS”前缀的东西。此外,我们可以阅读System.IO.Piplines文档,并发现没有“EnableSsl”方法。然而,在.NET Framework和.NET Core中都提供了SslStream类。该类允许您从几乎任何可用的流构建SslStream。

在.NET Framework和.NET Core中也提供了System.IO.Pipes命名空间。Pipes命名空间中可用的类非常有帮助。

  • AnonymousPipeClientStream
  • AnonymousPipeServerStream
  • NamedPipeClientStream
  • NamedPipeServerStream
  • PipeStream

所有这些类都返回一些继承自Stream的对象,因此可以在SslStream的构造函数中使用它们。

这如何与System.IO.Piplines命名空间相关?好吧...并不相关。在System.IO.Pipelines命名空间中定义的任何类、结构或接口都没有继承自Stream。因此,我们无法直接使用SslStream类。

相反,我们可以访问PipeReaders和PipeWriters。有时我们只能使用其中之一,但是让我们考虑一个双向管道,以便同时访问两者。

System.IO.Piplines命名空间提供了一个IDuplexPipe接口。如果我们想将PipeReader和PipeWriters包装在SSL流中,我们需要定义一个实现IDuplexPipe的新类型。

在这个新类型中:

  • 我们将定义一个SslStream。
  • 我们将使用通用管道作为输入和输出缓冲区。
  • PipeReader将使用输入缓冲区的读取器。我们将使用此输入缓冲区从SSL流获取数据。
  • PipeWriter将使用输出缓冲区的写入器。我们将使用此输出缓冲区将数据发送到SSL流。

以下是伪代码示例:

SslStreamDuplexPipe : IDuplexPipe
{ 
    SslStream sslStream;
    Pipe inputBuffer;
    Pipe outputBuffer;

    public PipeReader Input = inputBuffer.Reader;
    public PipeWriter Output = outputBuffer.Writer;

    ReadDataFromSslStream()
    {
        int bytes = sslStream.Read(new byte[2048], 0, 2048);
        inputBuffer.Writer.Advance(bytes)
        inputBuffer.Writer.Flush();
    }

    //and the reverse to write to the SslStream 
 }

正如您所看到的,我们仍在使用System.Net.Security命名空间中的SslStream类,只需要多走几步。

这是否意味着您基本上仍在使用streams?是的!但是,一旦您完全实现了SslStreamDuplexPipe类,您只需要使用pipes进行操作,而无需将SslStream包装在所有内容周围。

Marc Gravell 写了一篇更加详细的解释。其中第一个部分可以在此处找到:https://blog.marcgravell.com/2018/07/pipe-dreams-part-1.html

此外,您还可以了解有关各种.NET类的相关信息:


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接