- Initial commit
This commit is contained in:
10
Runtime/AssemblyInfo.cs
Normal file
10
Runtime/AssemblyInfo.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("3.0.7")]
|
||||
[assembly: AssemblyCompany("Tivadar György Nagy")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2024 Tivadar György Nagy")]
|
||||
[assembly: AssemblyDescription("Best WebSockets is a premier networking library for Unity, tailored specifically for seamless WebSocket integration. It's perfect for applications that require real-time, bi-directional communication such as chat applications, multiplayer games, and live interactive systems.")]
|
||||
|
||||
#if WITH_BURST
|
||||
[assembly: Unity.Burst.BurstCompile(CompileSynchronously = true, OptimizeFor = Unity.Burst.OptimizeFor.Performance)]
|
||||
#endif
|
||||
18
Runtime/AssemblyInfo.cs.meta
Normal file
18
Runtime/AssemblyInfo.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0088801e96f54ee4c947f9d8ad1ff8f6
|
||||
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/AssemblyInfo.cs
|
||||
uploadId: 737284
|
||||
8
Runtime/Extensions.meta
Normal file
8
Runtime/Extensions.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34bb8567fd5feb142affea2532722c3e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
48
Runtime/Extensions/IExtension.cs
Normal file
48
Runtime/Extensions/IExtension.cs
Normal file
@ -0,0 +1,48 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
|
||||
using Best.HTTP;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.WebSockets.Implementations.Frames;
|
||||
|
||||
namespace Best.WebSockets.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for websocket-extension implementations.
|
||||
/// </summary>
|
||||
public interface IExtension : IDisposable
|
||||
{
|
||||
/// <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
|
||||
18
Runtime/Extensions/IExtension.cs.meta
Normal file
18
Runtime/Extensions/IExtension.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b8334cd17e5fcb49a30d5cec459d376
|
||||
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/Extensions/IExtension.cs
|
||||
uploadId: 737284
|
||||
368
Runtime/Extensions/PerMessageCompression.cs
Normal file
368
Runtime/Extensions/PerMessageCompression.cs
Normal file
@ -0,0 +1,368 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Compression.Zlib;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
using Best.WebSockets.Implementations.Frames;
|
||||
|
||||
namespace Best.WebSockets.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Compression Extensions for WebSocket implementation.
|
||||
/// http://tools.ietf.org/html/rfc7692
|
||||
/// </summary>
|
||||
public sealed class PerMessageCompression : IExtension
|
||||
{
|
||||
public const int MinDataLengthToCompressDefault = 256;
|
||||
|
||||
private static readonly byte[] Trailer = new byte[] { 0x00, 0x00, 0xFF, 0xFF };
|
||||
|
||||
#region Public Properties
|
||||
|
||||
/// <summary>
|
||||
/// By including this extension parameter in an extension negotiation offer, a client informs the peer server
|
||||
/// of a hint that even if the server doesn't include the "client_no_context_takeover" extension parameter in
|
||||
/// the corresponding extension negotiation response to the offer, the client is not going to use context takeover.
|
||||
/// </summary>
|
||||
public bool ClientNoContextTakeover { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// By including this extension parameter in an extension negotiation offer, a client prevents the peer server from using context takeover.
|
||||
/// </summary>
|
||||
public bool ServerNoContextTakeover { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// This parameter indicates the base-2 logarithm of the LZ77 sliding window size of the client context.
|
||||
/// </summary>
|
||||
public int ClientMaxWindowBits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// This parameter indicates the base-2 logarithm of the LZ77 sliding window size of the server context.
|
||||
/// </summary>
|
||||
public int ServerMaxWindowBits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The compression level that the client will use to compress the frames.
|
||||
/// </summary>
|
||||
public CompressionLevel Level { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// What minimum data length will trigger the compression.
|
||||
/// </summary>
|
||||
public int MinimumDataLegthToCompress { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private fields
|
||||
|
||||
/// <summary>
|
||||
/// Cached object to support context takeover.
|
||||
/// </summary>
|
||||
private BufferPoolMemoryStream compressorOutputStream;
|
||||
private DeflateStream compressorDeflateStream;
|
||||
|
||||
/// <summary>
|
||||
/// Cached object to support context takeover.
|
||||
/// </summary>
|
||||
private BufferPoolMemoryStream decompressorInputStream;
|
||||
private BufferPoolMemoryStream decompressorOutputStream;
|
||||
private DeflateStream decompressorDeflateStream;
|
||||
|
||||
#endregion
|
||||
|
||||
public PerMessageCompression()
|
||||
:this(CompressionLevel.Default, false, false, ZlibConstants.WindowBitsMax, ZlibConstants.WindowBitsMax, MinDataLengthToCompressDefault)
|
||||
{ }
|
||||
|
||||
public PerMessageCompression(CompressionLevel level,
|
||||
bool clientNoContextTakeover,
|
||||
bool serverNoContextTakeover,
|
||||
int desiredClientMaxWindowBits,
|
||||
int desiredServerMaxWindowBits,
|
||||
int minDatalengthToCompress)
|
||||
{
|
||||
this.Level = level;
|
||||
this.ClientNoContextTakeover = clientNoContextTakeover;
|
||||
this.ServerNoContextTakeover = serverNoContextTakeover;
|
||||
this.ClientMaxWindowBits = desiredClientMaxWindowBits;
|
||||
this.ServerMaxWindowBits = desiredServerMaxWindowBits;
|
||||
this.MinimumDataLegthToCompress = minDatalengthToCompress;
|
||||
}
|
||||
|
||||
#region IExtension Implementation
|
||||
|
||||
/// <summary>
|
||||
/// This will start the permessage-deflate negotiation process.
|
||||
/// <seealso href="http://tools.ietf.org/html/rfc7692#section-5.1"/>
|
||||
/// </summary>
|
||||
public void AddNegotiation(HTTP.HTTPRequest request)
|
||||
{
|
||||
// The default header value that we will send out minimum.
|
||||
string headerValue = "permessage-deflate";
|
||||
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.1.1.1
|
||||
// A client MAY include the "server_no_context_takeover" extension parameter in an extension negotiation offer. This extension parameter has no value.
|
||||
// By including this extension parameter in an extension negotiation offer, a client prevents the peer server from using context takeover.
|
||||
// If the peer server doesn't use context takeover, the client doesn't need to reserve memory to retain the LZ77 sliding window between messages.
|
||||
if (this.ServerNoContextTakeover)
|
||||
headerValue += "; server_no_context_takeover";
|
||||
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.1.1.2
|
||||
// A client MAY include the "client_no_context_takeover" extension parameter in an extension negotiation offer.
|
||||
// This extension parameter has no value. By including this extension parameter in an extension negotiation offer,
|
||||
// a client informs the peer server of a hint that even if the server doesn't include the "client_no_context_takeover"
|
||||
// extension parameter in the corresponding extension negotiation response to the offer, the client is not going to use context takeover.
|
||||
if (this.ClientNoContextTakeover)
|
||||
headerValue += "; client_no_context_takeover";
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.1.2.1
|
||||
// By including this parameter in an extension negotiation offer, a client limits the LZ77 sliding window size that the server
|
||||
// will use to compress messages.If the peer server uses a small LZ77 sliding window to compress messages, the client can reduce the memory needed for the LZ77 sliding window.
|
||||
if (this.ServerMaxWindowBits != ZlibConstants.WindowBitsMax)
|
||||
headerValue += "; server_max_window_bits=" + this.ServerMaxWindowBits.ToString();
|
||||
else
|
||||
// Absence of this parameter in an extension negotiation offer indicates that the client can receive messages compressed using an LZ77 sliding window of up to 32,768 bytes.
|
||||
this.ServerMaxWindowBits = ZlibConstants.WindowBitsMax;
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.1.2.2
|
||||
// By including this parameter in an offer, a client informs the peer server that the client supports the "client_max_window_bits"
|
||||
// extension parameter in an extension negotiation response and, optionally, a hint by attaching a value to the parameter.
|
||||
if (this.ClientMaxWindowBits != ZlibConstants.WindowBitsMax)
|
||||
headerValue += "; client_max_window_bits=" + this.ClientMaxWindowBits.ToString();
|
||||
else
|
||||
{
|
||||
headerValue += "; client_max_window_bits";
|
||||
|
||||
// If the "client_max_window_bits" extension parameter in an extension negotiation offer has a value, the parameter also informs the
|
||||
// peer server of a hint that even if the server doesn't include the "client_max_window_bits" extension parameter in the corresponding
|
||||
// extension negotiation response with a value greater than the one in the extension negotiation offer or if the server doesn't include
|
||||
// the extension parameter at all, the client is not going to use an LZ77 sliding window size greater than the size specified
|
||||
// by the value in the extension negotiation offer to compress messages.
|
||||
this.ClientMaxWindowBits = ZlibConstants.WindowBitsMax;
|
||||
}
|
||||
|
||||
// Add the new header to the request.
|
||||
request.AddHeader("Sec-WebSocket-Extensions", headerValue);
|
||||
}
|
||||
|
||||
public bool ParseNegotiation(HTTP.HTTPResponse resp)
|
||||
{
|
||||
// Search for any returned neogitation offer
|
||||
var headerValues = resp.GetHeaderValues("Sec-WebSocket-Extensions");
|
||||
if (headerValues == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < headerValues.Count; ++i)
|
||||
{
|
||||
// If found, tokenize it
|
||||
HeaderParser parser = new HeaderParser(headerValues[i]);
|
||||
|
||||
for (int cv = 0; cv < parser.Values.Count; ++cv)
|
||||
{
|
||||
HeaderValue value = parser.Values[i];
|
||||
|
||||
if (!string.IsNullOrEmpty(value.Key) && value.Key.StartsWith("permessage-deflate", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HTTPManager.Logger.Information("PerMessageCompression", "Enabled with header: " + headerValues[i]);
|
||||
|
||||
HeaderValue option;
|
||||
if (value.TryGetOption("client_no_context_takeover", out option))
|
||||
this.ClientNoContextTakeover = true;
|
||||
|
||||
if (value.TryGetOption("server_no_context_takeover", out option))
|
||||
this.ServerNoContextTakeover = true;
|
||||
|
||||
if (value.TryGetOption("client_max_window_bits", out option))
|
||||
if (option.HasValue)
|
||||
{
|
||||
int windowBits;
|
||||
if (int.TryParse(option.Value, out windowBits))
|
||||
this.ClientMaxWindowBits = windowBits;
|
||||
}
|
||||
|
||||
if (value.TryGetOption("server_max_window_bits", out option))
|
||||
if (option.HasValue)
|
||||
{
|
||||
int windowBits;
|
||||
if (int.TryParse(option.Value, out windowBits))
|
||||
this.ServerMaxWindowBits = windowBits;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IExtension implementation to set the Rsv1 flag in the header if we are we will want to compress the data
|
||||
/// in the writer.
|
||||
/// </summary>
|
||||
public byte GetFrameHeader(WebSocketFrame writer, byte inFlag)
|
||||
{
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.2.3.1
|
||||
// the RSV1 bit is set only on the first frame.
|
||||
if ((writer.Type == WebSocketFrameTypes.Binary || writer.Type == WebSocketFrameTypes.Text) &&
|
||||
writer.Data != null && writer.Data.Count >= this.MinimumDataLegthToCompress)
|
||||
return (byte)(inFlag | 0x40);
|
||||
else
|
||||
return inFlag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IExtension implementation to be able to compress the data hold in the writer.
|
||||
/// </summary>
|
||||
public BufferSegment Encode(WebSocketFrame writer)
|
||||
{
|
||||
if (writer.Data == null)
|
||||
return BufferSegment.Empty;
|
||||
|
||||
// Is compressing enabled for this frame? If so, compress it.
|
||||
if ((writer.Header & 0x40) != 0)
|
||||
return Compress(writer.Data);
|
||||
else
|
||||
return writer.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IExtension implementation to possible decompress the data.
|
||||
/// </summary>
|
||||
public BufferSegment Decode(byte header, BufferSegment data)
|
||||
{
|
||||
// Is the server compressed the data? If so, decompress it.
|
||||
if ((header & 0x40) != 0)
|
||||
return Decompress(data);
|
||||
else
|
||||
return data;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helper Functions
|
||||
|
||||
/// <summary>
|
||||
/// A function to compress and return the data parameter with possible context takeover support (reusing the DeflateStream).
|
||||
/// </summary>
|
||||
private BufferSegment Compress(BufferSegment data)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
if (compressorOutputStream == null)
|
||||
compressorOutputStream = new BufferPoolMemoryStream();
|
||||
compressorOutputStream.SetLength(0);
|
||||
|
||||
if (compressorDeflateStream == null)
|
||||
{
|
||||
compressorDeflateStream = new DeflateStream(compressorOutputStream, CompressionMode.Compress, this.Level, false, this.ClientMaxWindowBits);
|
||||
compressorDeflateStream.FlushMode = FlushType.Sync;
|
||||
}
|
||||
|
||||
BufferSegment result = BufferSegment.Empty;
|
||||
try
|
||||
{
|
||||
compressorDeflateStream.Write(data.Data, data.Offset, data.Count);
|
||||
compressorDeflateStream.Flush();
|
||||
|
||||
compressorOutputStream.Position = 0;
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.2.1
|
||||
// Remove 4 octets (that are 0x00 0x00 0xff 0xff) from the tail end.
|
||||
// After this step, the last octet of the compressed data contains (possibly part of)
|
||||
// the DEFLATE header bits with the "BTYPE" bits set to 00.
|
||||
compressorOutputStream.SetLength(compressorOutputStream.Length - 4);
|
||||
|
||||
result = compressorOutputStream.ToBufferSegment();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (this.ClientNoContextTakeover)
|
||||
{
|
||||
compressorDeflateStream.Dispose();
|
||||
compressorDeflateStream = null;
|
||||
compressorOutputStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A function to decompress and return the data parameter with possible context takeover support (reusing the DeflateStream).
|
||||
/// </summary>
|
||||
private BufferSegment Decompress(BufferSegment data)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
if (decompressorInputStream == null)
|
||||
decompressorInputStream = new BufferPoolMemoryStream(data.Count + 4);
|
||||
|
||||
decompressorInputStream.Write(data.Data, data.Offset, data.Count);
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.2.2
|
||||
// Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the payload of the message.
|
||||
decompressorInputStream.Write(PerMessageCompression.Trailer, 0, PerMessageCompression.Trailer.Length);
|
||||
|
||||
decompressorInputStream.Position = 0;
|
||||
|
||||
if (decompressorDeflateStream == null)
|
||||
{
|
||||
decompressorDeflateStream = new DeflateStream(decompressorInputStream, CompressionMode.Decompress, CompressionLevel.Default, false, this.ServerMaxWindowBits);
|
||||
decompressorDeflateStream.FlushMode = FlushType.Sync;
|
||||
}
|
||||
|
||||
if (decompressorOutputStream == null)
|
||||
decompressorOutputStream = new BufferPoolMemoryStream();
|
||||
decompressorOutputStream.SetLength(0);
|
||||
|
||||
byte[] copyBuffer = BufferPool.Get(1024, true);
|
||||
int readCount;
|
||||
while ((readCount = decompressorDeflateStream.Read(copyBuffer, 0, copyBuffer.Length)) != 0)
|
||||
decompressorOutputStream.Write(copyBuffer, 0, readCount);
|
||||
|
||||
BufferPool.Release(copyBuffer);
|
||||
|
||||
decompressorDeflateStream.SetLength(0);
|
||||
|
||||
var result = decompressorOutputStream.ToBufferSegment();
|
||||
|
||||
if (this.ServerNoContextTakeover)
|
||||
{
|
||||
decompressorDeflateStream.Dispose();
|
||||
decompressorDeflateStream = null;
|
||||
decompressorInputStream = null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
bool _disposed;
|
||||
void CheckDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(PerMessageCompression));
|
||||
}
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
|
||||
compressorDeflateStream?.Dispose();
|
||||
compressorOutputStream?.Dispose();
|
||||
|
||||
decompressorDeflateStream?.Dispose();
|
||||
decompressorInputStream?.Dispose();
|
||||
decompressorOutputStream?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
18
Runtime/Extensions/PerMessageCompression.cs.meta
Normal file
18
Runtime/Extensions/PerMessageCompression.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3455ed9a28ed30498a715264c917d68
|
||||
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/Extensions/PerMessageCompression.cs
|
||||
uploadId: 737284
|
||||
8
Runtime/Implementations.meta
Normal file
8
Runtime/Implementations.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e315fcb38c58764097b10921fc465a9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Runtime/Implementations/Frames.meta
Normal file
8
Runtime/Implementations/Frames.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66d7911cf4ea4b442a861292e904cc95
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
348
Runtime/Implementations/Frames/WebSocketFrame.cs
Normal file
348
Runtime/Implementations/Frames/WebSocketFrame.cs
Normal 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
|
||||
18
Runtime/Implementations/Frames/WebSocketFrame.cs.meta
Normal file
18
Runtime/Implementations/Frames/WebSocketFrame.cs.meta
Normal 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
|
||||
253
Runtime/Implementations/Frames/WebSocketFrameReader.cs
Normal file
253
Runtime/Implementations/Frames/WebSocketFrameReader.cs
Normal 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
|
||||
18
Runtime/Implementations/Frames/WebSocketFrameReader.cs.meta
Normal file
18
Runtime/Implementations/Frames/WebSocketFrameReader.cs.meta
Normal 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
|
||||
51
Runtime/Implementations/Frames/WebSocketFrameTypes.cs
Normal file
51
Runtime/Implementations/Frames/WebSocketFrameTypes.cs
Normal 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
|
||||
18
Runtime/Implementations/Frames/WebSocketFrameTypes.cs.meta
Normal file
18
Runtime/Implementations/Frames/WebSocketFrameTypes.cs.meta
Normal 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
|
||||
168
Runtime/Implementations/HTTP2WebSocketStream.cs
Normal file
168
Runtime/Implementations/HTTP2WebSocketStream.cs
Normal 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
|
||||
18
Runtime/Implementations/HTTP2WebSocketStream.cs.meta
Normal file
18
Runtime/Implementations/HTTP2WebSocketStream.cs.meta
Normal 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
|
||||
728
Runtime/Implementations/OverHTTP1.cs
Normal file
728
Runtime/Implementations/OverHTTP1.cs
Normal 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
|
||||
18
Runtime/Implementations/OverHTTP1.cs.meta
Normal file
18
Runtime/Implementations/OverHTTP1.cs.meta
Normal 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
|
||||
584
Runtime/Implementations/OverHTTP2.cs
Normal file
584
Runtime/Implementations/OverHTTP2.cs
Normal 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
|
||||
18
Runtime/Implementations/OverHTTP2.cs.meta
Normal file
18
Runtime/Implementations/OverHTTP2.cs.meta
Normal 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
|
||||
8
Runtime/Implementations/Utils.meta
Normal file
8
Runtime/Implementations/Utils.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8c9d3df5d26747e4e933e4a7d69a9b4b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
56
Runtime/Implementations/Utils/LockedBufferSegmenStream.cs
Normal file
56
Runtime/Implementations/Utils/LockedBufferSegmenStream.cs
Normal 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
|
||||
@ -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
|
||||
283
Runtime/Implementations/WebGLBrowser.cs
Normal file
283
Runtime/Implementations/WebGLBrowser.cs
Normal 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
|
||||
18
Runtime/Implementations/WebGLBrowser.cs.meta
Normal file
18
Runtime/Implementations/WebGLBrowser.cs.meta
Normal 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
|
||||
262
Runtime/Implementations/WebSocketBaseImplementation.cs
Normal file
262
Runtime/Implementations/WebSocketBaseImplementation.cs
Normal 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
|
||||
}
|
||||
}
|
||||
18
Runtime/Implementations/WebSocketBaseImplementation.cs.meta
Normal file
18
Runtime/Implementations/WebSocketBaseImplementation.cs.meta
Normal 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
|
||||
8
Runtime/Plugins.meta
Normal file
8
Runtime/Plugins.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b3eb1b987a1d894bbb4b5c350a83237
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Runtime/Plugins/WebGL.meta
Normal file
8
Runtime/Plugins/WebGL.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2b34941212eaeb44cba28c82ac07d752
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
191
Runtime/Plugins/WebGL/BestHTTP_WebSocket.jslib
Normal file
191
Runtime/Plugins/WebGL/BestHTTP_WebSocket.jslib
Normal file
@ -0,0 +1,191 @@
|
||||
// https://docs.unity3d.com/6000.0/Documentation/Manual/web-interacting-browser-deprecated.html
|
||||
// Deprecated code | Replacement code
|
||||
// -----------------+-----------------
|
||||
// dynCall() | makeDynCall()
|
||||
|
||||
var Lib_BEST_HTTP_WebGL_WS_Bridge =
|
||||
{
|
||||
$ws: {
|
||||
webSocketInstances: {},
|
||||
nextInstanceId : 1,
|
||||
/*UTF8Decoder: new TextDecoder('utf8'),*/
|
||||
|
||||
Set : function(socket) {
|
||||
ws.webSocketInstances[ws.nextInstanceId] = socket;
|
||||
return ws.nextInstanceId++;
|
||||
},
|
||||
|
||||
Get : function(id) {
|
||||
return ws.webSocketInstances[id];
|
||||
},
|
||||
|
||||
Remove: function(id) {
|
||||
delete ws.webSocketInstances[id];
|
||||
},
|
||||
|
||||
_callOnClose: function(onClose, id, code, reason)
|
||||
{
|
||||
var length = lengthBytesUTF8(reason) + 1;
|
||||
var buffer = _malloc(length);
|
||||
|
||||
stringToUTF8Array(reason, HEAPU8, buffer, length);
|
||||
|
||||
{{{ makeDynCall('viii', 'onClose') }}}(id, code, buffer);
|
||||
|
||||
_free(buffer);
|
||||
},
|
||||
|
||||
_callOnError: function(errCallback, id, reason)
|
||||
{
|
||||
var length = lengthBytesUTF8(reason) + 1;
|
||||
var buffer = _malloc(length);
|
||||
|
||||
stringToUTF8Array(reason, HEAPU8, buffer, length);
|
||||
|
||||
{{{ makeDynCall('vii', 'errCallback') }}}(id, buffer);
|
||||
|
||||
_free(buffer);
|
||||
}
|
||||
},
|
||||
|
||||
WS_Create: function(url, protocol, onOpen, onText, onBinary, onError, onClose, allocator)
|
||||
{
|
||||
var urlStr = new URL(UTF8ToString(url)); ///*encodeURI*/(UTF8ToString(url)).replace(/\+/g, '%2B').replace(/%252[fF]/ig, '%2F');
|
||||
var proto = UTF8ToString(protocol);
|
||||
|
||||
console.log('WS_Create(' + urlStr + ', "' + proto + '")');
|
||||
|
||||
var socket = {
|
||||
onError: onError,
|
||||
onClose: onClose
|
||||
};
|
||||
|
||||
if (proto == '')
|
||||
socket.socketImpl = new WebSocket(urlStr);
|
||||
else
|
||||
socket.socketImpl = new WebSocket(urlStr, proto.trim().split(","));
|
||||
|
||||
var id = ws.nextInstanceId;
|
||||
socket.socketImpl.binaryType = "arraybuffer";
|
||||
|
||||
socket.socketImpl.onopen = function(e) {
|
||||
console.log(id + ' WS_Create - onOpen');
|
||||
|
||||
{{{ makeDynCall('vi', 'onOpen') }}}(id);
|
||||
};
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
socket.socketImpl.onmessage = function (e)
|
||||
{
|
||||
// Binary?
|
||||
if (e.data instanceof ArrayBuffer)
|
||||
{
|
||||
var byteArray = new Uint8Array(e.data);
|
||||
const array = {{{ makeDynCall('iii', 'allocator') }}}(id, byteArray.length);
|
||||
|
||||
const numArr = HEAPU8.subarray(array, array + 4);
|
||||
const originalLength = (numArr[0] << 24) | (numArr[1] << 16) | (numArr[2] << 8) | (numArr[3]);
|
||||
HEAPU8.set(byteArray, array);
|
||||
|
||||
{{{ makeDynCall('viiii', 'onBinary') }}}(id, array, originalLength, byteArray.length);
|
||||
}
|
||||
else // Text
|
||||
{
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encodeInto#buffer_sizing
|
||||
// "If the output allocation (typically within Wasm heap) is expected to be short-lived, it makes sense to allocate s.length * 3 bytes for the output,
|
||||
// in which case the first conversion attempt is guaranteed to convert the whole string."
|
||||
const length = e.data.length * 3;
|
||||
const array = {{{ makeDynCall('iii', 'allocator') }}}(id, length);
|
||||
|
||||
const numArr = HEAPU8.subarray(array, array + 4);
|
||||
const originalLength = (numArr[0] << 24) | (numArr[1] << 16) | (numArr[2] << 8) | (numArr[3]);
|
||||
const ret = encoder.encodeInto(e.data, HEAPU8.subarray(array));
|
||||
|
||||
{{{ makeDynCall('viiii', 'onText') }}}(id, array, originalLength, ret.written);
|
||||
}
|
||||
};
|
||||
socket.socketImpl.onclose = function (e) {
|
||||
console.log(id + ' WS_Create - onClose ' + e.code + ' ' + e.reason);
|
||||
|
||||
ws._callOnClose(onClose, id, e.code, e.reason);
|
||||
};
|
||||
|
||||
return ws.Set(socket);
|
||||
},
|
||||
|
||||
WS_GetState: function (id)
|
||||
{
|
||||
var socket = ws.Get(id);
|
||||
|
||||
if (typeof socket === 'undefined' ||
|
||||
socket == null ||
|
||||
typeof socket.socketImpl === 'undefined' ||
|
||||
socket.socketImpl == null)
|
||||
return 3; // closed
|
||||
|
||||
return socket.socketImpl.readyState;
|
||||
},
|
||||
|
||||
WS_GetBufferedAmount: function (id)
|
||||
{
|
||||
var socket = ws.Get(id);
|
||||
return socket.socketImpl.bufferedAmount;
|
||||
},
|
||||
|
||||
WS_Send_String: function (id, ptr, pos, length)
|
||||
{
|
||||
var socket = ws.Get(id);
|
||||
|
||||
var startPtr = ptr + pos;
|
||||
var endPtr = startPtr + length;
|
||||
|
||||
var UTF8Decoder = new TextDecoder('utf8');
|
||||
var str = UTF8Decoder.decode(HEAPU8.subarray ? HEAPU8.subarray(startPtr, endPtr) : new Uint8Array(HEAPU8.slice(startPtr, endPtr)));
|
||||
|
||||
try
|
||||
{
|
||||
socket.socketImpl.send(str);
|
||||
}
|
||||
catch(e) {
|
||||
ws._callOnError(socket.onError, id, ' ' + e.name + ': ' + e.message);
|
||||
}
|
||||
|
||||
return socket.socketImpl.bufferedAmount;
|
||||
},
|
||||
|
||||
WS_Send_Binary: function(id, ptr, pos, length)
|
||||
{
|
||||
var socket = ws.Get(id);
|
||||
|
||||
try
|
||||
{
|
||||
var buff = HEAPU8.subarray(ptr + pos, ptr + pos + length);
|
||||
socket.socketImpl.send(buff /*HEAPU8.buffer.slice(ptr + pos, ptr + pos + length)*/);
|
||||
}
|
||||
catch(e) {
|
||||
ws._callOnError(socket.onError, id, ' ' + e.name + ': ' + e.message);
|
||||
}
|
||||
|
||||
return socket.socketImpl.bufferedAmount;
|
||||
},
|
||||
|
||||
WS_Close: function (id, code, reason)
|
||||
{
|
||||
var socket = ws.Get(id);
|
||||
var reasonStr = UTF8ToString(reason);
|
||||
|
||||
console.log(id + ' WS_Close(' + code + ', ' + reasonStr + ')');
|
||||
|
||||
socket.socketImpl.close(/*ulong*/code, reasonStr);
|
||||
},
|
||||
|
||||
WS_Release: function(id)
|
||||
{
|
||||
console.log(id + ' WS_Release');
|
||||
|
||||
ws.Remove(id);
|
||||
}
|
||||
};
|
||||
|
||||
autoAddDeps(Lib_BEST_HTTP_WebGL_WS_Bridge, '$ws');
|
||||
mergeInto(LibraryManager.library, Lib_BEST_HTTP_WebGL_WS_Bridge);
|
||||
39
Runtime/Plugins/WebGL/BestHTTP_WebSocket.jslib.meta
Normal file
39
Runtime/Plugins/WebGL/BestHTTP_WebSocket.jslib.meta
Normal file
@ -0,0 +1,39 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8efe6cedf2832a647a33465b85e4f311
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
WebGL: WebGL
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 268757
|
||||
packageName: Best WebSockets
|
||||
packageVersion: 3.0.7
|
||||
assetPath: Packages/com.tivadar.best.websockets/Runtime/Plugins/WebGL/BestHTTP_WebSocket.jslib
|
||||
uploadId: 737284
|
||||
427
Runtime/WebSocket.cs
Normal file
427
Runtime/WebSocket.cs
Normal file
@ -0,0 +1,427 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
using Best.HTTP.HostSetting;
|
||||
using Best.HTTP.Request.Settings;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Hosts.Connections;
|
||||
using Best.WebSockets.Implementations;
|
||||
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
using Best.HTTP.Hosts.Connections.HTTP2;
|
||||
#endif
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
using Best.WebSockets.Extensions;
|
||||
#endif
|
||||
|
||||
using Best.HTTP.Shared.Compression.Zlib;
|
||||
|
||||
/*
|
||||
* This is a wrapper class organizing where and how the websocket request is sent out. If there's an already open HTTP/2 connection with
|
||||
* an ENABLE_CONNECT_PROTOCOL flag, it tries using the HTTP/2 connection first and if it fails, a new HTTP/1 connection is created.
|
||||
* */
|
||||
|
||||
namespace Best.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the WebSocket standard for duplex, two-way communications.
|
||||
/// </summary>
|
||||
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()
|
||||
{
|
||||
return new IExtension[] { new PerMessageCompression(/*compression level: */ CompressionLevel.Default,
|
||||
/*clientNoContextTakeover: */ false,
|
||||
/*serverNoContextTakeover: */ false,
|
||||
/*clientMaxWindowBits: */ ZlibConstants.WindowBitsMax,
|
||||
/*desiredServerMaxWindowBits: */ ZlibConstants.WindowBitsMax,
|
||||
/*minDatalengthToCompress: */ PerMessageCompression.MinDataLengthToCompressDefault) };
|
||||
}
|
||||
#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 <c>true</c> to start sending Ping frames to the WebSocket server.
|
||||
/// </summary>
|
||||
public bool SendPings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The delay between two Pings in milliseconds. Minimum value is 100ms, default is 10 seconds.
|
||||
/// </summary>
|
||||
public TimeSpan PingFrequency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="SendPings"/> set to <c>true</c>, the plugin will close the connection and emit an <see cref="OnClosed"/> 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 <see cref="Best.HTTP.HTTPRequest"/> object.
|
||||
/// </summary>
|
||||
public HTTP.HTTPRequest InternalRequest { get { return this.implementation.InternalRequest; } }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IExtension"/> implementations the plugin will negotiate with the server to use.
|
||||
/// </summary>
|
||||
public IExtension[] Extensions { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Latency calculated from ping-pong message round-trip times.
|
||||
/// </summary>
|
||||
public int Latency { get { return this.implementation.Latency; } }
|
||||
|
||||
/// <summary>
|
||||
/// When the WebSocket instance received the last message from the server.
|
||||
/// </summary>
|
||||
public DateTime LastMessageReceived { get { return this.implementation.LastMessageReceived; } }
|
||||
|
||||
/// <summary>
|
||||
/// When the <c>Websocket Over HTTP/2</c> implementation fails to connect and <see cref="WebSocketOverHTTP2Settings"/><c>.</c><see cref="WebSocketOverHTTP2Settings.EnableImplementationFallback"/> is <c>true</c>, the plugin tries to fall back to the HTTP/1 implementation.
|
||||
/// When this happens a new <see cref="InternalRequest"/> is created and all previous custom modifications (like added headers) are lost. With OnInternalRequestCreated these modifications can be reapplied.
|
||||
/// </summary>
|
||||
public Action<WebSocket, HTTP.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 Binary message received.
|
||||
/// The content of the <see cref="BufferSegment"/> must be used or copied to a new array in the callbacks because the plugin reuses the memory immediately after the callback by placing it back to the <see cref="BufferPool"/>!
|
||||
/// </summary>
|
||||
/// <remarks>Note that the memory will be reused when this event returns. Either process it in this call or make a copy from the received data.</remarks>
|
||||
public OnWebSocketBinaryNoAllocDelegate OnBinary;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the WebSocket connection is closed.
|
||||
/// </summary>
|
||||
public OnWebSocketClosedDelegate OnClosed;
|
||||
|
||||
/// <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
|
||||
/// <summary>
|
||||
/// Creates a WebSocket instance from the given uri.
|
||||
/// </summary>
|
||||
/// <param name="uri">Uri of the WebSocket endpoint.</param>
|
||||
/// <param name="origin">Where the WebSocket originating from.</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>
|
||||
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 <see cref="IExtension"/> 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
|
||||
|
||||
if (HTTPProtocolFactory.IsSecureProtocol(uri) &&
|
||||
HTTPManager.PerHostSettings.Get(uri).HTTP2ConnectionSettings.WebSocketOverHTTP2Settings.EnableWebSocketOverHTTP2)
|
||||
{
|
||||
// Try to find a HTTP/2 connection that supports the connect protocol.
|
||||
var connectionKey = HostKey.From(new UriBuilder("https", uri.Host, uri.Port).Uri, GetProxy(uri));
|
||||
|
||||
var con = HostManager.GetHostVariant(connectionKey).Find(c => {
|
||||
var httpConnection = c as HTTPOverTCPConnection;
|
||||
var http2Handler = httpConnection?.requestHandler as HTTP2ContentConsumer;
|
||||
|
||||
return http2Handler != null && http2Handler.settings.RemoteSettings[HTTP2Settings.ENABLE_CONNECT_PROTOCOL] != 0;
|
||||
});
|
||||
|
||||
if (con != null)
|
||||
{
|
||||
HTTPManager.Logger.Information("WebSocket", "Connection with enabled Connect Protocol found!", this.Context);
|
||||
|
||||
var httpConnection = con as HTTPOverTCPConnection;
|
||||
var http2Handler = httpConnection?.requestHandler as HTTP2ContentConsumer;
|
||||
|
||||
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>
|
||||
/// <remarks>It's a non-blocking call. To get notified when the WebSocket instance is considered open and can send/receive, use the <see cref="OnOpen"/> event.</remarks>
|
||||
public void Open()
|
||||
{
|
||||
this.implementation.StartOpen();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It will send the given textual message to the remote server.
|
||||
/// </summary>
|
||||
public void Send(string message)
|
||||
{
|
||||
if (!IsOpen)
|
||||
return;
|
||||
|
||||
this.implementation.Send(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It will send the given binary message to the remote server.
|
||||
/// </summary>
|
||||
public void Send(byte[] buffer)
|
||||
{
|
||||
if (!IsOpen)
|
||||
return;
|
||||
|
||||
this.implementation.Send(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It will send the given binary message to the remote server.
|
||||
/// </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 the data 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It will initiate the closing of the connection to the server.
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
if (State >= WebSocketStates.Closing)
|
||||
return;
|
||||
|
||||
this.implementation.StartClose(WebSocketStatusCodes.NormalClosure, "Bye!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It will initiate the closing of the connection to the server sending the given code and message.
|
||||
/// </summary>
|
||||
public void Close(WebSocketStatusCodes code, string message)
|
||||
{
|
||||
if (!IsOpen)
|
||||
return;
|
||||
|
||||
this.implementation.StartClose(code, message);
|
||||
}
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
internal ProxySettings GetProxy(Uri uri)
|
||||
{
|
||||
// WebSocket is not a request-response based protocol, so we need a 'tunnel' through the proxy
|
||||
var proxy = HTTPManager.Proxy as HTTP.Proxies.HTTPProxy;
|
||||
if (proxy != null && proxy.UseProxyForAddress(uri))
|
||||
proxy = new HTTP.Proxies.HTTPProxy(proxy.Address,
|
||||
proxy.Credentials,
|
||||
false, /*turn on 'tunneling'*/
|
||||
false, /*sendWholeUri*/
|
||||
proxy.NonTransparentForHTTPS);
|
||||
|
||||
return new ProxySettings { Proxy = proxy };
|
||||
}
|
||||
|
||||
internal void DisposeExtensions()
|
||||
{
|
||||
if (this.Extensions != null)
|
||||
{
|
||||
for (int i = 0; i < this.Extensions.Length; ++i)
|
||||
{
|
||||
var ext = this.Extensions[i];
|
||||
|
||||
try
|
||||
{
|
||||
ext?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("OverHTTP1", "OnInternalRequestCallback - extension dispose", ex, this.Context);
|
||||
}
|
||||
|
||||
this.Extensions[i] = null;
|
||||
}
|
||||
|
||||
this.Extensions = null;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
|
||||
internal static BufferSegment EncodeCloseData(WebSocketStatusCodes 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((ushort)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
|
||||
}
|
||||
}
|
||||
18
Runtime/WebSocket.cs.meta
Normal file
18
Runtime/WebSocket.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 188f56aaa31c94f498b24e9498a85a0c
|
||||
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/WebSocket.cs
|
||||
uploadId: 737284
|
||||
79
Runtime/WebSocketStatusCodes.cs
Normal file
79
Runtime/WebSocketStatusCodes.cs
Normal file
@ -0,0 +1,79 @@
|
||||
namespace Best.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// <see href="http://tools.ietf.org/html/rfc6455#section-7.4.1"/>
|
||||
/// </summary>
|
||||
public enum WebSocketStatusCodes : 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
|
||||
}
|
||||
}
|
||||
18
Runtime/WebSocketStatusCodes.cs.meta
Normal file
18
Runtime/WebSocketStatusCodes.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa4ede837cdd86248b95e6ff4854831e
|
||||
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/WebSocketStatusCodes.cs
|
||||
uploadId: 737284
|
||||
22
Runtime/com.Tivadar.Best.WebSockets.asmdef
Normal file
22
Runtime/com.Tivadar.Best.WebSockets.asmdef
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "com.Tivadar.Best.WebSockets",
|
||||
"rootNamespace": "Best.WebSockets",
|
||||
"references": [
|
||||
"Unity.Burst",
|
||||
"com.Tivadar.Best.HTTP"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": true,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [ {
|
||||
"name": "com.unity.burst",
|
||||
"expression": "1.8.7",
|
||||
"define": "WITH_BURST"
|
||||
}
|
||||
],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
14
Runtime/com.Tivadar.Best.WebSockets.asmdef.meta
Normal file
14
Runtime/com.Tivadar.Best.WebSockets.asmdef.meta
Normal file
@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bba0e49b0b1b9924ab06cd58867f661c
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 268757
|
||||
packageName: Best WebSockets
|
||||
packageVersion: 3.0.7
|
||||
assetPath: Packages/com.tivadar.best.websockets/Runtime/com.Tivadar.Best.WebSockets.asmdef
|
||||
uploadId: 737284
|
||||
5
Runtime/csc.rsp
Normal file
5
Runtime/csc.rsp
Normal file
@ -0,0 +1,5 @@
|
||||
#https://forum.unity.com/threads/rsp-file-per-assembly-or-folder.572758/
|
||||
|
||||
#https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/errors-warnings
|
||||
-warnaserror
|
||||
-nowarn:CS1701,CS0612,CS1591,CS1571,CS1572,CS1573,CS1574,CS1587
|
||||
14
Runtime/csc.rsp.meta
Normal file
14
Runtime/csc.rsp.meta
Normal file
@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a3b358778e405d448e3de83204258d4
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 268757
|
||||
packageName: Best WebSockets
|
||||
packageVersion: 3.0.7
|
||||
assetPath: Packages/com.tivadar.best.websockets/Runtime/csc.rsp
|
||||
uploadId: 737284
|
||||
Reference in New Issue
Block a user