- Initial commit
This commit is contained in:
368
Runtime/Extensions/PerMessageCompression.cs
Normal file
368
Runtime/Extensions/PerMessageCompression.cs
Normal file
@ -0,0 +1,368 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Compression.Zlib;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
using Best.WebSockets.Implementations.Frames;
|
||||
|
||||
namespace Best.WebSockets.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Compression Extensions for WebSocket implementation.
|
||||
/// http://tools.ietf.org/html/rfc7692
|
||||
/// </summary>
|
||||
public sealed class PerMessageCompression : IExtension
|
||||
{
|
||||
public const int MinDataLengthToCompressDefault = 256;
|
||||
|
||||
private static readonly byte[] Trailer = new byte[] { 0x00, 0x00, 0xFF, 0xFF };
|
||||
|
||||
#region Public Properties
|
||||
|
||||
/// <summary>
|
||||
/// By including this extension parameter in an extension negotiation offer, a client informs the peer server
|
||||
/// of a hint that even if the server doesn't include the "client_no_context_takeover" extension parameter in
|
||||
/// the corresponding extension negotiation response to the offer, the client is not going to use context takeover.
|
||||
/// </summary>
|
||||
public bool ClientNoContextTakeover { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// By including this extension parameter in an extension negotiation offer, a client prevents the peer server from using context takeover.
|
||||
/// </summary>
|
||||
public bool ServerNoContextTakeover { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// This parameter indicates the base-2 logarithm of the LZ77 sliding window size of the client context.
|
||||
/// </summary>
|
||||
public int ClientMaxWindowBits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// This parameter indicates the base-2 logarithm of the LZ77 sliding window size of the server context.
|
||||
/// </summary>
|
||||
public int ServerMaxWindowBits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The compression level that the client will use to compress the frames.
|
||||
/// </summary>
|
||||
public CompressionLevel Level { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// What minimum data length will trigger the compression.
|
||||
/// </summary>
|
||||
public int MinimumDataLegthToCompress { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private fields
|
||||
|
||||
/// <summary>
|
||||
/// Cached object to support context takeover.
|
||||
/// </summary>
|
||||
private BufferPoolMemoryStream compressorOutputStream;
|
||||
private DeflateStream compressorDeflateStream;
|
||||
|
||||
/// <summary>
|
||||
/// Cached object to support context takeover.
|
||||
/// </summary>
|
||||
private BufferPoolMemoryStream decompressorInputStream;
|
||||
private BufferPoolMemoryStream decompressorOutputStream;
|
||||
private DeflateStream decompressorDeflateStream;
|
||||
|
||||
#endregion
|
||||
|
||||
public PerMessageCompression()
|
||||
:this(CompressionLevel.Default, false, false, ZlibConstants.WindowBitsMax, ZlibConstants.WindowBitsMax, MinDataLengthToCompressDefault)
|
||||
{ }
|
||||
|
||||
public PerMessageCompression(CompressionLevel level,
|
||||
bool clientNoContextTakeover,
|
||||
bool serverNoContextTakeover,
|
||||
int desiredClientMaxWindowBits,
|
||||
int desiredServerMaxWindowBits,
|
||||
int minDatalengthToCompress)
|
||||
{
|
||||
this.Level = level;
|
||||
this.ClientNoContextTakeover = clientNoContextTakeover;
|
||||
this.ServerNoContextTakeover = serverNoContextTakeover;
|
||||
this.ClientMaxWindowBits = desiredClientMaxWindowBits;
|
||||
this.ServerMaxWindowBits = desiredServerMaxWindowBits;
|
||||
this.MinimumDataLegthToCompress = minDatalengthToCompress;
|
||||
}
|
||||
|
||||
#region IExtension Implementation
|
||||
|
||||
/// <summary>
|
||||
/// This will start the permessage-deflate negotiation process.
|
||||
/// <seealso href="http://tools.ietf.org/html/rfc7692#section-5.1"/>
|
||||
/// </summary>
|
||||
public void AddNegotiation(HTTP.HTTPRequest request)
|
||||
{
|
||||
// The default header value that we will send out minimum.
|
||||
string headerValue = "permessage-deflate";
|
||||
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.1.1.1
|
||||
// A client MAY include the "server_no_context_takeover" extension parameter in an extension negotiation offer. This extension parameter has no value.
|
||||
// By including this extension parameter in an extension negotiation offer, a client prevents the peer server from using context takeover.
|
||||
// If the peer server doesn't use context takeover, the client doesn't need to reserve memory to retain the LZ77 sliding window between messages.
|
||||
if (this.ServerNoContextTakeover)
|
||||
headerValue += "; server_no_context_takeover";
|
||||
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.1.1.2
|
||||
// A client MAY include the "client_no_context_takeover" extension parameter in an extension negotiation offer.
|
||||
// This extension parameter has no value. By including this extension parameter in an extension negotiation offer,
|
||||
// a client informs the peer server of a hint that even if the server doesn't include the "client_no_context_takeover"
|
||||
// extension parameter in the corresponding extension negotiation response to the offer, the client is not going to use context takeover.
|
||||
if (this.ClientNoContextTakeover)
|
||||
headerValue += "; client_no_context_takeover";
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.1.2.1
|
||||
// By including this parameter in an extension negotiation offer, a client limits the LZ77 sliding window size that the server
|
||||
// will use to compress messages.If the peer server uses a small LZ77 sliding window to compress messages, the client can reduce the memory needed for the LZ77 sliding window.
|
||||
if (this.ServerMaxWindowBits != ZlibConstants.WindowBitsMax)
|
||||
headerValue += "; server_max_window_bits=" + this.ServerMaxWindowBits.ToString();
|
||||
else
|
||||
// Absence of this parameter in an extension negotiation offer indicates that the client can receive messages compressed using an LZ77 sliding window of up to 32,768 bytes.
|
||||
this.ServerMaxWindowBits = ZlibConstants.WindowBitsMax;
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.1.2.2
|
||||
// By including this parameter in an offer, a client informs the peer server that the client supports the "client_max_window_bits"
|
||||
// extension parameter in an extension negotiation response and, optionally, a hint by attaching a value to the parameter.
|
||||
if (this.ClientMaxWindowBits != ZlibConstants.WindowBitsMax)
|
||||
headerValue += "; client_max_window_bits=" + this.ClientMaxWindowBits.ToString();
|
||||
else
|
||||
{
|
||||
headerValue += "; client_max_window_bits";
|
||||
|
||||
// If the "client_max_window_bits" extension parameter in an extension negotiation offer has a value, the parameter also informs the
|
||||
// peer server of a hint that even if the server doesn't include the "client_max_window_bits" extension parameter in the corresponding
|
||||
// extension negotiation response with a value greater than the one in the extension negotiation offer or if the server doesn't include
|
||||
// the extension parameter at all, the client is not going to use an LZ77 sliding window size greater than the size specified
|
||||
// by the value in the extension negotiation offer to compress messages.
|
||||
this.ClientMaxWindowBits = ZlibConstants.WindowBitsMax;
|
||||
}
|
||||
|
||||
// Add the new header to the request.
|
||||
request.AddHeader("Sec-WebSocket-Extensions", headerValue);
|
||||
}
|
||||
|
||||
public bool ParseNegotiation(HTTP.HTTPResponse resp)
|
||||
{
|
||||
// Search for any returned neogitation offer
|
||||
var headerValues = resp.GetHeaderValues("Sec-WebSocket-Extensions");
|
||||
if (headerValues == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < headerValues.Count; ++i)
|
||||
{
|
||||
// If found, tokenize it
|
||||
HeaderParser parser = new HeaderParser(headerValues[i]);
|
||||
|
||||
for (int cv = 0; cv < parser.Values.Count; ++cv)
|
||||
{
|
||||
HeaderValue value = parser.Values[i];
|
||||
|
||||
if (!string.IsNullOrEmpty(value.Key) && value.Key.StartsWith("permessage-deflate", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HTTPManager.Logger.Information("PerMessageCompression", "Enabled with header: " + headerValues[i]);
|
||||
|
||||
HeaderValue option;
|
||||
if (value.TryGetOption("client_no_context_takeover", out option))
|
||||
this.ClientNoContextTakeover = true;
|
||||
|
||||
if (value.TryGetOption("server_no_context_takeover", out option))
|
||||
this.ServerNoContextTakeover = true;
|
||||
|
||||
if (value.TryGetOption("client_max_window_bits", out option))
|
||||
if (option.HasValue)
|
||||
{
|
||||
int windowBits;
|
||||
if (int.TryParse(option.Value, out windowBits))
|
||||
this.ClientMaxWindowBits = windowBits;
|
||||
}
|
||||
|
||||
if (value.TryGetOption("server_max_window_bits", out option))
|
||||
if (option.HasValue)
|
||||
{
|
||||
int windowBits;
|
||||
if (int.TryParse(option.Value, out windowBits))
|
||||
this.ServerMaxWindowBits = windowBits;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IExtension implementation to set the Rsv1 flag in the header if we are we will want to compress the data
|
||||
/// in the writer.
|
||||
/// </summary>
|
||||
public byte GetFrameHeader(WebSocketFrame writer, byte inFlag)
|
||||
{
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.2.3.1
|
||||
// the RSV1 bit is set only on the first frame.
|
||||
if ((writer.Type == WebSocketFrameTypes.Binary || writer.Type == WebSocketFrameTypes.Text) &&
|
||||
writer.Data != null && writer.Data.Count >= this.MinimumDataLegthToCompress)
|
||||
return (byte)(inFlag | 0x40);
|
||||
else
|
||||
return inFlag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IExtension implementation to be able to compress the data hold in the writer.
|
||||
/// </summary>
|
||||
public BufferSegment Encode(WebSocketFrame writer)
|
||||
{
|
||||
if (writer.Data == null)
|
||||
return BufferSegment.Empty;
|
||||
|
||||
// Is compressing enabled for this frame? If so, compress it.
|
||||
if ((writer.Header & 0x40) != 0)
|
||||
return Compress(writer.Data);
|
||||
else
|
||||
return writer.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IExtension implementation to possible decompress the data.
|
||||
/// </summary>
|
||||
public BufferSegment Decode(byte header, BufferSegment data)
|
||||
{
|
||||
// Is the server compressed the data? If so, decompress it.
|
||||
if ((header & 0x40) != 0)
|
||||
return Decompress(data);
|
||||
else
|
||||
return data;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helper Functions
|
||||
|
||||
/// <summary>
|
||||
/// A function to compress and return the data parameter with possible context takeover support (reusing the DeflateStream).
|
||||
/// </summary>
|
||||
private BufferSegment Compress(BufferSegment data)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
if (compressorOutputStream == null)
|
||||
compressorOutputStream = new BufferPoolMemoryStream();
|
||||
compressorOutputStream.SetLength(0);
|
||||
|
||||
if (compressorDeflateStream == null)
|
||||
{
|
||||
compressorDeflateStream = new DeflateStream(compressorOutputStream, CompressionMode.Compress, this.Level, false, this.ClientMaxWindowBits);
|
||||
compressorDeflateStream.FlushMode = FlushType.Sync;
|
||||
}
|
||||
|
||||
BufferSegment result = BufferSegment.Empty;
|
||||
try
|
||||
{
|
||||
compressorDeflateStream.Write(data.Data, data.Offset, data.Count);
|
||||
compressorDeflateStream.Flush();
|
||||
|
||||
compressorOutputStream.Position = 0;
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.2.1
|
||||
// Remove 4 octets (that are 0x00 0x00 0xff 0xff) from the tail end.
|
||||
// After this step, the last octet of the compressed data contains (possibly part of)
|
||||
// the DEFLATE header bits with the "BTYPE" bits set to 00.
|
||||
compressorOutputStream.SetLength(compressorOutputStream.Length - 4);
|
||||
|
||||
result = compressorOutputStream.ToBufferSegment();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (this.ClientNoContextTakeover)
|
||||
{
|
||||
compressorDeflateStream.Dispose();
|
||||
compressorDeflateStream = null;
|
||||
compressorOutputStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A function to decompress and return the data parameter with possible context takeover support (reusing the DeflateStream).
|
||||
/// </summary>
|
||||
private BufferSegment Decompress(BufferSegment data)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
if (decompressorInputStream == null)
|
||||
decompressorInputStream = new BufferPoolMemoryStream(data.Count + 4);
|
||||
|
||||
decompressorInputStream.Write(data.Data, data.Offset, data.Count);
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.2.2
|
||||
// Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the payload of the message.
|
||||
decompressorInputStream.Write(PerMessageCompression.Trailer, 0, PerMessageCompression.Trailer.Length);
|
||||
|
||||
decompressorInputStream.Position = 0;
|
||||
|
||||
if (decompressorDeflateStream == null)
|
||||
{
|
||||
decompressorDeflateStream = new DeflateStream(decompressorInputStream, CompressionMode.Decompress, CompressionLevel.Default, false, this.ServerMaxWindowBits);
|
||||
decompressorDeflateStream.FlushMode = FlushType.Sync;
|
||||
}
|
||||
|
||||
if (decompressorOutputStream == null)
|
||||
decompressorOutputStream = new BufferPoolMemoryStream();
|
||||
decompressorOutputStream.SetLength(0);
|
||||
|
||||
byte[] copyBuffer = BufferPool.Get(1024, true);
|
||||
int readCount;
|
||||
while ((readCount = decompressorDeflateStream.Read(copyBuffer, 0, copyBuffer.Length)) != 0)
|
||||
decompressorOutputStream.Write(copyBuffer, 0, readCount);
|
||||
|
||||
BufferPool.Release(copyBuffer);
|
||||
|
||||
decompressorDeflateStream.SetLength(0);
|
||||
|
||||
var result = decompressorOutputStream.ToBufferSegment();
|
||||
|
||||
if (this.ServerNoContextTakeover)
|
||||
{
|
||||
decompressorDeflateStream.Dispose();
|
||||
decompressorDeflateStream = null;
|
||||
decompressorInputStream = null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
bool _disposed;
|
||||
void CheckDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(PerMessageCompression));
|
||||
}
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
|
||||
compressorDeflateStream?.Dispose();
|
||||
compressorOutputStream?.Dispose();
|
||||
|
||||
decompressorDeflateStream?.Dispose();
|
||||
decompressorInputStream?.Dispose();
|
||||
decompressorOutputStream?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user