提交Unity 联机Pro

This commit is contained in:
PC-20230316NUNE\Administrator
2024-08-17 14:27:18 +08:00
parent f00193b000
commit 894100ae37
7448 changed files with 854473 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5e44072d24b360245b3b38f37c984cee
folderAsset: yes
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,42 @@
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
using BestHTTP.PlatformSupport.Memory;
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(HTTPResponse 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>
BufferSegment Encode(WebSocketFrame writer);
/// <summary>
/// This function can be used the decode the server-sent data.
/// </summary>
BufferSegment Decode(byte header, BufferSegment data);
}
}
#endif

View File

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

View File

@@ -0,0 +1,339 @@
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
using System;
using BestHTTP.Extensions;
using BestHTTP.WebSocket.Frames;
using BestHTTP.Decompression.Zlib;
using BestHTTP.PlatformSupport.Memory;
namespace BestHTTP.WebSocket.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(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(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)
{
if (compressorOutputStream == null)
compressorOutputStream = new BufferPoolMemoryStream();
compressorOutputStream.SetLength(0);
if (compressorDeflateStream == null)
{
compressorDeflateStream = new DeflateStream(compressorOutputStream, CompressionMode.Compress, this.Level, true, 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;
}
}
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)
{
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, true, 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;
}
return result;
}
#endregion
}
}
#endif

