This commit is contained in:
2020-07-09 08:50:24 +08:00
parent 13d25f4707
commit c523462b82
1818 changed files with 174940 additions and 582 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a49aa65bb5ffb4c0f8b749ed90a36ebe
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,41 @@
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
using BestHTTP.WebSocket.Frames;
namespace BestHTTP.WebSocket.Extensions
{
public interface IExtension
{
/// <summary>
/// This is the first pass: here we can add headers to the request to initiate an extension negotiation.
/// </summary>
/// <param name="request"></param>
void AddNegotiation(HTTPRequest request);
/// <summary>
/// If the websocket upgrade succeded it will call this function to be able to parse the server's negotiation
/// response. Inside this function the IsEnabled should be set.
/// </summary>
bool ParseNegotiation(WebSocketResponse resp);
/// <summary>
/// This function should return a new header flag based on the inFlag parameter. The extension should set only the
/// Rsv1-3 bits in the header.
/// </summary>
byte GetFrameHeader(WebSocketFrame writer, byte inFlag);
/// <summary>
/// This function will be called to be able to transform the data that will be sent to the server.
/// </summary>
/// <param name="writer"></param>
/// <returns></returns>
byte[] Encode(WebSocketFrame writer);
/// <summary>
/// This function can be used the decode the server-sent data.
/// </summary>
byte[] Decode(byte header, byte[] data);
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8cb3b2d401944421487dadfa298d45ac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,334 @@
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
using System;
using BestHTTP.Extensions;
using BestHTTP.WebSocket.Frames;
using BestHTTP.Decompression.Zlib;
namespace BestHTTP.WebSocket.Extensions
{
/// <summary>
/// Compression Extensions for WebSocket implementation.
/// http://tools.ietf.org/html/rfc7692
/// </summary>
public sealed class PerMessageCompression : IExtension
{
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 System.IO.MemoryStream compressorOutputStream;
private DeflateStream compressorDeflateStream;
/// <summary>
/// Cached object to support context takeover.
/// </summary>
private System.IO.MemoryStream decompressorInputStream;
private System.IO.MemoryStream decompressorOutputStream;
private DeflateStream decompressorDeflateStream;
private byte[] copyBuffer = new byte[1024];
#endregion
public PerMessageCompression()
:this(CompressionLevel.Default, false, false, ZlibConstants.WindowBitsMax, ZlibConstants.WindowBitsMax, 10)
{ }
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(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(WebSocketResponse 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.Length >= 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 byte[] Encode(WebSocketFrame writer)
{
if (writer.Data == null)
return WebSocketFrame.NoData;
// 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 byte[] Decode(byte header, byte[] 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 byte[] Compress(byte[] data)
{
if (compressorOutputStream == null)
compressorOutputStream = new System.IO.MemoryStream();
compressorOutputStream.SetLength(0);
if (compressorDeflateStream == null)
{
compressorDeflateStream = new DeflateStream(compressorOutputStream, CompressionMode.Compress, this.Level, true, this.ClientMaxWindowBits);
compressorDeflateStream.FlushMode = FlushType.Sync;
}
byte[] result = null;
try
{
compressorDeflateStream.Write(data, 0, data.Length);
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.ToArray();
}
finally
{
if (this.ClientNoContextTakeover)
{
compressorDeflateStream.Dispose();
compressorDeflateStream = null;
}
}
return result;
}
/// <summary>
/// A function to decompress and return the data parameter with possible context takeover support (reusing the DeflateStream).
/// </summary>
private byte[] Decompress(byte[] data)
{
if (decompressorInputStream == null)
decompressorInputStream = new System.IO.MemoryStream(data.Length + 4);
decompressorInputStream.Write(data, 0, data.Length);
// 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, true, this.ServerMaxWindowBits);
decompressorDeflateStream.FlushMode = FlushType.Sync;
}
if (decompressorOutputStream == null)
decompressorOutputStream = new System.IO.MemoryStream();
decompressorOutputStream.SetLength(0);
int readCount;
while ((readCount = decompressorDeflateStream.Read(copyBuffer, 0, copyBuffer.Length)) != 0)
decompressorOutputStream.Write(copyBuffer, 0, readCount);
decompressorDeflateStream.SetLength(0);
byte[] result = decompressorOutputStream.ToArray();
if (this.ServerNoContextTakeover)
{
decompressorDeflateStream.Dispose();
decompressorDeflateStream = null;
}
return result;
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 13a5c54c5a2694b6ca164ce85433928e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f55fca6223b9c40f5b83f81277240c01
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,170 @@
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
using System;
using System.IO;
namespace BestHTTP.WebSocket.Frames
{
/// <summary>
/// Denotes a binary frame. The "Payload data" is arbitrary binary data whose interpretation is solely up to the application layer.
/// This is the base class of all other frame writers, as all frame can be represented as a byte array.
/// </summary>
public sealed class WebSocketFrame
{
public static readonly byte[] NoData = new byte[0];
public WebSocketFrameTypes Type { get; private set; }
public bool IsFinal { get; private set; }
public byte Header { get; private set; }
public byte[] Data { get; private set; }
public bool UseExtensions { get; private set; }
#region Constructors
public WebSocketFrame(WebSocket webSocket, WebSocketFrameTypes type, byte[] data)
:this(webSocket, type, data, true)
{ }
public WebSocketFrame(WebSocket webSocket, WebSocketFrameTypes type, byte[] data, bool useExtensions)
: this(webSocket, type, data, 0, data != null ? (UInt64)data.Length : 0, true, useExtensions)
{
}
public WebSocketFrame(WebSocket webSocket, WebSocketFrameTypes type, byte[] data, bool isFinal, bool useExtensions)
: this(webSocket, type, data, 0, data != null ? (UInt64)data.Length : 0, isFinal, useExtensions)
{
}
public WebSocketFrame(WebSocket webSocket, WebSocketFrameTypes type, byte[] data, UInt64 pos, UInt64 length, bool isFinal, bool useExtensions)
{
this.Type = type;
this.IsFinal = isFinal;
this.UseExtensions = useExtensions;
if (data != null)
{
this.Data = new byte[length];
Array.Copy(data, (int)pos, this.Data, 0, (int)length);
}
else
data = NoData;
// First byte: Final Bit + Rsv flags + OpCode
byte finalBit = (byte)(IsFinal ? 0x80 : 0x0);
this.Header = (byte)(finalBit | (byte)Type);
if (this.UseExtensions && webSocket != null && webSocket.Extensions != null)
{
for (int i = 0; i < webSocket.Extensions.Length; ++i)
{
var ext = webSocket.Extensions[i];
if (ext != null)
{
this.Header |= ext.GetFrameHeader(this, this.Header);
this.Data = ext.Encode(this);
}
}
}
}
#endregion
#region Public Functions
public byte[] Get()
{
if (Data == null)
Data = NoData;
using (var ms = new MemoryStream(this.Data.Length + 9))
{
// For the complete documentation for this section see:
// http://tools.ietf.org/html/rfc6455#section-5.2
// Write the header
ms.WriteByte(this.Header);
// The length of the "Payload data", in bytes: if 0-125, that is the payload length. If 126, the following 2 bytes interpreted as a
// 16-bit unsigned integer are the payload length. If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the
// most significant bit MUST be 0) are the payload length. Multibyte length quantities are expressed in network byte order.
if (this.Data.Length < 126)
ms.WriteByte((byte)(0x80 | (byte)this.Data.Length));
else if (this.Data.Length < UInt16.MaxValue)
{
ms.WriteByte((byte)(0x80 | 126));
byte[] len = BitConverter.GetBytes((UInt16)this.Data.Length);
if (BitConverter.IsLittleEndian)
Array.Reverse(len, 0, len.Length);
ms.Write(len, 0, len.Length);
}
else
{
ms.WriteByte((byte)(0x80 | 127));
byte[] len = BitConverter.GetBytes((UInt64)this.Data.Length);
if (BitConverter.IsLittleEndian)
Array.Reverse(len, 0, len.Length);
ms.Write(len, 0, len.Length);
}
// All frames sent from the client to the server are masked by a 32-bit value that is contained within the frame. This field is
// present if the mask bit is set to 1 and is absent if the mask bit is set to 0.
// If the data is being sent by the client, the frame(s) MUST be masked.
byte[] mask = BitConverter.GetBytes((Int32)this.GetHashCode());
ms.Write(mask, 0, mask.Length);
// Do the masking.
for (int i = 0; i < this.Data.Length; ++i)
ms.WriteByte((byte)(Data[i] ^ mask[i % 4]));
return ms.ToArray();
}
}
public WebSocketFrame[] Fragment(ushort maxFragmentSize)
{
if (this.Data == null)
return null;
// All control frames MUST have a payload length of 125 bytes or less and MUST NOT be fragmented.
if (this.Type != WebSocketFrameTypes.Binary && this.Type != WebSocketFrameTypes.Text)
return null;
if (this.Data.Length <= maxFragmentSize)
return null;
this.IsFinal = false;
// Clear final bit from the header flags
this.Header &= 0x7F;
// One chunk will remain in this fragment, so we have to allocate one less
int count = (this.Data.Length / maxFragmentSize) + (this.Data.Length % maxFragmentSize) - 1;
WebSocketFrame[] fragments = new WebSocketFrame[count];
// Skip one chunk, for the current one
UInt64 pos = maxFragmentSize;
while (pos < (UInt64)this.Data.Length)
{
UInt64 chunkLength = Math.Min(maxFragmentSize, (UInt64)this.Data.Length - pos);
fragments[fragments.Length - count--] = new WebSocketFrame(null, WebSocketFrameTypes.Continuation, this.Data, pos, chunkLength, pos + chunkLength >= (UInt64)this.Data.Length, false);
pos += chunkLength;
}
byte[] newData = new byte[maxFragmentSize];
Array.Copy(this.Data, 0, newData, 0, maxFragmentSize);
this.Data = newData;
return fragments;
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d2f7c577d761a412a99e996eda082191
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,201 @@
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
using System;
using System.Collections.Generic;
using System.IO;
using BestHTTP.Extensions;
using BestHTTP.WebSocket.Extensions;
namespace BestHTTP.WebSocket.Frames
{
/// <summary>
/// Represents an incoming WebSocket Frame.
/// </summary>
public sealed class WebSocketFrameReader
{
#region Properties
public byte Header { get; private set; }
/// <summary>
/// True if it's a final Frame in a sequence, or the only one.
/// </summary>
public bool IsFinal { get; private set; }
/// <summary>
/// The type of the Frame.
/// </summary>
public WebSocketFrameTypes Type { get; private set; }
/// <summary>
/// Indicates if there are any mask sent to decode the data.
/// </summary>
public bool HasMask { get; private set; }
/// <summary>
/// The length of the Data.
/// </summary>
public UInt64 Length { get; private set; }
/// <summary>
/// The sent byte array as a mask to decode the data.
/// </summary>
public byte[] Mask { get; private set; }
/// <summary>
/// The decoded array of bytes.
/// </summary>
public byte[] Data { get; private set; }
/// <summary>
/// Textual representation of the received Data.
/// </summary>
public string DataAsText { get; private set; }
#endregion
#region Internal & Private Functions
internal void Read(Stream stream)
{
// For the complete documentation for this section see:
// http://tools.ietf.org/html/rfc6455#section-5.2
this.Header = ReadByte(stream);
// The first byte is the Final Bit and the type of the frame
IsFinal = (this.Header & 0x80) != 0;
Type = (WebSocketFrameTypes)(this.Header & 0xF);
byte maskAndLength = ReadByte(stream);
// The second byte is the Mask Bit and the length of the payload data
HasMask = (maskAndLength & 0x80) != 0;
// if 0-125, that is the payload length.
Length = (UInt64)(maskAndLength & 127);
// If 126, the following 2 bytes interpreted as a 16-bit unsigned integer are the payload length.
if (Length == 126)
{
byte[] rawLen = new byte[2];
stream.ReadBuffer(rawLen);
if (BitConverter.IsLittleEndian)
Array.Reverse(rawLen, 0, rawLen.Length);
Length = (UInt64)BitConverter.ToUInt16(rawLen, 0);
}
else if (Length == 127)
{
// If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the
// most significant bit MUST be 0) are the payload length.
byte[] rawLen = new byte[8];
stream.ReadBuffer(rawLen);
if (BitConverter.IsLittleEndian)
Array.Reverse(rawLen, 0, rawLen.Length);
Length = (UInt64)BitConverter.ToUInt64(rawLen, 0);
}
// Read the Mask, if has any
if (HasMask)
{
Mask = new byte[4];
if (stream.Read(Mask, 0, 4) < Mask.Length)
throw ExceptionHelper.ServerClosedTCPStream();
}
Data = new byte[Length];
if (Length == 0L)
return;
int readLength = 0;
do
{
int read = stream.Read(Data, readLength, Data.Length - readLength);
if (read <= 0)
throw ExceptionHelper.ServerClosedTCPStream();
readLength += read;
} while (readLength < Data.Length);
// It would be great to speed this up with SSE
if (HasMask)
for (int i = 0; i < Data.Length; ++i)
Data[i] = (byte)(Data[i] ^ Mask[i % 4]);
}
private byte ReadByte(Stream stream)
{
int read = stream.ReadByte();
if (read < 0)
throw ExceptionHelper.ServerClosedTCPStream();
return (byte)read;
}
#endregion
#region Public Functions
/// <summary>
/// Assembles all fragments into a final frame. Call this on the last fragment of a frame.
/// </summary>
/// <param name="fragments">The list of previously downloaded and parsed fragments of the frame</param>
public void Assemble(List<WebSocketFrameReader> fragments)
{
// this way the following algorithms will handle this fragment's data too
fragments.Add(this);
UInt64 finalLength = 0;
for (int i = 0; i < fragments.Count; ++i)
finalLength += fragments[i].Length;
byte[] buffer = new byte[finalLength];
UInt64 pos = 0;
for (int i = 0; i < fragments.Count; ++i)
{
Array.Copy(fragments[i].Data, 0, buffer, (int)pos, (int)fragments[i].Length);
pos += fragments[i].Length;
}
// All fragments of a message are of the same type, as set by the first fragment's opcode.
this.Type = fragments[0].Type;
// Reserver flags may be contained only in the first fragment
this.Header = fragments[0].Header;
this.Length = finalLength;
this.Data = buffer;
}
/// <summary>
/// This function will decode the received data incrementally with the associated websocket's extensions.
/// </summary>
public void DecodeWithExtensions(WebSocket webSocket)
{
if (webSocket.Extensions != null)
for (int i = 0; i < webSocket.Extensions.Length; ++i)
{
var ext = webSocket.Extensions[i];
if (ext != null)
this.Data = ext.Decode(this.Header, this.Data);
}
if (this.Type == WebSocketFrameTypes.Text && this.Data != null)
this.DataAsText = System.Text.Encoding.UTF8.GetString(this.Data, 0, this.Data.Length);
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 02b9d3788c68045428a0ec2b4b0f5ad0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,48 @@
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
namespace BestHTTP.WebSocket.Frames
{
public enum WebSocketFrameTypes : byte
{
/// <summary>
/// A fragmented message's first frame's contain the type of the message(binary or text), all consecutive frame of that message must be a Continuation frame.
/// Last of these frame's Fin bit must be 1.
/// </summary>
/// <example>For a text message sent as three fragments, the first fragment would have an opcode of 0x1 (text) and a FIN bit clear,
/// the second fragment would have an opcode of 0x0 (Continuation) and a FIN bit clear,
/// and the third fragment would have an opcode of 0x0 (Continuation) and a FIN bit that is set.</example>
Continuation = 0x0,
Text = 0x1,
Binary = 0x2,
//Reserved1 = 0x3,
//Reserved2 = 0x4,
//Reserved3 = 0x5,
//Reserved4 = 0x6,
//Reserved5 = 0x7,
/// <summary>
/// The Close frame MAY contain a body (the "Application data" portion of the frame) that indicates a reason for closing,
/// such as an endpoint shutting down, an endpoint having received a frame too large, or an endpoint having received a frame that
/// does not conform to the format expected by the endpoint.
/// As the data is not guaranteed to be human readable, clients MUST NOT show it to end users.
/// </summary>
ConnectionClose = 0x8,
/// <summary>
/// The Ping frame contains an opcode of 0x9. A Ping frame MAY include "Application data".
/// </summary>
Ping = 0x9,
/// <summary>
/// A Pong frame sent in response to a Ping frame must have identical "Application data" as found in the message body of the Ping frame being replied to.
/// </summary>
Pong = 0xA,
//Reserved6 = 0xB,
//Reserved7 = 0xC,
//Reserved8 = 0xD,
//Reserved9 = 0xE,
//Reserved10 = 0xF,
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 68366ab5bdc1f47a4ba5f6d7106329f2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,752 @@
#if !BESTHTTP_DISABLE_WEBSOCKET
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using BestHTTP.Extensions;
#if UNITY_WEBGL && !UNITY_EDITOR
using System.Runtime.InteropServices;
#else
using BestHTTP.WebSocket.Frames;
using BestHTTP.WebSocket.Extensions;
#endif
namespace BestHTTP.WebSocket
{
public delegate void OnWebSocketOpenDelegate(WebSocket webSocket);
public delegate void OnWebSocketMessageDelegate(WebSocket webSocket, string message);
public delegate void OnWebSocketBinaryDelegate(WebSocket webSocket, byte[] data);
public delegate void OnWebSocketClosedDelegate(WebSocket webSocket, UInt16 code, string message);
public delegate void OnWebSocketErrorDelegate(WebSocket webSocket, Exception ex);
public delegate void OnWebSocketErrorDescriptionDelegate(WebSocket webSocket, string reason);
#if (!UNITY_WEBGL || UNITY_EDITOR)
public delegate void OnWebSocketIncompleteFrameDelegate(WebSocket webSocket, WebSocketFrameReader frame);
#else
delegate void OnWebGLWebSocketOpenDelegate(uint id);
delegate void OnWebGLWebSocketTextDelegate(uint id, string text);
delegate void OnWebGLWebSocketBinaryDelegate(uint id, IntPtr pBuffer, int length);
delegate void OnWebGLWebSocketErrorDelegate(uint id, string error);
delegate void OnWebGLWebSocketCloseDelegate(uint id, int code, string reason);
/// <summary>
/// States of the underlying browser's WebSocket implementation's state. It's available only in WebGL builds.
/// </summary>
public enum WebSocketStates : byte
{
Connecting = 0,
Open = 1,
Closing = 2,
Closed = 3
};
#endif
public sealed class WebSocket
{
#region Properties
/// <summary>
/// The connection to the WebSocket server is open.
/// </summary>
public bool IsOpen
{
get
{
#if (!UNITY_WEBGL || UNITY_EDITOR)
return webSocket != null && !webSocket.IsClosed;
#else
return ImplementationId != 0 && WS_GetState(ImplementationId) == WebSocketStates.Open;
#endif
}
}
#if (!UNITY_WEBGL || UNITY_EDITOR)
/// <summary>
/// Set to true to start a new thread to send Pings to the WebSocket server
/// </summary>
public bool StartPingThread { get; set; }
/// <summary>
/// The delay between two Pings in millisecs. Minimum value is 100, default is 1000.
/// </summary>
public int PingFrequency { get; set; }
/// <summary>
/// The internal HTTPRequest object.
/// </summary>
public HTTPRequest InternalRequest { get; private set; }
/// <summary>
/// IExtension implementations the plugin will negotiate with the server to use.
/// </summary>
public IExtension[] Extensions { get; private set; }
#endif
/// <summary>
/// Called when the connection to the WebSocket server is established.
/// </summary>
public OnWebSocketOpenDelegate OnOpen;
/// <summary>
/// Called when a new textual message is received from the server.
/// </summary>
public OnWebSocketMessageDelegate OnMessage;
/// <summary>
/// Called when a new binary message is received from the server.
/// </summary>
public OnWebSocketBinaryDelegate OnBinary;
/// <summary>
/// Called when the WebSocket connection is closed.
/// </summary>
public OnWebSocketClosedDelegate OnClosed;
/// <summary>
/// Called when an error is encountered. The Exception parameter may be null.
/// </summary>
public OnWebSocketErrorDelegate OnError;
/// <summary>
/// Called when an error is encountered. The parameter will be the description of the error.
/// </summary>
public OnWebSocketErrorDescriptionDelegate OnErrorDesc;
#if (!UNITY_WEBGL || UNITY_EDITOR)
/// <summary>
/// Called when an incomplete frame received. No attempt will be made to reassemble these fragments internally, and no reference are stored after this event to this frame.
/// </summary>
public OnWebSocketIncompleteFrameDelegate OnIncompleteFrame;
#endif
#endregion
#region Private Fields
#if (!UNITY_WEBGL || UNITY_EDITOR)
/// <summary>
/// Indicates wheter we sent out the connection request to the server.
/// </summary>
private bool requestSent;
/// <summary>
/// The internal WebSocketResponse object
/// </summary>
private WebSocketResponse webSocket;
#else
internal static Dictionary<uint, WebSocket> WebSockets = new Dictionary<uint, WebSocket>();
private uint ImplementationId;
private Uri Uri;
private string Protocol;
#endif
#endregion
#region Constructors
/// <summary>
/// Creates a WebSocket instance from the given uri.
/// </summary>
/// <param name="uri">The uri of the WebSocket server</param>
public WebSocket(Uri uri)
:this(uri, string.Empty, string.Empty)
{
#if (!UNITY_WEBGL || UNITY_EDITOR)
this.Extensions = new IExtension[] { new PerMessageCompression(/*compression level: */ Decompression.Zlib.CompressionLevel.Default,
/*clientNoContextTakeover: */ false,
/*serverNoContextTakeover: */ false,
/*clientMaxWindowBits: */ Decompression.Zlib.ZlibConstants.WindowBitsMax,
/*desiredServerMaxWindowBits: */ Decompression.Zlib.ZlibConstants.WindowBitsMax,
/*minDatalengthToCompress: */ 5) };
#endif
}
/// <summary>
/// Creates a WebSocket instance from the given uri, protocol and origin.
/// </summary>
/// <param name="uri">The uri of the WebSocket server</param>
/// <param name="origin">Servers that are not intended to process input from any web page but only for certain sites SHOULD verify the |Origin| field is an origin they expect.
/// If the origin indicated is unacceptable to the server, then it SHOULD respond to the WebSocket handshake with a reply containing HTTP 403 Forbidden status code.</param>
/// <param name="protocol">The application-level protocol that the client want to use(eg. "chat", "leaderboard", etc.). Can be null or empty string if not used.</param>
/// <param name="extensions">Optional IExtensions implementations</param>
public WebSocket(Uri uri, string origin, string protocol
#if !UNITY_WEBGL || UNITY_EDITOR
, params IExtension[] extensions
#endif
)
{
#if !UNITY_WEBGL || UNITY_EDITOR
// Set up some default values.
this.PingFrequency = 1000;
// If there no port set in the uri, we must set it now.
if (uri.Port == -1)
// Somehow if i use the UriBuilder it's not the same as if the uri is constructed from a string...
//uri = new UriBuilder(uri.Scheme, uri.Host, uri.Scheme.Equals("wss", StringComparison.OrdinalIgnoreCase) ? 443 : 80, uri.PathAndQuery).Uri;
uri = new Uri(uri.Scheme + "://" + uri.Host + ":" + (uri.Scheme.Equals("wss", StringComparison.OrdinalIgnoreCase) ? "443" : "80") + uri.GetRequestPathAndQueryURL());
InternalRequest = new HTTPRequest(uri, OnInternalRequestCallback);
// Called when the regular GET request is successfully upgraded to WebSocket
InternalRequest.OnUpgraded = OnInternalRequestUpgraded;
//http://tools.ietf.org/html/rfc6455#section-4
//The request MUST contain a |Host| header field whose value contains /host/ plus optionally ":" followed by /port/ (when not using the default port).
InternalRequest.SetHeader("Host", uri.Host + ":" + uri.Port);
// The request MUST contain an |Upgrade| header field whose value MUST include the "websocket" keyword.
InternalRequest.SetHeader("Upgrade", "websocket");
// The request MUST contain a |Connection| header field whose value MUST include the "Upgrade" token.
InternalRequest.SetHeader("Connection", "keep-alive, Upgrade");
// The request MUST include a header field with the name |Sec-WebSocket-Key|. The value of this header field MUST be a nonce consisting of a
// randomly selected 16-byte value that has been base64-encoded (see Section 4 of [RFC4648]). The nonce MUST be selected randomly for each connection.
InternalRequest.SetHeader("Sec-WebSocket-Key", GetSecKey(new object[] { this, InternalRequest, uri, new object() }));
// The request MUST include a header field with the name |Origin| [RFC6454] if the request is coming from a browser client.
// If the connection is from a non-browser client, the request MAY include this header field if the semantics of that client match the use-case described here for browser clients.
// More on Origin Considerations: http://tools.ietf.org/html/rfc6455#section-10.2
if (!string.IsNullOrEmpty(origin))
InternalRequest.SetHeader("Origin", origin);
// The request MUST include a header field with the name |Sec-WebSocket-Version|. The value of this header field MUST be 13.
InternalRequest.SetHeader("Sec-WebSocket-Version", "13");
if (!string.IsNullOrEmpty(protocol))
InternalRequest.SetHeader("Sec-WebSocket-Protocol", protocol);
// Disable caching
InternalRequest.SetHeader("Cache-Control", "no-cache");
InternalRequest.SetHeader("Pragma", "no-cache");
this.Extensions = extensions;
#if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR)
InternalRequest.DisableCache = true;
#endif
#if !BESTHTTP_DISABLE_PROXY
// WebSocket is not a request-response based protocol, so we need a 'tunnel' through the proxy
if (HTTPManager.Proxy != null)
InternalRequest.Proxy = new HTTPProxy(HTTPManager.Proxy.Address,
HTTPManager.Proxy.Credentials,
false, /*turn on 'tunneling'*/
false, /*sendWholeUri*/
HTTPManager.Proxy.NonTransparentForHTTPS);
#endif
#else
this.Uri = uri;
this.Protocol = protocol;
#endif
}
#endregion
#region Request Callbacks
#if (!UNITY_WEBGL || UNITY_EDITOR)
private void OnInternalRequestCallback(HTTPRequest req, HTTPResponse resp)
{
string reason = string.Empty;
switch (req.State)
{
// The request finished without any problem.
case HTTPRequestStates.Finished:
if (resp.IsSuccess || resp.StatusCode == 101)
{
HTTPManager.Logger.Information("WebSocket", string.Format("Request finished. Status Code: {0} Message: {1}", resp.StatusCode.ToString(), resp.Message));
return;
}
else
reason = string.Format("Request Finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2}",
resp.StatusCode,
resp.Message,
resp.DataAsText);
break;
// The request finished with an unexpected error. The request's Exception property may contain more info about the error.
case HTTPRequestStates.Error:
reason = "Request Finished with Error! " + (req.Exception != null ? ("Exception: " + req.Exception.Message + req.Exception.StackTrace) : string.Empty);
break;
// The request aborted, initiated by the user.
case HTTPRequestStates.Aborted:
reason = "Request Aborted!";
break;
// Connecting to the server is timed out.
case HTTPRequestStates.ConnectionTimedOut:
reason = "Connection Timed Out!";
break;
// The request didn't finished in the given time.
case HTTPRequestStates.TimedOut:
reason = "Processing the request Timed Out!";
break;
default:
return;
}
if (OnError != null)
OnError(this, req.Exception);
if (OnErrorDesc != null)
OnErrorDesc(this, reason);
if (OnError == null && OnErrorDesc == null)
HTTPManager.Logger.Error("WebSocket", reason);
}
private void OnInternalRequestUpgraded(HTTPRequest req, HTTPResponse resp)
{
webSocket = resp as WebSocketResponse;
if (webSocket == null)
{
if (OnError != null)
OnError(this, req.Exception);
if (OnErrorDesc != null)
{
string reason = string.Empty;
if (req.Exception != null)
reason = req.Exception.Message + " " + req.Exception.StackTrace;
OnErrorDesc(this, reason);
}
return;
}
webSocket.WebSocket = this;
if (this.Extensions != null)
{
for (int i = 0; i < this.Extensions.Length; ++i)
{
var ext = this.Extensions[i];
try
{
if (ext != null && !ext.ParseNegotiation(webSocket))
this.Extensions[i] = null; // Keep extensions only that successfully negotiated
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "ParseNegotiation", ex);
// Do not try to use a defective extension in the future
this.Extensions[i] = null;
}
}
}
if (OnOpen != null)
{
try
{
OnOpen(this);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnOpen", ex);
}
}
webSocket.OnText = (ws, msg) =>
{
if (OnMessage != null)
OnMessage(this, msg);
};
webSocket.OnBinary = (ws, bin) =>
{
if (OnBinary != null)
OnBinary(this, bin);
};
webSocket.OnClosed = (ws, code, msg) =>
{
if (OnClosed != null)
OnClosed(this, code, msg);
};
if (OnIncompleteFrame != null)
webSocket.OnIncompleteFrame = (ws, frame) =>
{
if (OnIncompleteFrame != null)
OnIncompleteFrame(this, frame);
};
if (StartPingThread)
webSocket.StartPinging(Math.Max(PingFrequency, 100));
webSocket.StartReceive();
}
#endif
#endregion
#region Public Interface
/// <summary>
/// Start the opening process.
/// </summary>
public void Open()
{
#if (!UNITY_WEBGL || UNITY_EDITOR)
if (requestSent)
throw new InvalidOperationException("Open already called! You can't reuse this WebSocket instance!");
if (this.Extensions != null)
{
try
{
for (int i = 0; i < this.Extensions.Length; ++i)
{
var ext = this.Extensions[i];
if (ext != null)
ext.AddNegotiation(InternalRequest);
}
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "Open", ex);
}
}
InternalRequest.Send();
requestSent = true;
#else
try
{
ImplementationId = WS_Create(this.Uri.OriginalString, this.Protocol, OnOpenCallback, OnTextCallback, OnBinaryCallback, OnErrorCallback, OnCloseCallback);
WebSockets.Add(ImplementationId, this);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "Open", ex);
}
#endif
}
/// <summary>
/// It will send the given message to the server in one frame.
/// </summary>
public void Send(string message)
{
if (!IsOpen)
return;
#if (!UNITY_WEBGL || UNITY_EDITOR)
webSocket.Send(message);
#else
WS_Send_String(this.ImplementationId, message);
#endif
}
/// <summary>
/// It will send the given data to the server in one frame.
/// </summary>
public void Send(byte[] buffer)
{
if (!IsOpen)
return;
#if (!UNITY_WEBGL || UNITY_EDITOR)
webSocket.Send(buffer);
#else
WS_Send_Binary(this.ImplementationId, buffer, 0, buffer.Length);
#endif
}
/// <summary>
/// Will send count bytes from a byte array, starting from offset.
/// </summary>
public void Send(byte[] buffer, ulong offset, ulong count)
{
if (!IsOpen)
return;
#if (!UNITY_WEBGL || UNITY_EDITOR)
webSocket.Send(buffer, offset, count);
#else
WS_Send_Binary(this.ImplementationId, buffer, (int)offset, (int)count);
#endif
}
#if (!UNITY_WEBGL || UNITY_EDITOR)
/// <summary>
/// It will send the given frame to the server.
/// </summary>
public void Send(WebSocketFrame frame)
{
if (IsOpen)
webSocket.Send(frame);
}
#endif
/// <summary>
/// It will initiate the closing of the connection to the server.
/// </summary>
public void Close()
{
if (!IsOpen)
return;
#if (!UNITY_WEBGL || UNITY_EDITOR)
webSocket.Close();
#else
WS_Close(this.ImplementationId, 1000, "Bye!");
#endif
}
/// <summary>
/// It will initiate the closing of the connection to the server sending the given code and message.
/// </summary>
public void Close(UInt16 code, string message)
{
if (!IsOpen)
return;
#if (!UNITY_WEBGL || UNITY_EDITOR)
webSocket.Close(code, message);
#else
WS_Close(this.ImplementationId, code, message);
#endif
}
public static byte[] EncodeCloseData(UInt16 code, string message)
{
//If there is a body, the first two bytes of the body MUST be a 2-byte unsigned integer
// (in network byte order) representing a status code with value /code/ defined in Section 7.4 (http://tools.ietf.org/html/rfc6455#section-7.4). Following the 2-byte integer,
// the body MAY contain UTF-8-encoded data with value /reason/, the interpretation of which is not defined by this specification.
// This data is not necessarily human readable but may be useful for debugging or passing information relevant to the script that opened the connection.
int msgLen = Encoding.UTF8.GetByteCount(message);
using (MemoryStream ms = new MemoryStream(2 + msgLen))
{
byte[] buff = BitConverter.GetBytes(code);
if (BitConverter.IsLittleEndian)
Array.Reverse(buff, 0, buff.Length);
ms.Write(buff, 0, buff.Length);
buff = Encoding.UTF8.GetBytes(message);
ms.Write(buff, 0, buff.Length);
return ms.ToArray();
}
}
#endregion
#region Private Helpers
#if !UNITY_WEBGL || UNITY_EDITOR
private string GetSecKey(object[] from)
{
byte[] keys = new byte[16];
int pos = 0;
for (int i = 0; i < from.Length; ++i)
{
byte[] hash = BitConverter.GetBytes((Int32)from[i].GetHashCode());
for (int cv = 0; cv < hash.Length && pos < keys.Length; ++cv)
keys[pos++] = hash[cv];
}
return Convert.ToBase64String(keys);
}
#endif
#endregion
#region WebGL Static Callbacks
#if UNITY_WEBGL && !UNITY_EDITOR
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketOpenDelegate))]
static void OnOpenCallback(uint id)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
if (ws.OnOpen != null)
{
try
{
ws.OnOpen(ws);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnOpen", ex);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnOpenCallback - No WebSocket found for id: " + id.ToString());
}
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketTextDelegate))]
static void OnTextCallback(uint id, string text)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
if (ws.OnMessage != null)
{
try
{
ws.OnMessage(ws, text);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnMessage", ex);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnTextCallback - No WebSocket found for id: " + id.ToString());
}
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketBinaryDelegate))]
static void OnBinaryCallback(uint id, IntPtr pBuffer, int length)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
if (ws.OnBinary != null)
{
try
{
byte[] buffer = new byte[length];
// Copy data from the 'unmanaged' memory to managed memory. Buffer will be reclaimed by the GC.
Marshal.Copy(pBuffer, buffer, 0, length);
ws.OnBinary(ws, buffer);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnBinary", ex);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnBinaryCallback - No WebSocket found for id: " + id.ToString());
}
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketErrorDelegate))]
static void OnErrorCallback(uint id, string error)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
WebSockets.Remove(id);
if (ws.OnError != null)
{
try
{
ws.OnError(ws, new Exception(error));
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnError", ex);
}
}
if (ws.OnErrorDesc != null)
{
try
{
ws.OnErrorDesc(ws, error);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnErrorDesc", ex);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnErrorCallback - No WebSocket found for id: " + id.ToString());
try
{
WS_Release(id);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "WS_Release", ex);
}
}
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketCloseDelegate))]
static void OnCloseCallback(uint id, int code, string reason)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
WebSockets.Remove(id);
if (ws.OnClosed != null)
{
try
{
ws.OnClosed(ws, (ushort)code, reason);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnClosed", ex);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnCloseCallback - No WebSocket found for id: " + id.ToString());
try
{
WS_Release(id);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "WS_Release", ex);
}
}
#endif
#endregion
#region WebGL Interface
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
static extern uint WS_Create(string url, string protocol, OnWebGLWebSocketOpenDelegate onOpen, OnWebGLWebSocketTextDelegate onText, OnWebGLWebSocketBinaryDelegate onBinary, OnWebGLWebSocketErrorDelegate onError, OnWebGLWebSocketCloseDelegate onClose);
[DllImport("__Internal")]
static extern WebSocketStates WS_GetState(uint id);
[DllImport("__Internal")]
static extern int WS_Send_String(uint id, string strData);
[DllImport("__Internal")]
static extern int WS_Send_Binary(uint id, byte[] buffer, int pos, int length);
[DllImport("__Internal")]
static extern void WS_Close(uint id, ushort code, string reason);
[DllImport("__Internal")]
static extern void WS_Release(uint id);
#endif
#endregion
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 56ff529b39d274989a3a1116fcfb7dd2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,446 @@
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
using System;
using System.IO;
using System.Threading;
using System.Collections.Generic;
using System.Text;
using BestHTTP.Extensions;
using BestHTTP.WebSocket.Frames;
namespace BestHTTP.WebSocket
{
public sealed class WebSocketResponse : HTTPResponse, IHeartbeat, IProtocol
{
#region Public Interface
/// <summary>
/// A reference to the original WebSocket instance. Used for accessing extensions.
/// </summary>
public WebSocket WebSocket { get; internal set; }
/// <summary>
/// Called when a Text message received
/// </summary>
public Action<WebSocketResponse, string> OnText;
/// <summary>
/// Called when a Binary message received
/// </summary>
public Action<WebSocketResponse, byte[]> OnBinary;
/// <summary>
/// Called when an incomplete frame received. No attempt will be made to reassemble these fragments.
/// </summary>
public Action<WebSocketResponse, WebSocketFrameReader> OnIncompleteFrame;
/// <summary>
/// Called when the connection closed.
/// </summary>
public Action<WebSocketResponse, UInt16, string> OnClosed;
/// <summary>
/// Indicates whether the connection to the server is closed or not.
/// </summary>
public bool IsClosed { get { return closed; } }
/// <summary>
/// On what frequency we have to send a ping to the server.
/// </summary>
public TimeSpan PingFrequnecy { get; private set; }
/// <summary>
/// Maximum size of a fragment's payload data. Its default value is 32767.
/// </summary>
public UInt16 MaxFragmentSize { get; private set; }
#endregion
#region Private Fields
private List<WebSocketFrameReader> IncompleteFrames = new List<WebSocketFrameReader>();
private List<WebSocketFrameReader> CompletedFrames = new List<WebSocketFrameReader>();
private WebSocketFrameReader CloseFrame;
private object FrameLock = new object();
private object SendLock = new object();
/// <summary>
/// True if we sent out a Close message to the server
/// </summary>
private bool closeSent;
/// <summary>
/// True if this WebSocket connection is closed
/// </summary>
private bool closed;
private DateTime lastPing = DateTime.MinValue;
#endregion
internal WebSocketResponse(HTTPRequest request, Stream stream, bool isStreamed, bool isFromCache)
: base(request, stream, isStreamed, isFromCache)
{
base.IsClosedManually = true;
closed = false;
MaxFragmentSize = UInt16.MaxValue / 2;
}
internal void StartReceive()
{
if (IsUpgraded)
{
#if NETFX_CORE
#pragma warning disable 4014
Windows.System.Threading.ThreadPool.RunAsync(ReceiveThreadFunc);
#pragma warning restore 4014
#else
ThreadPool.QueueUserWorkItem(ReceiveThreadFunc);
//new Thread(ReceiveThreadFunc)
// .Start();
#endif
}
}
#region Public interface for interacting with the server
/// <summary>
/// It will send the given message to the server in one frame.
/// </summary>
public void Send(string message)
{
if (message == null)
throw new ArgumentNullException("message must not be null!");
byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
Send(new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.Text, data));
}
/// <summary>
/// It will send the given data to the server in one frame.
/// </summary>
public void Send(byte[] data)
{
if (data == null)
throw new ArgumentNullException("data must not be null!");
WebSocketFrame frame = new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.Binary, data);
if (frame.Data != null && frame.Data.Length > this.MaxFragmentSize)
{
WebSocketFrame[] additionalFrames = frame.Fragment(this.MaxFragmentSize);
lock(SendLock)
{
Send(frame);
if (additionalFrames != null)
for (int i = 0; i < additionalFrames.Length; ++i)
Send(additionalFrames[i]);
}
}
else
Send(frame);
}
/// <summary>
/// Will send count bytes from a byte array, starting from offset.
/// </summary>
public void Send(byte[] data, ulong offset, ulong count)
{
if (data == null)
throw new ArgumentNullException("data must not be null!");
if (offset + count > (ulong)data.Length)
throw new ArgumentOutOfRangeException("offset + count >= data.Length");
WebSocketFrame frame = new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.Binary, data, offset, count, true, true);
if (frame.Data != null && frame.Data.Length > this.MaxFragmentSize)
{
WebSocketFrame[] additionalFrames = frame.Fragment(this.MaxFragmentSize);
lock (SendLock)
{
Send(frame);
if (additionalFrames != null)
for (int i = 0; i < additionalFrames.Length; ++i)
Send(additionalFrames[i]);
}
}
else
Send(frame);
}
/// <summary>
/// It will send the given frame to the server.
/// </summary>
public void Send(WebSocketFrame frame)
{
if (frame == null)
throw new ArgumentNullException("frame is null!");
if (closed)
return;
byte[] rawData = frame.Get();
lock (SendLock)
{
Stream.Write(rawData, 0, rawData.Length);
Stream.Flush();
if (frame.Type == WebSocketFrameTypes.ConnectionClose)
closeSent = true;
}
}
/// <summary>
/// It will initiate the closing of the connection to the server.
/// </summary>
public void Close()
{
Close(1000, "Bye!");
}
/// <summary>
/// It will initiate the closing of the connection to the server.
/// </summary>
public void Close(UInt16 code, string msg)
{
if (closed)
return;
Send(new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.ConnectionClose, WebSocket.EncodeCloseData(code, msg)));
}
public void StartPinging(int frequency)
{
if (frequency < 100)
throw new ArgumentException("frequency must be at least 100 millisec!");
PingFrequnecy = TimeSpan.FromMilliseconds(frequency);
HTTPManager.Heartbeats.Subscribe(this);
}
#endregion
#region Private Threading Functions
private void ReceiveThreadFunc(object param)
{
try
{
while (!closed)
{
try
{
WebSocketFrameReader frame = new WebSocketFrameReader();
frame.Read(Stream);
// A server MUST NOT mask any frames that it sends to the client. A client MUST close a connection if it detects a masked frame.
// In this case, it MAY use the status code 1002 (protocol error)
// (These rules might be relaxed in a future specification.)
if (frame.HasMask)
{
Close(1002, "Protocol Error: masked frame received from server!");
continue;
}
if (!frame.IsFinal)
{
if (OnIncompleteFrame == null)
IncompleteFrames.Add(frame);
else
lock (FrameLock) CompletedFrames.Add(frame);
continue;
}
switch (frame.Type)
{
// For a complete documentation and rules on fragmentation see http://tools.ietf.org/html/rfc6455#section-5.4
// A fragmented Frame's last fragment's opcode is 0 (Continuation) and the FIN bit is set to 1.
case WebSocketFrameTypes.Continuation:
// Do an assemble pass only if OnFragment is not set. Otherwise put it in the CompletedFrames, we will handle it in the HandleEvent phase.
if (OnIncompleteFrame == null)
{
frame.Assemble(IncompleteFrames);
// Remove all incomplete frames
IncompleteFrames.Clear();
// Control frames themselves MUST NOT be fragmented. So, its a normal text or binary frame. Go, handle it as usual.
goto case WebSocketFrameTypes.Binary;
}
else
lock (FrameLock) CompletedFrames.Add(frame);
break;
case WebSocketFrameTypes.Text:
case WebSocketFrameTypes.Binary:
frame.DecodeWithExtensions(WebSocket);
lock (FrameLock) CompletedFrames.Add(frame);
break;
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in response, unless it already received a Close frame.
case WebSocketFrameTypes.Ping:
if (!closeSent && !closed)
Send(new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.Pong, frame.Data));
break;
// If an endpoint receives a Close frame and did not previously send a Close frame, the endpoint MUST send a Close frame in response.
case WebSocketFrameTypes.ConnectionClose:
CloseFrame = frame;
if (!closeSent)
Send(new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.ConnectionClose, null));
closed = closeSent;
break;
}
}
#if !NETFX_CORE
catch (ThreadAbortException)
{
IncompleteFrames.Clear();
this.baseRequest.State = HTTPRequestStates.Aborted;
closed = true;
}
#endif
catch (Exception e)
{
if (HTTPUpdateDelegator.IsCreated)
{
this.baseRequest.Exception = e;
this.baseRequest.State = HTTPRequestStates.Error;
}
else
this.baseRequest.State = HTTPRequestStates.Aborted;
closed = true;
}
}
}
finally
{
HTTPManager.Heartbeats.Unsubscribe(this);
}
}
#endregion
#region Sending Out Events
/// <summary>
/// Internal function to send out received messages.
/// </summary>
void IProtocol.HandleEvents()
{
lock (FrameLock)
{
for (int i = 0; i < CompletedFrames.Count; ++i)
{
WebSocketFrameReader frame = CompletedFrames[i];
// Bugs in the clients shouldn't interrupt the code, so we need to try-catch and ignore any exception occurring here
try
{
switch (frame.Type)
{
case WebSocketFrameTypes.Continuation:
if (OnIncompleteFrame != null)
OnIncompleteFrame(this, frame);
break;
case WebSocketFrameTypes.Text:
// Any not Final frame is handled as a fragment
if (!frame.IsFinal)
goto case WebSocketFrameTypes.Continuation;
if (OnText != null)
OnText(this, frame.DataAsText);
break;
case WebSocketFrameTypes.Binary:
// Any not Final frame is handled as a fragment
if (!frame.IsFinal)
goto case WebSocketFrameTypes.Continuation;
if (OnBinary != null)
OnBinary(this, frame.Data);
break;
}
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocketResponse", "HandleEvents", ex);
}
}
CompletedFrames.Clear();
}//lock (ReadLock)
// 2015.05.09
// State checking added because if there is an error the OnClose called first, and then the OnError.
// Now, when there is an error only the OnError event will be called!
if (IsClosed && OnClosed != null && baseRequest.State == HTTPRequestStates.Processing)
{
try
{
UInt16 statusCode = 0;
string msg = string.Empty;
// If we received any data, we will get the status code and the message from it
if (CloseFrame != null && CloseFrame.Data != null && CloseFrame.Data.Length >= 2)
{
if (BitConverter.IsLittleEndian)
Array.Reverse(CloseFrame.Data, 0, 2);
statusCode = BitConverter.ToUInt16(CloseFrame.Data, 0);
if (CloseFrame.Data.Length > 2)
msg = Encoding.UTF8.GetString(CloseFrame.Data, 2, CloseFrame.Data.Length - 2);
}
OnClosed(this, statusCode, msg);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocketResponse", "HandleEvents - OnClosed", ex);
}
}
}
#endregion
#region IHeartbeat Implementation
void IHeartbeat.OnHeartbeatUpdate(TimeSpan dif)
{
if (lastPing == DateTime.MinValue)
{
lastPing = DateTime.UtcNow;
return;
}
if (DateTime.UtcNow - lastPing >= PingFrequnecy)
{
try
{
Send(new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.Ping, Encoding.UTF8.GetBytes(string.Empty)));
}
catch
{
this.closed = true;
HTTPManager.Heartbeats.Unsubscribe(this);
}
lastPing = DateTime.UtcNow;
}
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 532deccea1c7849078621f972d3fb482
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,83 @@
#if !BESTHTTP_DISABLE_WEBSOCKET
namespace BestHTTP.WebSocket
{
/// <summary>
/// http://tools.ietf.org/html/rfc6455#section-7.4.1
/// </summary>
public enum WebSocketStausCodes : uint
{
/// <summary>
/// Indicates a normal closure, meaning that the purpose for which the connection was established has been fulfilled.
/// </summary>
NormalClosure = 1000,
/// <summary>
/// Indicates that an endpoint is "going away", such as a server going down or a browser having navigated away from a page.
/// </summary>
GoingAway = 1001,
/// <summary>
/// Indicates that an endpoint is terminating the connection due to a protocol error.
/// </summary>
ProtocolError = 1002,
/// <summary>
/// Indicates that an endpoint is terminating the connection because it has received a type of data it cannot accept
/// (e.g., an endpoint that understands only text data MAY send this if it receives a binary message).
/// </summary>
WrongDataType = 1003,
/// <summary>
/// Reserved. The specific meaning might be defined in the future.
/// </summary>
Reserved = 1004,
/// <summary>
/// A reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint.
/// It is designated for use in applications expecting a status code to indicate that no status code was actually present.
/// </summary>
NoStatusCode = 1005,
/// <summary>
/// A reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint.
/// It is designated for use in applications expecting a status code to indicate that the connection was closed abnormally, e.g., without sending or receiving a Close control frame.
/// </summary>
ClosedAbnormally = 1006,
/// <summary>
/// Indicates that an endpoint is terminating the connection because it has received data within a message that was not consistent with the type of the message (e.g., non-UTF-8 [RFC3629] data within a text message).
/// </summary>
DataError = 1007,
/// <summary>
/// Indicates that an endpoint is terminating the connection because it has received a message that violates its policy.
/// This is a generic status code that can be returned when there is no other more suitable status code (e.g., 1003 or 1009) or if there is a need to hide specific details about the policy.
/// </summary>
PolicyError = 1008,
/// <summary>
/// Indicates that an endpoint is terminating the connection because it has received a message that is too big for it to process.
/// </summary>
TooBigMessage = 1009,
/// <summary>
/// Indicates that an endpoint (client) is terminating the connection because it has expected the server to negotiate one or more extension,
/// but the server didn't return them in the response message of the WebSocket handshake.
/// The list of extensions that are needed SHOULD appear in the /reason/ part of the Close frame. Note that this status code is not used by the server, because it can fail the WebSocket handshake instead.
/// </summary>
ExtensionExpected = 1010,
/// <summary>
/// Indicates that a server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request.
/// </summary>
WrongRequest = 1011,
/// <summary>
/// A reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint. It is designated for use in applications expecting a status code to indicate that the connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified).
/// </summary>
TLSHandshakeError = 1015
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 10f9c2e789b344d828e11e7a68583586
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: