- Initial commit

This commit is contained in:
Timur Kozanov
2026-07-03 05:03:02 +03:00
commit a2f2f85df8
49 changed files with 4460 additions and 0 deletions

View File

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

View File

@ -0,0 +1,348 @@
#if !UNITY_WEBGL || UNITY_EDITOR
using System;
using System.Runtime.CompilerServices;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.IL2CPP;
using Best.HTTP.Shared.PlatformSupport.Memory;
#if 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 Best.WebSockets.Implementations.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 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, copyData: true)
{
}
public WebSocketFrame(WebSocket webSocket, WebSocketFrameTypes type, BufferSegment data, bool copyData)
{
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 $"[WebSocketFrame Type: {this.Type}, Header: {this.Header:X2}, Data: {this.Data}]";
}
public void WriteTo(Action<BufferSegment, BufferSegment> callback, uint maxFragmentSize, bool mask, LoggingContext context)
{
if (HTTPManager.Logger.IsDiagnostic)
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.IsDiagnostic)
HTTPManager.Logger.Verbose("WebSocketFrame", string.Format("WriteFragment - Header: {0}, data chunk: {1}", header.ToString(), Data.ToString()), context);
callback(header, Data);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#if WITH_BURST
[BurstCompile(CompileSynchronously = true)]
#endif
public unsafe static void ApplyMask(
#if WITH_BURST
[NoAlias]
#endif
byte* pData,
int DataOffset,
int DataCount,
#if WITH_BURST
[NoAlias]
#endif
byte* pmask
)
{
int targetOffset = DataOffset + DataCount;
uint umask = *(uint*)pmask;
#if 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 !UNITY_ANDROID && !UNITY_IOS
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
#endif
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,18 @@
fileFormatVersion: 2
guid: 326466ccfd440a94693ff2379b4d5461
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/Runtime/Implementations/Frames/WebSocketFrame.cs
uploadId: 737284

View File

@ -0,0 +1,253 @@
#if !UNITY_WEBGL || UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
namespace Best.WebSockets.Implementations.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
private static bool HasEnoughBytesForAFrame(PeekableIncomingSegmentStream peekable)
{
// https://www.rfc-editor.org/rfc/rfc6455#section-5.2
// Minimum frame length is 2 bytes: header + zero length
if (peekable.Length < 2)
return false;
peekable.BeginPeek();
// header
peekable.PeekByte();
var maskAndLength = peekable.PeekByte();
var length = (ulong)(maskAndLength & 127);
if (length < 126)
{
return (ulong)peekable.Length >= (2 + length);
}
else if (length == 126)
{
return (ulong)peekable.Length >= (3 + length);
}
else if (length == 127)
{
return (ulong)peekable.Length >= (10 + length);
}
return true;
}
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 static 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,18 @@
fileFormatVersion: 2
guid: bea7b0019ac0e5b4c94ac0149e455e2d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/Runtime/Implementations/Frames/WebSocketFrameReader.cs
uploadId: 737284

View File

@ -0,0 +1,51 @@
#if !UNITY_WEBGL || UNITY_EDITOR
namespace Best.WebSockets.Implementations.Frames
{
/// <summary>
/// Enumeration for possible WebSocket frame types.
/// </summary>
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,18 @@
fileFormatVersion: 2
guid: c41264f4c8f16a34386bbe57d7d7fd4c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/Runtime/Implementations/Frames/WebSocketFrameTypes.cs
uploadId: 737284

View File

@ -0,0 +1,168 @@
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
using System;
using System.Collections.Generic;
using Best.HTTP;
using Best.HTTP.Hosts.Connections.HTTP2;
using Best.HTTP.Shared;
using Best.HTTP.Shared.PlatformSupport.Memory;
namespace Best.WebSockets.Implementations
{
public sealed class HTTP2WebSocketStream : HTTP2Stream
{
public override bool HasFrameToSend
{
get
{
// Don't let the connection sleep until
return this.outgoing.Count > 0 || // we already booked at least one frame in advance
(this.State == HTTP2StreamStates.Open &&
this.remoteWindow > 0 &&
this.lastReadCount > 0 &&
(this.websocketOverHTTP2.BufferedFramesCount > 0 || this.chunkQueue.Count > 0)); // we are in the middle of sending request data
}
}
public override TimeSpan NextInteraction => this.websocketOverHTTP2.GetNextInteraction();
private OverHTTP2 websocketOverHTTP2;
// local list of websocket header-data pairs
private List<KeyValuePair<BufferSegment, BufferSegment>> chunkQueue = new List<KeyValuePair<BufferSegment, BufferSegment>>();
public HTTP2WebSocketStream(uint id, HTTP2ContentConsumer parentHandler, HTTP2SettingsManager registry, HPACKEncoder hpackEncoder)
: base(id, parentHandler, registry, hpackEncoder)
{}
public override void Assign(HTTPRequest request)
{
base.Assign(request);
this.websocketOverHTTP2 = request.Tag as OverHTTP2;
this.websocketOverHTTP2.SetThreadSignaler(this._parentHandler);
}
protected override void ProcessIncomingDATAFrame(ref HTTP2FrameHeaderAndPayload frame)
{
try
{
if (this.State != HTTP2StreamStates.HalfClosedLocal && this.State != HTTP2StreamStates.Open)
{
// ERROR!
return;
}
this.downloaded += (uint)frame.Payload.Count;
this.websocketOverHTTP2.OnReadThread(frame.Payload);
// frame's buffer will be released later
frame.DontUseMemPool = true;
this.localWindow -= frame.Payload.Count;
if ((frame.Flags & (byte)HTTP2DataFlags.END_STREAM) != 0)
this.isEndSTRReceived = true;
if (this.isEndSTRReceived)
{
HTTPManager.Logger.Information(nameof(HTTP2WebSocketStream), string.Format("[{0}] All data arrived, data length: {1:N0}", this.Id, this.downloaded), this.Context);
FinishRequest();
if (this.State == HTTP2StreamStates.HalfClosedLocal)
this.State = HTTP2StreamStates.Closed;
else
this.State = HTTP2StreamStates.HalfClosedRemote;
}
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(HTTP2WebSocketStream), nameof(ProcessIncomingDATAFrame), ex, this.Context);
}
}
protected override void ProcessOpenState(List<HTTP2FrameHeaderAndPayload> outgoingFrames)
{
try
{
// remote Window can be negative! See https://httpwg.org/specs/rfc7540.html#InitialWindowSize
if (this.remoteWindow <= 0)
{
HTTPManager.Logger.Information(nameof(HTTP2WebSocketStream), string.Format("[{0}] Skipping data sending as remote Window is {1}!", this.Id, this.remoteWindow), this.Context);
return;
}
this.websocketOverHTTP2.PreReadCallback();
Int64 maxFragmentSize = Math.Min(Best.WebSockets.WebSocket.MaxFragmentSize, this.settings.RemoteSettings[HTTP2Settings.MAX_FRAME_SIZE]);
Int64 maxFrameSize = Math.Min(maxFragmentSize, this.remoteWindow);
if (chunkQueue.Count == 0)
{
if (this.websocketOverHTTP2.TryDequeueFrame(out var frame))
frame.WriteTo((header, data) => chunkQueue.Add(new KeyValuePair<BufferSegment, BufferSegment>(header, data)), (uint)maxFragmentSize, false, this.Context);
}
while (this.remoteWindow >= 6 && chunkQueue.Count > 0)
{
var kvp = chunkQueue[0];
BufferSegment header = kvp.Key;
BufferSegment data = kvp.Value;
int minBytes = header.Count;
int maxBytes = minBytes + data.Count;
// remote window is less than the minimum we have to send, or
// the frame has data but we have space only to send the websocket header
if (this.remoteWindow < minBytes || (maxBytes > minBytes && this.remoteWindow == minBytes))
return;
HTTP2FrameHeaderAndPayload headerFrame = new HTTP2FrameHeaderAndPayload();
headerFrame.Type = HTTP2FrameTypes.DATA;
headerFrame.StreamId = this.Id;
headerFrame.Payload = header;
headerFrame.DontUseMemPool = false;
if (data.Count > 0)
{
HTTP2FrameHeaderAndPayload dataFrame = new HTTP2FrameHeaderAndPayload();
dataFrame.Type = HTTP2FrameTypes.DATA;
dataFrame.StreamId = this.Id;
var buff = data.Slice(data.Offset, (int)Math.Min(data.Count, maxFrameSize));
dataFrame.Payload = buff;
data = data.Slice(buff.Offset + buff.Count);
if (data.Count == 0)
chunkQueue.RemoveAt(0);
else
chunkQueue[0] = new KeyValuePair<BufferSegment, BufferSegment>(header, data);
// release the buffer only with the final frame and with the final frame's last data chunk
bool isLast = (header.Data[header.Offset] & 0x80) != 0 /*&& chunkQueue.Count == 0*/;
dataFrame.DontUseMemPool = !isLast;
this.outgoing.Enqueue(headerFrame);
this.outgoing.Enqueue(dataFrame);
}
else
{
this.outgoing.Enqueue(headerFrame);
chunkQueue.RemoveAt(0);
}
}
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(HTTP2WebSocketStream), nameof(ProcessOpenState), ex, this.Context);
}
}
}
}
#endif

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 343296b29e940cb4dafe7e6da3b405f2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/Runtime/Implementations/HTTP2WebSocketStream.cs
uploadId: 737284