View File

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

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d07681f5c8c854d42b668e7c6f0a8764
folderAsset: yes
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,355 @@
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
using System;
using BestHTTP.Extensions;
using BestHTTP.PlatformSupport.Memory;
using System.Runtime.CompilerServices;
using BestHTTP.PlatformSupport.IL2CPP;
using BestHTTP.Logger;
#if BESTHTTP_WITH_BURST
using Unity.Burst;
using Unity.Burst.Intrinsics;
using static Unity.Burst.Intrinsics.X86.Avx2;
using static Unity.Burst.Intrinsics.X86.Sse2;
using static Unity.Burst.Intrinsics.Arm.Neon;
#endif
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>
#if BESTHTTP_WITH_BURST
[BurstCompile]
#endif
[Il2CppEagerStaticClassConstruction]
public struct WebSocketFrame
{
public WebSocketFrameTypes Type { get; private set; }
public BufferSegment Data { get; private set; }
public WebSocket Websocket { get; private set; }
public byte Header;
public WebSocketFrame(WebSocket webSocket, WebSocketFrameTypes type, BufferSegment data)
:this(webSocket, type, data, true)
{ }
public WebSocketFrame(WebSocket webSocket, WebSocketFrameTypes type, BufferSegment data, bool useExtensions)
: this(webSocket, type, data, true, useExtensions)
{
}
public WebSocketFrame(WebSocket webSocket, WebSocketFrameTypes type, BufferSegment data, bool isFinal, bool useExtensions)
:this(webSocket, type, data, isFinal, useExtensions, copyData: true)
{
}
public WebSocketFrame(WebSocket webSocket, WebSocketFrameTypes type, BufferSegment data, bool isFinal, bool useExtensions, bool copyData)
{
if (!isFinal)
throw new NotSupportedException("isFinal parameter is not true");
this.Type = type;
this.Websocket = webSocket;
this.Data = data;
if (this.Data.Data != null)
{
if (copyData)
{
var from = this.Data;
var buffer = BufferPool.Get(this.Data.Count, true);
this.Data = new BufferSegment(buffer, 0, this.Data.Count);
Array.Copy(from.Data, (int)from.Offset, this.Data.Data, this.Data.Offset, this.Data.Count);
}
}
else
this.Data = BufferSegment.Empty;
// We use the header only for storing extension flags only
this.Header = 0x00;
}
public override string ToString()
{
return string.Format("[WebSocketFrame Type: {0}, Header: {1:X2}, Data: {2}]", this.Type, this.Header, this.Data);
}
public void WriteTo(Action<BufferSegment, BufferSegment> callback, uint maxFragmentSize, bool mask, LoggingContext context)
{
if (HTTPManager.Logger.Level <= Logger.Loglevels.All)
HTTPManager.Logger.Verbose("WebSocketFrame", "WriteTo - Frame: " + ToString(), context);
if ((this.Type == WebSocketFrameTypes.Binary || this.Type == WebSocketFrameTypes.Text))
{
DoExtensions();
if (this.Data.Count > maxFragmentSize)
FragmentAndSend(callback, maxFragmentSize, mask, context);
else
WriteFragment(callback, (byte)(0x80 | this.Header | (byte)this.Type), this.Data, mask, context);
}
else
{
WriteFragment(callback, (byte)(0x80 | this.Header | (byte)this.Type), this.Data, mask, context);
}
}
private void DoExtensions()
{
if (this.Websocket != null && this.Websocket.Extensions != null)
{
for (int i = 0; i < this.Websocket.Extensions.Length; ++i)
{
var ext = this.Websocket.Extensions[i];
if (ext != null)
{
this.Header |= ext.GetFrameHeader(this, this.Header);
BufferSegment newData = ext.Encode(this);
if (newData != this.Data)
{
BufferPool.Release(this.Data);
this.Data = newData;
}
}
}
}
}
private void FragmentAndSend(Action<BufferSegment, BufferSegment> callback, uint maxFragmentSize, bool mask, LoggingContext context)
{
int pos = this.Data.Offset;
int endPos = this.Data.Offset + this.Data.Count;
byte header = (byte)(0x00 | this.Header | (byte)this.Type);
while (pos < endPos)
{
int chunkLength = Math.Min((int)maxFragmentSize, endPos - pos);
WriteFragment(callback: callback,
Header: header,
Data: this.Data.Slice((int)pos, (int)chunkLength),
mask: mask,
context: context);
pos += chunkLength;
// set only the IsFinal flag, every other flags are zero
header = (byte)(pos + chunkLength >= this.Data.Count ? 0x80 : 0x00);
}
}
private static unsafe void WriteFragment(Action<BufferSegment, BufferSegment> callback, byte Header, BufferSegment Data, bool mask, LoggingContext context)
{
// For the complete documentation for this section see:
// http://tools.ietf.org/html/rfc6455#section-5.2
// Header(1) + Len(8) + Mask (4)
byte[] wsHeader = BufferPool.Get(13, true);
int pos = 0;
// Write the header
wsHeader[pos++] = 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 (Data.Count < 126)
{
wsHeader[pos++] = (byte)(0x80 | (byte)Data.Count);
}
else if (Data.Count < UInt16.MaxValue)
{
wsHeader[pos++] = (byte)(0x80 | 126);
var count = (UInt16)Data.Count;
wsHeader[pos++] = (byte)(count >> 8);
wsHeader[pos++] = (byte)(count);
}
else
{
wsHeader[pos++] = (byte)(0x80 | 127);
var count = (UInt64)Data.Count;
wsHeader[pos++] = (byte)(count >> 56);
wsHeader[pos++] = (byte)(count >> 48);
wsHeader[pos++] = (byte)(count >> 40);
wsHeader[pos++] = (byte)(count >> 32);
wsHeader[pos++] = (byte)(count >> 24);
wsHeader[pos++] = (byte)(count >> 16);
wsHeader[pos++] = (byte)(count >> 8);
wsHeader[pos++] = (byte)(count);
}
if (Data != BufferSegment.Empty)
{
// 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.
uint hash = mask ? (uint)wsHeader.GetHashCode() : 0;
wsHeader[pos++] = (byte)(hash >> 24);
wsHeader[pos++] = (byte)(hash >> 16);
wsHeader[pos++] = (byte)(hash >> 8);
wsHeader[pos++] = (byte)(hash);
// Do the masking.
if (mask)
{
fixed (byte* pData = Data.Data/*, pmask = &wsHeader[pos - 4]*/)
{
byte* alignedMask = stackalloc byte[4];
alignedMask[0] = wsHeader[pos - 4];
alignedMask[1] = wsHeader[pos - 3];
alignedMask[2] = wsHeader[pos - 2];
alignedMask[3] = wsHeader[pos - 1];
ApplyMask(pData, Data.Offset, Data.Count, alignedMask);
}
}
}
else
{
wsHeader[pos++] = 0;
wsHeader[pos++] = 0;
wsHeader[pos++] = 0;
wsHeader[pos++] = 0;
}
var header = wsHeader.AsBuffer(pos);
if (HTTPManager.Logger.Level <= Logger.Loglevels.All)
HTTPManager.Logger.Verbose("WebSocketFrame", string.Format("WriteFragment - Header: {0}, data chunk: {1}", header.ToString(), Data.ToString()), context);
callback(header, Data);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#if BESTHTTP_WITH_BURST
[BurstCompile(CompileSynchronously = true)]
#endif
public unsafe static void ApplyMask(
#if BESTHTTP_WITH_BURST
[NoAlias]
#endif
byte* pData,
int DataOffset,
int DataCount,
#if BESTHTTP_WITH_BURST
[NoAlias]
#endif
byte* pmask
)
{
int targetOffset = DataOffset + DataCount;
uint umask = *(uint*)pmask;
#if BESTHTTP_WITH_BURST
if (targetOffset - DataOffset >= 32)
{
if (IsAvx2Supported)
{
v256 mask = new v256(umask);
v256 ldstrMask = new v256((byte)0xFF);
while (targetOffset - DataOffset >= 32)
{
// load data
v256 data = mm256_maskload_epi32(pData + DataOffset, ldstrMask);
// xor
v256 result = mm256_xor_si256(data, mask);
// store
mm256_maskstore_epi32(pData + DataOffset, ldstrMask, result);
// advance
DataOffset += 32;
}
}
}
if (targetOffset - DataOffset >= 16)
{
v128 mask = new v128(umask);
if (IsSse2Supported)
{
while (targetOffset - DataOffset >= 16)
{
// load data
v128 data = loadu_si128(pData + DataOffset);
// xor
var result = xor_si128(data, mask);
// store
storeu_si128(pData + DataOffset, result);
// advance
DataOffset += 16;
}
}
else if (IsNeonSupported)
{
while (targetOffset - DataOffset >= 16)
{
// load data
v128 data = vld1q_u8(pData + DataOffset);
// xor
v128 result = veorq_u8(data, mask);
// store
vst1q_u8(pData + DataOffset, result);
// advance
DataOffset += 16;
}
}
}
#endif
// fallback to calculate by reinterpret-casting to ulong
if (targetOffset - DataOffset >= 8)
{
ulong* ulpData = (ulong*)(pData + DataOffset);
#if UNITY_ANDROID && !UNITY_EDITOR
if ((long)ulpData % sizeof(ulong) == 0)
{
#endif
// duplicate the mask to fill up a whole ulong.
ulong ulmask = (((ulong)umask << 32) | umask);
while (targetOffset - DataOffset >= 8)
{
*ulpData = *ulpData ^ ulmask;
ulpData++;
DataOffset += 8;
}
#if UNITY_ANDROID && !UNITY_EDITOR
}
#endif
}
// process remaining bytes (0..7)
for (int i = DataOffset; i < targetOffset; ++i)
pData[i] = (byte)(pData[i] ^ pmask[(i - DataOffset) % 4]);
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,220 @@
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
using System;
using System.Collections.Generic;
using System.IO;
using BestHTTP.Extensions;
using BestHTTP.PlatformSupport.Memory;
namespace BestHTTP.WebSocket.Frames
{
/// <summary>
/// Represents an incoming WebSocket Frame.
/// </summary>
public struct 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>
/// The decoded array of bytes.
/// </summary>
public BufferSegment Data { get; private set; }
/// <summary>
/// Textual representation of the received Data.
/// </summary>
public string DataAsText { get; private set; }
#endregion
#region Internal & Private Functions
internal unsafe 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
if ((maskAndLength & 0x80) != 0)
throw new NotImplementedException($"Payload from the server is masked!");
// if 0-125, that is the payload length.
var 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 = BufferPool.Get(2, true);
stream.ReadBuffer(rawLen, 2);
if (BitConverter.IsLittleEndian)
Array.Reverse(rawLen, 0, 2);
length = (UInt64)BitConverter.ToUInt16(rawLen, 0);
BufferPool.Release(rawLen);
}
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 = BufferPool.Get(8, true);
stream.ReadBuffer(rawLen, 8);
if (BitConverter.IsLittleEndian)
Array.Reverse(rawLen, 0, 8);
length = (UInt64)BitConverter.ToUInt64(rawLen, 0);
BufferPool.Release(rawLen);
}
if (length == 0L)
{
Data = BufferSegment.Empty;
return;
}
var buffer = BufferPool.Get((long)length, true);
uint readLength = 0;
try
{
do
{
int read = stream.Read(buffer, (int)readLength, (int)(length - readLength));
if (read <= 0)
throw ExceptionHelper.ServerClosedTCPStream();
readLength += (uint)read;
} while (readLength < length);
}
catch
{
BufferPool.Release(buffer);
throw;
}
this.Data = new BufferSegment(buffer, 0, (int)length);
}
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 += (UInt64)fragments[i].Data.Count;
byte[] buffer = BufferPool.Get((long)finalLength, true);
UInt64 pos = 0;
for (int i = 0; i < fragments.Count; ++i)
{
if (fragments[i].Data.Count > 0)
Array.Copy(fragments[i].Data.Data, fragments[i].Data.Offset, buffer, (int)pos, (int)fragments[i].Data.Count);
fragments[i].ReleaseData();
pos += (UInt64)fragments[i].Data.Count;
}
// 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.Data = new BufferSegment(buffer, 0, (int)finalLength);
}
/// <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)
{
var newData = ext.Decode(this.Header, this.Data);
if (this.Data != newData)
{
this.ReleaseData();
this.Data = newData;
}
}
}
if (this.Type == WebSocketFrameTypes.Text)
{
if (this.Data != BufferSegment.Empty)
{
this.DataAsText = System.Text.Encoding.UTF8.GetString(this.Data.Data, this.Data.Offset, this.Data.Count);
this.ReleaseData();
}
else
HTTPManager.Logger.Warning("WebSocketFrameReader", "Empty Text frame received!");
}
}
public void ReleaseData()
{
BufferPool.Release(this.Data);
this.Data = BufferSegment.Empty;
}
public override string ToString()
{
return string.Format("[{0} Header: {1:X2}, IsFinal: {2}, Data: {3}]", this.Type.ToString(), this.Header, this.IsFinal, this.Data);
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 551161b05e7dd8c4d85aadf44c841056
MonoImporter:
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,10 @@
fileFormatVersion: 2
guid: 111b231c861908546b2c5acfa6d69bae
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,364 @@
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_WEBSOCKET
using System;
using BestHTTP.Connections;
using BestHTTP.Extensions;
using BestHTTP.PlatformSupport.Memory;
using BestHTTP.WebSocket.Frames;
namespace BestHTTP.WebSocket
{
internal sealed class OverHTTP1 : WebSocketBaseImplementation
{
public override bool IsOpen => webSocket != null && !webSocket.IsClosed;
public override int BufferedAmount => webSocket.BufferedAmount;
public override int Latency => this.webSocket.Latency;
public override DateTime LastMessageReceived => this.webSocket.lastMessage;
/// <summary>
/// Indicates whether we sent out the connection request to the server.
/// </summary>
private bool requestSent;
/// <summary>
/// The internal WebSocketResponse object
/// </summary>
private WebSocketResponse webSocket;
public OverHTTP1(WebSocket parent, Uri uri, string origin, string protocol) : base(parent, uri, origin, protocol)
{
string scheme = HTTPProtocolFactory.IsSecureProtocol(uri) ? "wss" : "ws";
int port = uri.Port != -1 ? uri.Port : (scheme.Equals("wss", StringComparison.OrdinalIgnoreCase) ? 443 : 80);
// 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;
base.Uri = new Uri(scheme + "://" + uri.Host + ":" + port + uri.GetRequestPathAndQueryURL());
}
protected override void CreateInternalRequest()
{
if (this._internalRequest != null)
return;
this._internalRequest = new HTTPRequest(base.Uri, OnInternalRequestCallback);
this._internalRequest.Context.Add("WebSocket", this.Parent.Context);
// Called when the regular GET request is successfully upgraded to WebSocket
this._internalRequest.OnUpgraded = OnInternalRequestUpgraded;
//http://tools.ietf.org/html/rfc6455#section-4
// The request MUST contain an |Upgrade| header field whose value MUST include the "websocket" keyword.
this._internalRequest.SetHeader("Upgrade", "websocket");
// The request MUST contain a |Connection| header field whose value MUST include the "Upgrade" token.
this._internalRequest.SetHeader("Connection", "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.
this._internalRequest.SetHeader("Sec-WebSocket-Key", WebSocket.GetSecKey(new object[] { this, InternalRequest, base.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))
this._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.
this._internalRequest.SetHeader("Sec-WebSocket-Version", "13");
if (!string.IsNullOrEmpty(Protocol))
this._internalRequest.SetHeader("Sec-WebSocket-Protocol", Protocol);
// Disable caching
this._internalRequest.SetHeader("Cache-Control", "no-cache");
this._internalRequest.SetHeader("Pragma", "no-cache");
#if !BESTHTTP_DISABLE_CACHING
this._internalRequest.DisableCache = true;
#endif
#if !BESTHTTP_DISABLE_PROXY && (!UNITY_WEBGL || UNITY_EDITOR)
this._internalRequest.Proxy = this.Parent.GetProxy(this.Uri);
#endif
this._internalRequest.OnBeforeRedirection += InternalRequest_OnBeforeRedirection;
if (this.Parent.OnInternalRequestCreated != null)
{
try
{
this.Parent.OnInternalRequestCreated(this.Parent, this._internalRequest);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP1", "CreateInternalRequest", ex, this.Parent.Context);
}
}
}
private bool InternalRequest_OnBeforeRedirection(HTTPRequest originalRequest, HTTPResponse response, Uri redirectUri)
{
// We have to re-select/reset the implementation in the parent Websocket, as the redirected request might gets served over a HTTP/2 connection!
this.Parent.SelectImplementation(redirectUri, originalRequest.GetFirstHeaderValue("Origin"), originalRequest.GetFirstHeaderValue("Sec-WebSocket-Protocol"))
.StartOpen();
originalRequest.Callback = null;
return false;
}
public override void StartClose(UInt16 code, string message)
{
if (this.State == WebSocketStates.Connecting)
{
if (this.InternalRequest != null)
this.InternalRequest.Abort();
this.State = WebSocketStates.Closed;
if (this.Parent.OnClosed != null)
this.Parent.OnClosed(this.Parent, (ushort)WebSocketStausCodes.NoStatusCode, string.Empty);
}
else
{
this.State = WebSocketStates.Closing;
webSocket.Close(code, message);
}
}
public override void StartOpen()
{
if (requestSent)
throw new InvalidOperationException("Open already called! You can't reuse this WebSocket instance!");
if (this.Parent.Extensions != null)
{
try
{
for (int i = 0; i < this.Parent.Extensions.Length; ++i)
{
var ext = this.Parent.Extensions[i];
if (ext != null)
ext.AddNegotiation(InternalRequest);
}
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP1", "Open", ex, this.Parent.Context);
}
}
InternalRequest.Send();
requestSent = true;
this.State = WebSocketStates.Connecting;
}
private void OnInternalRequestCallback(HTTPRequest req, HTTPResponse resp)
{
string reason = string.Empty;
switch (req.State)
{
case HTTPRequestStates.Finished:
HTTPManager.Logger.Information("OverHTTP1", string.Format("Request finished. Status Code: {0} Message: {1}", resp.StatusCode.ToString(), resp.Message), this.Parent.Context);
if (resp.StatusCode == 101)
{
// The request upgraded successfully.
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 (this.State != WebSocketStates.Connecting || !string.IsNullOrEmpty(reason))
{
if (this.Parent.OnError != null)
this.Parent.OnError(this.Parent, reason);
else if (!HTTPManager.IsQuitting)
HTTPManager.Logger.Error("OverHTTP1", reason, this.Parent.Context);
}
else if (this.Parent.OnClosed != null)
this.Parent.OnClosed(this.Parent, (ushort)WebSocketStausCodes.NormalClosure, "Closed while opening");
this.State = WebSocketStates.Closed;
if (!req.IsKeepAlive && resp != null && resp is WebSocketResponse)
(resp as WebSocketResponse).CloseStream();
}
private void OnInternalRequestUpgraded(HTTPRequest req, HTTPResponse resp)
{
HTTPManager.Logger.Information("OverHTTP1", "Internal request upgraded!", this.Parent.Context);
webSocket = resp as WebSocketResponse;
if (webSocket == null)
{
if (this.Parent.OnError != null)
{
string reason = string.Empty;
if (req.Exception != null)
reason = req.Exception.Message + " " + req.Exception.StackTrace;
this.Parent.OnError(this.Parent, reason);
}
this.State = WebSocketStates.Closed;
return;
}
// If Close called while we connected
if (this.State == WebSocketStates.Closed)
{
webSocket.CloseStream();
return;
}
if (!resp.HasHeader("sec-websocket-accept"))
{
this.State = WebSocketStates.Closed;
webSocket.CloseStream();
if (this.Parent.OnError != null)
this.Parent.OnError(this.Parent, "No Sec-Websocket-Accept header is sent by the server!");
return;
}
webSocket.WebSocket = this.Parent;
if (this.Parent.Extensions != null)
{
for (int i = 0; i < this.Parent.Extensions.Length; ++i)
{
var ext = this.Parent.Extensions[i];
try
{
if (ext != null && !ext.ParseNegotiation(webSocket))
this.Parent.Extensions[i] = null; // Keep extensions only that successfully negotiated
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP1", "ParseNegotiation", ex, this.Parent.Context);
// Do not try to use a defective extension in the future
this.Parent.Extensions[i] = null;
}
}
}
this.State = WebSocketStates.Open;
if (this.Parent.OnOpen != null)
{
try
{
this.Parent.OnOpen(this.Parent);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP1", "OnOpen", ex, this.Parent.Context);
}
}
webSocket.OnText = (ws, msg) =>
{
if (this.Parent.OnMessage != null)
this.Parent.OnMessage(this.Parent, msg);
};
webSocket.OnBinaryNoAlloc = (ws, frame) =>
{
if (this.Parent.OnBinary != null)
{
var bin = new byte[frame.Count];
Array.Copy(frame.Data, 0, bin, 0, frame.Count);
this.Parent.OnBinary(this.Parent, bin);
}
if (this.Parent.OnBinaryNoAlloc != null)
this.Parent.OnBinaryNoAlloc(this.Parent, frame);
};
webSocket.OnClosed = (ws, code, msg) =>
{
this.State = WebSocketStates.Closed;
if (this.Parent.OnClosed != null)
this.Parent.OnClosed(this.Parent, code, msg);
};
if (this.Parent.OnIncompleteFrame != null)
webSocket.OnIncompleteFrame = (ws, frame) =>
{
if (this.Parent.OnIncompleteFrame != null)
this.Parent.OnIncompleteFrame(this.Parent, frame);
};
if (this.Parent.StartPingThread)
webSocket.StartPinging(Math.Max(this.Parent.PingFrequency, 100));
webSocket.StartReceive();
}
public override void Send(string message)
{
webSocket.Send(message);
}
public override void Send(byte[] buffer)
{
webSocket.Send(buffer);
}
public override void Send(byte[] buffer, ulong offset, ulong count)
{
webSocket.Send(buffer, offset, count);
}
public override void SendAsBinary(BufferSegment data)
{
webSocket.Send(WebSocketFrameTypes.Binary, data);
}
public override void SendAsText(BufferSegment data)
{
webSocket.Send(WebSocketFrameTypes.Text, data);
}
public override void Send(WebSocketFrame frame)
{
webSocket.Send(frame);
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,728 @@
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL && !BESTHTTP_DISABLE_HTTP2 && !BESTHTTP_DISABLE_WEBSOCKET
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Text;
using BestHTTP.Connections.HTTP2;
using BestHTTP.Extensions;
using BestHTTP.Logger;
using BestHTTP.PlatformSupport.Memory;
using BestHTTP.WebSocket.Frames;
namespace BestHTTP.WebSocket
{
/// <summary>
/// Implements RFC 8441 (https://tools.ietf.org/html/rfc8441) to use Websocket over HTTP/2
/// </summary>
public sealed class OverHTTP2 : WebSocketBaseImplementation, IHeartbeat
{
public override int BufferedAmount { get => this._bufferedAmount; }
internal volatile int _bufferedAmount;
public override bool IsOpen => this.State == WebSocketStates.Open;
public override int Latency { get { return this.Parent.StartPingThread ? base.Latency : (int)this.http2Handler.Latency; } }
private List<WebSocketFrameReader> IncompleteFrames = new List<WebSocketFrameReader>();
private HTTP2Handler http2Handler;
/// <summary>
/// True if we sent out a Close message to the server
/// </summary>
internal volatile bool closeSent;
/// <summary>
/// When we sent out the last ping.
/// </summary>
private DateTime lastPing = DateTime.MinValue;
private bool waitingForPong = false;
/// <summary>
/// A circular buffer to store the last N rtt times calculated by the pong messages.
/// </summary>
private CircularBuffer<int> rtts = new CircularBuffer<int>(WebSocketResponse.RTTBufferCapacity);
private PeekableIncomingSegmentStream incomingSegmentStream = new PeekableIncomingSegmentStream();
private ConcurrentQueue<WebSocketFrameReader> CompletedFrames = new ConcurrentQueue<WebSocketFrameReader>();
internal ConcurrentQueue<WebSocketFrame> frames = new ConcurrentQueue<WebSocketFrame>();
public OverHTTP2(WebSocket parent, Uri uri, string origin, string protocol) : base(parent, uri, origin, protocol)
{
// use https scheme so it will be served over HTTP/2. Thre request's Tag will be set to this class' instance so HTTP2Handler will know it has to create a HTTP2WebSocketStream instance to
// process the request.
string scheme = "https";
int port = uri.Port != -1 ? uri.Port : 443;
base.Uri = new Uri(scheme + "://" + uri.Host + ":" + port + uri.GetRequestPathAndQueryURL());
}
internal void SetHTTP2Handler(HTTP2Handler handler) => this.http2Handler = handler;
protected override void CreateInternalRequest()
{
HTTPManager.Logger.Verbose("OverHTTP2", "CreateInternalRequest", this.Parent.Context);
base._internalRequest = new HTTPRequest(base.Uri, HTTPMethods.Connect, OnInternalRequestCallback);
base._internalRequest.Context.Add("WebSocket", this.Parent.Context);
base._internalRequest.SetHeader(":protocol", "websocket");
// 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.
base._internalRequest.SetHeader("sec-webSocket-key", WebSocket.GetSecKey(new object[] { this, InternalRequest, base.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(base.Origin))
base._internalRequest.SetHeader("origin", base.Origin);
// The request MUST include a header field with the name |Sec-WebSocket-Version|. The value of this header field MUST be 13.
base._internalRequest.SetHeader("sec-webSocket-version", "13");
if (!string.IsNullOrEmpty(base.Protocol))
base._internalRequest.SetHeader("sec-webSocket-protocol", base.Protocol);
// Disable caching
base._internalRequest.SetHeader("cache-control", "no-cache");
base._internalRequest.SetHeader("pragma", "no-cache");
#if !BESTHTTP_DISABLE_CACHING
base._internalRequest.DisableCache = true;
#endif
base._internalRequest.OnHeadersReceived += OnHeadersReceived;
// set a fake upload stream, so HPACKEncoder will not set the END_STREAM flag
base._internalRequest.UploadStream = new MemoryStream(0);
base._internalRequest.UseUploadStreamLength = false;
this.LastMessageReceived = DateTime.Now;
base._internalRequest.Tag = this;
if (this.Parent.OnInternalRequestCreated != null)
{
try
{
this.Parent.OnInternalRequestCreated(this.Parent, base._internalRequest);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", "CreateInternalRequest", ex, this.Parent.Context);
}
}
}
private void OnHeadersReceived(HTTPRequest req, HTTPResponse resp, Dictionary<string, List<string>> newHeaders)
{
HTTPManager.Logger.Verbose("OverHTTP2", $"OnHeadersReceived - StatusCode: {resp?.StatusCode}", this.Parent.Context);
if (resp != null && resp.StatusCode == 200)
{
if (this.Parent.Extensions != null)
{
for (int i = 0; i < this.Parent.Extensions.Length; ++i)
{
var ext = this.Parent.Extensions[i];
try
{
if (ext != null && !ext.ParseNegotiation(resp))
this.Parent.Extensions[i] = null; // Keep extensions only that successfully negotiated
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", "ParseNegotiation", ex, this.Parent.Context);
// Do not try to use a defective extension in the future
this.Parent.Extensions[i] = null;
}
}
}
this.State = WebSocketStates.Open;
if (this.Parent.OnOpen != null)
{
try
{
this.Parent.OnOpen(this.Parent);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", "OnOpen", ex, this.Parent.Context);
}
}
if (this.Parent.StartPingThread)
{
this.LastMessageReceived = DateTime.Now;
SendPing();
}
}
else
req.Abort();
}
private static bool CanReadFullFrame(PeekableIncomingSegmentStream stream)
{
if (stream.Length < 2)
return false;
stream.BeginPeek();
if (stream.PeekByte() == -1)
return false;
int maskAndLength = stream.PeekByte();
if (maskAndLength == -1)
return false;
// The second byte is the Mask Bit and the length of the payload data
var HasMask = (maskAndLength & 0x80) != 0;
// if 0-125, that is the payload length.
var 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 = BufferPool.Get(2, true);
for (int i = 0; i < 2; i++)
{
int data = stream.PeekByte();
if (data < 0)
return false;
rawLen[i] = (byte)data;
}
if (BitConverter.IsLittleEndian)
Array.Reverse(rawLen, 0, 2);
Length = (UInt64)BitConverter.ToUInt16(rawLen, 0);
BufferPool.Release(rawLen);
}
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 = BufferPool.Get(8, true);
for (int i = 0; i < 8; i++)
{
int data = stream.PeekByte();
if (data < 0)
return false;
rawLen[i] = (byte)data;
}
if (BitConverter.IsLittleEndian)
Array.Reverse(rawLen, 0, 8);
Length = (UInt64)BitConverter.ToUInt64(rawLen, 0);
BufferPool.Release(rawLen);
}
// Header + Mask&Length
Length += 2;
// 4 bytes for Mask if present
if (HasMask)
Length += 4;
return stream.Length >= (long)Length;
}
internal void OnReadThread(BufferSegment buffer)
{
this.LastMessageReceived = DateTime.Now;
this.incomingSegmentStream.Write(buffer);
while (CanReadFullFrame(this.incomingSegmentStream))
{
WebSocketFrameReader frame = new WebSocketFrameReader();
frame.Read(this.incomingSegmentStream);
if (HTTPManager.Logger.Level == Logger.Loglevels.All)
HTTPManager.Logger.Verbose("OverHTTP2", "Frame received: " + frame.ToString(), this.Parent.Context);
if (!frame.IsFinal)
{
if (this.Parent.OnIncompleteFrame == null)
IncompleteFrames.Add(frame);
else
CompletedFrames.Enqueue(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 (this.Parent.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
{
CompletedFrames.Enqueue(frame);
}
break;
case WebSocketFrameTypes.Text:
case WebSocketFrameTypes.Binary:
frame.DecodeWithExtensions(this.Parent);
CompletedFrames.Enqueue(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 && this.State != WebSocketStates.Closed)
{
// copy data set to true here, as the frame's data is released back to the pool after the switch
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Pong, frame.Data, true, true, true));
}
break;
case WebSocketFrameTypes.Pong:
// https://tools.ietf.org/html/rfc6455#section-5.5
// A Pong frame MAY be sent unsolicited. This serves as a
// unidirectional heartbeat. A response to an unsolicited Pong frame is
// not expected.
if (!waitingForPong)
break;
waitingForPong = false;
// the difference between the current time and the time when the ping message is sent
TimeSpan diff = DateTime.Now - lastPing;
// add it to the buffer
this.rtts.Add((int)diff.TotalMilliseconds);
// and calculate the new latency
base.Latency = CalculateLatency();
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:
HTTPManager.Logger.Information("OverHTTP2", "ConnectionClose packet received!", this.Parent.Context);
CompletedFrames.Enqueue(frame);
if (!closeSent)
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.ConnectionClose, BufferSegment.Empty));
this.State = WebSocketStates.Closed;
break;
}
}
}
private void OnInternalRequestCallback(HTTPRequest req, HTTPResponse resp)
{
HTTPManager.Logger.Verbose("OverHTTP2", $"OnInternalRequestCallback - this.State: {this.State}", this.Parent.Context);
// If it's already closed, all events are called too.
if (this.State == WebSocketStates.Closed)
return;
if (this.State == WebSocketStates.Connecting && HTTPManager.HTTP2Settings.WebSocketOverHTTP2Settings.EnableImplementationFallback)
{
this.Parent.FallbackToHTTP1();
return;
}
string reason = string.Empty;
switch (req.State)
{
case HTTPRequestStates.Finished:
HTTPManager.Logger.Information("OverHTTP2", string.Format("Request finished. Status Code: {0} Message: {1}", resp.StatusCode.ToString(), resp.Message), this.Parent.Context);
if (resp.StatusCode == 101)
{
// The request upgraded successfully.
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 (this.State != WebSocketStates.Connecting || !string.IsNullOrEmpty(reason))
{
if (this.Parent.OnError != null)
{
try
{
this.Parent.OnError(this.Parent, reason);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", "OnError", ex, this.Parent.Context);
}
}
else if (!HTTPManager.IsQuitting)
HTTPManager.Logger.Error("OverHTTP2", reason, this.Parent.Context);
}
else if (this.Parent.OnClosed != null)
{
try
{
this.Parent.OnClosed(this.Parent, (ushort)WebSocketStausCodes.NormalClosure, "Closed while opening");
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", "OnClosed", ex, this.Parent.Context);
}
}
this.State = WebSocketStates.Closed;
}
public override void StartOpen()
{
HTTPManager.Logger.Verbose("OverHTTP2", "StartOpen", this.Parent.Context);
if (this.Parent.Extensions != null)
{
try
{
for (int i = 0; i < this.Parent.Extensions.Length; ++i)
{
var ext = this.Parent.Extensions[i];
if (ext != null)
ext.AddNegotiation(base.InternalRequest);
}
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", "Open", ex, this.Parent.Context);
}
}
base.InternalRequest.Send();
HTTPManager.Heartbeats.Subscribe(this);
this.State = WebSocketStates.Connecting;
}
public override void StartClose(ushort code, string message)
{
HTTPManager.Logger.Verbose("OverHTTP2", "StartClose", this.Parent.Context);
if (this.State == WebSocketStates.Connecting)
{
if (this.InternalRequest != null)
this.InternalRequest.Abort();
this.State = WebSocketStates.Closed;
if (this.Parent.OnClosed != null)
this.Parent.OnClosed(this.Parent, code, message);
}
else
{
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.ConnectionClose, WebSocket.EncodeCloseData(code, message), true, false, false));
this.State = WebSocketStates.Closing;
}
}
public override void Send(string message)
{
if (message == null)
throw new ArgumentNullException("message must not be null!");
int count = System.Text.Encoding.UTF8.GetByteCount(message);
byte[] data = BufferPool.Get(count, true);
System.Text.Encoding.UTF8.GetBytes(message, 0, message.Length, data, 0);
SendAsText(data.AsBuffer(0, count));
}
public override void Send(byte[] buffer)
{
if (buffer == null)
throw new ArgumentNullException("data must not be null!");
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Binary, new BufferSegment(buffer, 0, buffer.Length)));
}
public override 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");
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Binary, new BufferSegment(data, (int)offset, (int)count), true, true));
}
public override void Send(WebSocketFrame frame)
{
if (this.State == WebSocketStates.Closed || closeSent)
return;
this.frames.Enqueue(frame);
this.http2Handler.SignalRunnerThread();
this._bufferedAmount += frame.Data.Count;
if (frame.Type == WebSocketFrameTypes.ConnectionClose)
this.closeSent = true;
}
public override void SendAsBinary(BufferSegment data)
{
Send(WebSocketFrameTypes.Binary, data);
}
public override void SendAsText(BufferSegment data)
{
Send(WebSocketFrameTypes.Text, data);
}
private void Send(WebSocketFrameTypes type, BufferSegment data)
{
Send(new WebSocketFrame(this.Parent, type, data, true, true, false));
}
private int CalculateLatency()
{
if (this.rtts.Count == 0)
return 0;
int sumLatency = 0;
for (int i = 0; i < this.rtts.Count; ++i)
sumLatency += this.rtts[i];
return sumLatency / this.rtts.Count;
}
internal void PreReadCallback()
{
if (this.Parent.StartPingThread)
{
DateTime now = DateTime.Now;
if (!waitingForPong && now - LastMessageReceived >= TimeSpan.FromMilliseconds(this.Parent.PingFrequency))
SendPing();
if (waitingForPong && now - lastPing > this.Parent.CloseAfterNoMessage)
{
if (this.State != WebSocketStates.Closed)
{
HTTPManager.Logger.Warning("OverHTTP2",
string.Format("No message received in the given time! Closing WebSocket. LastPing: {0}, PingFrequency: {1}, Close After: {2}, Now: {3}",
this.lastPing, TimeSpan.FromMilliseconds(this.Parent.PingFrequency), this.Parent.CloseAfterNoMessage, now), this.Parent.Context);
CloseWithError("No message received in the given time!");
}
}
}
}
public void OnHeartbeatUpdate(TimeSpan dif)
{
DateTime now = DateTime.Now;
switch (this.State)
{
case WebSocketStates.Connecting:
if (now - this.InternalRequest.Timing.Start >= this.Parent.CloseAfterNoMessage)
{
if (HTTPManager.HTTP2Settings.WebSocketOverHTTP2Settings.EnableImplementationFallback)
{
this.State = WebSocketStates.Closed;
this.InternalRequest.OnHeadersReceived = null;
this.InternalRequest.Callback = null;
this.Parent.FallbackToHTTP1();
}
else
{
CloseWithError("WebSocket Over HTTP/2 Implementation failed to connect in the given time!");
}
}
break;
default:
while (CompletedFrames.TryDequeue(out var frame))
{
// 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 (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Verbose("OverHTTP2", "HandleEvents - OnIncompleteFrame", this.Parent.Context);
if (this.Parent.OnIncompleteFrame != null)
this.Parent.OnIncompleteFrame(this.Parent, frame);
break;
case WebSocketFrameTypes.Text:
// Any not Final frame is handled as a fragment
if (!frame.IsFinal)
goto case WebSocketFrameTypes.Continuation;
if (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Verbose("OverHTTP2", $"HandleEvents - OnText(\"{frame.DataAsText}\")", this.Parent.Context);
if (this.Parent.OnMessage != null)
this.Parent.OnMessage(this.Parent, frame.DataAsText);
break;
case WebSocketFrameTypes.Binary:
// Any not Final frame is handled as a fragment
if (!frame.IsFinal)
goto case WebSocketFrameTypes.Continuation;
if (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Verbose("OverHTTP2", $"HandleEvents - OnBinary({frame.Data})", this.Parent.Context);
if (this.Parent.OnBinary != null)
{
var data = new byte[frame.Data.Count];
Array.Copy(frame.Data.Data, frame.Data.Offset, data, 0, frame.Data.Count);
this.Parent.OnBinary(this.Parent, data);
}
if (this.Parent.OnBinaryNoAlloc != null)
this.Parent.OnBinaryNoAlloc(this.Parent, frame.Data);
break;
case WebSocketFrameTypes.ConnectionClose:
HTTPManager.Logger.Verbose("OverHTTP2", "HandleEvents - Calling OnClosed", this.Parent.Context);
if (this.Parent.OnClosed != null)
{
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 && */ frame.Data != BufferSegment.Empty && frame.Data.Count >= 2)
{
if (BitConverter.IsLittleEndian)
Array.Reverse(frame.Data.Data, frame.Data.Offset, 2);
statusCode = BitConverter.ToUInt16(frame.Data.Data, frame.Data.Offset);
if (frame.Data.Count > 2)
msg = Encoding.UTF8.GetString(frame.Data.Data, frame.Data.Offset + 2, frame.Data.Count - 2);
frame.ReleaseData();
}
this.Parent.OnClosed(this.Parent, statusCode, msg);
this.Parent.OnClosed = null;
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", "HandleEvents - OnClosed", ex, this.Parent.Context);
}
}
HTTPManager.Heartbeats.Unsubscribe(this);
break;
}
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", string.Format("HandleEvents({0})", frame.ToString()), ex, this.Parent.Context);
}
finally
{
frame.ReleaseData();
}
}
break;
}
}
/// <summary>
/// Next interaction relative to *now*.
/// </summary>
public TimeSpan GetNextInteraction()
{
if (waitingForPong)
return TimeSpan.MaxValue;
return (LastMessageReceived + TimeSpan.FromMilliseconds(this.Parent.PingFrequency)) - DateTime.Now;
}
private void SendPing()
{
HTTPManager.Logger.Information("OverHTTP2", "Sending Ping frame, waiting for a pong...", this.Parent.Context);
lastPing = DateTime.Now;
waitingForPong = true;
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Ping, BufferSegment.Empty));
}
private void CloseWithError(string message)
{
HTTPManager.Logger.Verbose("OverHTTP2", $"CloseWithError(\"{message}\")", this.Parent.Context);
this.State = WebSocketStates.Closed;
if (this.Parent.OnError != null)
{
try
{
this.Parent.OnError(this.Parent, message);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", "OnError", ex, this.Parent.Context);
}
}
this.InternalRequest.Abort();
}
}
}
#endif

View File

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

View File

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

View File

@@ -0,0 +1,60 @@
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL && !BESTHTTP_DISABLE_HTTP2 && !BESTHTTP_DISABLE_WEBSOCKET
using System;
using System.Collections.Generic;
using System.IO;
using BestHTTP.Extensions;
using BestHTTP.PlatformSupport.Memory;
namespace BestHTTP.WebSocket.Implementations.Utils
{
public sealed class LockedBufferSegmenStream : BufferSegmentStream
{
public bool IsClosed { get; private set; }
public override int Read(byte[] buffer, int offset, int count)
{
lock (base.bufferList)
{
if (this.IsClosed && base.bufferList.Count == 0)
return 0;
int sumReadCount = base.Read(buffer, offset, count);
return sumReadCount == 0 ? -1 : sumReadCount;
}
}
public override void Write(BufferSegment bufferSegment)
{
lock (base.bufferList)
{
if (this.IsClosed)
return;
base.Write(bufferSegment);
}
}
public override void Reset()
{
lock (base.bufferList)
{
base.Reset();
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Reset();
}
public override void Close()
{
this.IsClosed = true;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,270 @@
#if (UNITY_WEBGL && !UNITY_EDITOR) && !BESTHTTP_DISABLE_WEBSOCKET
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using BestHTTP.PlatformSupport.Memory;
namespace BestHTTP.WebSocket
{
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);
internal sealed class WebGLBrowser : WebSocketBaseImplementation
{
public override WebSocketStates State => ImplementationId != 0 ? WS_GetState(ImplementationId) : WebSocketStates.Unknown;
public override bool IsOpen => ImplementationId != 0 && WS_GetState(ImplementationId) == WebSocketStates.Open;
public override int BufferedAmount => WS_GetBufferedAmount(ImplementationId);
internal static Dictionary<uint, WebSocket> WebSockets = new Dictionary<uint, WebSocket>();
private uint ImplementationId;
public WebGLBrowser(WebSocket parent, Uri uri, string origin, string protocol) : base(parent, uri, origin, protocol)
{
}
public override void StartOpen()
{
try
{
ImplementationId = WS_Create(this.Uri.OriginalString, this.Protocol, OnOpenCallback, OnTextCallback, OnBinaryCallback, OnErrorCallback, OnCloseCallback);
WebSockets.Add(ImplementationId, this.Parent);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "Open", ex, this.Parent.Context);
}
}
public override void StartClose(UInt16 code, string message)
{
WS_Close(this.ImplementationId, code, message);
}
public override void Send(string message)
{
var count = System.Text.Encoding.UTF8.GetByteCount(message);
var buffer = BufferPool.Get(count, true);
System.Text.Encoding.UTF8.GetBytes(message, 0, message.Length, buffer, 0);
WS_Send_String(this.ImplementationId, buffer, 0, count);
BufferPool.Release(buffer);
}
public override void Send(byte[] buffer)
{
WS_Send_Binary(this.ImplementationId, buffer, 0, buffer.Length);
}
public override void Send(byte[] buffer, ulong offset, ulong count)
{
WS_Send_Binary(this.ImplementationId, buffer, (int)offset, (int)count);
}
public override void SendAsBinary(BufferSegment data)
{
WS_Send_Binary(this.ImplementationId, data.Data, data.Offset, data.Count);
BufferPool.Release(data);
}
public override void SendAsText(BufferSegment data)
{
WS_Send_String(this.ImplementationId, data.Data, data.Offset, data.Count);
BufferPool.Release(data);
}
[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_GetBufferedAmount(uint id);
[DllImport("__Internal")]
static extern int WS_Send_String(uint id, byte[] strData, int pos, int length);
[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);
[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, ws.Context);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnOpenCallback - No WebSocket found for id: " + id.ToString(), ws.Context);
}
[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, ws.Context);
}
}
}
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, ws.Context);
}
}
if (ws.OnBinaryNoAlloc != null)
{
try
{
byte[] buffer = BufferPool.Get(length, true);
// We still have to do a copy here, but at least the buffer will be released back to the pool.
// Copy data from the 'unmanaged' memory to managed memory. Buffer will be reclaimed by the GC.
Marshal.Copy(pBuffer, buffer, 0, length);
ws.OnBinaryNoAlloc(ws, new BufferSegment(buffer, 0, length));
BufferPool.Release(buffer);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnBinary", ex, ws.Context);
}
}
}
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, error);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnError", ex, ws.Context);
}
}
}
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)
{
// To match non-webgl behavior, we have to treat this client-side generated message as an error
if (code == (int)WebSocketStausCodes.ClosedAbnormally)
{
OnErrorCallback(id, "Abnormal disconnection.");
return;
}
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, ws.Context);
}
}
}
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

