mirror of
https://gitee.com/jisol/jisol-game/
synced 2025-09-27 02:36:14 +00:00
提交Unity 联机Pro
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e44072d24b360245b3b38f37c984cee
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,42 @@
|
||||
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
|
||||
|
||||
using BestHTTP.PlatformSupport.Memory;
|
||||
using BestHTTP.WebSocket.Frames;
|
||||
|
||||
namespace BestHTTP.WebSocket.Extensions
|
||||
{
|
||||
public interface IExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// This is the first pass: here we can add headers to the request to initiate an extension negotiation.
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
void AddNegotiation(HTTPRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// If the websocket upgrade succeded it will call this function to be able to parse the server's negotiation
|
||||
/// response. Inside this function the IsEnabled should be set.
|
||||
/// </summary>
|
||||
bool ParseNegotiation(HTTPResponse resp);
|
||||
|
||||
/// <summary>
|
||||
/// This function should return a new header flag based on the inFlag parameter. The extension should set only the
|
||||
/// Rsv1-3 bits in the header.
|
||||
/// </summary>
|
||||
byte GetFrameHeader(WebSocketFrame writer, byte inFlag);
|
||||
|
||||
/// <summary>
|
||||
/// This function will be called to be able to transform the data that will be sent to the server.
|
||||
/// </summary>
|
||||
/// <param name="writer"></param>
|
||||
/// <returns></returns>
|
||||
BufferSegment Encode(WebSocketFrame writer);
|
||||
|
||||
/// <summary>
|
||||
/// This function can be used the decode the server-sent data.
|
||||
/// </summary>
|
||||
BufferSegment Decode(byte header, BufferSegment data);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ef9ac8246255ea449a40171fb654053
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,339 @@
|
||||
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
|
||||
|
||||
using System;
|
||||
using BestHTTP.Extensions;
|
||||
using BestHTTP.WebSocket.Frames;
|
||||
using BestHTTP.Decompression.Zlib;
|
||||
using BestHTTP.PlatformSupport.Memory;
|
||||
|
||||
namespace BestHTTP.WebSocket.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Compression Extensions for WebSocket implementation.
|
||||
/// http://tools.ietf.org/html/rfc7692
|
||||
/// </summary>
|
||||
public sealed class PerMessageCompression : IExtension
|
||||
{
|
||||
public const int MinDataLengthToCompressDefault = 256;
|
||||
|
||||
private static readonly byte[] Trailer = new byte[] { 0x00, 0x00, 0xFF, 0xFF };
|
||||
|
||||
#region Public Properties
|
||||
|
||||
/// <summary>
|
||||
/// By including this extension parameter in an extension negotiation offer, a client informs the peer server
|
||||
/// of a hint that even if the server doesn't include the "client_no_context_takeover" extension parameter in
|
||||
/// the corresponding extension negotiation response to the offer, the client is not going to use context takeover.
|
||||
/// </summary>
|
||||
public bool ClientNoContextTakeover { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// By including this extension parameter in an extension negotiation offer, a client prevents the peer server from using context takeover.
|
||||
/// </summary>
|
||||
public bool ServerNoContextTakeover { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// This parameter indicates the base-2 logarithm of the LZ77 sliding window size of the client context.
|
||||
/// </summary>
|
||||
public int ClientMaxWindowBits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// This parameter indicates the base-2 logarithm of the LZ77 sliding window size of the server context.
|
||||
/// </summary>
|
||||
public int ServerMaxWindowBits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The compression level that the client will use to compress the frames.
|
||||
/// </summary>
|
||||
public CompressionLevel Level { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// What minimum data length will trigger the compression.
|
||||
/// </summary>
|
||||
public int MinimumDataLegthToCompress { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private fields
|
||||
|
||||
/// <summary>
|
||||
/// Cached object to support context takeover.
|
||||
/// </summary>
|
||||
private BufferPoolMemoryStream compressorOutputStream;
|
||||
private DeflateStream compressorDeflateStream;
|
||||
|
||||
/// <summary>
|
||||
/// Cached object to support context takeover.
|
||||
/// </summary>
|
||||
private BufferPoolMemoryStream decompressorInputStream;
|
||||
private BufferPoolMemoryStream decompressorOutputStream;
|
||||
private DeflateStream decompressorDeflateStream;
|
||||
|
||||
#endregion
|
||||
|
||||
public PerMessageCompression()
|
||||
:this(CompressionLevel.Default, false, false, ZlibConstants.WindowBitsMax, ZlibConstants.WindowBitsMax, MinDataLengthToCompressDefault)
|
||||
{ }
|
||||
|
||||
public PerMessageCompression(CompressionLevel level,
|
||||
bool clientNoContextTakeover,
|
||||
bool serverNoContextTakeover,
|
||||
int desiredClientMaxWindowBits,
|
||||
int desiredServerMaxWindowBits,
|
||||
int minDatalengthToCompress)
|
||||
{
|
||||
this.Level = level;
|
||||
this.ClientNoContextTakeover = clientNoContextTakeover;
|
||||
this.ServerNoContextTakeover = serverNoContextTakeover;
|
||||
this.ClientMaxWindowBits = desiredClientMaxWindowBits;
|
||||
this.ServerMaxWindowBits = desiredServerMaxWindowBits;
|
||||
this.MinimumDataLegthToCompress = minDatalengthToCompress;
|
||||
}
|
||||
|
||||
#region IExtension Implementation
|
||||
|
||||
/// <summary>
|
||||
/// This will start the permessage-deflate negotiation process.
|
||||
/// <seealso href="http://tools.ietf.org/html/rfc7692#section-5.1"/>
|
||||
/// </summary>
|
||||
public void AddNegotiation(HTTPRequest request)
|
||||
{
|
||||
// The default header value that we will send out minimum.
|
||||
string headerValue = "permessage-deflate";
|
||||
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.1.1.1
|
||||
// A client MAY include the "server_no_context_takeover" extension parameter in an extension negotiation offer. This extension parameter has no value.
|
||||
// By including this extension parameter in an extension negotiation offer, a client prevents the peer server from using context takeover.
|
||||
// If the peer server doesn't use context takeover, the client doesn't need to reserve memory to retain the LZ77 sliding window between messages.
|
||||
if (this.ServerNoContextTakeover)
|
||||
headerValue += "; server_no_context_takeover";
|
||||
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.1.1.2
|
||||
// A client MAY include the "client_no_context_takeover" extension parameter in an extension negotiation offer.
|
||||
// This extension parameter has no value. By including this extension parameter in an extension negotiation offer,
|
||||
// a client informs the peer server of a hint that even if the server doesn't include the "client_no_context_takeover"
|
||||
// extension parameter in the corresponding extension negotiation response to the offer, the client is not going to use context takeover.
|
||||
if (this.ClientNoContextTakeover)
|
||||
headerValue += "; client_no_context_takeover";
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.1.2.1
|
||||
// By including this parameter in an extension negotiation offer, a client limits the LZ77 sliding window size that the server
|
||||
// will use to compress messages.If the peer server uses a small LZ77 sliding window to compress messages, the client can reduce the memory needed for the LZ77 sliding window.
|
||||
if (this.ServerMaxWindowBits != ZlibConstants.WindowBitsMax)
|
||||
headerValue += "; server_max_window_bits=" + this.ServerMaxWindowBits.ToString();
|
||||
else
|
||||
// Absence of this parameter in an extension negotiation offer indicates that the client can receive messages compressed using an LZ77 sliding window of up to 32,768 bytes.
|
||||
this.ServerMaxWindowBits = ZlibConstants.WindowBitsMax;
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.1.2.2
|
||||
// By including this parameter in an offer, a client informs the peer server that the client supports the "client_max_window_bits"
|
||||
// extension parameter in an extension negotiation response and, optionally, a hint by attaching a value to the parameter.
|
||||
if (this.ClientMaxWindowBits != ZlibConstants.WindowBitsMax)
|
||||
headerValue += "; client_max_window_bits=" + this.ClientMaxWindowBits.ToString();
|
||||
else
|
||||
{
|
||||
headerValue += "; client_max_window_bits";
|
||||
|
||||
// If the "client_max_window_bits" extension parameter in an extension negotiation offer has a value, the parameter also informs the
|
||||
// peer server of a hint that even if the server doesn't include the "client_max_window_bits" extension parameter in the corresponding
|
||||
// extension negotiation response with a value greater than the one in the extension negotiation offer or if the server doesn't include
|
||||
// the extension parameter at all, the client is not going to use an LZ77 sliding window size greater than the size specified
|
||||
// by the value in the extension negotiation offer to compress messages.
|
||||
this.ClientMaxWindowBits = ZlibConstants.WindowBitsMax;
|
||||
}
|
||||
|
||||
// Add the new header to the request.
|
||||
request.AddHeader("Sec-WebSocket-Extensions", headerValue);
|
||||
}
|
||||
|
||||
public bool ParseNegotiation(HTTPResponse resp)
|
||||
{
|
||||
// Search for any returned neogitation offer
|
||||
var headerValues = resp.GetHeaderValues("Sec-WebSocket-Extensions");
|
||||
if (headerValues == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < headerValues.Count; ++i)
|
||||
{
|
||||
// If found, tokenize it
|
||||
HeaderParser parser = new HeaderParser(headerValues[i]);
|
||||
|
||||
for (int cv = 0; cv < parser.Values.Count; ++cv)
|
||||
{
|
||||
HeaderValue value = parser.Values[i];
|
||||
|
||||
if (!string.IsNullOrEmpty(value.Key) && value.Key.StartsWith("permessage-deflate", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HTTPManager.Logger.Information("PerMessageCompression", "Enabled with header: " + headerValues[i]);
|
||||
|
||||
HeaderValue option;
|
||||
if (value.TryGetOption("client_no_context_takeover", out option))
|
||||
this.ClientNoContextTakeover = true;
|
||||
|
||||
if (value.TryGetOption("server_no_context_takeover", out option))
|
||||
this.ServerNoContextTakeover = true;
|
||||
|
||||
if (value.TryGetOption("client_max_window_bits", out option))
|
||||
if (option.HasValue)
|
||||
{
|
||||
int windowBits;
|
||||
if (int.TryParse(option.Value, out windowBits))
|
||||
this.ClientMaxWindowBits = windowBits;
|
||||
}
|
||||
|
||||
if (value.TryGetOption("server_max_window_bits", out option))
|
||||
if (option.HasValue)
|
||||
{
|
||||
int windowBits;
|
||||
if (int.TryParse(option.Value, out windowBits))
|
||||
this.ServerMaxWindowBits = windowBits;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IExtension implementation to set the Rsv1 flag in the header if we are we will want to compress the data
|
||||
/// in the writer.
|
||||
/// </summary>
|
||||
public byte GetFrameHeader(WebSocketFrame writer, byte inFlag)
|
||||
{
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.2.3.1
|
||||
// the RSV1 bit is set only on the first frame.
|
||||
if ((writer.Type == WebSocketFrameTypes.Binary || writer.Type == WebSocketFrameTypes.Text) &&
|
||||
writer.Data != null && writer.Data.Count >= this.MinimumDataLegthToCompress)
|
||||
return (byte)(inFlag | 0x40);
|
||||
else
|
||||
return inFlag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IExtension implementation to be able to compress the data hold in the writer.
|
||||
/// </summary>
|
||||
public BufferSegment Encode(WebSocketFrame writer)
|
||||
{
|
||||
if (writer.Data == null)
|
||||
return BufferSegment.Empty;
|
||||
|
||||
// Is compressing enabled for this frame? If so, compress it.
|
||||
if ((writer.Header & 0x40) != 0)
|
||||
return Compress(writer.Data);
|
||||
else
|
||||
return writer.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IExtension implementation to possible decompress the data.
|
||||
/// </summary>
|
||||
public BufferSegment Decode(byte header, BufferSegment data)
|
||||
{
|
||||
// Is the server compressed the data? If so, decompress it.
|
||||
if ((header & 0x40) != 0)
|
||||
return Decompress(data);
|
||||
else
|
||||
return data;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helper Functions
|
||||
|
||||
/// <summary>
|
||||
/// A function to compress and return the data parameter with possible context takeover support (reusing the DeflateStream).
|
||||
/// </summary>
|
||||
private BufferSegment Compress(BufferSegment data)
|
||||
{
|
||||
if (compressorOutputStream == null)
|
||||
compressorOutputStream = new BufferPoolMemoryStream();
|
||||
compressorOutputStream.SetLength(0);
|
||||
|
||||
if (compressorDeflateStream == null)
|
||||
{
|
||||
compressorDeflateStream = new DeflateStream(compressorOutputStream, CompressionMode.Compress, this.Level, true, this.ClientMaxWindowBits);
|
||||
compressorDeflateStream.FlushMode = FlushType.Sync;
|
||||
}
|
||||
|
||||
BufferSegment result = BufferSegment.Empty;
|
||||
try
|
||||
{
|
||||
compressorDeflateStream.Write(data.Data, data.Offset, data.Count);
|
||||
compressorDeflateStream.Flush();
|
||||
|
||||
compressorOutputStream.Position = 0;
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.2.1
|
||||
// Remove 4 octets (that are 0x00 0x00 0xff 0xff) from the tail end. After this step, the last octet of the compressed data contains (possibly part of) the DEFLATE header bits with the "BTYPE" bits set to 00.
|
||||
compressorOutputStream.SetLength(compressorOutputStream.Length - 4);
|
||||
|
||||
result = compressorOutputStream.ToBufferSegment();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (this.ClientNoContextTakeover)
|
||||
{
|
||||
compressorDeflateStream.Dispose();
|
||||
compressorDeflateStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A function to decompress and return the data parameter with possible context takeover support (reusing the DeflateStream).
|
||||
/// </summary>
|
||||
private BufferSegment Decompress(BufferSegment data)
|
||||
{
|
||||
if (decompressorInputStream == null)
|
||||
decompressorInputStream = new BufferPoolMemoryStream(data.Count + 4);
|
||||
|
||||
decompressorInputStream.Write(data.Data, data.Offset, data.Count);
|
||||
|
||||
// http://tools.ietf.org/html/rfc7692#section-7.2.2
|
||||
// Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the payload of the message.
|
||||
decompressorInputStream.Write(PerMessageCompression.Trailer, 0, PerMessageCompression.Trailer.Length);
|
||||
|
||||
decompressorInputStream.Position = 0;
|
||||
|
||||
if (decompressorDeflateStream == null)
|
||||
{
|
||||
decompressorDeflateStream = new DeflateStream(decompressorInputStream, CompressionMode.Decompress, CompressionLevel.Default, true, this.ServerMaxWindowBits);
|
||||
decompressorDeflateStream.FlushMode = FlushType.Sync;
|
||||
}
|
||||
|
||||
if (decompressorOutputStream == null)
|
||||
decompressorOutputStream = new BufferPoolMemoryStream();
|
||||
decompressorOutputStream.SetLength(0);
|
||||
|
||||
byte[] copyBuffer = BufferPool.Get(1024, true);
|
||||
int readCount;
|
||||
while ((readCount = decompressorDeflateStream.Read(copyBuffer, 0, copyBuffer.Length)) != 0)
|
||||
decompressorOutputStream.Write(copyBuffer, 0, readCount);
|
||||
|
||||
BufferPool.Release(copyBuffer);
|
||||
|
||||
decompressorDeflateStream.SetLength(0);
|
||||
|
||||
var result = decompressorOutputStream.ToBufferSegment();
|
||||
|
||||
if (this.ServerNoContextTakeover)
|
||||
{
|
||||
decompressorDeflateStream.Dispose();
|
||||
decompressorDeflateStream = null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c6122052eb4822438f8d080c0f8574f
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d07681f5c8c854d42b668e7c6f0a8764
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,355 @@
|
||||
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
|
||||
|
||||
using System;
|
||||
using BestHTTP.Extensions;
|
||||
using BestHTTP.PlatformSupport.Memory;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BestHTTP.PlatformSupport.IL2CPP;
|
||||
using BestHTTP.Logger;
|
||||
|
||||
#if BESTHTTP_WITH_BURST
|
||||
using Unity.Burst;
|
||||
using Unity.Burst.Intrinsics;
|
||||
using static Unity.Burst.Intrinsics.X86.Avx2;
|
||||
using static Unity.Burst.Intrinsics.X86.Sse2;
|
||||
using static Unity.Burst.Intrinsics.Arm.Neon;
|
||||
#endif
|
||||
|
||||
namespace BestHTTP.WebSocket.Frames
|
||||
{
|
||||
/// <summary>
|
||||
/// Denotes a binary frame. The "Payload data" is arbitrary binary data whose interpretation is solely up to the application layer.
|
||||
/// This is the base class of all other frame writers, as all frame can be represented as a byte array.
|
||||
/// </summary>
|
||||
#if BESTHTTP_WITH_BURST
|
||||
[BurstCompile]
|
||||
#endif
|
||||
[Il2CppEagerStaticClassConstruction]
|
||||
public struct WebSocketFrame
|
||||
{
|
||||
public WebSocketFrameTypes Type { get; private set; }
|
||||
|
||||
public BufferSegment Data { get; private set; }
|
||||
|
||||
public WebSocket Websocket { get; private set; }
|
||||
|
||||
public byte Header;
|
||||
|
||||
public WebSocketFrame(WebSocket webSocket, WebSocketFrameTypes type, BufferSegment data)
|
||||
:this(webSocket, type, data, true)
|
||||
{ }
|
||||
|
||||
public WebSocketFrame(WebSocket webSocket, WebSocketFrameTypes type, BufferSegment data, bool useExtensions)
|
||||
: this(webSocket, type, data, true, useExtensions)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketFrame(WebSocket webSocket, WebSocketFrameTypes type, BufferSegment data, bool isFinal, bool useExtensions)
|
||||
:this(webSocket, type, data, isFinal, useExtensions, copyData: true)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public WebSocketFrame(WebSocket webSocket, WebSocketFrameTypes type, BufferSegment data, bool isFinal, bool useExtensions, bool copyData)
|
||||
{
|
||||
if (!isFinal)
|
||||
throw new NotSupportedException("isFinal parameter is not true");
|
||||
this.Type = type;
|
||||
this.Websocket = webSocket;
|
||||
this.Data = data;
|
||||
|
||||
if (this.Data.Data != null)
|
||||
{
|
||||
if (copyData)
|
||||
{
|
||||
var from = this.Data;
|
||||
|
||||
var buffer = BufferPool.Get(this.Data.Count, true);
|
||||
this.Data = new BufferSegment(buffer, 0, this.Data.Count);
|
||||
|
||||
Array.Copy(from.Data, (int)from.Offset, this.Data.Data, this.Data.Offset, this.Data.Count);
|
||||
}
|
||||
}
|
||||
else
|
||||
this.Data = BufferSegment.Empty;
|
||||
|
||||
// We use the header only for storing extension flags only
|
||||
this.Header = 0x00;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[WebSocketFrame Type: {0}, Header: {1:X2}, Data: {2}]", this.Type, this.Header, this.Data);
|
||||
}
|
||||
|
||||
public void WriteTo(Action<BufferSegment, BufferSegment> callback, uint maxFragmentSize, bool mask, LoggingContext context)
|
||||
{
|
||||
|
||||
|
||||
if (HTTPManager.Logger.Level <= Logger.Loglevels.All)
|
||||
HTTPManager.Logger.Verbose("WebSocketFrame", "WriteTo - Frame: " + ToString(), context);
|
||||
|
||||
if ((this.Type == WebSocketFrameTypes.Binary || this.Type == WebSocketFrameTypes.Text))
|
||||
{
|
||||
DoExtensions();
|
||||
if (this.Data.Count > maxFragmentSize)
|
||||
FragmentAndSend(callback, maxFragmentSize, mask, context);
|
||||
else
|
||||
WriteFragment(callback, (byte)(0x80 | this.Header | (byte)this.Type), this.Data, mask, context);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteFragment(callback, (byte)(0x80 | this.Header | (byte)this.Type), this.Data, mask, context);
|
||||
}
|
||||
}
|
||||
|
||||
private void DoExtensions()
|
||||
{
|
||||
if (this.Websocket != null && this.Websocket.Extensions != null)
|
||||
{
|
||||
for (int i = 0; i < this.Websocket.Extensions.Length; ++i)
|
||||
{
|
||||
var ext = this.Websocket.Extensions[i];
|
||||
if (ext != null)
|
||||
{
|
||||
this.Header |= ext.GetFrameHeader(this, this.Header);
|
||||
BufferSegment newData = ext.Encode(this);
|
||||
|
||||
if (newData != this.Data)
|
||||
{
|
||||
BufferPool.Release(this.Data);
|
||||
|
||||
this.Data = newData;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FragmentAndSend(Action<BufferSegment, BufferSegment> callback, uint maxFragmentSize, bool mask, LoggingContext context)
|
||||
{
|
||||
int pos = this.Data.Offset;
|
||||
int endPos = this.Data.Offset + this.Data.Count;
|
||||
|
||||
byte header = (byte)(0x00 | this.Header | (byte)this.Type);
|
||||
|
||||
while (pos < endPos)
|
||||
{
|
||||
int chunkLength = Math.Min((int)maxFragmentSize, endPos - pos);
|
||||
|
||||
WriteFragment(callback: callback,
|
||||
Header: header,
|
||||
Data: this.Data.Slice((int)pos, (int)chunkLength),
|
||||
mask: mask,
|
||||
context: context);
|
||||
|
||||
pos += chunkLength;
|
||||
|
||||
// set only the IsFinal flag, every other flags are zero
|
||||
header = (byte)(pos + chunkLength >= this.Data.Count ? 0x80 : 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void WriteFragment(Action<BufferSegment, BufferSegment> callback, byte Header, BufferSegment Data, bool mask, LoggingContext context)
|
||||
{
|
||||
// For the complete documentation for this section see:
|
||||
// http://tools.ietf.org/html/rfc6455#section-5.2
|
||||
|
||||
// Header(1) + Len(8) + Mask (4)
|
||||
byte[] wsHeader = BufferPool.Get(13, true);
|
||||
int pos = 0;
|
||||
|
||||
// Write the header
|
||||
wsHeader[pos++] = Header;
|
||||
|
||||
// The length of the "Payload data", in bytes: if 0-125, that is the payload length. If 126, the following 2 bytes interpreted as a
|
||||
// 16-bit unsigned integer are the payload length. If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the
|
||||
// most significant bit MUST be 0) are the payload length. Multibyte length quantities are expressed in network byte order.
|
||||
if (Data.Count < 126)
|
||||
{
|
||||
wsHeader[pos++] = (byte)(0x80 | (byte)Data.Count);
|
||||
}
|
||||
else if (Data.Count < UInt16.MaxValue)
|
||||
{
|
||||
wsHeader[pos++] = (byte)(0x80 | 126);
|
||||
var count = (UInt16)Data.Count;
|
||||
wsHeader[pos++] = (byte)(count >> 8);
|
||||
wsHeader[pos++] = (byte)(count);
|
||||
}
|
||||
else
|
||||
{
|
||||
wsHeader[pos++] = (byte)(0x80 | 127);
|
||||
|
||||
var count = (UInt64)Data.Count;
|
||||
wsHeader[pos++] = (byte)(count >> 56);
|
||||
wsHeader[pos++] = (byte)(count >> 48);
|
||||
wsHeader[pos++] = (byte)(count >> 40);
|
||||
wsHeader[pos++] = (byte)(count >> 32);
|
||||
wsHeader[pos++] = (byte)(count >> 24);
|
||||
wsHeader[pos++] = (byte)(count >> 16);
|
||||
wsHeader[pos++] = (byte)(count >> 8);
|
||||
wsHeader[pos++] = (byte)(count);
|
||||
}
|
||||
|
||||
if (Data != BufferSegment.Empty)
|
||||
{
|
||||
// All frames sent from the client to the server are masked by a 32-bit value that is contained within the frame. This field is
|
||||
// present if the mask bit is set to 1 and is absent if the mask bit is set to 0.
|
||||
// If the data is being sent by the client, the frame(s) MUST be masked.
|
||||
|
||||
uint hash = mask ? (uint)wsHeader.GetHashCode() : 0;
|
||||
|
||||
wsHeader[pos++] = (byte)(hash >> 24);
|
||||
wsHeader[pos++] = (byte)(hash >> 16);
|
||||
wsHeader[pos++] = (byte)(hash >> 8);
|
||||
wsHeader[pos++] = (byte)(hash);
|
||||
|
||||
// Do the masking.
|
||||
if (mask)
|
||||
{
|
||||
fixed (byte* pData = Data.Data/*, pmask = &wsHeader[pos - 4]*/)
|
||||
{
|
||||
byte* alignedMask = stackalloc byte[4];
|
||||
alignedMask[0] = wsHeader[pos - 4];
|
||||
alignedMask[1] = wsHeader[pos - 3];
|
||||
alignedMask[2] = wsHeader[pos - 2];
|
||||
alignedMask[3] = wsHeader[pos - 1];
|
||||
|
||||
ApplyMask(pData, Data.Offset, Data.Count, alignedMask);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
wsHeader[pos++] = 0;
|
||||
wsHeader[pos++] = 0;
|
||||
wsHeader[pos++] = 0;
|
||||
wsHeader[pos++] = 0;
|
||||
}
|
||||
|
||||
var header = wsHeader.AsBuffer(pos);
|
||||
|
||||
if (HTTPManager.Logger.Level <= Logger.Loglevels.All)
|
||||
HTTPManager.Logger.Verbose("WebSocketFrame", string.Format("WriteFragment - Header: {0}, data chunk: {1}", header.ToString(), Data.ToString()), context);
|
||||
|
||||
callback(header, Data);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
#if BESTHTTP_WITH_BURST
|
||||
[BurstCompile(CompileSynchronously = true)]
|
||||
#endif
|
||||
public unsafe static void ApplyMask(
|
||||
#if BESTHTTP_WITH_BURST
|
||||
[NoAlias]
|
||||
#endif
|
||||
byte* pData,
|
||||
int DataOffset,
|
||||
int DataCount,
|
||||
#if BESTHTTP_WITH_BURST
|
||||
[NoAlias]
|
||||
#endif
|
||||
byte* pmask
|
||||
)
|
||||
{
|
||||
int targetOffset = DataOffset + DataCount;
|
||||
uint umask = *(uint*)pmask;
|
||||
|
||||
#if BESTHTTP_WITH_BURST
|
||||
if (targetOffset - DataOffset >= 32)
|
||||
{
|
||||
if (IsAvx2Supported)
|
||||
{
|
||||
v256 mask = new v256(umask);
|
||||
v256 ldstrMask = new v256((byte)0xFF);
|
||||
|
||||
while (targetOffset - DataOffset >= 32)
|
||||
{
|
||||
// load data
|
||||
v256 data = mm256_maskload_epi32(pData + DataOffset, ldstrMask);
|
||||
|
||||
// xor
|
||||
v256 result = mm256_xor_si256(data, mask);
|
||||
|
||||
// store
|
||||
mm256_maskstore_epi32(pData + DataOffset, ldstrMask, result);
|
||||
|
||||
// advance
|
||||
DataOffset += 32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetOffset - DataOffset >= 16)
|
||||
{
|
||||
v128 mask = new v128(umask);
|
||||
|
||||
if (IsSse2Supported)
|
||||
{
|
||||
while (targetOffset - DataOffset >= 16)
|
||||
{
|
||||
// load data
|
||||
v128 data = loadu_si128(pData + DataOffset);
|
||||
|
||||
// xor
|
||||
var result = xor_si128(data, mask);
|
||||
|
||||
// store
|
||||
storeu_si128(pData + DataOffset, result);
|
||||
|
||||
// advance
|
||||
DataOffset += 16;
|
||||
}
|
||||
}
|
||||
else if (IsNeonSupported)
|
||||
{
|
||||
while (targetOffset - DataOffset >= 16)
|
||||
{
|
||||
// load data
|
||||
v128 data = vld1q_u8(pData + DataOffset);
|
||||
|
||||
// xor
|
||||
v128 result = veorq_u8(data, mask);
|
||||
|
||||
// store
|
||||
vst1q_u8(pData + DataOffset, result);
|
||||
|
||||
// advance
|
||||
DataOffset += 16;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// fallback to calculate by reinterpret-casting to ulong
|
||||
if (targetOffset - DataOffset >= 8)
|
||||
{
|
||||
ulong* ulpData = (ulong*)(pData + DataOffset);
|
||||
|
||||
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||
if ((long)ulpData % sizeof(ulong) == 0)
|
||||
{
|
||||
#endif
|
||||
// duplicate the mask to fill up a whole ulong.
|
||||
ulong ulmask = (((ulong)umask << 32) | umask);
|
||||
|
||||
while (targetOffset - DataOffset >= 8)
|
||||
{
|
||||
*ulpData = *ulpData ^ ulmask;
|
||||
|
||||
ulpData++;
|
||||
DataOffset += 8;
|
||||
}
|
||||
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// process remaining bytes (0..7)
|
||||
for (int i = DataOffset; i < targetOffset; ++i)
|
||||
pData[i] = (byte)(pData[i] ^ pmask[(i - DataOffset) % 4]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f674a60005f409438aa505311bf2e7a
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,220 @@
|
||||
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
using BestHTTP.Extensions;
|
||||
using BestHTTP.PlatformSupport.Memory;
|
||||
|
||||
namespace BestHTTP.WebSocket.Frames
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an incoming WebSocket Frame.
|
||||
/// </summary>
|
||||
public struct WebSocketFrameReader
|
||||
{
|
||||
#region Properties
|
||||
|
||||
public byte Header { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if it's a final Frame in a sequence, or the only one.
|
||||
/// </summary>
|
||||
public bool IsFinal { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of the Frame.
|
||||
/// </summary>
|
||||
public WebSocketFrameTypes Type { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The decoded array of bytes.
|
||||
/// </summary>
|
||||
public BufferSegment Data { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Textual representation of the received Data.
|
||||
/// </summary>
|
||||
public string DataAsText { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal & Private Functions
|
||||
|
||||
internal unsafe void Read(Stream stream)
|
||||
{
|
||||
// For the complete documentation for this section see:
|
||||
// http://tools.ietf.org/html/rfc6455#section-5.2
|
||||
|
||||
this.Header = ReadByte(stream);
|
||||
|
||||
// The first byte is the Final Bit and the type of the frame
|
||||
IsFinal = (this.Header & 0x80) != 0;
|
||||
Type = (WebSocketFrameTypes)(this.Header & 0xF);
|
||||
|
||||
byte maskAndLength = ReadByte(stream);
|
||||
|
||||
// The second byte is the Mask Bit and the length of the payload data
|
||||
if ((maskAndLength & 0x80) != 0)
|
||||
throw new NotImplementedException($"Payload from the server is masked!");
|
||||
|
||||
// if 0-125, that is the payload length.
|
||||
var length = (UInt64)(maskAndLength & 127);
|
||||
|
||||
// If 126, the following 2 bytes interpreted as a 16-bit unsigned integer are the payload length.
|
||||
if (length == 126)
|
||||
{
|
||||
byte[] rawLen = BufferPool.Get(2, true);
|
||||
|
||||
stream.ReadBuffer(rawLen, 2);
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(rawLen, 0, 2);
|
||||
|
||||
length = (UInt64)BitConverter.ToUInt16(rawLen, 0);
|
||||
|
||||
BufferPool.Release(rawLen);
|
||||
}
|
||||
else if (length == 127)
|
||||
{
|
||||
// If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the
|
||||
// most significant bit MUST be 0) are the payload length.
|
||||
|
||||
byte[] rawLen = BufferPool.Get(8, true);
|
||||
|
||||
stream.ReadBuffer(rawLen, 8);
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(rawLen, 0, 8);
|
||||
|
||||
length = (UInt64)BitConverter.ToUInt64(rawLen, 0);
|
||||
|
||||
BufferPool.Release(rawLen);
|
||||
}
|
||||
|
||||
if (length == 0L)
|
||||
{
|
||||
Data = BufferSegment.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
var buffer = BufferPool.Get((long)length, true);
|
||||
|
||||
uint readLength = 0;
|
||||
|
||||
try
|
||||
{
|
||||
do
|
||||
{
|
||||
int read = stream.Read(buffer, (int)readLength, (int)(length - readLength));
|
||||
|
||||
if (read <= 0)
|
||||
throw ExceptionHelper.ServerClosedTCPStream();
|
||||
|
||||
readLength += (uint)read;
|
||||
} while (readLength < length);
|
||||
}
|
||||
catch
|
||||
{
|
||||
BufferPool.Release(buffer);
|
||||
throw;
|
||||
}
|
||||
this.Data = new BufferSegment(buffer, 0, (int)length);
|
||||
}
|
||||
|
||||
private byte ReadByte(Stream stream)
|
||||
{
|
||||
int read = stream.ReadByte();
|
||||
|
||||
if (read < 0)
|
||||
throw ExceptionHelper.ServerClosedTCPStream();
|
||||
|
||||
return (byte)read;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Functions
|
||||
|
||||
/// <summary>
|
||||
/// Assembles all fragments into a final frame. Call this on the last fragment of a frame.
|
||||
/// </summary>
|
||||
/// <param name="fragments">The list of previously downloaded and parsed fragments of the frame</param>
|
||||
public void Assemble(List<WebSocketFrameReader> fragments)
|
||||
{
|
||||
// this way the following algorithms will handle this fragment's data too
|
||||
fragments.Add(this);
|
||||
|
||||
UInt64 finalLength = 0;
|
||||
for (int i = 0; i < fragments.Count; ++i)
|
||||
finalLength += (UInt64)fragments[i].Data.Count;
|
||||
|
||||
byte[] buffer = BufferPool.Get((long)finalLength, true);
|
||||
UInt64 pos = 0;
|
||||
for (int i = 0; i < fragments.Count; ++i)
|
||||
{
|
||||
if (fragments[i].Data.Count > 0)
|
||||
Array.Copy(fragments[i].Data.Data, fragments[i].Data.Offset, buffer, (int)pos, (int)fragments[i].Data.Count);
|
||||
fragments[i].ReleaseData();
|
||||
|
||||
pos += (UInt64)fragments[i].Data.Count;
|
||||
}
|
||||
|
||||
// All fragments of a message are of the same type, as set by the first fragment's opcode.
|
||||
this.Type = fragments[0].Type;
|
||||
|
||||
// Reserver flags may be contained only in the first fragment
|
||||
|
||||
this.Header = fragments[0].Header;
|
||||
this.Data = new BufferSegment(buffer, 0, (int)finalLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function will decode the received data incrementally with the associated websocket's extensions.
|
||||
/// </summary>
|
||||
public void DecodeWithExtensions(WebSocket webSocket)
|
||||
{
|
||||
if (webSocket.Extensions != null)
|
||||
for (int i = 0; i < webSocket.Extensions.Length; ++i)
|
||||
{
|
||||
var ext = webSocket.Extensions[i];
|
||||
if (ext != null)
|
||||
{
|
||||
var newData = ext.Decode(this.Header, this.Data);
|
||||
if (this.Data != newData)
|
||||
{
|
||||
this.ReleaseData();
|
||||
this.Data = newData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.Type == WebSocketFrameTypes.Text)
|
||||
{
|
||||
if (this.Data != BufferSegment.Empty)
|
||||
{
|
||||
this.DataAsText = System.Text.Encoding.UTF8.GetString(this.Data.Data, this.Data.Offset, this.Data.Count);
|
||||
this.ReleaseData();
|
||||
}
|
||||
else
|
||||
HTTPManager.Logger.Warning("WebSocketFrameReader", "Empty Text frame received!");
|
||||
}
|
||||
}
|
||||
|
||||
public void ReleaseData()
|
||||
{
|
||||
BufferPool.Release(this.Data);
|
||||
this.Data = BufferSegment.Empty;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0} Header: {1:X2}, IsFinal: {2}, Data: {3}]", this.Type.ToString(), this.Header, this.IsFinal, this.Data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 551161b05e7dd8c4d85aadf44c841056
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,48 @@
|
||||
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
|
||||
|
||||
namespace BestHTTP.WebSocket.Frames
|
||||
{
|
||||
public enum WebSocketFrameTypes : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// A fragmented message's first frame's contain the type of the message(binary or text), all consecutive frame of that message must be a Continuation frame.
|
||||
/// Last of these frame's Fin bit must be 1.
|
||||
/// </summary>
|
||||
/// <example>For a text message sent as three fragments, the first fragment would have an opcode of 0x1 (text) and a FIN bit clear,
|
||||
/// the second fragment would have an opcode of 0x0 (Continuation) and a FIN bit clear,
|
||||
/// and the third fragment would have an opcode of 0x0 (Continuation) and a FIN bit that is set.</example>
|
||||
Continuation = 0x0,
|
||||
Text = 0x1,
|
||||
Binary = 0x2,
|
||||
//Reserved1 = 0x3,
|
||||
//Reserved2 = 0x4,
|
||||
//Reserved3 = 0x5,
|
||||
//Reserved4 = 0x6,
|
||||
//Reserved5 = 0x7,
|
||||
|
||||
/// <summary>
|
||||
/// The Close frame MAY contain a body (the "Application data" portion of the frame) that indicates a reason for closing,
|
||||
/// such as an endpoint shutting down, an endpoint having received a frame too large, or an endpoint having received a frame that
|
||||
/// does not conform to the format expected by the endpoint.
|
||||
/// As the data is not guaranteed to be human readable, clients MUST NOT show it to end users.
|
||||
/// </summary>
|
||||
ConnectionClose = 0x8,
|
||||
|
||||
/// <summary>
|
||||
/// The Ping frame contains an opcode of 0x9. A Ping frame MAY include "Application data".
|
||||
/// </summary>
|
||||
Ping = 0x9,
|
||||
|
||||
/// <summary>
|
||||
/// A Pong frame sent in response to a Ping frame must have identical "Application data" as found in the message body of the Ping frame being replied to.
|
||||
/// </summary>
|
||||
Pong = 0xA,
|
||||
//Reserved6 = 0xB,
|
||||
//Reserved7 = 0xC,
|
||||
//Reserved8 = 0xD,
|
||||
//Reserved9 = 0xE,
|
||||
//Reserved10 = 0xF,
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 111b231c861908546b2c5acfa6d69bae
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2ac4a2034710f14c833fe68b7337340
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,364 @@
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_WEBSOCKET
|
||||
using System;
|
||||
|
||||
using BestHTTP.Connections;
|
||||
using BestHTTP.Extensions;
|
||||
using BestHTTP.PlatformSupport.Memory;
|
||||
using BestHTTP.WebSocket.Frames;
|
||||
|
||||
namespace BestHTTP.WebSocket
|
||||
{
|
||||
internal sealed class OverHTTP1 : WebSocketBaseImplementation
|
||||
{
|
||||
public override bool IsOpen => webSocket != null && !webSocket.IsClosed;
|
||||
|
||||
public override int BufferedAmount => webSocket.BufferedAmount;
|
||||
|
||||
public override int Latency => this.webSocket.Latency;
|
||||
public override DateTime LastMessageReceived => this.webSocket.lastMessage;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether we sent out the connection request to the server.
|
||||
/// </summary>
|
||||
private bool requestSent;
|
||||
|
||||
/// <summary>
|
||||
/// The internal WebSocketResponse object
|
||||
/// </summary>
|
||||
private WebSocketResponse webSocket;
|
||||
|
||||
public OverHTTP1(WebSocket parent, Uri uri, string origin, string protocol) : base(parent, uri, origin, protocol)
|
||||
{
|
||||
string scheme = HTTPProtocolFactory.IsSecureProtocol(uri) ? "wss" : "ws";
|
||||
int port = uri.Port != -1 ? uri.Port : (scheme.Equals("wss", StringComparison.OrdinalIgnoreCase) ? 443 : 80);
|
||||
|
||||
// Somehow if i use the UriBuilder it's not the same as if the uri is constructed from a string...
|
||||
//uri = new UriBuilder(uri.Scheme, uri.Host, uri.Scheme.Equals("wss", StringComparison.OrdinalIgnoreCase) ? 443 : 80, uri.PathAndQuery).Uri;
|
||||
base.Uri = new Uri(scheme + "://" + uri.Host + ":" + port + uri.GetRequestPathAndQueryURL());
|
||||
}
|
||||
|
||||
protected override void CreateInternalRequest()
|
||||
{
|
||||
if (this._internalRequest != null)
|
||||
return;
|
||||
|
||||
this._internalRequest = new HTTPRequest(base.Uri, OnInternalRequestCallback);
|
||||
|
||||
this._internalRequest.Context.Add("WebSocket", this.Parent.Context);
|
||||
|
||||
// Called when the regular GET request is successfully upgraded to WebSocket
|
||||
this._internalRequest.OnUpgraded = OnInternalRequestUpgraded;
|
||||
|
||||
//http://tools.ietf.org/html/rfc6455#section-4
|
||||
|
||||
// The request MUST contain an |Upgrade| header field whose value MUST include the "websocket" keyword.
|
||||
this._internalRequest.SetHeader("Upgrade", "websocket");
|
||||
|
||||
// The request MUST contain a |Connection| header field whose value MUST include the "Upgrade" token.
|
||||
this._internalRequest.SetHeader("Connection", "Upgrade");
|
||||
|
||||
// The request MUST include a header field with the name |Sec-WebSocket-Key|. The value of this header field MUST be a nonce consisting of a
|
||||
// randomly selected 16-byte value that has been base64-encoded (see Section 4 of [RFC4648]). The nonce MUST be selected randomly for each connection.
|
||||
this._internalRequest.SetHeader("Sec-WebSocket-Key", WebSocket.GetSecKey(new object[] { this, InternalRequest, base.Uri, new object() }));
|
||||
|
||||
// The request MUST include a header field with the name |Origin| [RFC6454] if the request is coming from a browser client.
|
||||
// If the connection is from a non-browser client, the request MAY include this header field if the semantics of that client match the use-case described here for browser clients.
|
||||
// More on Origin Considerations: http://tools.ietf.org/html/rfc6455#section-10.2
|
||||
if (!string.IsNullOrEmpty(Origin))
|
||||
this._internalRequest.SetHeader("Origin", Origin);
|
||||
|
||||
// The request MUST include a header field with the name |Sec-WebSocket-Version|. The value of this header field MUST be 13.
|
||||
this._internalRequest.SetHeader("Sec-WebSocket-Version", "13");
|
||||
|
||||
if (!string.IsNullOrEmpty(Protocol))
|
||||
this._internalRequest.SetHeader("Sec-WebSocket-Protocol", Protocol);
|
||||
|
||||
// Disable caching
|
||||
this._internalRequest.SetHeader("Cache-Control", "no-cache");
|
||||
this._internalRequest.SetHeader("Pragma", "no-cache");
|
||||
|
||||
#if !BESTHTTP_DISABLE_CACHING
|
||||
this._internalRequest.DisableCache = true;
|
||||
#endif
|
||||
|
||||
#if !BESTHTTP_DISABLE_PROXY && (!UNITY_WEBGL || UNITY_EDITOR)
|
||||
this._internalRequest.Proxy = this.Parent.GetProxy(this.Uri);
|
||||
#endif
|
||||
|
||||
this._internalRequest.OnBeforeRedirection += InternalRequest_OnBeforeRedirection;
|
||||
|
||||
if (this.Parent.OnInternalRequestCreated != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Parent.OnInternalRequestCreated(this.Parent, this._internalRequest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("OverHTTP1", "CreateInternalRequest", ex, this.Parent.Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool InternalRequest_OnBeforeRedirection(HTTPRequest originalRequest, HTTPResponse response, Uri redirectUri)
|
||||
{
|
||||
// We have to re-select/reset the implementation in the parent Websocket, as the redirected request might gets served over a HTTP/2 connection!
|
||||
this.Parent.SelectImplementation(redirectUri, originalRequest.GetFirstHeaderValue("Origin"), originalRequest.GetFirstHeaderValue("Sec-WebSocket-Protocol"))
|
||||
.StartOpen();
|
||||
|
||||
originalRequest.Callback = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void StartClose(UInt16 code, string message)
|
||||
{
|
||||
if (this.State == WebSocketStates.Connecting)
|
||||
{
|
||||
if (this.InternalRequest != null)
|
||||
this.InternalRequest.Abort();
|
||||
|
||||
this.State = WebSocketStates.Closed;
|
||||
if (this.Parent.OnClosed != null)
|
||||
this.Parent.OnClosed(this.Parent, (ushort)WebSocketStausCodes.NoStatusCode, string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.State = WebSocketStates.Closing;
|
||||
webSocket.Close(code, message);
|
||||
}
|
||||
}
|
||||
|
||||
public override void StartOpen()
|
||||
{
|
||||
if (requestSent)
|
||||
throw new InvalidOperationException("Open already called! You can't reuse this WebSocket instance!");
|
||||
|
||||
if (this.Parent.Extensions != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < this.Parent.Extensions.Length; ++i)
|
||||
{
|
||||
var ext = this.Parent.Extensions[i];
|
||||
if (ext != null)
|
||||
ext.AddNegotiation(InternalRequest);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("OverHTTP1", "Open", ex, this.Parent.Context);
|
||||
}
|
||||
}
|
||||
|
||||
InternalRequest.Send();
|
||||
requestSent = true;
|
||||
this.State = WebSocketStates.Connecting;
|
||||
}
|
||||
|
||||
private void OnInternalRequestCallback(HTTPRequest req, HTTPResponse resp)
|
||||
{
|
||||
string reason = string.Empty;
|
||||
|
||||
switch (req.State)
|
||||
{
|
||||
case HTTPRequestStates.Finished:
|
||||
HTTPManager.Logger.Information("OverHTTP1", string.Format("Request finished. Status Code: {0} Message: {1}", resp.StatusCode.ToString(), resp.Message), this.Parent.Context);
|
||||
|
||||
if (resp.StatusCode == 101)
|
||||
{
|
||||
// The request upgraded successfully.
|
||||
return;
|
||||
}
|
||||
else
|
||||
reason = string.Format("Request Finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2}",
|
||||
resp.StatusCode,
|
||||
resp.Message,
|
||||
resp.DataAsText);
|
||||
break;
|
||||
|
||||
// The request finished with an unexpected error. The request's Exception property may contain more info about the error.
|
||||
case HTTPRequestStates.Error:
|
||||
reason = "Request Finished with Error! " + (req.Exception != null ? ("Exception: " + req.Exception.Message + req.Exception.StackTrace) : string.Empty);
|
||||
break;
|
||||
|
||||
// The request aborted, initiated by the user.
|
||||
case HTTPRequestStates.Aborted:
|
||||
reason = "Request Aborted!";
|
||||
break;
|
||||
|
||||
// Connecting to the server is timed out.
|
||||
case HTTPRequestStates.ConnectionTimedOut:
|
||||
reason = "Connection Timed Out!";
|
||||
break;
|
||||
|
||||
// The request didn't finished in the given time.
|
||||
case HTTPRequestStates.TimedOut:
|
||||
reason = "Processing the request Timed Out!";
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.State != WebSocketStates.Connecting || !string.IsNullOrEmpty(reason))
|
||||
{
|
||||
if (this.Parent.OnError != null)
|
||||
this.Parent.OnError(this.Parent, reason);
|
||||
else if (!HTTPManager.IsQuitting)
|
||||
HTTPManager.Logger.Error("OverHTTP1", reason, this.Parent.Context);
|
||||
}
|
||||
else if (this.Parent.OnClosed != null)
|
||||
this.Parent.OnClosed(this.Parent, (ushort)WebSocketStausCodes.NormalClosure, "Closed while opening");
|
||||
|
||||
this.State = WebSocketStates.Closed;
|
||||
|
||||
if (!req.IsKeepAlive && resp != null && resp is WebSocketResponse)
|
||||
(resp as WebSocketResponse).CloseStream();
|
||||
}
|
||||
|
||||
private void OnInternalRequestUpgraded(HTTPRequest req, HTTPResponse resp)
|
||||
{
|
||||
HTTPManager.Logger.Information("OverHTTP1", "Internal request upgraded!", this.Parent.Context);
|
||||
|
||||
webSocket = resp as WebSocketResponse;
|
||||
|
||||
if (webSocket == null)
|
||||
{
|
||||
if (this.Parent.OnError != null)
|
||||
{
|
||||
string reason = string.Empty;
|
||||
if (req.Exception != null)
|
||||
reason = req.Exception.Message + " " + req.Exception.StackTrace;
|
||||
|
||||
this.Parent.OnError(this.Parent, reason);
|
||||
}
|
||||
|
||||
this.State = WebSocketStates.Closed;
|
||||
return;
|
||||
}
|
||||
|
||||
// If Close called while we connected
|
||||
if (this.State == WebSocketStates.Closed)
|
||||
{
|
||||
webSocket.CloseStream();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resp.HasHeader("sec-websocket-accept"))
|
||||
{
|
||||
this.State = WebSocketStates.Closed;
|
||||
webSocket.CloseStream();
|
||||
|
||||
if (this.Parent.OnError != null)
|
||||
this.Parent.OnError(this.Parent, "No Sec-Websocket-Accept header is sent by the server!");
|
||||
return;
|
||||
}
|
||||
|
||||
webSocket.WebSocket = this.Parent;
|
||||
|
||||
if (this.Parent.Extensions != null)
|
||||
{
|
||||
for (int i = 0; i < this.Parent.Extensions.Length; ++i)
|
||||
{
|
||||
var ext = this.Parent.Extensions[i];
|
||||
|
||||
try
|
||||
{
|
||||
if (ext != null && !ext.ParseNegotiation(webSocket))
|
||||
this.Parent.Extensions[i] = null; // Keep extensions only that successfully negotiated
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("OverHTTP1", "ParseNegotiation", ex, this.Parent.Context);
|
||||
|
||||
// Do not try to use a defective extension in the future
|
||||
this.Parent.Extensions[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.State = WebSocketStates.Open;
|
||||
if (this.Parent.OnOpen != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Parent.OnOpen(this.Parent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("OverHTTP1", "OnOpen", ex, this.Parent.Context);
|
||||
}
|
||||
}
|
||||
|
||||
webSocket.OnText = (ws, msg) =>
|
||||
{
|
||||
if (this.Parent.OnMessage != null)
|
||||
this.Parent.OnMessage(this.Parent, msg);
|
||||
};
|
||||
|
||||
webSocket.OnBinaryNoAlloc = (ws, frame) =>
|
||||
{
|
||||
if (this.Parent.OnBinary != null)
|
||||
{
|
||||
var bin = new byte[frame.Count];
|
||||
Array.Copy(frame.Data, 0, bin, 0, frame.Count);
|
||||
this.Parent.OnBinary(this.Parent, bin);
|
||||
}
|
||||
|
||||
if (this.Parent.OnBinaryNoAlloc != null)
|
||||
this.Parent.OnBinaryNoAlloc(this.Parent, frame);
|
||||
};
|
||||
|
||||
webSocket.OnClosed = (ws, code, msg) =>
|
||||
{
|
||||
this.State = WebSocketStates.Closed;
|
||||
|
||||
if (this.Parent.OnClosed != null)
|
||||
this.Parent.OnClosed(this.Parent, code, msg);
|
||||
};
|
||||
|
||||
if (this.Parent.OnIncompleteFrame != null)
|
||||
webSocket.OnIncompleteFrame = (ws, frame) =>
|
||||
{
|
||||
if (this.Parent.OnIncompleteFrame != null)
|
||||
this.Parent.OnIncompleteFrame(this.Parent, frame);
|
||||
};
|
||||
|
||||
if (this.Parent.StartPingThread)
|
||||
webSocket.StartPinging(Math.Max(this.Parent.PingFrequency, 100));
|
||||
|
||||
webSocket.StartReceive();
|
||||
}
|
||||
|
||||
public override void Send(string message)
|
||||
{
|
||||
webSocket.Send(message);
|
||||
}
|
||||
|
||||
public override void Send(byte[] buffer)
|
||||
{
|
||||
webSocket.Send(buffer);
|
||||
}
|
||||
|
||||
public override void Send(byte[] buffer, ulong offset, ulong count)
|
||||
{
|
||||
webSocket.Send(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override void SendAsBinary(BufferSegment data)
|
||||
{
|
||||
webSocket.Send(WebSocketFrameTypes.Binary, data);
|
||||
}
|
||||
|
||||
public override void SendAsText(BufferSegment data)
|
||||
{
|
||||
webSocket.Send(WebSocketFrameTypes.Text, data);
|
||||
}
|
||||
|
||||
public override void Send(WebSocketFrame frame)
|
||||
{
|
||||
webSocket.Send(frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 042433932184b3747ab2c9857b763ef7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,728 @@
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL && !BESTHTTP_DISABLE_HTTP2 && !BESTHTTP_DISABLE_WEBSOCKET
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using BestHTTP.Connections.HTTP2;
|
||||
using BestHTTP.Extensions;
|
||||
using BestHTTP.Logger;
|
||||
using BestHTTP.PlatformSupport.Memory;
|
||||
using BestHTTP.WebSocket.Frames;
|
||||
|
||||
namespace BestHTTP.WebSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements RFC 8441 (https://tools.ietf.org/html/rfc8441) to use Websocket over HTTP/2
|
||||
/// </summary>
|
||||
public sealed class OverHTTP2 : WebSocketBaseImplementation, IHeartbeat
|
||||
{
|
||||
public override int BufferedAmount { get => this._bufferedAmount; }
|
||||
internal volatile int _bufferedAmount;
|
||||
|
||||
public override bool IsOpen => this.State == WebSocketStates.Open;
|
||||
|
||||
public override int Latency { get { return this.Parent.StartPingThread ? base.Latency : (int)this.http2Handler.Latency; } }
|
||||
|
||||
private List<WebSocketFrameReader> IncompleteFrames = new List<WebSocketFrameReader>();
|
||||
private HTTP2Handler http2Handler;
|
||||
|
||||
/// <summary>
|
||||
/// True if we sent out a Close message to the server
|
||||
/// </summary>
|
||||
internal volatile bool closeSent;
|
||||
|
||||
/// <summary>
|
||||
/// When we sent out the last ping.
|
||||
/// </summary>
|
||||
private DateTime lastPing = DateTime.MinValue;
|
||||
|
||||
private bool waitingForPong = false;
|
||||
|
||||
/// <summary>
|
||||
/// A circular buffer to store the last N rtt times calculated by the pong messages.
|
||||
/// </summary>
|
||||
private CircularBuffer<int> rtts = new CircularBuffer<int>(WebSocketResponse.RTTBufferCapacity);
|
||||
|
||||
private PeekableIncomingSegmentStream incomingSegmentStream = new PeekableIncomingSegmentStream();
|
||||
private ConcurrentQueue<WebSocketFrameReader> CompletedFrames = new ConcurrentQueue<WebSocketFrameReader>();
|
||||
internal ConcurrentQueue<WebSocketFrame> frames = new ConcurrentQueue<WebSocketFrame>();
|
||||
|
||||
public OverHTTP2(WebSocket parent, Uri uri, string origin, string protocol) : base(parent, uri, origin, protocol)
|
||||
{
|
||||
// use https scheme so it will be served over HTTP/2. Thre request's Tag will be set to this class' instance so HTTP2Handler will know it has to create a HTTP2WebSocketStream instance to
|
||||
// process the request.
|
||||
string scheme = "https";
|
||||
int port = uri.Port != -1 ? uri.Port : 443;
|
||||
|
||||
base.Uri = new Uri(scheme + "://" + uri.Host + ":" + port + uri.GetRequestPathAndQueryURL());
|
||||
}
|
||||
|
||||
internal void SetHTTP2Handler(HTTP2Handler handler) => this.http2Handler = handler;
|
||||
|
||||
protected override void CreateInternalRequest()
|
||||
{
|
||||
HTTPManager.Logger.Verbose("OverHTTP2", "CreateInternalRequest", this.Parent.Context);
|
||||
|
||||
base._internalRequest = new HTTPRequest(base.Uri, HTTPMethods.Connect, OnInternalRequestCallback);
|
||||
base._internalRequest.Context.Add("WebSocket", this.Parent.Context);
|
||||
|
||||
base._internalRequest.SetHeader(":protocol", "websocket");
|
||||
|
||||
// The request MUST include a header field with the name |Sec-WebSocket-Key|. The value of this header field MUST be a nonce consisting of a
|
||||
// randomly selected 16-byte value that has been base64-encoded (see Section 4 of [RFC4648]). The nonce MUST be selected randomly for each connection.
|
||||
base._internalRequest.SetHeader("sec-webSocket-key", WebSocket.GetSecKey(new object[] { this, InternalRequest, base.Uri, new object() }));
|
||||
|
||||
// The request MUST include a header field with the name |Origin| [RFC6454] if the request is coming from a browser client.
|
||||
// If the connection is from a non-browser client, the request MAY include this header field if the semantics of that client match the use-case described here for browser clients.
|
||||
// More on Origin Considerations: http://tools.ietf.org/html/rfc6455#section-10.2
|
||||
if (!string.IsNullOrEmpty(base.Origin))
|
||||
base._internalRequest.SetHeader("origin", base.Origin);
|
||||
|
||||
// The request MUST include a header field with the name |Sec-WebSocket-Version|. The value of this header field MUST be 13.
|
||||
base._internalRequest.SetHeader("sec-webSocket-version", "13");
|
||||
|
||||
if (!string.IsNullOrEmpty(base.Protocol))
|
||||
base._internalRequest.SetHeader("sec-webSocket-protocol", base.Protocol);
|
||||
|
||||
// Disable caching
|
||||
base._internalRequest.SetHeader("cache-control", "no-cache");
|
||||
base._internalRequest.SetHeader("pragma", "no-cache");
|
||||
|
||||
#if !BESTHTTP_DISABLE_CACHING
|
||||
base._internalRequest.DisableCache = true;
|
||||
#endif
|
||||
|
||||
|
||||
base._internalRequest.OnHeadersReceived += OnHeadersReceived;
|
||||
|
||||
// set a fake upload stream, so HPACKEncoder will not set the END_STREAM flag
|
||||
base._internalRequest.UploadStream = new MemoryStream(0);
|
||||
base._internalRequest.UseUploadStreamLength = false;
|
||||
|
||||
this.LastMessageReceived = DateTime.Now;
|
||||
base._internalRequest.Tag = this;
|
||||
|
||||
if (this.Parent.OnInternalRequestCreated != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Parent.OnInternalRequestCreated(this.Parent, base._internalRequest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("OverHTTP2", "CreateInternalRequest", ex, this.Parent.Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHeadersReceived(HTTPRequest req, HTTPResponse resp, Dictionary<string, List<string>> newHeaders)
|
||||
{
|
||||
HTTPManager.Logger.Verbose("OverHTTP2", $"OnHeadersReceived - StatusCode: {resp?.StatusCode}", this.Parent.Context);
|
||||
|
||||
if (resp != null && resp.StatusCode == 200)
|
||||
{
|
||||
if (this.Parent.Extensions != null)
|
||||
{
|
||||
for (int i = 0; i < this.Parent.Extensions.Length; ++i)
|
||||
{
|
||||
var ext = this.Parent.Extensions[i];
|
||||
|
||||
try
|
||||
{
|
||||
if (ext != null && !ext.ParseNegotiation(resp))
|
||||
this.Parent.Extensions[i] = null; // Keep extensions only that successfully negotiated
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("OverHTTP2", "ParseNegotiation", ex, this.Parent.Context);
|
||||
|
||||
// Do not try to use a defective extension in the future
|
||||
this.Parent.Extensions[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.State = WebSocketStates.Open;
|
||||
|
||||
if (this.Parent.OnOpen != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Parent.OnOpen(this.Parent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("OverHTTP2", "OnOpen", ex, this.Parent.Context);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.Parent.StartPingThread)
|
||||
{
|
||||
this.LastMessageReceived = DateTime.Now;
|
||||
SendPing();
|
||||
}
|
||||
}
|
||||
else
|
||||
req.Abort();
|
||||
}
|
||||
|
||||
private static bool CanReadFullFrame(PeekableIncomingSegmentStream stream)
|
||||
{
|
||||
if (stream.Length < 2)
|
||||
return false;
|
||||
|
||||
stream.BeginPeek();
|
||||
|
||||
if (stream.PeekByte() == -1)
|
||||
return false;
|
||||
|
||||
int maskAndLength = stream.PeekByte();
|
||||
if (maskAndLength == -1)
|
||||
return false;
|
||||
|
||||
// The second byte is the Mask Bit and the length of the payload data
|
||||
var HasMask = (maskAndLength & 0x80) != 0;
|
||||
|
||||
// if 0-125, that is the payload length.
|
||||
var Length = (UInt64)(maskAndLength & 127);
|
||||
|
||||
// If 126, the following 2 bytes interpreted as a 16-bit unsigned integer are the payload length.
|
||||
if (Length == 126)
|
||||
{
|
||||
byte[] rawLen = BufferPool.Get(2, true);
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
int data = stream.PeekByte();
|
||||
if (data < 0)
|
||||
return false;
|
||||
|
||||
rawLen[i] = (byte)data;
|
||||
}
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(rawLen, 0, 2);
|
||||
|
||||
Length = (UInt64)BitConverter.ToUInt16(rawLen, 0);
|
||||
|
||||
BufferPool.Release(rawLen);
|
||||
}
|
||||
else if (Length == 127)
|
||||
{
|
||||
// If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the
|
||||
// most significant bit MUST be 0) are the payload length.
|
||||
|
||||
byte[] rawLen = BufferPool.Get(8, true);
|
||||
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
int data = stream.PeekByte();
|
||||
if (data < 0)
|
||||
return false;
|
||||
|
||||
rawLen[i] = (byte)data;
|
||||
}
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(rawLen, 0, 8);
|
||||
|
||||
Length = (UInt64)BitConverter.ToUInt64(rawLen, 0);
|
||||
|
||||
BufferPool.Release(rawLen);
|
||||
}
|
||||
|
||||
// Header + Mask&Length
|
||||
Length += 2;
|
||||
|
||||
// 4 bytes for Mask if present
|
||||
if (HasMask)
|
||||
Length += 4;
|
||||
|
||||
return stream.Length >= (long)Length;
|
||||
}
|
||||
|
||||
internal void OnReadThread(BufferSegment buffer)
|
||||
{
|
||||
this.LastMessageReceived = DateTime.Now;
|
||||
|
||||
this.incomingSegmentStream.Write(buffer);
|
||||
|
||||
while (CanReadFullFrame(this.incomingSegmentStream))
|
||||
{
|
||||
WebSocketFrameReader frame = new WebSocketFrameReader();
|
||||
frame.Read(this.incomingSegmentStream);
|
||||
|
||||
if (HTTPManager.Logger.Level == Logger.Loglevels.All)
|
||||
HTTPManager.Logger.Verbose("OverHTTP2", "Frame received: " + frame.ToString(), this.Parent.Context);
|
||||
|
||||
if (!frame.IsFinal)
|
||||
{
|
||||
if (this.Parent.OnIncompleteFrame == null)
|
||||
IncompleteFrames.Add(frame);
|
||||
else
|
||||
CompletedFrames.Enqueue(frame);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (frame.Type)
|
||||
{
|
||||
// For a complete documentation and rules on fragmentation see http://tools.ietf.org/html/rfc6455#section-5.4
|
||||
// A fragmented Frame's last fragment's opcode is 0 (Continuation) and the FIN bit is set to 1.
|
||||
case WebSocketFrameTypes.Continuation:
|
||||
// Do an assemble pass only if OnFragment is not set. Otherwise put it in the CompletedFrames, we will handle it in the HandleEvent phase.
|
||||
if (this.Parent.OnIncompleteFrame == null)
|
||||
{
|
||||
frame.Assemble(IncompleteFrames);
|
||||
|
||||
// Remove all incomplete frames
|
||||
IncompleteFrames.Clear();
|
||||
|
||||
// Control frames themselves MUST NOT be fragmented. So, its a normal text or binary frame. Go, handle it as usual.
|
||||
goto case WebSocketFrameTypes.Binary;
|
||||
}
|
||||
else
|
||||
{
|
||||
CompletedFrames.Enqueue(frame);
|
||||
}
|
||||
break;
|
||||
|
||||
case WebSocketFrameTypes.Text:
|
||||
case WebSocketFrameTypes.Binary:
|
||||
frame.DecodeWithExtensions(this.Parent);
|
||||
CompletedFrames.Enqueue(frame);
|
||||
break;
|
||||
|
||||
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in response, unless it already received a Close frame.
|
||||
case WebSocketFrameTypes.Ping:
|
||||
if (!closeSent && this.State != WebSocketStates.Closed)
|
||||
{
|
||||
// copy data set to true here, as the frame's data is released back to the pool after the switch
|
||||
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Pong, frame.Data, true, true, true));
|
||||
}
|
||||
break;
|
||||
|
||||
case WebSocketFrameTypes.Pong:
|
||||
// https://tools.ietf.org/html/rfc6455#section-5.5
|
||||
// A Pong frame MAY be sent unsolicited. This serves as a
|
||||
// unidirectional heartbeat. A response to an unsolicited Pong frame is
|
||||
// not expected.
|
||||
if (!waitingForPong)
|
||||
break;
|
||||
|
||||
waitingForPong = false;
|
||||
// the difference between the current time and the time when the ping message is sent
|
||||
TimeSpan diff = DateTime.Now - lastPing;
|
||||
|
||||
// add it to the buffer
|
||||
this.rtts.Add((int)diff.TotalMilliseconds);
|
||||
|
||||
// and calculate the new latency
|
||||
base.Latency = CalculateLatency();
|
||||
break;
|
||||
|
||||
// If an endpoint receives a Close frame and did not previously send a Close frame, the endpoint MUST send a Close frame in response.
|
||||
case WebSocketFrameTypes.ConnectionClose:
|
||||
HTTPManager.Logger.Information("OverHTTP2", "ConnectionClose packet received!", this.Parent.Context);
|
||||
|
||||
CompletedFrames.Enqueue(frame);
|
||||
|
||||
if (!closeSent)
|
||||
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.ConnectionClose, BufferSegment.Empty));
|
||||
|
||||
this.State = WebSocketStates.Closed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInternalRequestCallback(HTTPRequest req, HTTPResponse resp)
|
||||
{
|
||||
HTTPManager.Logger.Verbose("OverHTTP2", $"OnInternalRequestCallback - this.State: {this.State}", this.Parent.Context);
|
||||
|
||||
// If it's already closed, all events are called too.
|
||||
if (this.State == WebSocketStates.Closed)
|
||||
return;
|
||||
|
||||
if (this.State == WebSocketStates.Connecting && HTTPManager.HTTP2Settings.WebSocketOverHTTP2Settings.EnableImplementationFallback)
|
||||
{
|
||||
this.Parent.FallbackToHTTP1();
|
||||
return;
|
||||
}
|
||||
|
||||
string reason = string.Empty;
|
||||
|
||||
switch (req.State)
|
||||
{
|
||||
case HTTPRequestStates.Finished:
|
||||
HTTPManager.Logger.Information("OverHTTP2", string.Format("Request finished. Status Code: {0} Message: {1}", resp.StatusCode.ToString(), resp.Message), this.Parent.Context);
|
||||
|
||||
if (resp.StatusCode == 101)
|
||||
{
|
||||
// The request upgraded successfully.
|
||||
return;
|
||||
}
|
||||
else
|
||||
reason = string.Format("Request Finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2}",
|
||||
resp.StatusCode,
|
||||
resp.Message,
|
||||
resp.DataAsText);
|
||||
break;
|
||||
|
||||
// The request finished with an unexpected error. The request's Exception property may contain more info about the error.
|
||||
case HTTPRequestStates.Error:
|
||||
reason = "Request Finished with Error! " + (req.Exception != null ? ("Exception: " + req.Exception.Message + req.Exception.StackTrace) : string.Empty);
|
||||
break;
|
||||
|
||||
// The request aborted, initiated by the user.
|
||||
case HTTPRequestStates.Aborted:
|
||||
reason = "Request Aborted!";
|
||||
break;
|
||||
|
||||
// Connecting to the server is timed out.
|
||||
case HTTPRequestStates.ConnectionTimedOut:
|
||||
reason = "Connection Timed Out!";
|
||||
break;
|
||||
|
||||
// The request didn't finished in the given time.
|
||||
case HTTPRequestStates.TimedOut:
|
||||
reason = "Processing the request Timed Out!";
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.State != WebSocketStates.Connecting || !string.IsNullOrEmpty(reason))
|
||||
{
|
||||
if (this.Parent.OnError != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Parent.OnError(this.Parent, reason);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("OverHTTP2", "OnError", ex, this.Parent.Context);
|
||||
}
|
||||
}
|
||||
else if (!HTTPManager.IsQuitting)
|
||||
HTTPManager.Logger.Error("OverHTTP2", reason, this.Parent.Context);
|
||||
}
|
||||
else if (this.Parent.OnClosed != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Parent.OnClosed(this.Parent, (ushort)WebSocketStausCodes.NormalClosure, "Closed while opening");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("OverHTTP2", "OnClosed", ex, this.Parent.Context);
|
||||
}
|
||||
}
|
||||
|
||||
this.State = WebSocketStates.Closed;
|
||||
}
|
||||
|
||||
public override void StartOpen()
|
||||
{
|
||||
HTTPManager.Logger.Verbose("OverHTTP2", "StartOpen", this.Parent.Context);
|
||||
|
||||
if (this.Parent.Extensions != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < this.Parent.Extensions.Length; ++i)
|
||||
{
|
||||
var ext = this.Parent.Extensions[i];
|
||||
if (ext != null)
|
||||
ext.AddNegotiation(base.InternalRequest);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("OverHTTP2", "Open", ex, this.Parent.Context);
|
||||
}
|
||||
}
|
||||
|
||||
base.InternalRequest.Send();
|
||||
HTTPManager.Heartbeats.Subscribe(this);
|
||||
|
||||
this.State = WebSocketStates.Connecting;
|
||||
}
|
||||
|
||||
public override void StartClose(ushort code, string message)
|
||||
{
|
||||
HTTPManager.Logger.Verbose("OverHTTP2", "StartClose", this.Parent.Context);
|
||||
|
||||
if (this.State == WebSocketStates.Connecting)
|
||||
{
|
||||
if (this.InternalRequest != null)
|
||||
this.InternalRequest.Abort();
|
||||
|
||||
this.State = WebSocketStates.Closed;
|
||||
if (this.Parent.OnClosed != null)
|
||||
this.Parent.OnClosed(this.Parent, code, message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.ConnectionClose, WebSocket.EncodeCloseData(code, message), true, false, false));
|
||||
this.State = WebSocketStates.Closing;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Send(string message)
|
||||
{
|
||||
if (message == null)
|
||||
throw new ArgumentNullException("message must not be null!");
|
||||
|
||||
int count = System.Text.Encoding.UTF8.GetByteCount(message);
|
||||
byte[] data = BufferPool.Get(count, true);
|
||||
System.Text.Encoding.UTF8.GetBytes(message, 0, message.Length, data, 0);
|
||||
|
||||
SendAsText(data.AsBuffer(0, count));
|
||||
}
|
||||
|
||||
public override void Send(byte[] buffer)
|
||||
{
|
||||
if (buffer == null)
|
||||
throw new ArgumentNullException("data must not be null!");
|
||||
|
||||
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Binary, new BufferSegment(buffer, 0, buffer.Length)));
|
||||
}
|
||||
|
||||
public override void Send(byte[] data, ulong offset, ulong count)
|
||||
{
|
||||
if (data == null)
|
||||
throw new ArgumentNullException("data must not be null!");
|
||||
if (offset + count > (ulong)data.Length)
|
||||
throw new ArgumentOutOfRangeException("offset + count >= data.Length");
|
||||
|
||||
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Binary, new BufferSegment(data, (int)offset, (int)count), true, true));
|
||||
}
|
||||
|
||||
public override void Send(WebSocketFrame frame)
|
||||
{
|
||||
if (this.State == WebSocketStates.Closed || closeSent)
|
||||
return;
|
||||
|
||||
this.frames.Enqueue(frame);
|
||||
this.http2Handler.SignalRunnerThread();
|
||||
this._bufferedAmount += frame.Data.Count;
|
||||
|
||||
if (frame.Type == WebSocketFrameTypes.ConnectionClose)
|
||||
this.closeSent = true;
|
||||
}
|
||||
|
||||
public override void SendAsBinary(BufferSegment data)
|
||||
{
|
||||
Send(WebSocketFrameTypes.Binary, data);
|
||||
}
|
||||
|
||||
public override void SendAsText(BufferSegment data)
|
||||
{
|
||||
Send(WebSocketFrameTypes.Text, data);
|
||||
}
|
||||
|
||||
private void Send(WebSocketFrameTypes type, BufferSegment data)
|
||||
{
|
||||
Send(new WebSocketFrame(this.Parent, type, data, true, true, false));
|
||||
}
|
||||
|
||||
private int CalculateLatency()
|
||||
{
|
||||
if (this.rtts.Count == 0)
|
||||
return 0;
|
||||
|
||||
int sumLatency = 0;
|
||||
for (int i = 0; i < this.rtts.Count; ++i)
|
||||
sumLatency += this.rtts[i];
|
||||
|
||||
return sumLatency / this.rtts.Count;
|
||||
}
|
||||
|
||||
internal void PreReadCallback()
|
||||
{
|
||||
if (this.Parent.StartPingThread)
|
||||
{
|
||||
DateTime now = DateTime.Now;
|
||||
|
||||
if (!waitingForPong && now - LastMessageReceived >= TimeSpan.FromMilliseconds(this.Parent.PingFrequency))
|
||||
SendPing();
|
||||
|
||||
if (waitingForPong && now - lastPing > this.Parent.CloseAfterNoMessage)
|
||||
{
|
||||
if (this.State != WebSocketStates.Closed)
|
||||
{
|
||||
HTTPManager.Logger.Warning("OverHTTP2",
|
||||
string.Format("No message received in the given time! Closing WebSocket. LastPing: {0}, PingFrequency: {1}, Close After: {2}, Now: {3}",
|
||||
this.lastPing, TimeSpan.FromMilliseconds(this.Parent.PingFrequency), this.Parent.CloseAfterNoMessage, now), this.Parent.Context);
|
||||
|
||||
CloseWithError("No message received in the given time!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnHeartbeatUpdate(TimeSpan dif)
|
||||
{
|
||||
DateTime now = DateTime.Now;
|
||||
|
||||
switch (this.State)
|
||||
{
|
||||
case WebSocketStates.Connecting:
|
||||
if (now - this.InternalRequest.Timing.Start >= this.Parent.CloseAfterNoMessage)
|
||||
{
|
||||
if (HTTPManager.HTTP2Settings.WebSocketOverHTTP2Settings.EnableImplementationFallback)
|
||||
{
|
||||
this.State = WebSocketStates.Closed;
|
||||
this.InternalRequest.OnHeadersReceived = null;
|
||||
this.InternalRequest.Callback = null;
|
||||
this.Parent.FallbackToHTTP1();
|
||||
}
|
||||
else
|
||||
{
|
||||
CloseWithError("WebSocket Over HTTP/2 Implementation failed to connect in the given time!");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
while (CompletedFrames.TryDequeue(out var frame))
|
||||
{
|
||||
// Bugs in the clients shouldn't interrupt the code, so we need to try-catch and ignore any exception occurring here
|
||||
try
|
||||
{
|
||||
switch (frame.Type)
|
||||
{
|
||||
case WebSocketFrameTypes.Continuation:
|
||||
if (HTTPManager.Logger.Level == Loglevels.All)
|
||||
HTTPManager.Logger.Verbose("OverHTTP2", "HandleEvents - OnIncompleteFrame", this.Parent.Context);
|
||||
if (this.Parent.OnIncompleteFrame != null)
|
||||
this.Parent.OnIncompleteFrame(this.Parent, frame);
|
||||
break;
|
||||
|
||||
case WebSocketFrameTypes.Text:
|
||||
// Any not Final frame is handled as a fragment
|
||||
if (!frame.IsFinal)
|
||||
goto case WebSocketFrameTypes.Continuation;
|
||||
|
||||
if (HTTPManager.Logger.Level == Loglevels.All)
|
||||
HTTPManager.Logger.Verbose("OverHTTP2", $"HandleEvents - OnText(\"{frame.DataAsText}\")", this.Parent.Context);
|
||||
|
||||
if (this.Parent.OnMessage != null)
|
||||
this.Parent.OnMessage(this.Parent, frame.DataAsText);
|
||||
break;
|
||||
|
||||
case WebSocketFrameTypes.Binary:
|
||||
// Any not Final frame is handled as a fragment
|
||||
if (!frame.IsFinal)
|
||||
goto case WebSocketFrameTypes.Continuation;
|
||||
|
||||
if (HTTPManager.Logger.Level == Loglevels.All)
|
||||
HTTPManager.Logger.Verbose("OverHTTP2", $"HandleEvents - OnBinary({frame.Data})", this.Parent.Context);
|
||||
|
||||
if (this.Parent.OnBinary != null)
|
||||
{
|
||||
var data = new byte[frame.Data.Count];
|
||||
Array.Copy(frame.Data.Data, frame.Data.Offset, data, 0, frame.Data.Count);
|
||||
this.Parent.OnBinary(this.Parent, data);
|
||||
}
|
||||
|
||||
if (this.Parent.OnBinaryNoAlloc != null)
|
||||
this.Parent.OnBinaryNoAlloc(this.Parent, frame.Data);
|
||||
break;
|
||||
|
||||
case WebSocketFrameTypes.ConnectionClose:
|
||||
HTTPManager.Logger.Verbose("OverHTTP2", "HandleEvents - Calling OnClosed", this.Parent.Context);
|
||||
if (this.Parent.OnClosed != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
UInt16 statusCode = 0;
|
||||
string msg = string.Empty;
|
||||
|
||||
// If we received any data, we will get the status code and the message from it
|
||||
if (/*CloseFrame != null && */ frame.Data != BufferSegment.Empty && frame.Data.Count >= 2)
|
||||
{
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(frame.Data.Data, frame.Data.Offset, 2);
|
||||
statusCode = BitConverter.ToUInt16(frame.Data.Data, frame.Data.Offset);
|
||||
|
||||
if (frame.Data.Count > 2)
|
||||
msg = Encoding.UTF8.GetString(frame.Data.Data, frame.Data.Offset + 2, frame.Data.Count - 2);
|
||||
|
||||
frame.ReleaseData();
|
||||
}
|
||||
|
||||
this.Parent.OnClosed(this.Parent, statusCode, msg);
|
||||
this.Parent.OnClosed = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("OverHTTP2", "HandleEvents - OnClosed", ex, this.Parent.Context);
|
||||
}
|
||||
}
|
||||
|
||||
HTTPManager.Heartbeats.Unsubscribe(this);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("OverHTTP2", string.Format("HandleEvents({0})", frame.ToString()), ex, this.Parent.Context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
frame.ReleaseData();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Next interaction relative to *now*.
|
||||
/// </summary>
|
||||
public TimeSpan GetNextInteraction()
|
||||
{
|
||||
if (waitingForPong)
|
||||
return TimeSpan.MaxValue;
|
||||
|
||||
return (LastMessageReceived + TimeSpan.FromMilliseconds(this.Parent.PingFrequency)) - DateTime.Now;
|
||||
}
|
||||
|
||||
private void SendPing()
|
||||
{
|
||||
HTTPManager.Logger.Information("OverHTTP2", "Sending Ping frame, waiting for a pong...", this.Parent.Context);
|
||||
|
||||
lastPing = DateTime.Now;
|
||||
waitingForPong = true;
|
||||
|
||||
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Ping, BufferSegment.Empty));
|
||||
}
|
||||
|
||||
private void CloseWithError(string message)
|
||||
{
|
||||
HTTPManager.Logger.Verbose("OverHTTP2", $"CloseWithError(\"{message}\")", this.Parent.Context);
|
||||
|
||||
this.State = WebSocketStates.Closed;
|
||||
|
||||
if (this.Parent.OnError != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Parent.OnError(this.Parent, message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("OverHTTP2", "OnError", ex, this.Parent.Context);
|
||||
}
|
||||
}
|
||||
|
||||
this.InternalRequest.Abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aadf012d28ce0f3458e08598d3b745f6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9b2365dee16ce748bf7849f48d85410
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,60 @@
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL && !BESTHTTP_DISABLE_HTTP2 && !BESTHTTP_DISABLE_WEBSOCKET
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
using BestHTTP.Extensions;
|
||||
using BestHTTP.PlatformSupport.Memory;
|
||||
|
||||
namespace BestHTTP.WebSocket.Implementations.Utils
|
||||
{
|
||||
public sealed class LockedBufferSegmenStream : BufferSegmentStream
|
||||
{
|
||||
public bool IsClosed { get; private set; }
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
lock (base.bufferList)
|
||||
{
|
||||
if (this.IsClosed && base.bufferList.Count == 0)
|
||||
return 0;
|
||||
|
||||
int sumReadCount = base.Read(buffer, offset, count);
|
||||
|
||||
return sumReadCount == 0 ? -1 : sumReadCount;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(BufferSegment bufferSegment)
|
||||
{
|
||||
lock (base.bufferList)
|
||||
{
|
||||
if (this.IsClosed)
|
||||
return;
|
||||
|
||||
base.Write(bufferSegment);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
lock (base.bufferList)
|
||||
{
|
||||
base.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
this.IsClosed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 311f1dc3057a76e45964b3b63c4a5e79
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,270 @@
|
||||
#if (UNITY_WEBGL && !UNITY_EDITOR) && !BESTHTTP_DISABLE_WEBSOCKET
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using BestHTTP.PlatformSupport.Memory;
|
||||
|
||||
namespace BestHTTP.WebSocket
|
||||
{
|
||||
delegate void OnWebGLWebSocketOpenDelegate(uint id);
|
||||
delegate void OnWebGLWebSocketTextDelegate(uint id, string text);
|
||||
delegate void OnWebGLWebSocketBinaryDelegate(uint id, IntPtr pBuffer, int length);
|
||||
delegate void OnWebGLWebSocketErrorDelegate(uint id, string error);
|
||||
delegate void OnWebGLWebSocketCloseDelegate(uint id, int code, string reason);
|
||||
|
||||
internal sealed class WebGLBrowser : WebSocketBaseImplementation
|
||||
{
|
||||
public override WebSocketStates State => ImplementationId != 0 ? WS_GetState(ImplementationId) : WebSocketStates.Unknown;
|
||||
|
||||
public override bool IsOpen => ImplementationId != 0 && WS_GetState(ImplementationId) == WebSocketStates.Open;
|
||||
public override int BufferedAmount => WS_GetBufferedAmount(ImplementationId);
|
||||
|
||||
internal static Dictionary<uint, WebSocket> WebSockets = new Dictionary<uint, WebSocket>();
|
||||
|
||||
private uint ImplementationId;
|
||||
|
||||
public WebGLBrowser(WebSocket parent, Uri uri, string origin, string protocol) : base(parent, uri, origin, protocol)
|
||||
{
|
||||
}
|
||||
|
||||
public override void StartOpen()
|
||||
{
|
||||
try
|
||||
{
|
||||
ImplementationId = WS_Create(this.Uri.OriginalString, this.Protocol, OnOpenCallback, OnTextCallback, OnBinaryCallback, OnErrorCallback, OnCloseCallback);
|
||||
WebSockets.Add(ImplementationId, this.Parent);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("WebSocket", "Open", ex, this.Parent.Context);
|
||||
}
|
||||
}
|
||||
|
||||
public override void StartClose(UInt16 code, string message)
|
||||
{
|
||||
WS_Close(this.ImplementationId, code, message);
|
||||
}
|
||||
|
||||
public override void Send(string message)
|
||||
{
|
||||
var count = System.Text.Encoding.UTF8.GetByteCount(message);
|
||||
var buffer = BufferPool.Get(count, true);
|
||||
|
||||
System.Text.Encoding.UTF8.GetBytes(message, 0, message.Length, buffer, 0);
|
||||
|
||||
WS_Send_String(this.ImplementationId, buffer, 0, count);
|
||||
|
||||
BufferPool.Release(buffer);
|
||||
}
|
||||
|
||||
public override void Send(byte[] buffer)
|
||||
{
|
||||
WS_Send_Binary(this.ImplementationId, buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
public override void Send(byte[] buffer, ulong offset, ulong count)
|
||||
{
|
||||
WS_Send_Binary(this.ImplementationId, buffer, (int)offset, (int)count);
|
||||
}
|
||||
|
||||
public override void SendAsBinary(BufferSegment data)
|
||||
{
|
||||
WS_Send_Binary(this.ImplementationId, data.Data, data.Offset, data.Count);
|
||||
BufferPool.Release(data);
|
||||
}
|
||||
|
||||
public override void SendAsText(BufferSegment data)
|
||||
{
|
||||
WS_Send_String(this.ImplementationId, data.Data, data.Offset, data.Count);
|
||||
BufferPool.Release(data);
|
||||
}
|
||||
|
||||
[DllImport("__Internal")]
|
||||
static extern uint WS_Create(string url, string protocol, OnWebGLWebSocketOpenDelegate onOpen, OnWebGLWebSocketTextDelegate onText, OnWebGLWebSocketBinaryDelegate onBinary, OnWebGLWebSocketErrorDelegate onError, OnWebGLWebSocketCloseDelegate onClose);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
static extern WebSocketStates WS_GetState(uint id);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
static extern int WS_GetBufferedAmount(uint id);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
static extern int WS_Send_String(uint id, byte[] strData, int pos, int length);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
static extern int WS_Send_Binary(uint id, byte[] buffer, int pos, int length);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
static extern void WS_Close(uint id, ushort code, string reason);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
static extern void WS_Release(uint id);
|
||||
|
||||
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketOpenDelegate))]
|
||||
static void OnOpenCallback(uint id)
|
||||
{
|
||||
WebSocket ws;
|
||||
if (WebSockets.TryGetValue(id, out ws))
|
||||
{
|
||||
if (ws.OnOpen != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
ws.OnOpen(ws);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("WebSocket", "OnOpen", ex, ws.Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
HTTPManager.Logger.Warning("WebSocket", "OnOpenCallback - No WebSocket found for id: " + id.ToString(), ws.Context);
|
||||
}
|
||||
|
||||
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketTextDelegate))]
|
||||
static void OnTextCallback(uint id, string text)
|
||||
{
|
||||
WebSocket ws;
|
||||
if (WebSockets.TryGetValue(id, out ws))
|
||||
{
|
||||
if (ws.OnMessage != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
ws.OnMessage(ws, text);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("WebSocket", "OnMessage", ex, ws.Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
HTTPManager.Logger.Warning("WebSocket", "OnTextCallback - No WebSocket found for id: " + id.ToString());
|
||||
}
|
||||
|
||||
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketBinaryDelegate))]
|
||||
static void OnBinaryCallback(uint id, IntPtr pBuffer, int length)
|
||||
{
|
||||
WebSocket ws;
|
||||
if (WebSockets.TryGetValue(id, out ws))
|
||||
{
|
||||
if (ws.OnBinary != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] buffer = new byte[length];
|
||||
|
||||
// Copy data from the 'unmanaged' memory to managed memory. Buffer will be reclaimed by the GC.
|
||||
Marshal.Copy(pBuffer, buffer, 0, length);
|
||||
|
||||
ws.OnBinary(ws, buffer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("WebSocket", "OnBinary", ex, ws.Context);
|
||||
}
|
||||
}
|
||||
|
||||
if (ws.OnBinaryNoAlloc != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] buffer = BufferPool.Get(length, true);
|
||||
|
||||
// We still have to do a copy here, but at least the buffer will be released back to the pool.
|
||||
// Copy data from the 'unmanaged' memory to managed memory. Buffer will be reclaimed by the GC.
|
||||
Marshal.Copy(pBuffer, buffer, 0, length);
|
||||
|
||||
ws.OnBinaryNoAlloc(ws, new BufferSegment(buffer, 0, length));
|
||||
|
||||
BufferPool.Release(buffer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("WebSocket", "OnBinary", ex, ws.Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
HTTPManager.Logger.Warning("WebSocket", "OnBinaryCallback - No WebSocket found for id: " + id.ToString());
|
||||
}
|
||||
|
||||
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketErrorDelegate))]
|
||||
static void OnErrorCallback(uint id, string error)
|
||||
{
|
||||
WebSocket ws;
|
||||
if (WebSockets.TryGetValue(id, out ws))
|
||||
{
|
||||
WebSockets.Remove(id);
|
||||
|
||||
if (ws.OnError != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
ws.OnError(ws, error);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("WebSocket", "OnError", ex, ws.Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
HTTPManager.Logger.Warning("WebSocket", "OnErrorCallback - No WebSocket found for id: " + id.ToString());
|
||||
|
||||
try
|
||||
{
|
||||
WS_Release(id);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("WebSocket", "WS_Release", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketCloseDelegate))]
|
||||
static void OnCloseCallback(uint id, int code, string reason)
|
||||
{
|
||||
// To match non-webgl behavior, we have to treat this client-side generated message as an error
|
||||
if (code == (int)WebSocketStausCodes.ClosedAbnormally)
|
||||
{
|
||||
OnErrorCallback(id, "Abnormal disconnection.");
|
||||
return;
|
||||
}
|
||||
|
||||
WebSocket ws;
|
||||
if (WebSockets.TryGetValue(id, out ws))
|
||||
{
|
||||
WebSockets.Remove(id);
|
||||
|
||||
if (ws.OnClosed != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
ws.OnClosed(ws, (ushort)code, reason);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("WebSocket", "OnClosed", ex, ws.Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
HTTPManager.Logger.Warning("WebSocket", "OnCloseCallback - No WebSocket found for id: " + id.ToString());
|
||||
|
||||
try
|
||||
{
|
||||
WS_Release(id);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("WebSocket", "WS_Release", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f992398667c95b4f8694562f4bd37e5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,98 @@
|
||||
#if !BESTHTTP_DISABLE_WEBSOCKET
|
||||
using System;
|
||||
|
||||
using BestHTTP.PlatformSupport.Memory;
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
using BestHTTP.WebSocket.Frames;
|
||||
#endif
|
||||
|
||||
namespace BestHTTP.WebSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// States of the underlying implementation's state.
|
||||
/// </summary>
|
||||
public enum WebSocketStates : byte
|
||||
{
|
||||
Connecting = 0,
|
||||
Open = 1,
|
||||
Closing = 2,
|
||||
Closed = 3,
|
||||
Unknown
|
||||
};
|
||||
|
||||
public delegate void OnWebSocketOpenDelegate(WebSocket webSocket);
|
||||
public delegate void OnWebSocketMessageDelegate(WebSocket webSocket, string message);
|
||||
public delegate void OnWebSocketBinaryDelegate(WebSocket webSocket, byte[] data);
|
||||
public delegate void OnWebSocketBinaryNoAllocDelegate(WebSocket webSocket, BufferSegment data);
|
||||
public delegate void OnWebSocketClosedDelegate(WebSocket webSocket, UInt16 code, string message);
|
||||
public delegate void OnWebSocketErrorDelegate(WebSocket webSocket, string reason);
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
public delegate void OnWebSocketIncompleteFrameDelegate(WebSocket webSocket, WebSocketFrameReader frame);
|
||||
#endif
|
||||
|
||||
public abstract class WebSocketBaseImplementation
|
||||
{
|
||||
public virtual WebSocketStates State { get; protected set; }
|
||||
public virtual bool IsOpen { get; protected set; }
|
||||
public virtual int BufferedAmount { get; protected set; }
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
public HTTPRequest InternalRequest
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this._internalRequest == null)
|
||||
CreateInternalRequest();
|
||||
|
||||
return this._internalRequest;
|
||||
}
|
||||
}
|
||||
protected HTTPRequest _internalRequest;
|
||||
|
||||
public virtual int Latency { get; protected set; }
|
||||
public virtual DateTime LastMessageReceived { get; protected set; }
|
||||
#endif
|
||||
|
||||
public WebSocket Parent { get; }
|
||||
public Uri Uri { get; protected set; }
|
||||
public string Origin { get; }
|
||||
public string Protocol { get; }
|
||||
|
||||
public WebSocketBaseImplementation(WebSocket parent, Uri uri, string origin, string protocol)
|
||||
{
|
||||
this.Parent = parent;
|
||||
this.Uri = uri;
|
||||
this.Origin = origin;
|
||||
this.Protocol = protocol;
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
this.LastMessageReceived = DateTime.MinValue;
|
||||
|
||||
// Set up some default values.
|
||||
this.Parent.PingFrequency = 10_000;
|
||||
this.Parent.CloseAfterNoMessage = TimeSpan.FromSeconds(2);
|
||||
#endif
|
||||
}
|
||||
|
||||
public abstract void StartOpen();
|
||||
public abstract void StartClose(UInt16 code, string message);
|
||||
|
||||
public abstract void Send(string message);
|
||||
public abstract void Send(byte[] buffer);
|
||||
public abstract void Send(byte[] buffer, ulong offset, ulong count);
|
||||
public abstract void SendAsBinary(BufferSegment data);
|
||||
public abstract void SendAsText(BufferSegment data);
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
protected abstract void CreateInternalRequest();
|
||||
|
||||
/// <summary>
|
||||
/// It will send the given frame to the server.
|
||||
/// </summary>
|
||||
public abstract void Send(WebSocketFrame frame);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba79e3bbef722f24881197bc223ded5c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
416
JNFrame2/Assets/Plugins/Best HTTP/Source/WebSocket/WebSocket.cs
Normal file
416
JNFrame2/Assets/Plugins/Best HTTP/Source/WebSocket/WebSocket.cs
Normal file
@@ -0,0 +1,416 @@
|
||||
#if !BESTHTTP_DISABLE_WEBSOCKET
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using BestHTTP.Extensions;
|
||||
using BestHTTP.Connections;
|
||||
using BestHTTP.Logger;
|
||||
using BestHTTP.PlatformSupport.Memory;
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
using BestHTTP.WebSocket.Frames;
|
||||
using BestHTTP.WebSocket.Extensions;
|
||||
#endif
|
||||
|
||||
namespace BestHTTP.WebSocket
|
||||
{
|
||||
public sealed class WebSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum payload size of a websocket frame. Its default value is 32 KiB.
|
||||
/// </summary>
|
||||
public static uint MaxFragmentSize = UInt16.MaxValue / 2;
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
public static IExtension[] GetDefaultExtensions()
|
||||
{
|
||||
#if !BESTHTTP_DISABLE_GZIP
|
||||
return new IExtension[] { new PerMessageCompression(/*compression level: */ Decompression.Zlib.CompressionLevel.Default,
|
||||
/*clientNoContextTakeover: */ false,
|
||||
/*serverNoContextTakeover: */ false,
|
||||
/*clientMaxWindowBits: */ Decompression.Zlib.ZlibConstants.WindowBitsMax,
|
||||
/*desiredServerMaxWindowBits: */ Decompression.Zlib.ZlibConstants.WindowBitsMax,
|
||||
/*minDatalengthToCompress: */ PerMessageCompression.MinDataLengthToCompressDefault) };
|
||||
#else
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
public WebSocketStates State { get { return this.implementation.State; } }
|
||||
|
||||
/// <summary>
|
||||
/// The connection to the WebSocket server is open.
|
||||
/// </summary>
|
||||
public bool IsOpen { get { return this.implementation.IsOpen; } }
|
||||
|
||||
/// <summary>
|
||||
/// Data waiting to be written to the wire.
|
||||
/// </summary>
|
||||
public int BufferedAmount { get { return this.implementation.BufferedAmount; } }
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Set to true to start a new thread to send Pings to the WebSocket server
|
||||
/// </summary>
|
||||
public bool StartPingThread { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The delay between two Pings in milliseconds. Minimum value is 100ms, default is 10 seconds.
|
||||
/// </summary>
|
||||
public int PingFrequency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If StartPingThread set to true, the plugin will close the connection and emit an OnError event if no
|
||||
/// message is received from the server in the given time. Its default value is 2 sec.
|
||||
/// </summary>
|
||||
public TimeSpan CloseAfterNoMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The internal HTTPRequest object.
|
||||
/// </summary>
|
||||
public HTTPRequest InternalRequest { get { return this.implementation.InternalRequest; } }
|
||||
|
||||
/// <summary>
|
||||
/// IExtension implementations the plugin will negotiate with the server to use.
|
||||
/// </summary>
|
||||
public IExtension[] Extensions { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Latency calculated from the ping-pong message round-trip times.
|
||||
/// </summary>
|
||||
public int Latency { get { return this.implementation.Latency; } }
|
||||
|
||||
/// <summary>
|
||||
/// When we received the last message from the server.
|
||||
/// </summary>
|
||||
public DateTime LastMessageReceived { get { return this.implementation.LastMessageReceived; } }
|
||||
|
||||
/// <summary>
|
||||
/// When the Websocket Over HTTP/2 implementation fails to connect and EnableImplementationFallback is true, the plugin tries to fall back to the HTTP/1 implementation.
|
||||
/// When this happens a new InternalRequest is created and all previous custom modifications (like added headers) are lost. With OnInternalRequestCreated these modifications can be reapplied.
|
||||
/// </summary>
|
||||
public Action<WebSocket, HTTPRequest> OnInternalRequestCreated;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Called when the connection to the WebSocket server is established.
|
||||
/// </summary>
|
||||
public OnWebSocketOpenDelegate OnOpen;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a new textual message is received from the server.
|
||||
/// </summary>
|
||||
public OnWebSocketMessageDelegate OnMessage;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a new binary message is received from the server.
|
||||
/// </summary>
|
||||
public OnWebSocketBinaryDelegate OnBinary;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Binary message received. It's a more performant version than the OnBinary event, as the memory will be reused.
|
||||
/// </summary>
|
||||
public OnWebSocketBinaryNoAllocDelegate OnBinaryNoAlloc;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the WebSocket connection is closed.
|
||||
/// </summary>
|
||||
public OnWebSocketClosedDelegate OnClosed;
|
||||
|
||||
/// <summary>
|
||||
/// Called when an error is encountered. The parameter will be the description of the error.
|
||||
/// </summary>
|
||||
public OnWebSocketErrorDelegate OnError;
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// Called when an incomplete frame received. No attempt will be made to reassemble these fragments internally, and no reference are stored after this event to this frame.
|
||||
/// </summary>
|
||||
public OnWebSocketIncompleteFrameDelegate OnIncompleteFrame;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Logging context of this websocket instance.
|
||||
/// </summary>
|
||||
public LoggingContext Context { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The underlying, real implementation.
|
||||
/// </summary>
|
||||
private WebSocketBaseImplementation implementation;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a WebSocket instance from the given uri.
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri of the WebSocket server</param>
|
||||
public WebSocket(Uri uri)
|
||||
:this(uri, string.Empty, string.Empty)
|
||||
{
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR)
|
||||
this.Extensions = WebSocket.GetDefaultExtensions();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
public WebSocket(Uri uri, string origin, string protocol)
|
||||
:this(uri, origin, protocol, null)
|
||||
{
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR)
|
||||
this.Extensions = WebSocket.GetDefaultExtensions();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Creates a WebSocket instance from the given uri, protocol and origin.
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri of the WebSocket server</param>
|
||||
/// <param name="origin">Servers that are not intended to process input from any web page but only for certain sites SHOULD verify the |Origin| field is an origin they expect.
|
||||
/// If the origin indicated is unacceptable to the server, then it SHOULD respond to the WebSocket handshake with a reply containing HTTP 403 Forbidden status code.</param>
|
||||
/// <param name="protocol">The application-level protocol that the client want to use(eg. "chat", "leaderboard", etc.). Can be null or empty string if not used.</param>
|
||||
/// <param name="extensions">Optional IExtensions implementations</param>
|
||||
public WebSocket(Uri uri, string origin, string protocol
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
, params IExtension[] extensions
|
||||
#endif
|
||||
)
|
||||
|
||||
{
|
||||
this.Context = new LoggingContext(this);
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
this.Extensions = extensions;
|
||||
#endif
|
||||
|
||||
SelectImplementation(uri, origin, protocol);
|
||||
|
||||
// Under WebGL when only the WebSocket protocol is used Setup() isn't called, so we have to call it here.
|
||||
HTTPManager.Setup();
|
||||
}
|
||||
|
||||
internal WebSocketBaseImplementation SelectImplementation(Uri uri, string origin, string protocol)
|
||||
{
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
#if !BESTHTTP_DISABLE_ALTERNATE_SSL && !BESTHTTP_DISABLE_HTTP2
|
||||
if (HTTPManager.HTTP2Settings.WebSocketOverHTTP2Settings.EnableWebSocketOverHTTP2 && HTTPProtocolFactory.IsSecureProtocol(uri))
|
||||
{
|
||||
// Try to find a HTTP/2 connection that supports the connect protocol.
|
||||
var connectionKey = Core.HostDefinition.GetKeyFor(new UriBuilder("https", uri.Host, uri.Port).Uri
|
||||
#if !BESTHTTP_DISABLE_PROXY && (!UNITY_WEBGL || UNITY_EDITOR)
|
||||
, GetProxy(uri)
|
||||
#endif
|
||||
);
|
||||
|
||||
var con = BestHTTP.Core.HostManager.GetHost(uri.Host).GetHostDefinition(connectionKey).Find(c => {
|
||||
var httpConnection = c as HTTPConnection;
|
||||
var http2Handler = httpConnection?.requestHandler as Connections.HTTP2.HTTP2Handler;
|
||||
|
||||
return http2Handler != null && http2Handler.settings.RemoteSettings[Connections.HTTP2.HTTP2Settings.ENABLE_CONNECT_PROTOCOL] != 0;
|
||||
});
|
||||
|
||||
if (con != null)
|
||||
{
|
||||
HTTPManager.Logger.Information("WebSocket", "Connection with enabled Connect Protocol found!", this.Context);
|
||||
|
||||
var httpConnection = con as HTTPConnection;
|
||||
var http2Handler = httpConnection?.requestHandler as Connections.HTTP2.HTTP2Handler;
|
||||
|
||||
this.implementation = new OverHTTP2(this, uri, origin, protocol);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (this.implementation == null)
|
||||
this.implementation = new OverHTTP1(this, uri, origin, protocol);
|
||||
#else
|
||||
this.implementation = new WebGLBrowser(this, uri, origin, protocol);
|
||||
#endif
|
||||
|
||||
return this.implementation;
|
||||
}
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
internal void FallbackToHTTP1()
|
||||
{
|
||||
HTTPManager.Logger.Verbose("WebSocket", "FallbackToHTTP1", this.Context);
|
||||
|
||||
if (this.implementation == null)
|
||||
return;
|
||||
|
||||
this.implementation = new OverHTTP1(this, this.implementation.Uri, this.implementation.Origin, this.implementation.Protocol);
|
||||
this.implementation.StartOpen();
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Start the opening process.
|
||||
/// </summary>
|
||||
public void Open()
|
||||
{
|
||||
this.implementation.StartOpen();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It will send the given message to the server in one frame.
|
||||
/// </summary>
|
||||
public void Send(string message)
|
||||
{
|
||||
if (!IsOpen)
|
||||
return;
|
||||
|
||||
this.implementation.Send(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It will send the given data to the server in one frame.
|
||||
/// </summary>
|
||||
public void Send(byte[] buffer)
|
||||
{
|
||||
if (!IsOpen)
|
||||
return;
|
||||
|
||||
this.implementation.Send(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will send count bytes from a byte array, starting from offset.
|
||||
/// </summary>
|
||||
public void Send(byte[] buffer, ulong offset, ulong count)
|
||||
{
|
||||
if (!IsOpen)
|
||||
return;
|
||||
|
||||
this.implementation.Send(buffer, offset, count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will send the data in one or more binary frame and takes ownership over it calling BufferPool.Release when sent.
|
||||
/// </summary>
|
||||
public void SendAsBinary(BufferSegment data)
|
||||
{
|
||||
if (!IsOpen)
|
||||
{
|
||||
BufferPool.Release(data);
|
||||
return;
|
||||
}
|
||||
|
||||
this.implementation.SendAsBinary(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will send data as a text frame and takes owenership over the memory region releasing it to the BufferPool as soon as possible.
|
||||
/// </summary>
|
||||
public void SendAsText(BufferSegment data)
|
||||
{
|
||||
if (!IsOpen)
|
||||
{
|
||||
BufferPool.Release(data);
|
||||
return;
|
||||
}
|
||||
|
||||
this.implementation.SendAsText(data);
|
||||
}
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// It will send the given frame to the server.
|
||||
/// </summary>
|
||||
public void Send(WebSocketFrame frame)
|
||||
{
|
||||
if (!IsOpen)
|
||||
return;
|
||||
|
||||
this.implementation.Send(frame);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// It will initiate the closing of the connection to the server.
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
if (State >= WebSocketStates.Closing)
|
||||
return;
|
||||
|
||||
this.implementation.StartClose(1000, "Bye!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It will initiate the closing of the connection to the server sending the given code and message.
|
||||
/// </summary>
|
||||
public void Close(UInt16 code, string message)
|
||||
{
|
||||
if (!IsOpen)
|
||||
return;
|
||||
|
||||
this.implementation.StartClose(code, message);
|
||||
}
|
||||
|
||||
#if !BESTHTTP_DISABLE_PROXY && (!UNITY_WEBGL || UNITY_EDITOR)
|
||||
internal Proxy GetProxy(Uri uri)
|
||||
{
|
||||
// WebSocket is not a request-response based protocol, so we need a 'tunnel' through the proxy
|
||||
HTTPProxy proxy = HTTPManager.Proxy as HTTPProxy;
|
||||
if (proxy != null && proxy.UseProxyForAddress(uri))
|
||||
proxy = new HTTPProxy(proxy.Address,
|
||||
proxy.Credentials,
|
||||
false, /*turn on 'tunneling'*/
|
||||
false, /*sendWholeUri*/
|
||||
proxy.NonTransparentForHTTPS);
|
||||
|
||||
return proxy;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
|
||||
public static BufferSegment EncodeCloseData(UInt16 code, string message)
|
||||
{
|
||||
//If there is a body, the first two bytes of the body MUST be a 2-byte unsigned integer
|
||||
// (in network byte order) representing a status code with value /code/ defined in Section 7.4 (http://tools.ietf.org/html/rfc6455#section-7.4). Following the 2-byte integer,
|
||||
// the body MAY contain UTF-8-encoded data with value /reason/, the interpretation of which is not defined by this specification.
|
||||
// This data is not necessarily human readable but may be useful for debugging or passing information relevant to the script that opened the connection.
|
||||
int msgLen = Encoding.UTF8.GetByteCount(message);
|
||||
using (var ms = new BufferPoolMemoryStream(2 + msgLen))
|
||||
{
|
||||
byte[] buff = BitConverter.GetBytes(code);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(buff, 0, buff.Length);
|
||||
|
||||
ms.Write(buff, 0, buff.Length);
|
||||
|
||||
buff = Encoding.UTF8.GetBytes(message);
|
||||
ms.Write(buff, 0, buff.Length);
|
||||
|
||||
buff = ms.ToArray();
|
||||
|
||||
return buff.AsBuffer(buff.Length);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string GetSecKey(object[] from)
|
||||
{
|
||||
const int keysLength = 16;
|
||||
byte[] keys = BufferPool.Get(keysLength, true);
|
||||
int pos = 0;
|
||||
|
||||
for (int i = 0; i < from.Length; ++i)
|
||||
{
|
||||
byte[] hash = BitConverter.GetBytes((Int32)from[i].GetHashCode());
|
||||
|
||||
for (int cv = 0; cv < hash.Length && pos < keysLength; ++cv)
|
||||
keys[pos++] = hash[cv];
|
||||
}
|
||||
|
||||
var result = Convert.ToBase64String(keys, 0, keysLength);
|
||||
BufferPool.Release(keys);
|
||||
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32e0cd2913acd794aa3fc7d414fe78e4
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,708 @@
|
||||
#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
|
||||
using BestHTTP.Extensions;
|
||||
using BestHTTP.WebSocket.Frames;
|
||||
using BestHTTP.Core;
|
||||
using BestHTTP.PlatformSupport.Memory;
|
||||
using BestHTTP.Logger;
|
||||
using BestHTTP.Connections;
|
||||
|
||||
namespace BestHTTP.WebSocket
|
||||
{
|
||||
public sealed class WebSocketResponse : HTTPResponse, IProtocol
|
||||
{
|
||||
/// <summary>
|
||||
/// Capacity of the RTT buffer where the latencies are kept.
|
||||
/// </summary>
|
||||
public static int RTTBufferCapacity = 5;
|
||||
|
||||
#region Public Interface
|
||||
|
||||
/// <summary>
|
||||
/// A reference to the original WebSocket instance. Used for accessing extensions.
|
||||
/// </summary>
|
||||
public WebSocket WebSocket { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Text message received
|
||||
/// </summary>
|
||||
public Action<WebSocketResponse, string> OnText;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Binary message received
|
||||
/// </summary>
|
||||
public Action<WebSocketResponse, byte[]> OnBinary;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Binary message received. It's a more performant version than the OnBinary event, as the memory will be reused.
|
||||
/// </summary>
|
||||
public Action<WebSocketResponse, BufferSegment> OnBinaryNoAlloc;
|
||||
|
||||
/// <summary>
|
||||
/// Called when an incomplete frame received. No attempt will be made to reassemble these fragments.
|
||||
/// </summary>
|
||||
public Action<WebSocketResponse, WebSocketFrameReader> OnIncompleteFrame;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the connection closed.
|
||||
/// </summary>
|
||||
public Action<WebSocketResponse, UInt16, string> OnClosed;
|
||||
|
||||
/// <summary>
|
||||
/// IProtocol's ConnectionKey property.
|
||||
/// </summary>
|
||||
public HostConnectionKey ConnectionKey { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the connection to the server is closed or not.
|
||||
/// </summary>
|
||||
public bool IsClosed { get { return closed; } }
|
||||
|
||||
/// <summary>
|
||||
/// IProtocol.LoggingContext implementation.
|
||||
/// </summary>
|
||||
LoggingContext IProtocol.LoggingContext { get => this.Context; }
|
||||
|
||||
/// <summary>
|
||||
/// On what frequency we have to send a ping to the server.
|
||||
/// </summary>
|
||||
public TimeSpan PingFrequnecy { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum size of a fragment's payload data. Its default value is WebSocket.MaxFragmentSize's value.
|
||||
/// </summary>
|
||||
public uint MaxFragmentSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Length of unsent, buffered up data in bytes.
|
||||
/// </summary>
|
||||
public int BufferedAmount { get { return this._bufferedAmount; } }
|
||||
private int _bufferedAmount;
|
||||
|
||||
/// <summary>
|
||||
/// Calculated latency from the Round-Trip Times we store in the rtts field.
|
||||
/// </summary>
|
||||
public int Latency { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// When we received the last frame.
|
||||
/// </summary>
|
||||
public DateTime lastMessage = DateTime.MinValue;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
|
||||
private List<WebSocketFrameReader> IncompleteFrames = new List<WebSocketFrameReader>();
|
||||
private ConcurrentQueue<WebSocketFrameReader> CompletedFrames = new ConcurrentQueue<WebSocketFrameReader>();
|
||||
private WebSocketFrameReader CloseFrame;
|
||||
|
||||
private ConcurrentQueue<WebSocketFrame> unsentFrames = new ConcurrentQueue<WebSocketFrame>();
|
||||
private volatile AutoResetEvent newFrameSignal = new AutoResetEvent(false);
|
||||
private int sendThreadCreated = 0;
|
||||
private int closedThreads = 0;
|
||||
|
||||
/// <summary>
|
||||
/// True if we sent out a Close message to the server
|
||||
/// </summary>
|
||||
private volatile bool closeSent;
|
||||
|
||||
/// <summary>
|
||||
/// True if this WebSocket connection is closed
|
||||
/// </summary>
|
||||
private volatile bool closed;
|
||||
|
||||
/// <summary>
|
||||
/// When we sent out the last ping.
|
||||
/// </summary>
|
||||
private DateTime lastPing = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// True if waiting for an answer to our ping request. Ping timeout is used only why waitingForPong is true.
|
||||
/// </summary>
|
||||
private volatile bool waitingForPong = false;
|
||||
|
||||
/// <summary>
|
||||
/// A circular buffer to store the last N rtt times calculated by the pong messages.
|
||||
/// </summary>
|
||||
private CircularBuffer<int> rtts = new CircularBuffer<int>(WebSocketResponse.RTTBufferCapacity);
|
||||
|
||||
#endregion
|
||||
|
||||
internal WebSocketResponse(HTTPRequest request, Stream stream, bool isStreamed, bool isFromCache)
|
||||
: base(request, stream, isStreamed, isFromCache)
|
||||
{
|
||||
base.IsClosedManually = true;
|
||||
this.ConnectionKey = new HostConnectionKey(this.baseRequest.CurrentUri.Host, HostDefinition.GetKeyForRequest(this.baseRequest));
|
||||
|
||||
closed = false;
|
||||
MaxFragmentSize = WebSocket.MaxFragmentSize;
|
||||
}
|
||||
|
||||
internal void StartReceive()
|
||||
{
|
||||
if (IsUpgraded)
|
||||
BestHTTP.PlatformSupport.Threading.ThreadedRunner.RunLongLiving(ReceiveThreadFunc);
|
||||
}
|
||||
|
||||
internal void CloseStream()
|
||||
{
|
||||
if (base.Stream != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
base.Stream.Dispose();
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
}
|
||||
}
|
||||
|
||||
#region Public interface for interacting with the server
|
||||
|
||||
/// <summary>
|
||||
/// It will send the given message to the server in one frame.
|
||||
/// </summary>
|
||||
public void Send(string message)
|
||||
{
|
||||
if (message == null)
|
||||
throw new ArgumentNullException("message must not be null!");
|
||||
|
||||
int count = System.Text.Encoding.UTF8.GetByteCount(message);
|
||||
byte[] data = BufferPool.Get(count, true);
|
||||
System.Text.Encoding.UTF8.GetBytes(message, 0, message.Length, data, 0);
|
||||
|
||||
Send(WebSocketFrameTypes.Text, data.AsBuffer(count));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It will send the given data to the server in one frame.
|
||||
/// </summary>
|
||||
public void Send(byte[] data)
|
||||
{
|
||||
if (data == null)
|
||||
throw new ArgumentNullException("data must not be null!");
|
||||
|
||||
WebSocketFrame frame = new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.Binary, new BufferSegment(data, 0, data.Length));
|
||||
Send(frame);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will send count bytes from a byte array, starting from offset.
|
||||
/// </summary>
|
||||
public void Send(byte[] data, ulong offset, ulong count)
|
||||
{
|
||||
if (data == null)
|
||||
throw new ArgumentNullException("data must not be null!");
|
||||
if (offset + count > (ulong)data.Length)
|
||||
throw new ArgumentOutOfRangeException("offset + count >= data.Length");
|
||||
|
||||
WebSocketFrame frame = new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.Binary, new BufferSegment(data, (int)offset, (int)count), true, true);
|
||||
Send(frame);
|
||||
}
|
||||
|
||||
public void Send(WebSocketFrameTypes type, BufferSegment data)
|
||||
{
|
||||
WebSocketFrame frame = new WebSocketFrame(this.WebSocket, type, data, true, true, false);
|
||||
Send(frame);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It will send the given frame to the server.
|
||||
/// </summary>
|
||||
public void Send(WebSocketFrame frame)
|
||||
{
|
||||
if (closed || closeSent)
|
||||
return;
|
||||
|
||||
this.unsentFrames.Enqueue(frame);
|
||||
|
||||
if (Interlocked.CompareExchange(ref this.sendThreadCreated, 1, 0) == 0)
|
||||
{
|
||||
HTTPManager.Logger.Information("WebSocketResponse", "Send - Creating thread", this.Context);
|
||||
|
||||
BestHTTP.PlatformSupport.Threading.ThreadedRunner.RunLongLiving(SendThreadFunc);
|
||||
}
|
||||
|
||||
Interlocked.Add(ref this._bufferedAmount, frame.Data.Count);
|
||||
|
||||
//if (HTTPManager.Logger.Level <= Logger.Loglevels.All)
|
||||
// HTTPManager.Logger.Information("WebSocketResponse", "Signaling SendThread!", this.Context);
|
||||
|
||||
newFrameSignal.Set();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It will initiate the closing of the connection to the server.
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
Close(1000, "Bye!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It will initiate the closing of the connection to the server.
|
||||
/// </summary>
|
||||
public void Close(UInt16 code, string msg)
|
||||
{
|
||||
if (closed)
|
||||
return;
|
||||
|
||||
HTTPManager.Logger.Verbose("WebSocketResponse", string.Format("Close({0}, \"{1}\")", code, msg), this.Context);
|
||||
|
||||
WebSocketFrame frame;
|
||||
while (this.unsentFrames.TryDequeue(out frame))
|
||||
{
|
||||
if (frame.Data.Data != null)
|
||||
{
|
||||
BufferPool.Release(frame.Data);
|
||||
Interlocked.Add(ref this._bufferedAmount, -frame.Data.Count);
|
||||
}
|
||||
}
|
||||
|
||||
Send(new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.ConnectionClose, WebSocket.EncodeCloseData(code, msg)));
|
||||
}
|
||||
|
||||
public void StartPinging(int frequency)
|
||||
{
|
||||
if (frequency < 100)
|
||||
throw new ArgumentException("frequency must be at least 100 milliseconds!");
|
||||
|
||||
PingFrequnecy = TimeSpan.FromMilliseconds(frequency);
|
||||
lastMessage = DateTime.UtcNow;
|
||||
|
||||
SendPing();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Threading Functions
|
||||
|
||||
private void SendThreadFunc()
|
||||
{
|
||||
PlatformSupport.Threading.ThreadedRunner.SetThreadName("BestHTTP.WebSocket Send");
|
||||
|
||||
try
|
||||
{
|
||||
bool mask = !HTTPProtocolFactory.IsSecureProtocol(this.baseRequest.CurrentUri);
|
||||
using (WriteOnlyBufferedStream bufferedStream = new WriteOnlyBufferedStream(this.Stream, 16 * 1024))
|
||||
{
|
||||
while (!closed && !closeSent)
|
||||
{
|
||||
//if (HTTPManager.Logger.Level <= Logger.Loglevels.All)
|
||||
// HTTPManager.Logger.Information("WebSocketResponse", "SendThread - Waiting...", this.Context);
|
||||
|
||||
TimeSpan waitTime = TimeSpan.FromMilliseconds(int.MaxValue);
|
||||
|
||||
if (this.PingFrequnecy != TimeSpan.Zero)
|
||||
{
|
||||
DateTime now = DateTime.UtcNow;
|
||||
waitTime = lastMessage + PingFrequnecy - now;
|
||||
|
||||
if (waitTime <= TimeSpan.Zero)
|
||||
{
|
||||
if (!waitingForPong && now - lastMessage >= PingFrequnecy)
|
||||
{
|
||||
if (!SendPing())
|
||||
continue;
|
||||
}
|
||||
|
||||
waitTime = PingFrequnecy;
|
||||
}
|
||||
|
||||
if (waitingForPong && now - lastPing > this.WebSocket.CloseAfterNoMessage)
|
||||
{
|
||||
HTTPManager.Logger.Warning("WebSocketResponse",
|
||||
string.Format("No message received in the given time! Closing WebSocket. LastPing: {0}, PingFrequency: {1}, Close After: {2}, Now: {3}",
|
||||
this.lastPing, this.PingFrequnecy, this.WebSocket.CloseAfterNoMessage, now), this.Context);
|
||||
|
||||
CloseWithError(HTTPRequestStates.Error, "No message received in the given time!");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
newFrameSignal.WaitOne(waitTime);
|
||||
|
||||
try
|
||||
{
|
||||
//if (HTTPManager.Logger.Level <= Logger.Loglevels.All)
|
||||
// HTTPManager.Logger.Information("WebSocketResponse", "SendThread - Wait is over, about " + this.unsentFrames.Count.ToString() + " new frames!", this.Context);
|
||||
|
||||
WebSocketFrame frame;
|
||||
while (this.unsentFrames.TryDequeue(out frame))
|
||||
{
|
||||
// save data count as per-message deflate can compress, and it would be different after calling WriteTo
|
||||
int originalFrameDataLength = frame.Data.Count;
|
||||
|
||||
if (!closeSent)
|
||||
{
|
||||
frame.WriteTo((header, chunk) =>
|
||||
{
|
||||
bufferedStream.Write(header.Data, header.Offset, header.Count);
|
||||
BufferPool.Release(header);
|
||||
|
||||
if (chunk != BufferSegment.Empty)
|
||||
bufferedStream.Write(chunk.Data, chunk.Offset, chunk.Count);
|
||||
}, MaxFragmentSize, mask, this.Context);
|
||||
BufferPool.Release(frame.Data);
|
||||
|
||||
if (frame.Type == WebSocketFrameTypes.ConnectionClose)
|
||||
closeSent = true;
|
||||
}
|
||||
|
||||
Interlocked.Add(ref this._bufferedAmount, -originalFrameDataLength);
|
||||
}
|
||||
|
||||
bufferedStream.Flush();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (HTTPUpdateDelegator.IsCreated)
|
||||
{
|
||||
this.baseRequest.Exception = ex;
|
||||
this.baseRequest.State = HTTPRequestStates.Error;
|
||||
}
|
||||
else
|
||||
this.baseRequest.State = HTTPRequestStates.Aborted;
|
||||
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
HTTPManager.Logger.Information("WebSocketResponse", string.Format("Ending Send thread. Closed: {0}, closeSent: {1}", closed, closeSent), this.Context);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (HTTPManager.Logger.Level == Loglevels.All)
|
||||
HTTPManager.Logger.Exception("WebSocketResponse", "SendThread", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref sendThreadCreated, 0);
|
||||
|
||||
HTTPManager.Logger.Information("WebSocketResponse", "SendThread - Closed!", this.Context);
|
||||
|
||||
TryToCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
private void ReceiveThreadFunc()
|
||||
{
|
||||
PlatformSupport.Threading.ThreadedRunner.SetThreadName("BestHTTP.WebSocket Receive");
|
||||
|
||||
try
|
||||
{
|
||||
while (!closed)
|
||||
{
|
||||
try
|
||||
{
|
||||
WebSocketFrameReader frame = new WebSocketFrameReader();
|
||||
frame.Read(this.Stream);
|
||||
|
||||
if (HTTPManager.Logger.Level == Logger.Loglevels.All)
|
||||
HTTPManager.Logger.Information("WebSocketResponse", "Frame received: " + frame.ToString(), this.Context);
|
||||
|
||||
lastMessage = DateTime.UtcNow;
|
||||
|
||||
if (!frame.IsFinal)
|
||||
{
|
||||
if (OnIncompleteFrame == null)
|
||||
IncompleteFrames.Add(frame);
|
||||
else
|
||||
CompletedFrames.Enqueue(frame);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (frame.Type)
|
||||
{
|
||||
// For a complete documentation and rules on fragmentation see http://tools.ietf.org/html/rfc6455#section-5.4
|
||||
// A fragmented Frame's last fragment's opcode is 0 (Continuation) and the FIN bit is set to 1.
|
||||
case WebSocketFrameTypes.Continuation:
|
||||
// Do an assemble pass only if OnFragment is not set. Otherwise put it in the CompletedFrames, we will handle it in the HandleEvent phase.
|
||||
if (OnIncompleteFrame == null)
|
||||
{
|
||||
frame.Assemble(IncompleteFrames);
|
||||
|
||||
// Remove all incomplete frames
|
||||
IncompleteFrames.Clear();
|
||||
|
||||
// Control frames themselves MUST NOT be fragmented. So, its a normal text or binary frame. Go, handle it as usual.
|
||||
goto case WebSocketFrameTypes.Binary;
|
||||
}
|
||||
else
|
||||
{
|
||||
CompletedFrames.Enqueue(frame);
|
||||
ProtocolEventHelper.EnqueueProtocolEvent(new ProtocolEventInfo(this));
|
||||
}
|
||||
break;
|
||||
|
||||
case WebSocketFrameTypes.Text:
|
||||
case WebSocketFrameTypes.Binary:
|
||||
frame.DecodeWithExtensions(WebSocket);
|
||||
CompletedFrames.Enqueue(frame);
|
||||
ProtocolEventHelper.EnqueueProtocolEvent(new ProtocolEventInfo(this));
|
||||
break;
|
||||
|
||||
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in response, unless it already received a Close frame.
|
||||
case WebSocketFrameTypes.Ping:
|
||||
if (!closeSent && !closed)
|
||||
Send(new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.Pong, frame.Data, true, true));
|
||||
break;
|
||||
|
||||
case WebSocketFrameTypes.Pong:
|
||||
try
|
||||
{
|
||||
// the difference between the current time and the time when the ping message is sent
|
||||
TimeSpan diff = TimeSpan.FromTicks(this.lastMessage.Ticks - this.lastPing.Ticks);
|
||||
|
||||
// add it to the buffer
|
||||
this.rtts.Add((int)diff.TotalMilliseconds);
|
||||
|
||||
// and calculate the new latency
|
||||
this.Latency = CalculateLatency();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// https://tools.ietf.org/html/rfc6455#section-5.5
|
||||
// A Pong frame MAY be sent unsolicited. This serves as a
|
||||
// unidirectional heartbeat. A response to an unsolicited Pong frame is
|
||||
// not expected.
|
||||
}
|
||||
finally
|
||||
{
|
||||
waitingForPong = false;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// If an endpoint receives a Close frame and did not previously send a Close frame, the endpoint MUST send a Close frame in response.
|
||||
case WebSocketFrameTypes.ConnectionClose:
|
||||
HTTPManager.Logger.Information("WebSocketResponse", "ConnectionClose packet received!", this.Context);
|
||||
|
||||
CloseFrame = frame;
|
||||
if (!closeSent)
|
||||
Send(new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.ConnectionClose, BufferSegment.Empty));
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (HTTPUpdateDelegator.IsCreated)
|
||||
{
|
||||
this.baseRequest.Exception = e;
|
||||
this.baseRequest.State = HTTPRequestStates.Error;
|
||||
}
|
||||
else
|
||||
this.baseRequest.State = HTTPRequestStates.Aborted;
|
||||
|
||||
closed = true;
|
||||
newFrameSignal.Set();
|
||||
}
|
||||
}
|
||||
|
||||
HTTPManager.Logger.Information("WebSocketResponse", "Ending Read thread! closed: " + closed, this.Context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
HTTPManager.Logger.Information("WebSocketResponse", "ReceiveThread - Closed!", this.Context);
|
||||
|
||||
TryToCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sending Out Events
|
||||
|
||||
/// <summary>
|
||||
/// Internal function to send out received messages.
|
||||
/// </summary>
|
||||
void IProtocol.HandleEvents()
|
||||
{
|
||||
WebSocketFrameReader frame;
|
||||
while (CompletedFrames.TryDequeue(out frame))
|
||||
{
|
||||
// Bugs in the clients shouldn't interrupt the code, so we need to try-catch and ignore any exception occurring here
|
||||
try
|
||||
{
|
||||
switch (frame.Type)
|
||||
{
|
||||
case WebSocketFrameTypes.Continuation:
|
||||
if (HTTPManager.Logger.Level == Loglevels.All)
|
||||
HTTPManager.Logger.Verbose("WebSocketResponse", "HandleEvents - OnIncompleteFrame: " + frame.ToString(), this.Context);
|
||||
|
||||
if (OnIncompleteFrame != null)
|
||||
OnIncompleteFrame(this, frame);
|
||||
break;
|
||||
|
||||
case WebSocketFrameTypes.Text:
|
||||
// Any not Final frame is handled as a fragment
|
||||
if (!frame.IsFinal)
|
||||
goto case WebSocketFrameTypes.Continuation;
|
||||
|
||||
if (HTTPManager.Logger.Level == Loglevels.All)
|
||||
HTTPManager.Logger.Verbose("WebSocketResponse", "HandleEvents - OnText: " + frame.DataAsText, this.Context);
|
||||
|
||||
if (OnText != null)
|
||||
OnText(this, frame.DataAsText);
|
||||
break;
|
||||
|
||||
case WebSocketFrameTypes.Binary:
|
||||
// Any not Final frame is handled as a fragment
|
||||
if (!frame.IsFinal)
|
||||
goto case WebSocketFrameTypes.Continuation;
|
||||
|
||||
if (HTTPManager.Logger.Level == Loglevels.All)
|
||||
HTTPManager.Logger.Verbose("WebSocketResponse", "HandleEvents - OnBinary: " + frame.ToString(), this.Context);
|
||||
|
||||
if (OnBinary != null)
|
||||
{
|
||||
var data = new byte[frame.Data.Count];
|
||||
Array.Copy(frame.Data.Data, frame.Data.Offset, data, 0, frame.Data.Count);
|
||||
OnBinary(this, data);
|
||||
}
|
||||
|
||||
if (OnBinaryNoAlloc != null)
|
||||
OnBinaryNoAlloc(this, frame.Data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("WebSocketResponse", string.Format("HandleEvents({0})", frame.ToString()), ex, this.Context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
frame.ReleaseData();
|
||||
}
|
||||
}
|
||||
|
||||
// 2015.05.09
|
||||
// State checking added because if there is an error the OnClose called first, and then the OnError.
|
||||
// Now, when there is an error only the OnError event will be called!
|
||||
if (IsClosed && OnClosed != null && baseRequest.State == HTTPRequestStates.Processing)
|
||||
{
|
||||
HTTPManager.Logger.Verbose("WebSocketResponse", "HandleEvents - Calling OnClosed", this.Context);
|
||||
try
|
||||
{
|
||||
UInt16 statusCode = 0;
|
||||
string msg = string.Empty;
|
||||
|
||||
// If we received any data, we will get the status code and the message from it
|
||||
if (/*CloseFrame != null && */CloseFrame.Data != BufferSegment.Empty && CloseFrame.Data.Count >= 2)
|
||||
{
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(CloseFrame.Data.Data, CloseFrame.Data.Offset, 2);
|
||||
statusCode = BitConverter.ToUInt16(CloseFrame.Data.Data, CloseFrame.Data.Offset);
|
||||
|
||||
if (CloseFrame.Data.Count > 2)
|
||||
msg = Encoding.UTF8.GetString(CloseFrame.Data.Data, CloseFrame.Data.Offset + 2, CloseFrame.Data.Count - 2);
|
||||
|
||||
CloseFrame.ReleaseData();
|
||||
}
|
||||
|
||||
OnClosed(this, statusCode, msg);
|
||||
OnClosed = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("WebSocketResponse", "HandleEvents - OnClosed", ex, this.Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool SendPing()
|
||||
{
|
||||
HTTPManager.Logger.Information("WebSocketResponse", "Sending Ping frame, waiting for a pong...", this.Context);
|
||||
|
||||
lastPing = DateTime.UtcNow;
|
||||
waitingForPong = true;
|
||||
|
||||
try
|
||||
{
|
||||
var pingFrame = new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.Ping, BufferSegment.Empty);
|
||||
|
||||
Send(pingFrame);
|
||||
}
|
||||
catch
|
||||
{
|
||||
HTTPManager.Logger.Information("WebSocketResponse", "Error while sending PING message! Closing WebSocket.", this.Context);
|
||||
CloseWithError(HTTPRequestStates.Error, "Error while sending PING message!");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CloseWithError(HTTPRequestStates state, string message)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
this.baseRequest.Exception = new Exception(message);
|
||||
this.baseRequest.State = state;
|
||||
|
||||
this.closed = true;
|
||||
|
||||
CloseStream();
|
||||
ProtocolEventHelper.EnqueueProtocolEvent(new ProtocolEventInfo(this));
|
||||
}
|
||||
|
||||
private int CalculateLatency()
|
||||
{
|
||||
if (this.rtts.Count == 0)
|
||||
return 0;
|
||||
|
||||
int sumLatency = 0;
|
||||
for (int i = 0; i < this.rtts.Count; ++i)
|
||||
sumLatency += this.rtts[i];
|
||||
|
||||
return sumLatency / this.rtts.Count;
|
||||
}
|
||||
|
||||
void IProtocol.CancellationRequested()
|
||||
{
|
||||
CloseWithError(HTTPRequestStates.Aborted, null);
|
||||
}
|
||||
|
||||
private void TryToCleanup()
|
||||
{
|
||||
if (Interlocked.Increment(ref this.closedThreads) == 2)
|
||||
{
|
||||
ProtocolEventHelper.EnqueueProtocolEvent(new ProtocolEventInfo(this));
|
||||
(newFrameSignal as IDisposable).Dispose();
|
||||
newFrameSignal = null;
|
||||
|
||||
CloseStream();
|
||||
|
||||
HTTPManager.Logger.Information("WebSocketResponse", "TryToCleanup - finished!", this.Context);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return this.ConnectionKey.ToString();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
IncompleteFrames.Clear();
|
||||
CompletedFrames.Clear();
|
||||
unsentFrames.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46daae4eb8da43145b58819255462ded
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,83 @@
|
||||
#if !BESTHTTP_DISABLE_WEBSOCKET
|
||||
|
||||
namespace BestHTTP.WebSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// http://tools.ietf.org/html/rfc6455#section-7.4.1
|
||||
/// </summary>
|
||||
public enum WebSocketStausCodes : ushort
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates a normal closure, meaning that the purpose for which the connection was established has been fulfilled.
|
||||
/// </summary>
|
||||
NormalClosure = 1000,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that an endpoint is "going away", such as a server going down or a browser having navigated away from a page.
|
||||
/// </summary>
|
||||
GoingAway = 1001,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that an endpoint is terminating the connection due to a protocol error.
|
||||
/// </summary>
|
||||
ProtocolError = 1002,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that an endpoint is terminating the connection because it has received a type of data it cannot accept
|
||||
/// (e.g., an endpoint that understands only text data MAY send this if it receives a binary message).
|
||||
/// </summary>
|
||||
WrongDataType = 1003,
|
||||
|
||||
/// <summary>
|
||||
/// Reserved. The specific meaning might be defined in the future.
|
||||
/// </summary>
|
||||
Reserved = 1004,
|
||||
|
||||
/// <summary>
|
||||
/// A reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint.
|
||||
/// It is designated for use in applications expecting a status code to indicate that no status code was actually present.
|
||||
/// </summary>
|
||||
NoStatusCode = 1005,
|
||||
|
||||
/// <summary>
|
||||
/// A reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint.
|
||||
/// It is designated for use in applications expecting a status code to indicate that the connection was closed abnormally, e.g., without sending or receiving a Close control frame.
|
||||
/// </summary>
|
||||
ClosedAbnormally = 1006,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that an endpoint is terminating the connection because it has received data within a message that was not consistent with the type of the message (e.g., non-UTF-8 [RFC3629] data within a text message).
|
||||
/// </summary>
|
||||
DataError = 1007,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that an endpoint is terminating the connection because it has received a message that violates its policy.
|
||||
/// This is a generic status code that can be returned when there is no other more suitable status code (e.g., 1003 or 1009) or if there is a need to hide specific details about the policy.
|
||||
/// </summary>
|
||||
PolicyError = 1008,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that an endpoint is terminating the connection because it has received a message that is too big for it to process.
|
||||
/// </summary>
|
||||
TooBigMessage = 1009,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that an endpoint (client) is terminating the connection because it has expected the server to negotiate one or more extension,
|
||||
/// but the server didn't return them in the response message of the WebSocket handshake.
|
||||
/// The list of extensions that are needed SHOULD appear in the /reason/ part of the Close frame. Note that this status code is not used by the server, because it can fail the WebSocket handshake instead.
|
||||
/// </summary>
|
||||
ExtensionExpected = 1010,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request.
|
||||
/// </summary>
|
||||
WrongRequest = 1011,
|
||||
|
||||
/// <summary>
|
||||
/// A reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint. It is designated for use in applications expecting a status code to indicate that the connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified).
|
||||
/// </summary>
|
||||
TLSHandshakeError = 1015
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b782b81b6b7755d478dc07521f33f09c
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Reference in New Issue
Block a user