View File

@ -0,0 +1,728 @@
#if !UNITY_WEBGL || UNITY_EDITOR
using Best.HTTP;
using Best.HTTP.Hosts.Connections;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
using Best.HTTP.Shared.PlatformSupport.Threading;
using Best.HTTP.Shared.Streams;
using Best.WebSockets.Implementations.Frames;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Text;
using System.Threading;
namespace Best.WebSockets.Implementations
{
/// <summary>
/// Implements WebSocket communication through an HTTP/1 connection.
/// </summary>
internal sealed class OverHTTP1 : WebSocketBaseImplementation, IContentConsumer, IHeartbeat
{
public PeekableContentProviderStream ContentProvider { get; private set; }
/// <summary>
/// Indicates whether we sent out the connection request to the server.
/// </summary>
private bool requestSent;
private volatile bool _closed;
private UInt16 _closeCode;
private string _closeMessage;
private ConcurrentQueue<WebSocketFrame> unsentFrames = new ConcurrentQueue<WebSocketFrame>();
private volatile AutoResetEvent newFrameSignal = new AutoResetEvent(false);
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.DownloadSettings.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.DownloadSettings.DisableCache = true;
#if !UNITY_WEBGL || UNITY_EDITOR
this._internalRequest.ProxySettings = this.Parent.GetProxy(this.Uri);
#endif
this._internalRequest.RedirectSettings.OnBeforeRedirection += InternalRequest_OnBeforeRedirection;
if (this.Parent.OnInternalRequestCreated != null)
{
try
{
this.Parent.OnInternalRequestCreated(this.Parent, this._internalRequest);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(OverHTTP1), "CreateInternalRequest", ex, this.Parent.Context);
}
}
this._internalRequest.OnCancellationRequested += OnCancellationRequested;
}
private void OnCancellationRequested(HTTPRequest req)
{
HTTPManager.Logger.Information(nameof(OverHTTP1), $"{nameof(OnCancellationRequested)}", this.Parent.Context);
this._internalRequest.OnCancellationRequested -= OnCancellationRequested;
this._closed = true;
this.newFrameSignal?.Set();
}
private bool InternalRequest_OnBeforeRedirection(HTTPRequest originalRequest, HTTPResponse response, Uri redirectUri)
{
HTTPManager.Logger.Information(nameof(OverHTTP1), $"{nameof(InternalRequest_OnBeforeRedirection)}", this.Parent.Context);
// 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;
}
private bool OnInternalRequestUpgraded(HTTPRequest req, HTTPResponse resp, PeekableContentProviderStream contentProvider)
{
HTTPManager.Logger.Information(nameof(OverHTTP1), $"{nameof(OnInternalRequestUpgraded)}", this.Parent.Context);
if (this.State == WebSocketStates.Closed)
return false;
if (!resp.HasHeader("sec-websocket-accept"))
throw new Exception("No Sec-Websocket-Accept header is sent by the server!");
base.ParseExtensionResponse(resp);
// Save the provider
this.ContentProvider = contentProvider;
// Websocket continously reading from the stream, but it could stuck with frames larger than the MaxBufferSize.
if (this.ContentProvider is ITCPStreamerContentConsumer consumer && consumer is not null)
consumer.MaxBufferSize = long.MaxValue;
// Switch the comsumer to this websocket implementation instead of the http1 consumer.
contentProvider.SetTwoWayBinding(this);
// Start send thread
Best.HTTP.Shared.PlatformSupport.Threading.ThreadedRunner.RunLongLiving(SendThread);
return true;
}
private void OnInternalRequestCallback(HTTPRequest req, HTTPResponse resp)
{
HTTPManager.Logger.Information(nameof(OverHTTP1), $"{nameof(OnInternalRequestCallback)}", this.Parent.Context);
Cleanup();
string reason = string.Empty;
switch (req.State)
{
case HTTPRequestStates.Finished:
HTTPManager.Logger.Information(nameof(OverHTTP1), string.Format("Request finished. Status Code: {0} Message: {1}", resp.StatusCode.ToString(), resp.Message), this.Parent.Context);
if (resp.IsUpgraded)
{
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 = req.Exception != null ? req.Exception.Message : 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(nameof(OverHTTP1), reason, this.Parent.Context);
}*/
if (this.Parent.OnClosed != null)
{
this.Parent.OnClosed(this.Parent,
!string.IsNullOrEmpty(reason) ? WebSocketStatusCodes.ClosedAbnormally : WebSocketStatusCodes.NormalClosure,
reason ?? "Closed while opening");
}
this._closed = true;
this.State = WebSocketStates.Closed;
this.newFrameSignal?.Set();
this.ContentProvider?.Unbind();
}
private void SendThread()
{
HTTPManager.Logger.Information(nameof(OverHTTP1), "SendThread - created", this.Parent.Context);
ThreadedRunner.SetThreadName("Best.WebSockets Send");
try
{
bool doMask = !HTTPProtocolFactory.IsSecureProtocol(this.Uri);
var pingFreq = this.Parent.SendPings ? this.Parent.PingFrequency : TimeSpan.Zero;
using (WriteOnlyBufferedStream bufferedStream = new WriteOnlyBufferedStream(this.ContentProvider as Stream, 16 * 1024, this.Parent.Context))
{
while (!this._closed)
{
//if (HTTPManager.Logger.Level <= Logger.Loglevels.All)
// HTTPManager.Logger.Information(nameof(OverHTTP1), "SendThread - Waiting...", this.Context);
TimeSpan waitTime = TimeSpan.FromMilliseconds(int.MaxValue);
if (pingFreq != TimeSpan.Zero)
{
DateTime now = DateTime.UtcNow;
DateTime pingTime = this.lastPing;
waitTime = pingTime + pingFreq - now;
if (waitTime <= TimeSpan.Zero)
{
if (!waitingForPong && now - pingTime >= pingFreq)
{
SendPing();
pingTime = this.lastPing;
}
waitTime = this.Parent.CloseAfterNoMessage;
}
if (waitingForPong && (now - pingTime > this.Parent.CloseAfterNoMessage) && (now - LastMessageReceived > this.Parent.CloseAfterNoMessage))
{
HTTPManager.Logger.Warning(nameof(OverHTTP1),
$"No pong received in the given time! LastPing: {pingTime}, PingFrequency: {pingFreq}, Close After: {this.Parent.CloseAfterNoMessage}, Now: {now}",
this.Parent.Context);
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this._internalRequest, HTTPRequestStates.Error, new Exception("No PONG received in the given time!")));
this._closed = true;
continue;
}
}
newFrameSignal.WaitOne(waitTime);
try
{
//if (HTTPManager.Logger.Level <= Logger.Loglevels.All)
// HTTPManager.Logger.Information(nameof(OverHTTP1), "SendThread - Wait is over, about " + this.unsentFrames.Count.ToString() + " new frames!", this.Context);
WebSocketFrame frame;
while (!this._closeSent && 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;
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);
}, WebSocket.MaxFragmentSize, doMask, this.Parent.Context);
BufferPool.Release(frame.Data);
if (frame.Type == WebSocketFrameTypes.ConnectionClose)
{
this._closeSent = true;
if (this._closeReceived)
{
this._closed = true;
this.State = WebSocketStates.Closed;
}
}
Interlocked.Add(ref this._bufferedAmount, -originalFrameDataLength);
}
bufferedStream.Flush();
}
catch (Exception ex)
{
//this._internalRequest.Timing.Finish(Timing_Name);
if (HTTPUpdateDelegator.IsCreated)
{
//this._internalRequest.Exception = ex;
//this._internalRequest.State = HTTPRequestStates.Error;
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this._internalRequest, HTTPRequestStates.Error, ex));
}
else
{
//this._internalRequest.State = HTTPRequestStates.Aborted;
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this._internalRequest, HTTPRequestStates.Aborted, null));
}
HTTPManager.Logger.Exception(nameof(OverHTTP1), "Frame sending", ex, this.Parent.Context);
this._closed = true;
this.State = WebSocketStates.Closed;
}
}
HTTPManager.Logger.Information(nameof(OverHTTP1), string.Format("Ending Send thread. Closed: {0}, closeSent: {1}", this._closed, this._closeSent), this.Parent.Context);
}
}
catch (Exception ex)
{
if (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Exception(nameof(OverHTTP1), "SendThread", ex, this.Parent.Context);
}
finally
{
HTTPManager.Logger.Information(nameof(OverHTTP1), "SendThread - Closed!", this.Parent.Context);
this.newFrameSignal?.Dispose();
this.newFrameSignal = null;
}
}
private void SendPing()
{
HTTPManager.Logger.Information(nameof(OverHTTP1), "Sending Ping frame, waiting for a pong...", this.Parent.Context);
lastPing = DateTime.UtcNow;
waitingForPong = true;
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Ping, BufferSegment.Empty));
}
public override void StartOpen()
{
HTTPManager.Logger.Information(nameof(OverHTTP1), $"{nameof(StartOpen)}", this.Parent.Context);
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(nameof(OverHTTP1), "Open", ex, this.Parent.Context);
}
}
InternalRequest.Send();
requestSent = true;
this.State = WebSocketStates.Connecting;
HTTPManager.Heartbeats.Subscribe(this);
}
public override void StartClose(WebSocketStatusCodes code, string message)
{
HTTPManager.Logger.Information(nameof(OverHTTP1), $"{nameof(StartClose)}({code}, {message})", 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, WebSocketStatusCodes.NormalClosure, string.Empty);
}
else
{
this.State = WebSocketStates.Closing;
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.ConnectionClose, WebSocket.EncodeCloseData(code, message), false));
}
}
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);
Send(WebSocketFrameTypes.Text, data.AsBuffer(count));
}
public override void Send(byte[] data)
{
if (data == null)
throw new ArgumentNullException("data must not be null!");
WebSocketFrame frame = new WebSocketFrame(this.Parent, WebSocketFrameTypes.Binary, new BufferSegment(data, 0, data.Length));
Send(frame);
}
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");
WebSocketFrame frame = new WebSocketFrame(this.Parent, WebSocketFrameTypes.Binary, new BufferSegment(data, (int)offset, (int)count), true);
Send(frame);
}
public void Send(WebSocketFrameTypes type, BufferSegment data)
{
WebSocketFrame frame = new WebSocketFrame(this.Parent, type, data, false);
Send(frame);
}
public override void SendAsBinary(BufferSegment data)
{
Send(WebSocketFrameTypes.Binary, data);
}
public override void SendAsText(BufferSegment data)
{
Send(WebSocketFrameTypes.Text, data);
}
public override void Send(WebSocketFrame frame)
{
if (this._closed || this._closeSent)
return;
this.unsentFrames.Enqueue(frame);
Interlocked.Add(ref this._bufferedAmount, frame.Data.Count);
newFrameSignal.Set();
}
public void SetBinding(PeekableContentProviderStream stream)
{
this.ContentProvider = stream;
// Read any frames already in the buffers
OnContent();
}
public void UnsetBinding()
{
this.ContentProvider?.Dispose();
this.ContentProvider = null;
}
public void OnContent()
{
this.LastMessageReceived = DateTime.UtcNow;
if (this._closeReceived || this._closed)
return;
while (CanReadFullFrame(this.ContentProvider))
{
WebSocketFrameReader frame = new WebSocketFrameReader();
frame.Read(this.ContentProvider);
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(OverHTTP1), "Frame received: " + frame.ToString(), this.Parent.Context);
if (!frame.IsFinal)
{
IncompleteFrames.Add(frame);
continue;
}
switch (frame.Type)
{
// For a complete documentation and rules on fragmentation see http://tools.ietf.org/html/rfc6455#section-5.4
// A fragmented Frame's last fragment's opcode is 0 (Continuation) and the FIN bit is set to 1.
case WebSocketFrameTypes.Continuation:
// Do an assemble pass only if OnFragment is not set. Otherwise put it in the CompletedFrames, we will handle it in the HandleEvent phase.
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;
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));
}
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.UtcNow - 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:
this._closeReceived = true;
HTTPManager.Logger.Information(nameof(OverHTTP1), $"ConnectionClose packet received! ({this._closeReceived}, {this._closeSent})", this.Parent.Context);
CompletedFrames.Enqueue(frame);
break;
}
}
}
public void OnConnectionClosed()
{
if (this._closed)
return;
//this._internalRequest.Timing.Finish(Timing_Name);
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this._internalRequest, HTTPRequestStates.Error, new Exception("Connection closed unexpectedly!")));
}
public void OnError(Exception ex)
{
if (this._closed)
return;
//this._internalRequest.Timing.Finish(Timing_Name);
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this._internalRequest, HTTPRequestStates.Error, ex));
}
public void OnHeartbeatUpdate(DateTime now, TimeSpan dif)
{
if (HTTPManager.IsQuitting)
this.StartClose(WebSocketStatusCodes.GoingAway, "Editor closing");
switch (this.State)
{
case WebSocketStates.Connecting:
if (requestSent && this._internalRequest?.Response?.IsUpgraded is bool upgraded && upgraded)
{
this.State = WebSocketStates.Open;
// The request upgraded successfully.
if (this.Parent.OnOpen != null)
this.Parent.OnOpen(this.Parent);
OnHeartbeatUpdate(now, dif);
}
break;
case WebSocketStates.Closing:
// TODO: define and handle a timeout
HandleCompletedFrames();
break;
case WebSocketStates.Closed:
HandleCompletedFrames();
HTTPManager.Heartbeats.Unsubscribe(this);
this.ContentProvider?.Unbind();
if (this._internalRequest != null && this._internalRequest.State < HTTPRequestStates.Finished)
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this._internalRequest, HTTPRequestStates.Finished, null));
// TODO: go through any lists and queues to empty and recycle buffer segments
// this.unsentFrames.TryDequeue(out frame)
if (this.Parent.OnClosed != null)
{
try
{
this.Parent.OnClosed(this.Parent, (WebSocketStatusCodes)this._closeCode, this._closeMessage);
this.Parent.OnClosed = null;
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(OverHTTP1), "HandleEvents - OnClosed", ex, this.Parent.Context);
}
}
break;
default:
HandleCompletedFrames();
break;
}
}
private void HandleCompletedFrames()
{
while (CompletedFrames.TryDequeue(out var frame))
{
try
{
switch (frame.Type)
{
case WebSocketFrameTypes.Continuation:
if (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Verbose(nameof(OverHTTP1), "HandleEvents - OnIncompleteFrame", this.Parent.Context);
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(nameof(OverHTTP1), $"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(nameof(OverHTTP1), $"HandleEvents - OnBinary({frame.Data})", this.Parent.Context);
if (this.Parent.OnBinary != null)
this.Parent.OnBinary(this.Parent, frame.Data);
break;
case WebSocketFrameTypes.ConnectionClose:
HTTPManager.Logger.Verbose(nameof(OverHTTP1), "HandleEvents - Calling OnClosed", this.Parent.Context);
if (!this._closeSent)
{
this.State = WebSocketStates.Closing;
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.ConnectionClose, BufferSegment.Empty));
}
else
{
this._closed = true;
this.State = WebSocketStates.Closed;
this.newFrameSignal?.Set();
}
if (frame.Data != BufferSegment.Empty && frame.Data.Count >= 2)
{
if (BitConverter.IsLittleEndian)
Array.Reverse(frame.Data.Data, frame.Data.Offset, 2);
this._closeCode = BitConverter.ToUInt16(frame.Data.Data, frame.Data.Offset);
if (frame.Data.Count > 2)
this._closeMessage = Encoding.UTF8.GetString(frame.Data.Data, frame.Data.Offset + 2, frame.Data.Count - 2);
frame.ReleaseData();
}
break;
}
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(OverHTTP1), string.Format("HandleEvents({0})", frame.ToString()), ex, this.Parent.Context);
}
finally
{
frame.ReleaseData();
}
}
}
}
}
#endif

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: c7c7b94536f8416468021fa15f00b11e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/Runtime/Implementations/OverHTTP1.cs
uploadId: 737284

View File

@ -0,0 +1,584 @@
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using Best.HTTP;
using Best.HTTP.Hosts.Connections;
using Best.HTTP.Hosts.Connections.HTTP2;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.WebSockets.Implementations.Frames;
namespace Best.WebSockets.Implementations
{
/// <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 Latency { get { return base.Latency; } }
public int BufferedFramesCount { get => base.frames.Count; }
private IThreadSignaler threadSignaler;
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. The 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 SetThreadSignaler(IThreadSignaler signaler) => this.threadSignaler = signaler;
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.DownloadSettings.DisableCache = true;
base._internalRequest.DownloadSettings.OnHeadersReceived += OnHeadersReceived;
// set a fake upload stream, so HPACKEncoder will not set the END_STREAM flag
base._internalRequest.UploadSettings.UploadStream = new MemoryStream(0);
// TODO:
//base._internalRequest.UseUploadStreamLength = false;
this.LastMessageReceived = DateTime.UtcNow;
base._internalRequest.Tag = (CustomHTTP2StreamFactory)HTTP2WebSocketStreamFactory;
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 HTTP2WebSocketStream HTTP2WebSocketStreamFactory(HTTPRequest request, uint id, HTTP2ContentConsumer parentHandler, HTTP2SettingsManager registry, HPACKEncoder hpackEncoder)
{
request.Tag = this;
return new HTTP2WebSocketStream(id, parentHandler, registry, hpackEncoder);
}
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)
{
base.ParseExtensionResponse(resp);
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.SendPings)
{
this.LastMessageReceived = DateTime.UtcNow;
SendPing();
}
}
else
req.Abort();
}
internal void OnReadThread(BufferSegment buffer)
{
this.LastMessageReceived = DateTime.UtcNow;
this.incomingSegmentStream.Write(buffer);
while (CanReadFullFrame(this.incomingSegmentStream))
{
WebSocketFrameReader frame = new WebSocketFrameReader();
frame.Read(this.incomingSegmentStream);
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose("OverHTTP2", "Frame received: " + frame.ToString(), this.Parent.Context);
if (!frame.IsFinal)
{
IncompleteFrames.Add(frame);
continue;
}
switch (frame.Type)
{
// For a complete documentation and rules on fragmentation see http://tools.ietf.org/html/rfc6455#section-5.4
// A fragmented Frame's last fragment's opcode is 0 (Continuation) and the FIN bit is set to 1.
case WebSocketFrameTypes.Continuation:
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;
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));
}
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.UtcNow - 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);
Cleanup();
// If it's already closed, all events are called too.
if (this.State == WebSocketStates.Closed)
return;
if (this.State == WebSocketStates.Connecting && HTTPManager.PerHostSettings.Get(this.Uri).HTTP2ConnectionSettings.WebSocketOverHTTP2Settings.EnableImplementationFallback)
{
this.Parent.FallbackToHTTP1();
HTTPManager.Heartbeats.Unsubscribe(this);
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.Parent.OnClosed != null)
{
try
{
this.Parent.OnClosed(this.Parent,
!string.IsNullOrEmpty(reason) ? WebSocketStatusCodes.ClosedAbnormally : WebSocketStatusCodes.NormalClosure,
reason ?? "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(WebSocketStatusCodes 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, WebSocketStatusCodes.NormalClosure, message);
}
else
{
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.ConnectionClose, WebSocket.EncodeCloseData(code, message), 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));
}
public override void Send(WebSocketFrame frame)
{
if (this.State == WebSocketStates.Closed || _closeSent)
return;
this.frames.Enqueue(frame);
this.threadSignaler.SignalThread();
Interlocked.Add(ref base._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, false));
}
internal void PreReadCallback()
{
if (this.Parent.SendPings)
{
DateTime now = DateTime.UtcNow;
if (!waitingForPong && now - LastMessageReceived >= 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, this.Parent.PingFrequency, this.Parent.CloseAfterNoMessage, now), this.Parent.Context);
CloseWithError("No message received in the given time!");
}
}
}
}
public void OnHeartbeatUpdate(DateTime now, TimeSpan dif)
{
switch (this.State)
{
case WebSocketStates.Connecting:
if (now - this.InternalRequest.Timing.Created >= this.Parent.CloseAfterNoMessage)
{
if (HTTPManager.PerHostSettings.Get(this.Uri).HTTP2ConnectionSettings.WebSocketOverHTTP2Settings.EnableImplementationFallback)
{
this.State = WebSocketStates.Closed;
this.InternalRequest.DownloadSettings.OnHeadersReceived = null;
this.InternalRequest.Callback = null;
this.Parent.FallbackToHTTP1();
HTTPManager.Heartbeats.Unsubscribe(this);
}
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);
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)
this.Parent.OnBinary(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, (WebSocketStatusCodes)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 + this.Parent.PingFrequency) - DateTime.UtcNow;
}
private void SendPing()
{
HTTPManager.Logger.Information("OverHTTP2", "Sending Ping frame, waiting for a pong...", this.Parent.Context);
lastPing = DateTime.UtcNow;
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.OnClosed != null)
{
try
{
this.Parent.OnClosed(this.Parent, WebSocketStatusCodes.ClosedAbnormally, message);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", "CloseWithError", ex, this.Parent.Context);
}
}
this.InternalRequest.Abort();
HTTPManager.Heartbeats.Unsubscribe(this);
}
internal bool TryDequeueFrame(out WebSocketFrame frame)
{
if (base.frames.TryDequeue(out frame))
{
Interlocked.Add(ref base._bufferedAmount, frame.Data.Count);
return true;
}
return false;
}
}
}
#endif

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 4ad5000fcffa4a444a7f317f3f589200
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/Runtime/Implementations/OverHTTP2.cs
uploadId: 737284

View File

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

View File

@ -0,0 +1,56 @@
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
namespace Best.WebSockets.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,18 @@
fileFormatVersion: 2
guid: bc8a4ee1efb2f884695f2ec1339025e4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/Runtime/Implementations/Utils/LockedBufferSegmenStream.cs
uploadId: 737284

View File

@ -0,0 +1,283 @@
#if UNITY_WEBGL && !UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Best.HTTP.Shared;
using Best.HTTP.Shared.PlatformSupport.Memory;
namespace Best.WebSockets.Implementations
{
delegate void OnWebGLWebSocketOpenDelegate(uint id);
delegate void OnWebGLWebSocketTextDelegate(uint id, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 2)] byte[] textBuffer, int allocatedLength, int length);
delegate void OnWebGLWebSocketBinaryDelegate(uint id, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 2)] byte[] buffer, int allocatedLength, int length);
delegate void OnWebGLWebSocketErrorDelegate(uint id, string error);
delegate void OnWebGLWebSocketCloseDelegate(uint id, int code, string reason);
delegate IntPtr OnWebGLAllocArray(int nativeId, int size);
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, Allocator);
WebSockets.Add(ImplementationId, this.Parent);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "Open", ex, this.Parent.Context);
}
}
public override void StartClose(WebSocketStatusCodes code, string message)
{
WS_Close(this.ImplementationId, (ushort)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,
OnWebGLAllocArray allocator);
[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(OnWebGLAllocArray))]
static unsafe IntPtr Allocator(int nativeId, int length)
{
byte[] buffer = BufferPool.Get(length, true);
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(WebGLBrowser), $"Allocator - allocated: {buffer.Length}");
buffer[0] = (byte)(buffer.Length >> 24);
buffer[1] = (byte)(buffer.Length >> 16);
buffer[2] = (byte)(buffer.Length >> 8);
buffer[3] = (byte)(buffer.Length);
fixed (byte* ptr = buffer)
{
var p = (IntPtr)ptr;
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(WebGLBrowser), $"({p}) <= Allocator({nativeId}, {length})");
return p;
}
}
[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, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 2)] byte[] textBuffer, int allocatedLength, int length)
{
try
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
if (ws.OnMessage != null)
{
try
{
var text = System.Text.Encoding.UTF8.GetString(textBuffer, 0, length);
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(WebGLBrowser), $"{id}, {textBuffer}, {length} => {text}", ws.Context);
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());
}
finally
{
BufferPool.Release(textBuffer);
}
}
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketBinaryDelegate))]
static void OnBinaryCallback(uint id, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 2)] byte[] buffer, int allocatedLength, int length)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
if (ws.OnBinary != null)
{
try
{
ws.OnBinary(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.OnClosed != null)
{
try
{
ws.OnClosed(ws, WebSocketStatusCodes.ClosedAbnormally, 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)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
WebSockets.Remove(id);
if (ws.OnClosed != null)
{
try
{
ws.OnClosed(ws, (WebSocketStatusCodes)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,18 @@
fileFormatVersion: 2
guid: 00814e591b0212b4ca8a4ae3c6cea571
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/Runtime/Implementations/WebGLBrowser.cs
uploadId: 737284

View File

@ -0,0 +1,262 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
#if !UNITY_WEBGL || UNITY_EDITOR
using Best.WebSockets.Implementations.Frames;
#endif
namespace Best.WebSockets.Implementations
{
/// <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 OnWebSocketBinaryNoAllocDelegate(WebSocket webSocket, BufferSegment data);
public delegate void OnWebSocketClosedDelegate(WebSocket webSocket, WebSocketStatusCodes code, string message);
#if !UNITY_WEBGL || UNITY_EDITOR
public delegate void OnWebSocketIncompleteFrameDelegate(WebSocket webSocket, WebSocketFrameReader frame);
#endif
/// <summary>
/// Abstract class for concrete websocket communication implementations.
/// </summary>
public abstract class WebSocketBaseImplementation
{
/// <summary>
/// Capacity of the RTT buffer where the latencies are kept.
/// </summary>
public static int RTTBufferCapacity = 5;
public const string Timing_Name = "Websocket";
public virtual WebSocketStates State { get; protected set; }
#if UNITY_WEBGL && !UNITY_EDITOR
public virtual bool IsOpen { get; protected set; }
public virtual int BufferedAmount { get; protected set; }
#else
public bool IsOpen => this.State == WebSocketStates.Open;
public int BufferedAmount { get => this._bufferedAmount; }
protected volatile int _bufferedAmount;
public HTTP.HTTPRequest InternalRequest
{
get
{
if (this._internalRequest == null)
CreateInternalRequest();
return this._internalRequest;
}
}
protected HTTP.HTTPRequest _internalRequest;
public virtual int Latency { get; protected set; }
public virtual DateTime LastMessageReceived { get; protected set; }
/// <summary>
/// A circular buffer to store the last N rtt times calculated by the pong messages.
/// </summary>
protected CircularBuffer<int> rtts = new CircularBuffer<int>(WebSocketBaseImplementation.RTTBufferCapacity);
/// <summary>
/// When we sent out the last ping.
/// </summary>
protected DateTime lastPing = DateTime.MinValue;
protected bool waitingForPong = false;
protected List<WebSocketFrameReader> IncompleteFrames = new List<WebSocketFrameReader>();
protected PeekableIncomingSegmentStream incomingSegmentStream = new PeekableIncomingSegmentStream();
protected ConcurrentQueue<WebSocketFrameReader> CompletedFrames = new ConcurrentQueue<WebSocketFrameReader>();
protected ConcurrentQueue<WebSocketFrame> frames = new ConcurrentQueue<WebSocketFrame>();
/// <summary>
/// True if we sent out a Close message to the server
/// </summary>
internal volatile bool _closeSent;
internal volatile bool _closeReceived;
#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 = TimeSpan.FromMilliseconds(10_000);
this.Parent.CloseAfterNoMessage = TimeSpan.FromSeconds(2);
#endif
}
public abstract void StartOpen();
public abstract void StartClose(WebSocketStatusCodes 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 void ParseExtensionResponse(HTTP.HTTPResponse resp)
{
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("WebSocketBaseImplementation", "ParseNegotiation", ex, this.Parent.Context);
// Do not try to use a defective extension in the future
this.Parent.Extensions[i] = null;
}
}
}
}
protected abstract void CreateInternalRequest();
/// <summary>
/// It will send the given frame to the server.
/// </summary>
public abstract void Send(WebSocketFrame frame);
protected virtual void Cleanup()
{
for (int i = 0; i < this.IncompleteFrames.Count; ++i)
{
var frame = this.IncompleteFrames[i];
BufferPool.Release(frame.Data);
}
this.IncompleteFrames.Clear();
this.Parent.DisposeExtensions();
}
protected 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;
}
public static bool CanReadFullFrame(PeekableStream stream)
{
if (stream.Length < 2)
return false;
stream.BeginPeek();
int headerLength = 2;
int header = stream.PeekByte();
if (header == -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 (HasMask)
throw new NotSupportedException("Server-sent frames must not be masked!");
// if 0-125, that is the payload length.
int payloadLength = (int)(maskAndLength & 127);
// If 126, the following 2 bytes interpreted as a 16-bit unsigned integer are the payload length.
if (payloadLength == 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);
payloadLength = (int)BitConverter.ToUInt16(rawLen, 0);
headerLength += 2;
BufferPool.Release(rawLen);
}
else if (payloadLength == 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);
payloadLength = (int)BitConverter.ToUInt64(rawLen, 0);
headerLength += 8;
BufferPool.Release(rawLen);
}
return stream.Length >= (headerLength + payloadLength);
}
#endif
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: c1a975643d2b82840a918d261fe4e8f9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/Runtime/Implementations/WebSocketBaseImplementation.cs
uploadId: 737284