View File

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

View File

@@ -0,0 +1,98 @@
#if !BESTHTTP_DISABLE_WEBSOCKET
using System;
using BestHTTP.PlatformSupport.Memory;
#if !UNITY_WEBGL || UNITY_EDITOR
using BestHTTP.WebSocket.Frames;
#endif
namespace BestHTTP.WebSocket
{
/// <summary>
/// States of the underlying implementation's state.
/// </summary>
public enum WebSocketStates : byte
{
Connecting = 0,
Open = 1,
Closing = 2,
Closed = 3,
Unknown
};
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 OnWebSocketBinaryNoAllocDelegate(WebSocket webSocket, BufferSegment data);
public delegate void OnWebSocketClosedDelegate(WebSocket webSocket, UInt16 code, string message);
public delegate void OnWebSocketErrorDelegate(WebSocket webSocket, string reason);
#if !UNITY_WEBGL || UNITY_EDITOR
public delegate void OnWebSocketIncompleteFrameDelegate(WebSocket webSocket, WebSocketFrameReader frame);
#endif
public abstract class WebSocketBaseImplementation
{
public virtual WebSocketStates State { get; protected set; }
public virtual bool IsOpen { get; protected set; }
public virtual int BufferedAmount { get; protected set; }
#if !UNITY_WEBGL || UNITY_EDITOR
public HTTPRequest InternalRequest
{
get
{
if (this._internalRequest == null)
CreateInternalRequest();
return this._internalRequest;
}
}
protected HTTPRequest _internalRequest;
public virtual int Latency { get; protected set; }
public virtual DateTime LastMessageReceived { get; protected set; }
#endif
public WebSocket Parent { get; }
public Uri Uri { get; protected set; }
public string Origin { get; }
public string Protocol { get; }
public WebSocketBaseImplementation(WebSocket parent, Uri uri, string origin, string protocol)
{
this.Parent = parent;
this.Uri = uri;
this.Origin = origin;
this.Protocol = protocol;
#if !UNITY_WEBGL || UNITY_EDITOR
this.LastMessageReceived = DateTime.MinValue;
// Set up some default values.
this.Parent.PingFrequency = 10_000;
this.Parent.CloseAfterNoMessage = TimeSpan.FromSeconds(2);
#endif
}
public abstract void StartOpen();
public abstract void StartClose(UInt16 code, string message);
public abstract void Send(string message);
public abstract void Send(byte[] buffer);
public abstract void Send(byte[] buffer, ulong offset, ulong count);
public abstract void SendAsBinary(BufferSegment data);
public abstract void SendAsText(BufferSegment data);
#if !UNITY_WEBGL || UNITY_EDITOR
protected abstract void CreateInternalRequest();
/// <summary>
/// It will send the given frame to the server.
/// </summary>
public abstract void Send(WebSocketFrame frame);
#endif
}
}
#endif

