【问题标题】:Why does SslStream.Read always set TcpClient.Available to 0, when NetworkStream.Read doesn't为什么 SslStream.Read 总是将 TcpClient.Available 设置为 0,而 NetworkStream.Read 没有
【发布时间】:2025-12-28 07:05:06
【问题描述】:

我有一个连接到服务器的TcpClient 客户端,它会将消息发送回客户端。

使用NetworkStream.Read 类读取此数据时,我可以使用count 参数指定要读取的字节数,这将在读取完成后将TcpClient.Available 减少count。来自docs

计数Int32
从当前流中读取的最大字节数。

例如:

public static void ReadResponse()
{
    if (client.Available > 0) // Assume client.Available is 500 here
    {
        byte[] buffer = new byte[12]; // I only want to read the first 12 bytes, this could be a header or something
        var read = 0;

        NetworkStream stream = client.GetStream();
        while (read < buffer.Length)
        {
            read = stream.Read(buffer, 0, buffer.Length);
        }
    // breakpoint
    }
}

这会将TcpClient 上可用的500 个字节的前12 个字节读入buffer,并在断点处检查client.Available 将产生488 (500 - 12) 的(预期)结果。

现在,当我尝试做完全相同的事情,但这次使用 SslStream 时,结果出乎我的意料。

public static void ReadResponse()
{
    if (client.Available > 0) // Assume client.Available is 500 here
    {
        byte[] buffer = new byte[12]; // I only want to read the first 12 bytes, this could be a header or something
        var read = 0;

        SslStream stream = new SslStream(client.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null);

        while (read < buffer.Length)
        {
            read = stream.Read(buffer, 0, buffer.Length);
        }
    // breakpoint
    }
}

此代码将按预期将前 12 个字节读入buffer。但是,现在在断点处检查 client.Available 时会产生 0 的结果。

与普通的NetworkStream.Read 一样,SslStream.Readdocumentation 声明count 表示要读取的最大字节数。

计数Int32
一个Int32,包含要从此流中读取的最大字节数。

虽然它只读取这 12 个字节,但我想知道剩下的 488 个字节在哪里。

SslStreamTcpClient 的文档中,我找不到任何表明使用SslStream.Read 会刷新流或以其他方式清空client.Available 的内容。这样做的原因是什么(以及这在哪里记录)?


this question 要求与TcpClient.Available 等效,这不是我所要求的。我想知道为什么会发生这种情况,这里没有介绍。

【问题讨论】:

    标签: c# tcpclient networkstream sslstream


    【解决方案1】:

    请记住,出于效率原因,或者因为解密过程不能逐字节工作并且需要数据块可用,SslStream 可能会一次从底层 TcpStream 读取大块并在内部缓冲它们.因此,您的 TcpClient 包含 0 个可用字节这一事实没有任何意义,因为这些字节可能位于 SslStream 内的缓冲区中。


    此外,您读取 12 个字节的代码不正确,这可能会影响您所看到的内容。

    请记住,Stream.Read 返回的字节数可能比您预期的要少。对Stream.Read 的后续调用将返回在该调用期间读取的字节数,而不是全部。

    所以你需要这样的东西:

    int read = 0;
    while (read < buffer.Length)
    {
        int readThisTime = stream.Read(buffer, read, buffer.Length - read);
        if (readThisTime == 0)
        {
            // The end of the stream has been reached: throw an error?
        }
        read += readThisTime;
    }
    

    【讨论】:

    • 我使用您提供的方法进行了尝试,它产生了完全相同的结果(尽管正如您所说,它现在可以防止读取比预期更少的字节)。不过,在仅读取 n 字节中的 12 个字节后,它仍然会清除 Available
    • 我明白了。那么会发生什么,创建new SslStreamTcpClient.GetStream() 的可用缓冲区复制到它自己的 缓冲区中,清除客户端的流使其返回0。
    • @Remy 是的。我的意思是,不能保证它会读取 整个 缓冲区:碰巧它决定这次至少读取 500 个字节。
    【解决方案2】:

    当您从 TLS 流中读取数据时,它会过度读取,从而维护尚未解密或 已解密的数据的内部缓冲区但尚未消耗。这是流中常用的方法,尤其是当它们改变内容(压缩、加密等)时,因为输入和输出有效负载大小之间不一定存在 1:1 的相关性,并且它可能是需要从源读取整个帧 - 即您不能只读取 3 个字节 - API 需要读取整个帧(例如,512 个字节),解密 ,给 你想要的 3 个,并保留剩下的 509 个,下次你问的时候给你。这意味着它通常需要从源(在本例中为套接字)消耗比它提供给您的更多。

    出于性能原因,许多流 API 也会这样做,例如 StreamReader 从底层 Stream 过度读取并在内部维护尚未解码的字节的 byteBuffer 和已解码字符的 charBuffer可供消费。您的问题将类似于:

    使用StreamReader时,我只读取了3个字符,但我的Stream有512个字节;为什么?

    【讨论】:

    • 感谢您的深入解释和比较!这种行为是否记录在某处,或者更多的是处理加密流时的“隐式”事情?我无法在有关 (ssl)streams 的文档中找到它,但可能是找错地方了。
    • @Remy 我会转过身来说:从哪里记录从流中消费只需要确切当前调用所需的内容? 记录的是,如果您使用SslStream 包装一个流,并继续阅读:您最终将获得所有解密的字节。超出此范围的任何期望都未在任何地方指定。
    最近更新 更多