The company I work for, Sierra Bravo Corp, got its start connecting legacy PICK systems to the modern world. They accomplished this via a proprietary client-server protocol we internally call db_server (Official name is SierraDBC or BravoConnecter). Sure there are other PICK connection technologies out there none of them support multiple PICK systems and OS Platforms. Currently we support : D3, Universe, UniData, JBase, MvBase and others I don't remember right now.
I maintain the .NET client library. It was originally a port of the Java library which was a port of the COM library. It's come quite a long way since then. Recently we've found the need for compression to be added for some of our client. One in particular has many employees out in the field connecting via AirCards where bandwidth availability can be a problem. On top of that the application needs to retrieve quite a bit of real time data from the home office. Retrieving 2mb of client history is not unheard of.
After the server developer added compression to the result stream I started working on the client end. I figured it should be a snap. If we have compression turned on, just pass in a DeflateStream (fed by the NetworkStream of the TcpClient connection) to the StreamReader we already use to retrieve the results.
StreamReader ResultReader;
if (IsConnectionTypeSet(ConnectionOptionTypes.EnableCompression))
{
DeflateStream dfs = new DeflateStream(ns,CompressionMode.Decompress);
ResultReader = new StreamReader(dfs, Encoding.ASCII, false);
}
else
{
ResultReader = new StreamReader(ns, Encoding.ASCII, false);
}One would think that's all that it would take, right?
I was sadly mistaken. I would receive an exception (System.IO.InvalidDataException:"Block length does not match with its complement.") every time I would try to read from the stream.
The issue lies in the use of zlib for the compressed stream. zlib and DEFLATE use the same algorithm for compression. The difference is zlib sends two bytes of header data. So all the answers I found were to pop off the first two bytes of the stream.
StreamReader ResultReader;
if (IsConnectionTypeSet(ConnectionOptionTypes.EnableCompression))
{
DeflateStream dfs = new DeflateStream(ns,CompressionMode.Decompress);
ResultReader = new StreamReader(dfs, Encoding.ASCII, false);
ns.ReadByte();
ns.ReadByte();
}
else
{
ResultReader = new StreamReader(ns, Encoding.ASCII, false);
} This worked just fine but I was annoyed about how ugly it looked. I needed to move that ugliness out of there. So I wrote a ZlibStream class to do this for me. I made it for only Decompressing streams since I didn't have the budget to go and actually implement the zlib headers.
[ZlibStream.cs]
/// <summary>
/// Supports decompressing a DeflateStream created by the zlib library
/// </summary>
class ZlibStream : DeflateStream
{
#region Fields
private bool HasRead = false;
#endregion
#region Constructors
/// <summary>
/// Initiates ZlibStream in Decompress mode
/// </summary>
/// <param name="stream">One of the System.IO.Compression.CompressionMode values that indicates the action to take</param>
public ZlibStream(Stream stream)
: base(stream, CompressionMode.Decompress)
{
}
/// <summary>
/// Initiates ZlibStream in Decompress mode
/// </summary>
/// <param name="stream">One of the System.IO.Compression.CompressionMode values that indicates the action to take</param>
/// <param name="leaveOpen">true to leave the stream open; otherwise, false.</param>
public ZlibStream(Stream stream, bool leaveOpen)
: base(stream, CompressionMode.Decompress, leaveOpen)
{
}
#endregion
#region Public Methods
public override int Read(byte[] array, int offset, int count)
{
if (HasRead == false)
{
this.BaseStream.ReadByte();
this.BaseStream.ReadByte();
this.HasRead = true;
}
return base.Read(array, offset, count);
}
#endregion
}As you can see it is very simple. Since all the StreamReader does is call the Read method I just needed to remove those bytes during the first pass. This class is extremely simple and limited in its functionality. Doing Asynchronous reads with BeginRead will not work. I checked via reflector and it does not use the DeflateStream.Read method. It handles the BaseStream's Read methods on its own.
The one thing I may need to expand is doing the popping of the two bytes. Currently it's not checking to see if the stream has any bytes to read. So far in testing this hasn't been a problem. I'm going to try and see if I can create a situation where the bytes are not available initially. I have a suspicion that the ReadByte() waits for a Byte to be available and errors out on the Timeout value
So my result is as elegant as can be
StreamReader ResultReader;
if (IsConnectionTypeSet(ConnectionOptionTypes.EnableCompression))
{
ZlibStream dfs = new ZlibStream(ns);
ResultReader = new StreamReader(dfs, Encoding.ASCII, false);
}
else
{
ResultReader = new StreamReader(ns, Encoding.ASCII, false);
}