View File

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

View File

@@ -0,0 +1,416 @@
#if !BESTHTTP_DISABLE_WEBSOCKET
using System;
using System.Text;
using BestHTTP.Extensions;
using BestHTTP.Connections;
using BestHTTP.Logger;
using BestHTTP.PlatformSupport.Memory;
#if !UNITY_WEBGL || UNITY_EDITOR
using BestHTTP.WebSocket.Frames;
using BestHTTP.WebSocket.Extensions;
#endif
namespace BestHTTP.WebSocket
{
public sealed class WebSocket
{
/// <summary>
/// Maximum payload size of a websocket frame. Its default value is 32 KiB.
/// </summary>
public static uint MaxFragmentSize = UInt16.MaxValue / 2;
#if !UNITY_WEBGL || UNITY_EDITOR
public static IExtension[] GetDefaultExtensions()
{
#if !BESTHTTP_DISABLE_GZIP
return 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: */ PerMessageCompression.MinDataLengthToCompressDefault) };
#else
return null;
#endif
}
#endif
public WebSocketStates State { get { return this.implementation.State; } }
/// <summary>
/// The connection to the WebSocket server is open.
/// </summary>
public bool IsOpen { get { return this.implementation.IsOpen; } }
/// <summary>
/// Data waiting to be written to the wire.
/// </summary>
public int BufferedAmount { get { return this.implementation.BufferedAmount; } }
#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 milliseconds. Minimum value is 100ms, default is 10 seconds.
/// </summary>
public int PingFrequency { get; set; }
/// <summary>
/// If StartPingThread set to true, the plugin will close the connection and emit an OnError event if no
/// message is received from the server in the given time. Its default value is 2 sec.
/// </summary>
public TimeSpan CloseAfterNoMessage { get; set; }
/// <summary>
/// The internal HTTPRequest object.
/// </summary>
public HTTPRequest InternalRequest { get { return this.implementation.InternalRequest; } }
/// <summary>
/// IExtension implementations the plugin will negotiate with the server to use.
/// </summary>
public IExtension[] Extensions { get; private set; }
/// <summary>
/// Latency calculated from the ping-pong message round-trip times.
/// </summary>
public int Latency { get { return this.implementation.Latency; } }
/// <summary>
/// When we received the last message from the server.
/// </summary>
public DateTime LastMessageReceived { get { return this.implementation.LastMessageReceived; } }
/// <summary>
/// When the Websocket Over HTTP/2 implementation fails to connect and EnableImplementationFallback is true, the plugin tries to fall back to the HTTP/1 implementation.
/// When this happens a new InternalRequest is created and all previous custom modifications (like added headers) are lost. With OnInternalRequestCreated these modifications can be reapplied.
/// </summary>
public Action<WebSocket, HTTPRequest> OnInternalRequestCreated;
#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 a Binary message received. It's a more performant version than the OnBinary event, as the memory will be reused.
/// </summary>
public OnWebSocketBinaryNoAllocDelegate OnBinaryNoAlloc;
/// <summary>
/// Called when the WebSocket connection is closed.
/// </summary>
public OnWebSocketClosedDelegate OnClosed;
/// <summary>
/// Called when an error is encountered. The parameter will be the description of the error.
/// </summary>
public OnWebSocketErrorDelegate OnError;
#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
/// <summary>
/// Logging context of this websocket instance.
/// </summary>
public LoggingContext Context { get; private set; }
/// <summary>
/// The underlying, real implementation.
/// </summary>
private WebSocketBaseImplementation implementation;
/// <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 = WebSocket.GetDefaultExtensions();
#endif
}
#if !UNITY_WEBGL || UNITY_EDITOR
public WebSocket(Uri uri, string origin, string protocol)
:this(uri, origin, protocol, null)
{
#if (!UNITY_WEBGL || UNITY_EDITOR)
this.Extensions = WebSocket.GetDefaultExtensions();
#endif
}
#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
)
{
this.Context = new LoggingContext(this);
#if !UNITY_WEBGL || UNITY_EDITOR
this.Extensions = extensions;
#endif
SelectImplementation(uri, origin, protocol);
// Under WebGL when only the WebSocket protocol is used Setup() isn't called, so we have to call it here.
HTTPManager.Setup();
}
internal WebSocketBaseImplementation SelectImplementation(Uri uri, string origin, string protocol)
{
#if !UNITY_WEBGL || UNITY_EDITOR
#if !BESTHTTP_DISABLE_ALTERNATE_SSL && !BESTHTTP_DISABLE_HTTP2
if (HTTPManager.HTTP2Settings.WebSocketOverHTTP2Settings.EnableWebSocketOverHTTP2 && HTTPProtocolFactory.IsSecureProtocol(uri))
{
// Try to find a HTTP/2 connection that supports the connect protocol.
var connectionKey = Core.HostDefinition.GetKeyFor(new UriBuilder("https", uri.Host, uri.Port).Uri
#if !BESTHTTP_DISABLE_PROXY && (!UNITY_WEBGL || UNITY_EDITOR)
, GetProxy(uri)
#endif
);
var con = BestHTTP.Core.HostManager.GetHost(uri.Host).GetHostDefinition(connectionKey).Find(c => {
var httpConnection = c as HTTPConnection;
var http2Handler = httpConnection?.requestHandler as Connections.HTTP2.HTTP2Handler;
return http2Handler != null && http2Handler.settings.RemoteSettings[Connections.HTTP2.HTTP2Settings.ENABLE_CONNECT_PROTOCOL] != 0;
});
if (con != null)
{
HTTPManager.Logger.Information("WebSocket", "Connection with enabled Connect Protocol found!", this.Context);
var httpConnection = con as HTTPConnection;
var http2Handler = httpConnection?.requestHandler as Connections.HTTP2.HTTP2Handler;
this.implementation = new OverHTTP2(this, uri, origin, protocol);
}
}
#endif
if (this.implementation == null)
this.implementation = new OverHTTP1(this, uri, origin, protocol);
#else
this.implementation = new WebGLBrowser(this, uri, origin, protocol);
#endif
return this.implementation;
}
#if !UNITY_WEBGL || UNITY_EDITOR
internal void FallbackToHTTP1()
{
HTTPManager.Logger.Verbose("WebSocket", "FallbackToHTTP1", this.Context);
if (this.implementation == null)
return;
this.implementation = new OverHTTP1(this, this.implementation.Uri, this.implementation.Origin, this.implementation.Protocol);
this.implementation.StartOpen();
}
#endif
/// <summary>
/// Start the opening process.
/// </summary>
public void Open()
{
this.implementation.StartOpen();
}
/// <summary>
/// It will send the given message to the server in one frame.
/// </summary>
public void Send(string message)
{
if (!IsOpen)
return;
this.implementation.Send(message);
}
/// <summary>
/// It will send the given data to the server in one frame.
/// </summary>
public void Send(byte[] buffer)
{
if (!IsOpen)
return;
this.implementation.Send(buffer);
}
/// <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;
this.implementation.Send(buffer, offset, count);
}
/// <summary>
/// Will send the data in one or more binary frame and takes ownership over it calling BufferPool.Release when sent.
/// </summary>
public void SendAsBinary(BufferSegment data)
{
if (!IsOpen)
{
BufferPool.Release(data);
return;
}
this.implementation.SendAsBinary(data);
}
/// <summary>
/// Will send data as a text frame and takes owenership over the memory region releasing it to the BufferPool as soon as possible.
/// </summary>
public void SendAsText(BufferSegment data)
{
if (!IsOpen)
{
BufferPool.Release(data);
return;
}
this.implementation.SendAsText(data);
}
#if !UNITY_WEBGL || UNITY_EDITOR
/// <summary>
/// It will send the given frame to the server.
/// </summary>
public void Send(WebSocketFrame frame)
{
if (!IsOpen)
return;
this.implementation.Send(frame);
}
#endif
/// <summary>
/// It will initiate the closing of the connection to the server.
/// </summary>
public void Close()
{
if (State >= WebSocketStates.Closing)
return;
this.implementation.StartClose(1000, "Bye!");
}
/// <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;
this.implementation.StartClose(code, message);
}
#if !BESTHTTP_DISABLE_PROXY && (!UNITY_WEBGL || UNITY_EDITOR)
internal Proxy GetProxy(Uri uri)
{
// WebSocket is not a request-response based protocol, so we need a 'tunnel' through the proxy
HTTPProxy proxy = HTTPManager.Proxy as HTTPProxy;
if (proxy != null && proxy.UseProxyForAddress(uri))
proxy = new HTTPProxy(proxy.Address,
proxy.Credentials,
false, /*turn on 'tunneling'*/
false, /*sendWholeUri*/
proxy.NonTransparentForHTTPS);
return proxy;
}
#endif
#if !UNITY_WEBGL || UNITY_EDITOR
public static BufferSegment 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 (var ms = new BufferPoolMemoryStream(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);
buff = ms.ToArray();
return buff.AsBuffer(buff.Length);
}
}
internal static string GetSecKey(object[] from)
{
const int keysLength = 16;
byte[] keys = BufferPool.Get(keysLength, true);
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 < keysLength; ++cv)
keys[pos++] = hash[cv];
}
var result = Convert.ToBase64String(keys, 0, keysLength);
BufferPool.Release(keys);
return result;
}
#endif
}
}
#endif

View File

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

View File

@@ -0,0 +1,708 @@
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
using System;
using System.IO;
using System.Threading;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Text;
using BestHTTP.Extensions;
using BestHTTP.WebSocket.Frames;
using BestHTTP.Core;
using BestHTTP.PlatformSupport.Memory;
using BestHTTP.Logger;
using BestHTTP.Connections;
namespace BestHTTP.WebSocket
{
public sealed class WebSocketResponse : HTTPResponse, IProtocol
{
/// <summary>
/// Capacity of the RTT buffer where the latencies are kept.
/// </summary>
public static int RTTBufferCapacity = 5;
#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 a Binary message received. It's a more performant version than the OnBinary event, as the memory will be reused.
/// </summary>
public Action<WebSocketResponse, BufferSegment> OnBinaryNoAlloc;
/// <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>
/// IProtocol's ConnectionKey property.
/// </summary>
public HostConnectionKey ConnectionKey { get; private set; }
/// <summary>
/// Indicates whether the connection to the server is closed or not.
/// </summary>
public bool IsClosed { get { return closed; } }
/// <summary>
/// IProtocol.LoggingContext implementation.
/// </summary>
LoggingContext IProtocol.LoggingContext { get => this.Context; }
/// <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 WebSocket.MaxFragmentSize's value.
/// </summary>
public uint MaxFragmentSize { get; set; }
/// <summary>
/// Length of unsent, buffered up data in bytes.
/// </summary>
public int BufferedAmount { get { return this._bufferedAmount; } }
private int _bufferedAmount;
/// <summary>
/// Calculated latency from the Round-Trip Times we store in the rtts field.
/// </summary>
public int Latency { get; private set; }
/// <summary>
/// When we received the last frame.
/// </summary>
public DateTime lastMessage = DateTime.MinValue;
#endregion
#region Private Fields
private List<WebSocketFrameReader> IncompleteFrames = new List<WebSocketFrameReader>();
private ConcurrentQueue<WebSocketFrameReader> CompletedFrames = new ConcurrentQueue<WebSocketFrameReader>();
private WebSocketFrameReader CloseFrame;
private ConcurrentQueue<WebSocketFrame> unsentFrames = new ConcurrentQueue<WebSocketFrame>();
private volatile AutoResetEvent newFrameSignal = new AutoResetEvent(false);
private int sendThreadCreated = 0;
private int closedThreads = 0;
/// <summary>
/// True if we sent out a Close message to the server
/// </summary>
private volatile bool closeSent;
/// <summary>
/// True if this WebSocket connection is closed
/// </summary>
private volatile bool closed;
/// <summary>
/// When we sent out the last ping.
/// </summary>
private DateTime lastPing = DateTime.MinValue;
/// <summary>
/// True if waiting for an answer to our ping request. Ping timeout is used only why waitingForPong is true.
/// </summary>
private volatile bool waitingForPong = false;
/// <summary>
/// A circular buffer to store the last N rtt times calculated by the pong messages.
/// </summary>
private CircularBuffer<int> rtts = new CircularBuffer<int>(WebSocketResponse.RTTBufferCapacity);
#endregion
internal WebSocketResponse(HTTPRequest request, Stream stream, bool isStreamed, bool isFromCache)
: base(request, stream, isStreamed, isFromCache)
{
base.IsClosedManually = true;
this.ConnectionKey = new HostConnectionKey(this.baseRequest.CurrentUri.Host, HostDefinition.GetKeyForRequest(this.baseRequest));
closed = false;
MaxFragmentSize = WebSocket.MaxFragmentSize;
}
internal void StartReceive()
{
if (IsUpgraded)
BestHTTP.PlatformSupport.Threading.ThreadedRunner.RunLongLiving(ReceiveThreadFunc);
}
internal void CloseStream()
{
if (base.Stream != null)
{
try
{
base.Stream.Dispose();
}
catch
{ }
}
}
#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!");
int count = System.Text.Encoding.UTF8.GetByteCount(message);
byte[] data = BufferPool.Get(count, true);
System.Text.Encoding.UTF8.GetBytes(message, 0, message.Length, data, 0);
Send(WebSocketFrameTypes.Text, data.AsBuffer(count));
}
/// <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, new BufferSegment(data, 0, data.Length));
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, new BufferSegment(data, (int)offset, (int)count), true, true);
Send(frame);
}
public void Send(WebSocketFrameTypes type, BufferSegment data)
{
WebSocketFrame frame = new WebSocketFrame(this.WebSocket, type, data, true, true, false);
Send(frame);
}
/// <summary>
/// It will send the given frame to the server.
/// </summary>
public void Send(WebSocketFrame frame)
{
if (closed || closeSent)
return;
this.unsentFrames.Enqueue(frame);
if (Interlocked.CompareExchange(ref this.sendThreadCreated, 1, 0) == 0)
{
HTTPManager.Logger.Information("WebSocketResponse", "Send - Creating thread", this.Context);
BestHTTP.PlatformSupport.Threading.ThreadedRunner.RunLongLiving(SendThreadFunc);
}
Interlocked.Add(ref this._bufferedAmount, frame.Data.Count);
//if (HTTPManager.Logger.Level <= Logger.Loglevels.All)
// HTTPManager.Logger.Information("WebSocketResponse", "Signaling SendThread!", this.Context);
newFrameSignal.Set();
}
/// <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;
HTTPManager.Logger.Verbose("WebSocketResponse", string.Format("Close({0}, \"{1}\")", code, msg), this.Context);
WebSocketFrame frame;
while (this.unsentFrames.TryDequeue(out frame))
{
if (frame.Data.Data != null)
{
BufferPool.Release(frame.Data);
Interlocked.Add(ref this._bufferedAmount, -frame.Data.Count);
}
}
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 milliseconds!");
PingFrequnecy = TimeSpan.FromMilliseconds(frequency);
lastMessage = DateTime.UtcNow;
SendPing();
}
#endregion
#region Private Threading Functions
private void SendThreadFunc()
{
PlatformSupport.Threading.ThreadedRunner.SetThreadName("BestHTTP.WebSocket Send");
try
{
bool mask = !HTTPProtocolFactory.IsSecureProtocol(this.baseRequest.CurrentUri);
using (WriteOnlyBufferedStream bufferedStream = new WriteOnlyBufferedStream(this.Stream, 16 * 1024))
{
while (!closed && !closeSent)
{
//if (HTTPManager.Logger.Level <= Logger.Loglevels.All)
// HTTPManager.Logger.Information("WebSocketResponse", "SendThread - Waiting...", this.Context);
TimeSpan waitTime = TimeSpan.FromMilliseconds(int.MaxValue);
if (this.PingFrequnecy != TimeSpan.Zero)
{
DateTime now = DateTime.UtcNow;
waitTime = lastMessage + PingFrequnecy - now;
if (waitTime <= TimeSpan.Zero)
{
if (!waitingForPong && now - lastMessage >= PingFrequnecy)
{
if (!SendPing())
continue;
}
waitTime = PingFrequnecy;
}
if (waitingForPong && now - lastPing > this.WebSocket.CloseAfterNoMessage)
{
HTTPManager.Logger.Warning("WebSocketResponse",
string.Format("No message received in the given time! Closing WebSocket. LastPing: {0}, PingFrequency: {1}, Close After: {2}, Now: {3}",
this.lastPing, this.PingFrequnecy, this.WebSocket.CloseAfterNoMessage, now), this.Context);
CloseWithError(HTTPRequestStates.Error, "No message received in the given time!");
continue;
}
}
newFrameSignal.WaitOne(waitTime);
try
{
//if (HTTPManager.Logger.Level <= Logger.Loglevels.All)
// HTTPManager.Logger.Information("WebSocketResponse", "SendThread - Wait is over, about " + this.unsentFrames.Count.ToString() + " new frames!", this.Context);
WebSocketFrame frame;
while (this.unsentFrames.TryDequeue(out frame))
{
// save data count as per-message deflate can compress, and it would be different after calling WriteTo
int originalFrameDataLength = frame.Data.Count;
if (!closeSent)
{
frame.WriteTo((header, chunk) =>
{
bufferedStream.Write(header.Data, header.Offset, header.Count);
BufferPool.Release(header);
if (chunk != BufferSegment.Empty)
bufferedStream.Write(chunk.Data, chunk.Offset, chunk.Count);
}, MaxFragmentSize, mask, this.Context);
BufferPool.Release(frame.Data);
if (frame.Type == WebSocketFrameTypes.ConnectionClose)
closeSent = true;
}
Interlocked.Add(ref this._bufferedAmount, -originalFrameDataLength);
}
bufferedStream.Flush();
}
catch (Exception ex)
{
if (HTTPUpdateDelegator.IsCreated)
{
this.baseRequest.Exception = ex;
this.baseRequest.State = HTTPRequestStates.Error;
}
else
this.baseRequest.State = HTTPRequestStates.Aborted;
closed = true;
}
}
HTTPManager.Logger.Information("WebSocketResponse", string.Format("Ending Send thread. Closed: {0}, closeSent: {1}", closed, closeSent), this.Context);
}
}
catch (Exception ex)
{
if (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Exception("WebSocketResponse", "SendThread", ex);
}
finally
{
Interlocked.Exchange(ref sendThreadCreated, 0);
HTTPManager.Logger.Information("WebSocketResponse", "SendThread - Closed!", this.Context);
TryToCleanup();
}
}
private void ReceiveThreadFunc()
{
PlatformSupport.Threading.ThreadedRunner.SetThreadName("BestHTTP.WebSocket Receive");
try
{
while (!closed)
{
try
{
WebSocketFrameReader frame = new WebSocketFrameReader();
frame.Read(this.Stream);
if (HTTPManager.Logger.Level == Logger.Loglevels.All)
HTTPManager.Logger.Information("WebSocketResponse", "Frame received: " + frame.ToString(), this.Context);
lastMessage = DateTime.UtcNow;
if (!frame.IsFinal)
{
if (OnIncompleteFrame == null)
IncompleteFrames.Add(frame);
else
CompletedFrames.Enqueue(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
{
CompletedFrames.Enqueue(frame);
ProtocolEventHelper.EnqueueProtocolEvent(new ProtocolEventInfo(this));
}
break;
case WebSocketFrameTypes.Text:
case WebSocketFrameTypes.Binary:
frame.DecodeWithExtensions(WebSocket);
CompletedFrames.Enqueue(frame);
ProtocolEventHelper.EnqueueProtocolEvent(new ProtocolEventInfo(this));
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, true, true));
break;
case WebSocketFrameTypes.Pong:
try
{
// the difference between the current time and the time when the ping message is sent
TimeSpan diff = TimeSpan.FromTicks(this.lastMessage.Ticks - this.lastPing.Ticks);
// add it to the buffer
this.rtts.Add((int)diff.TotalMilliseconds);
// and calculate the new latency
this.Latency = CalculateLatency();
}
catch
{
// https://tools.ietf.org/html/rfc6455#section-5.5
// A Pong frame MAY be sent unsolicited. This serves as a
// unidirectional heartbeat. A response to an unsolicited Pong frame is
// not expected.
}
finally
{
waitingForPong = false;
}
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:
HTTPManager.Logger.Information("WebSocketResponse", "ConnectionClose packet received!", this.Context);
CloseFrame = frame;
if (!closeSent)
Send(new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.ConnectionClose, BufferSegment.Empty));
closed = true;
break;
}
}
catch (Exception e)
{
if (HTTPUpdateDelegator.IsCreated)
{
this.baseRequest.Exception = e;
this.baseRequest.State = HTTPRequestStates.Error;
}
else
this.baseRequest.State = HTTPRequestStates.Aborted;
closed = true;
newFrameSignal.Set();
}
}
HTTPManager.Logger.Information("WebSocketResponse", "Ending Read thread! closed: " + closed, this.Context);
}
finally
{
HTTPManager.Logger.Information("WebSocketResponse", "ReceiveThread - Closed!", this.Context);
TryToCleanup();
}
}
#endregion
#region Sending Out Events
/// <summary>
/// Internal function to send out received messages.
/// </summary>
void IProtocol.HandleEvents()
{
WebSocketFrameReader frame;
while (CompletedFrames.TryDequeue(out frame))
{
// 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 (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Verbose("WebSocketResponse", "HandleEvents - OnIncompleteFrame: " + frame.ToString(), this.Context);
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 (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Verbose("WebSocketResponse", "HandleEvents - OnText: " + frame.DataAsText, this.Context);
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 (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Verbose("WebSocketResponse", "HandleEvents - OnBinary: " + frame.ToString(), this.Context);
if (OnBinary != null)
{
var data = new byte[frame.Data.Count];
Array.Copy(frame.Data.Data, frame.Data.Offset, data, 0, frame.Data.Count);
OnBinary(this, data);
}
if (OnBinaryNoAlloc != null)
OnBinaryNoAlloc(this, frame.Data);
break;
}
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocketResponse", string.Format("HandleEvents({0})", frame.ToString()), ex, this.Context);
}
finally
{
frame.ReleaseData();
}
}
// 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)
{
HTTPManager.Logger.Verbose("WebSocketResponse", "HandleEvents - Calling OnClosed", this.Context);
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 != BufferSegment.Empty && CloseFrame.Data.Count >= 2)
{
if (BitConverter.IsLittleEndian)
Array.Reverse(CloseFrame.Data.Data, CloseFrame.Data.Offset, 2);
statusCode = BitConverter.ToUInt16(CloseFrame.Data.Data, CloseFrame.Data.Offset);
if (CloseFrame.Data.Count > 2)
msg = Encoding.UTF8.GetString(CloseFrame.Data.Data, CloseFrame.Data.Offset + 2, CloseFrame.Data.Count - 2);
CloseFrame.ReleaseData();
}
OnClosed(this, statusCode, msg);
OnClosed = null;
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocketResponse", "HandleEvents - OnClosed", ex, this.Context);
}
}
}
#endregion
private bool SendPing()
{
HTTPManager.Logger.Information("WebSocketResponse", "Sending Ping frame, waiting for a pong...", this.Context);
lastPing = DateTime.UtcNow;
waitingForPong = true;
try
{
var pingFrame = new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.Ping, BufferSegment.Empty);
Send(pingFrame);
}
catch
{
HTTPManager.Logger.Information("WebSocketResponse", "Error while sending PING message! Closing WebSocket.", this.Context);
CloseWithError(HTTPRequestStates.Error, "Error while sending PING message!");
return false;
}
return true;
}
private void CloseWithError(HTTPRequestStates state, string message)
{
if (!string.IsNullOrEmpty(message))
this.baseRequest.Exception = new Exception(message);
this.baseRequest.State = state;
this.closed = true;
CloseStream();
ProtocolEventHelper.EnqueueProtocolEvent(new ProtocolEventInfo(this));
}
private int CalculateLatency()
{
if (this.rtts.Count == 0)
return 0;
int sumLatency = 0;
for (int i = 0; i < this.rtts.Count; ++i)
sumLatency += this.rtts[i];
return sumLatency / this.rtts.Count;
}
void IProtocol.CancellationRequested()
{
CloseWithError(HTTPRequestStates.Aborted, null);
}
private void TryToCleanup()
{
if (Interlocked.Increment(ref this.closedThreads) == 2)
{
ProtocolEventHelper.EnqueueProtocolEvent(new ProtocolEventInfo(this));
(newFrameSignal as IDisposable).Dispose();
newFrameSignal = null;
CloseStream();
HTTPManager.Logger.Information("WebSocketResponse", "TryToCleanup - finished!", this.Context);
}
}
public override string ToString()
{
return this.ConnectionKey.ToString();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
IncompleteFrames.Clear();
CompletedFrames.Clear();
unsentFrames.Clear();
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 46daae4eb8da43145b58819255462ded
MonoImporter:
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 : ushort
{
/// <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,10 @@
fileFormatVersion: 2
guid: b782b81b6b7755d478dc07521f33f09c
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: