为什么我的 DeflateStream 无法通过 TCP 正确接收数据?

Why is my DeflateStream not receiving data correctly over TCP?

我在本地计算机上的客户端和服务器设置上有一个 TcpClient class。我一直在使用网络流来促进两者之间的来回通信成功。

展望未来,我正尝试在通信中实施压缩。我试过 GZipStream 和 DeflateStream。我决定专注于 DeflateStream。但是,现在连接挂起而没有读取数据。

我尝试了 4 种不同的实现,但由于服务器端未读取传入数据和连接超时而全部失败。我将重点介绍我最近尝试过的两个实现,据我所知应该可行。

客户端被分解为这个请求:有 2 个独立的实现,一个有 streamwriter,一个没有。

textToSend = ENQUIRY + START_OF_TEXT + textToSend + END_OF_TEXT;

// Send XML Request
byte[] request = Encoding.UTF8.GetBytes(textToSend);

using (DeflateStream streamOut = new DeflateStream(netStream, CompressionMode.Compress, true))
{
    //using (StreamWriter sw = new StreamWriter(streamOut))
    //{
    //    sw.Write(textToSend);
    //    sw.Flush();
    streamOut.Write(request, 0, request.Length);
    streamOut.Flush();
    //}
}

服务器收到请求,我做
1.) 快速阅读第一个字符,如果它符合我的预期
2.) 我继续阅读其余部分。

第一次读取工作正常,如果我想读取整个流,它就在那里。但是我只想读取第一个字符并对其求值,然后在我的 LongReadStream 方法中继续。

当我尝试继续读取流时,没有可读取的数据。我猜测数据在第一次读取时丢失了,但我不确定如何确定。 当我使用普通的 NetworkStream 时,所有这些代码都可以正常工作。

这是服务器端代码。

private void ProcessRequests()
{
    //  This method reads the first byte of data correctly and if I want to
    // I can read the entire request here.  However, I want to leave
    // all that data until I want it below in my LongReadStream method.
    if (QuickReadStream(_netStream, receiveBuffer, 1) != ENQUIRY)
    {
        // Invalid Request, close connection
        clientIsFinished = true;
        _client.Client.Disconnect(true);
        _client.Close();
        return;
    }



    while (!clientIsFinished)  // Keep reading text until client sends END_TRANSMISSION
    {
        // Inside this method there is no data and the connection times out waiting for data
        receiveText = LongReadStream(_netStream, _client);

        // Continue talking with Client...
    }
    _client.Client.Shutdown(SocketShutdown.Both);
    _client.Client.Disconnect(true);
    _client.Close();
}


private string LongReadStream(NetworkStream stream, TcpClient c)
{
    bool foundEOT = false;
    StringBuilder sbFullText = new StringBuilder();
    int readLength, totalBytesRead = 0;
    string currentReadText;
    c.ReceiveBufferSize = DEFAULT_BUFFERSIZE * 100;

    byte[] bigReadBuffer = new byte[c.ReceiveBufferSize];

    while (!foundEOT)
    {
        using (var decompressStream = new DeflateStream(stream, CompressionMode.Decompress, true))
        {
            //using (StreamReader sr = new StreamReader(decompressStream))
            //{
                //currentReadText = sr.ReadToEnd();
            //}
            readLength = decompressStream.Read(bigReadBuffer, 0, c.ReceiveBufferSize);
            currentReadText = Encoding.UTF8.GetString(bigReadBuffer, 0, readLength);
            totalBytesRead += readLength;
        }

        sbFullText.Append(currentReadText);

        if (currentReadText.EndsWith(END_OF_TEXT))
        {
            foundEOT = true;
            sbFullText.Length = sbFullText.Length - 1;
        }
        else
        {
            sbFullText.Append(currentReadText);
        }

        // Validate data code removed for simplicity


    }
    c.ReceiveBufferSize = DEFAULT_BUFFERSIZE;
    c.ReceiveTimeout = timeOutMilliseconds;
    return sbFullText.ToString();

}



private string QuickReadStream(NetworkStream stream, byte[] receiveBuffer, int receiveBufferSize)
{
    using (DeflateStream zippy = new DeflateStream(stream, CompressionMode.Decompress, true))
    {
        int bytesIn = zippy.Read(receiveBuffer, 0, receiveBufferSize);
        var returnValue = Encoding.UTF8.GetString(receiveBuffer, 0, bytesIn);
        return returnValue;
    }
}

编辑 NetworkStream 有一个基础套接字 属性,它有一个可用的 属性。 MSDN 对可用 属性.

说了这个

Gets the amount of data that has been received from the network and is available to be read.

下面的调用之前 Available 是 77。读取 1 个字节后值为 0。

//receiveBufferSize = 1
int bytesIn = zippy.Read(receiveBuffer, 0, receiveBufferSize);

似乎没有任何关于 DeflateStream 消耗整个底层流的文档,我不知道为什么它会在明确调用读取特定数量的字节时做这样的事情。

有谁知道为什么会发生这种情况,或者是否有办法保留基础数据以供将来读取?基于此 'feature' 和之前的 article that I read 说明必须关闭 DeflateStream 才能完成发送(刷新将不起作用)似乎 DeflateStreams 在网络使用方面可能受到限制,特别是如果有人希望对抗 DOS通过在接受完整流之前测试传入数据进行攻击。

在查看您的代码时我能想到的基本缺陷是对网络流和压缩工作方式的可能误解。

我认为如果您继续使用一个 DeflateStream,您的代码可能会工作。但是,您在快速阅读中使用了一个,然后创建了另一个。

我会试着用一个例子来解释我的推理。假设您有 8 个字节的原始数据要以压缩方式通过网络发送。现在让我们假设,原始数据的每个字节(8 位)都将以压缩形式压缩为 6 位。现在让我们看看您的代码对此做了什么。

从网络流中,您不能读取少于 1 个字节。你不能只拿 1 位。您可以使用 1 个字节、2 个字节或任意数量的字节,但不能使用位。

但是如果你只想接收原始数据的1个字节,你需要读取压缩数据的第一个完整字节。但是,只有 6 位压缩数据代表未压缩数据的第一个字节。第一个字节的最后 2 位用于原始数据的第二个字节。

现在如果你在那里切断流,剩下的是网络流中没有任何意义且无法解压缩的 5 个字节。

deflate 算法比那个更复杂,因此如果它不允许您在某一时刻停止从 NetworkStream 读取并从中间继续使用新的 DeflateStream,那么它是非常有意义的。为了将数据解压缩到它们的原始形式,必须存在解压缩的上下文。一旦你在快速阅读中处理了第一个 DeflateStream,这个上下文就消失了,你不能继续。

因此,要解决您的问题,请尝试只创建一个 DeflateStream 并将其传递给您的函数,然后处理它。

这在很多方面都被破坏了。

  1. 您假设读取调用将读取您想要的确切字节数。不过,它可能会以一个字节块的形式读取所有内容。
  2. DeflateStream 有一个内部缓冲区。它不能是任何其他方式:输入字节不对应 1:1 到输出字节。必须有一些内部缓冲。您必须使用一个这样的流。
  3. 与 UTF-8 相同的问题:UTF-8 编码的字符串无法在字节边界处拆分。有时,您的 Unicode 数据会出现乱码。
  4. 不要碰 ReceiveBufferSize,它没有任何帮助。
  5. 我认为您不能可靠地刷新 deflate 流,因为输出可能位于部分字节位置。您可能应该设计一种消息帧格式,在该格式中您将压缩长度作为未压缩的整数添加到前面。然后,在长度之后发送压缩的 deflate 流。这是可以可靠解码的。

解决这些问题并不容易。

既然你似乎控制了客户端和服务器,你应该放弃所有这些并且不要设计你自己的网络协议。使用更高级别的机制,例如 Web 服务、HTTP、protobuf。什么都比你那里的好。

我上面发布的代码基本上有一些问题。首先是当我读取数据时,我没有做任何事情来确保数据全部被读入。根据微软文档

The Read operation reads as much data as is available, up to the number of bytes specified by the size parameter.

就我而言,我不确定我的读取是否会获得我预期的所有数据。

只需使用此代码即可完成此操作。

byte[] data= new byte[packageSize];
    bytesRead = _netStream.Read(data, 0, packageSize);
    while (bytesRead < packageSize)
        bytesRead += _netStream.Read(data, bytesRead, packageSize - bytesRead);

除了这个问题之外,我在使用 DeflateStream 时遇到了一个根本问题——即我不应该使用 DeflateStream 来写入底层的 NetworkStream。正确的做法是先使用 DeflateStream 将数据压缩成 ByteArray,然后直接使用 NetworkStream 发送那个 ByteArray。

使用这种方法有助于正确压缩网络上的数据并属性在另一端读取数据。

您可能会指出我必须知道数据的大小,这是事实。每个调用都有一个 8 字节 'header',其中包括压缩数据的大小和未压缩时数据的大小。虽然我认为完全不需要第二个。

代码在这里。请注意,变量 compressedSize 有两个用途。

 int packageSize = streamIn.Read(sizeOfDataInBytes, 0, 4);
 while (packageSize!= 4)
 {
    packageSize+= streamIn.Read(sizeOfDataInBytes, packageSize, 4 - packageSize);
 }
 packageSize= BitConverter.ToInt32(sizeOfDataInBytes, 0);

根据这些信息,我可以正确使用我首先向您展示的代码来完整获取内容。

一旦我有了完整的压缩字节数组,我就可以像这样获取传入数据:

var output = new MemoryStream();
using (var stream = new MemoryStream(bufferIn))
{
    using (var decompress = new DeflateStream(stream, CompressionMode.Decompress))
    {
        decompress.CopyTo(output);;
    }
}
output.Position = 0;
var unCompressedArray = output.ToArray();
output.Close();
output.Dispose();
return Encoding.UTF8.GetString(unCompressedArray);