Created
April 7, 2016 09:11
-
-
Save campersau/149a4569e57f3010f054399e7c3e0ec2 to your computer and use it in GitHub Desktop.
Griffin.Framework HTTP GZIP Message Encoder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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