Skip to content

Instantly share code, notes, and snippets.

@campersau
Created April 7, 2016 09:11
Show Gist options
  • Save campersau/149a4569e57f3010f054399e7c3e0ec2 to your computer and use it in GitHub Desktop.
Save campersau/149a4569e57f3010f054399e7c3e0ec2 to your computer and use it in GitHub Desktop.
Griffin.Framework HTTP GZIP Message Encoder
using System;
using System.IO;
using Griffin.Net.Channels;
using System.IO.Compression;
using System.Text;
namespace Griffin.Net.Protocols.Http
{
/// <summary>
/// Used to encode request/response into a byte stream.
/// </summary>
public class HttpMessageGzipEncoder : IMessageEncoder
{
private readonly byte[] _buffer = new byte[65535];
private int _bytesToSend;
private bool _isHeaderSent;
private HttpMessage _message;
private int _offset;
private readonly MemoryStream _stream;
private int _totalAmountToSend;
private readonly StreamWriter _writer;
private readonly byte[] _tempBuffer = new byte[65535];
private MemoryStream _chunkedStream;
private Stream _compressor;
private bool _chunked = false;
private bool _finished = false;
private static byte[] ChunkEnd = Encoding.UTF8.GetBytes("\r\n");
private static byte[] ChunkFinal = Encoding.UTF8.GetBytes("0\r\n\r\n");
/// <summary>
/// Initializes a new instance of the <see cref="HttpMessageEncoder"/> class.
/// </summary>
public HttpMessageEncoder()
{
_stream = new MemoryStream(_buffer);
_stream.SetLength(0);
_writer = new StreamWriter(_stream);
}
/// <summary>
/// Are about to send a new message
/// </summary>
/// <param name="message">Message to send</param>
/// <remarks>
/// Can be used to prepare the next message. for instance serialize it etc.
/// </remarks>
/// <exception cref="NotSupportedException">Message is of a type that the encoder cannot handle.</exception>
public void Prepare(object message)
{
if (!(message is HttpMessage))
throw new InvalidOperationException("This encoder only supports messages deriving from 'HttpMessage'");
_message = (HttpMessage)message;
if (_message.Body == null || _message.Body.Length == 0)
_message.Headers["Content-Length"] = "0";
else if (_message.ContentLength == 0)
{
_message.ContentLength = (int)_message.Body.Length;
if (_message.Body.Position == _message.Body.Length)
_message.Body.Position = 0;
}
}
/// <summary>
/// Buffer structure used for socket send operations.
/// </summary>
/// <param name="buffer">
/// Do note that there are not buffer attached to the structure, you have to assign one yourself using
/// <see cref="ISocketBuffer.SetBuffer(int,int)" />. This choice was made
/// to prevent unnecessary copy operations.
/// </param>
public void Send(ISocketBuffer buffer)
{
// last send operation did not send all bytes enqueued in the buffer
// so let's just continue until doing next message
if (_bytesToSend > 0)
{
buffer.SetBuffer(_buffer, _offset, _bytesToSend);
return;
}
// continuing with the message body
if (_isHeaderSent)
{
if (!_chunked)
{
var bytes = Math.Min(_totalAmountToSend, _buffer.Length);
_message.Body.Read(_buffer, 0, bytes);
_bytesToSend = bytes;
buffer.SetBuffer(_buffer, 0, bytes);
}
else
{
_stream.SetLength(0);
// if compressed bytes are lager than original ones they haven't been sent...
if (_chunkedStream.Position < _chunkedStream.Length)
{
var bytesToSend = Math.Min(_chunkedStream.Length - _chunkedStream.Position, _buffer.Length - 15); // -15 for chunk delimiters
var chunkedBytesLength = _chunkedStream.Read(_tempBuffer, 0, (int)bytesToSend);
var chunkStart = Encoding.UTF8.GetBytes(chunkedBytesLength.ToString("x") + "\r\n");
_stream.Write(chunkStart, 0, chunkStart.Length);
_stream.Write(_tempBuffer, 0, chunkedBytesLength);
_stream.Write(ChunkEnd, 0, ChunkEnd.Length);
if (_message.Body.Position == _message.Body.Length && _chunkedStream.Position == _chunkedStream.Length) // finished
{
_stream.Write(ChunkFinal, 0, ChunkFinal.Length);
_finished = true;
}
_bytesToSend = (int)_stream.Length;
_totalAmountToSend = int.MaxValue; // we do not know how much will be sent
buffer.SetBuffer(_buffer, 0, _bytesToSend);
}
else
{
_chunkedStream.SetLength(0);
var start = _chunkedStream.Position;
do
{
var originalLength = _message.Body.Read(_tempBuffer, 0, _tempBuffer.Length);
_compressor.Write(_tempBuffer, 0, originalLength);
if (_message.Body.Position == _message.Body.Length)
{
_compressor.Dispose(); // finished reading => call dispose to "flush" gzipstream
}
} while (start == _chunkedStream.Position); // wait until compressor flush
var end = _chunkedStream.Position;
var bytesToSend = Math.Min(end - start, _buffer.Length - 15); // -15 for chunk delimiters
_chunkedStream.Position = start;
var chunkedBytesLength = _chunkedStream.Read(_tempBuffer, 0, (int)bytesToSend);
var chunkStart = Encoding.UTF8.GetBytes(chunkedBytesLength.ToString("x") + "\r\n");
_stream.Write(chunkStart, 0, chunkStart.Length);
_stream.Write(_tempBuffer, 0, chunkedBytesLength);
_stream.Write(ChunkEnd, 0, ChunkEnd.Length);
if (_message.Body.Position == _message.Body.Length && _chunkedStream.Position == _chunkedStream.Length) // finished
{
_stream.Write(ChunkFinal, 0, ChunkFinal.Length);
_finished = true;
}
_bytesToSend = (int)_stream.Length;
_totalAmountToSend = int.MaxValue; // we do not know how much will be sent
buffer.SetBuffer(_buffer, 0, _bytesToSend);
}
}
return;
}
// detect gzip
HttpResponse response = _message as HttpResponse;
if (response != null &&
response.Body != null &&
response.Body.Length > 1024 && // at least 1024 bytes
!response.Headers.Contains("Content-Encoding") &&
response.Request != null &&
response.Request.Headers.Contains("Accept-Encoding") &&
response.Request.Headers["Accept-Encoding"].Contains("gzip"))
{
_message.Headers["Content-Encoding"] = "gzip";
_message.Headers["Transfer-Encoding"] = "chunked";
_message.Headers.Remove("Content-Length");
_chunkedStream = new MemoryStream();
_compressor = new GZipStream(_chunkedStream, CompressionMode.Compress, true);
_chunked = true;
}
_writer.WriteLine(_message.StatusLine);
foreach (var header in _message.Headers)
{
_writer.Write("{0}: {1}\r\n", header, _message.Headers[header]);
}
foreach (HttpServer.ResponseCookie cookie in _message.Cookies)
_writer.Write("Set-Cookie: {0}\r\n", cookie);
_writer.Write("\r\n");
_writer.Flush();
_isHeaderSent = true;
buffer.UserToken = _message;
if (_message.Body == null || _message.ContentLength == 0)
{
_bytesToSend = (int)_stream.Length;
_totalAmountToSend = _bytesToSend;
buffer.SetBuffer(_buffer, 0, (int)_stream.Length);
return;
}
if (!_chunked)
{
var bytesLeft = _buffer.Length - _stream.Length;
var bytesToSend = Math.Min(_message.ContentLength, (int)bytesLeft);
var offset = (int)_stream.Position;
_message.Body.Read(_buffer, offset, bytesToSend);
_bytesToSend = (int)_stream.Length + bytesToSend;
_totalAmountToSend = (int)_stream.Length + _message.ContentLength;
buffer.SetBuffer(_buffer, 0, _bytesToSend);
}
else
{
var start = _chunkedStream.Position;
do
{
var originalLength = _message.Body.Read(_tempBuffer, 0, _tempBuffer.Length);
_compressor.Write(_tempBuffer, 0, originalLength);
if (_message.Body.Position == _message.Body.Length)
{
_compressor.Dispose(); // finished reading => call dispose to "flush" gzipstream
}
} while (start == _chunkedStream.Position); // wait until compressor flush
var end = _chunkedStream.Position;
var bytesToSend = Math.Min(end - start, _buffer.Length - (int)_stream.Length - 15); // -15 for chunk delimiters
_chunkedStream.Position = start;
var chunkedBytesLength = _chunkedStream.Read(_tempBuffer, 0, (int)bytesToSend);
var chunkStart = Encoding.UTF8.GetBytes(chunkedBytesLength.ToString("x") + "\r\n");
_stream.Write(chunkStart, 0, chunkStart.Length);
_stream.Write(_tempBuffer, 0, chunkedBytesLength);
_stream.Write(ChunkEnd, 0, ChunkEnd.Length);
if (_message.Body.Position == _message.Body.Length && _chunkedStream.Position == _chunkedStream.Length) // finished
{
_stream.Write(ChunkFinal, 0, ChunkFinal.Length);
_finished = true;
}
_bytesToSend = (int)_stream.Length;
_totalAmountToSend = int.MaxValue; // we do not know how much will be sent
buffer.SetBuffer(_buffer, 0, _bytesToSend);
}
}
/// <summary>
/// The previous <see cref="IMessageEncoder.Send" /> has just completed.
/// </summary>
/// <param name="bytesTransferred"></param>
/// <remarks><c>true</c> if the message have been sent successfully; otherwise <c>false</c>.</remarks>
public bool OnSendCompleted(int bytesTransferred)
{
_totalAmountToSend -= bytesTransferred;
_bytesToSend -= bytesTransferred;
_offset += bytesTransferred;
if (_bytesToSend <= 0)
_offset = 0;
if (_totalAmountToSend == 0 || _finished)
{
Clear();
}
return _totalAmountToSend <= 0;
}
/// <summary>
/// Remove everything used for the last message
/// </summary>
public void Clear()
{
if (_message != null)
{
try
{
if (_message.Body != null)
{
_message.Body.Dispose();
}
}
catch (Exception) { /* ignore */ }
_message = null;
}
_bytesToSend = 0;
_isHeaderSent = false;
_stream.SetLength(0);
_totalAmountToSend = 0;
if (_compressor != null)
{
try
{
_compressor.Dispose();
}
catch (Exception) { /* ignore */ }
_compressor = null;
}
if (_chunkedStream != null)
{
try
{
_chunkedStream.Dispose();
}
catch (Exception) { /* ignore */ }
_chunkedStream = null;
}
_chunked = false;
_finished = false;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment