- Initial commit

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

18
CHANGELOG.md Normal file
View File

@ -0,0 +1,18 @@
# Changelog
## 3.0.0 (2023-01-01)
__Additions and improvements__
- New namespace hierarchy, instead of `BestHTTP.WebSocket` the WebSocket class is in the `Best.Websockets` namespace.
- The `OnBinary` event now receives a `BufferSegment` and the memory will be reused after the event.
- The `OnClosed` event now receives an [WebSocketStatusCodes](https://bestdocshub.pages.dev/WebSockets/api-reference/WebSockets/WebSocketStatusCodes.md) enum as its status code.
__Removals__
- Removed the `OnBinaryNoAlloc` event.
__Fixes__
- Fixed confusing naming by renaming `StartPingThread` to `SendPings`
- Fixed confusing behavior because of the two closure events `OnError` and `OnClosed` by mergind the two into one `OnClosed` event. The behavior of `OnClosed` is now matching what browsers have.

14
CHANGELOG.md.meta Normal file
View File

@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 78a59743cf639004bb78494b2a199075
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/CHANGELOG.md
uploadId: 737284

1
LICENSE.md Normal file
View File

@ -0,0 +1 @@
The plugin's license falls under the Unity Asset Store EULA that can be found here under Appendix 1: https://unity3d.com/legal/as_terms

14
LICENSE.md.meta Normal file
View File

@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 59407affe84cf3e468fa506e7411ea8b
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/LICENSE.md
uploadId: 737284

88
README.md Normal file
View File

@ -0,0 +1,88 @@
Best WebSockets is a premier networking library for Unity, tailored specifically for seamless WebSocket integration.
It's perfect for applications that require real-time, bi-directional communication such as chat applications, multiplayer games, and live interactive systems.
Warning! **Dependency Alert**
Best WebSockets relies on the Best HTTP package!
Ensure you have it installed and set up in your Unity project before diving into Best WebSockets. Learn more about the [installation of Best HTTP](https://bestdocshub.pages.dev/HTTP//HTTP/installation.md).
## Overview
In the fast-paced digital landscape, real-time communication is crucial for a multitude of applications.
Whether it's sending instantaneous game state updates, chat messages, or receiving live feeds, WebSockets provide an edge in facilitating these real-time interactions.
Best WebSockets is crafted to effortlessly integrate this technology into your Unity projects, making bi-directional communication straightforward and efficient.
## Key Features
- **Supported Unity Versions:** Best WebSockets is compatible with Unity versions starting from :fontawesome-brands-unity: **2021.1 onwards**.
- **Cross-Platform:** Best WebSockets seamlessly operates across a wide variety of Unity platforms, ensuring its applicability for diverse development projects. Specifically, it supports:
- :fontawesome-solid-desktop: **Desktop:** Windows, Linux, MacOS
- :fontawesome-solid-mobile: **Mobile:** iOS, Android
- :material-microsoft-windows: **Universal Windows Platform (UWP)**
- :material-web: **Web Browsers:** WebGL
This vast platform compatibility assures that Best WebSockets is an excellent choice for any project, regardless of your target platform or audience.
- **Persistent Connections:** Unlike traditional request-response communication, WebSockets offer a persistent, low-latency connection that's perfect for applications that need instant communication.
- **Binary and Text Data:** Whether you're sending textual messages or binary data like images and files, Best WebSockets is equipped to handle both with ease.
- **Secure Communication:** With support for WSS:// (WebSocket over TLS), your application's data remains secure and encrypted.
- **Built-In Profiler Support:** To ensure peak performance and help debug potential issues, Best WebSockets integrates with the base [Best HTTP profiler](https://bestdocshub.pages.dev/Shared/profiler/index.md):
- **Memory Profiler:** Examine the library's internal memory usage, helping identify potential bottlenecks or memory leaks.
- **Network Profiler:** Delve deep into your network operations, monitoring data transfers, open and closed connections, and much more.
- **Custom Protocols:** Easily extend and adapt your WebSocket communication with custom protocols, allowing for versatile application-specific communication styles.
- **Compression:** Data compression capabilities ensure efficient bandwidth usage, leading to faster data transfers and reduced latency.
## Documentation Sections
Delve into the details and start using Best WebSockets in your projects:
- [Installation Guide:](https://bestdocshub.pages.dev/WebSockets/installation.md) Kick off with Best WebSockets by setting up the package and configuring your Unity project.
- [Upgrade Guide:](https://bestdocshub.pages.dev/WebSockets/upgrade-guide.md) Transitioning from an earlier version? Find out the latest improvements and how to smoothly upgrade to the most recent release.
- [Getting Started:](https://bestdocshub.pages.dev/WebSockets/getting-started/index.md) Embark on your WebSocket journey, understand the basics, and configure Best WebSockets tailored to your application's needs.
- [Advanced Topics:](https://bestdocshub.pages.dev/WebSockets/intermediate-topics/index.md) Enhance your knowledge with deeper insights into WebSocket topics, like custom protocols, security, and more.
This documentation is designed for developers of all backgrounds and expertise.
Whether you're new to Unity or a seasoned professional, these guides will assist you in maximizing the capabilities of Best WebSockets.
Dive in now and elevate your Unity projects with superior real-time communication features using Best WebSockets!
## Installation Guide
!!! Warning "Dependency Alert"
Before installing Best WebSockets, ensure you have the [Best HTTP package](../HTTP/index.md) installed and set up in your Unity project. If you haven't done so yet, refer to the [Best HTTP Installation Guide](../HTTP/installation.md).
Getting started with Best WebSockets requires a prior installation of the Best HTTP package. Once Best HTTP is set up, integrating Best WebSockets into your Unity project is a breeze.
### Installing from the Unity Asset Store using the Package Manager Window
1. **Purchase:** If you haven't previously purchased the package, proceed to do so. Once purchased, Unity will recognize your purchase, and you can install the package directly from within the Unity Editor. If you already own the package, you can skip these steps.
1. **Visit the Unity Asset Store:** Navigate to the [Unity Asset Store](https://assetstore.unity.com/publishers/4137?aid=1101lfX8E) using your web browser.
2. **Search for Best WebSockets:** Locate and choose the official Best WebSockets package.
3. **Buy Best WebSockets:** By clicking on the `Buy Now` button go though the purchase process.
2. **Open Unity & Access the Package Manager:** Start Unity and select your project. Head to [Window > Package Manager](https://docs.unity3d.com/Manual/upm-ui.html).
3. **Select 'My Assets':** In the Package Manager, switch to the [My Assets](https://docs.unity3d.com/Manual/upm-ui-import.html) tab to view all accessible assets.
4. **Find Best WebSockets and Download:** Scroll to find "Best WebSockets". Click to view its details. If it isn't downloaded, you'll notice a Download button. Click and wait. After downloading, this button will change to Import.
5. **Import the Package:** Once downloaded, click the Import button. Unity will display all Best WebSockets' assets. Ensure all are selected and click Import.
6. **Confirmation:** After the import, Best WebSockets will integrate into your project, signaling a successful installation.
### Installing from a .unitypackage file
If you have a .unitypackage file for Best WebSockets, follow these steps:
1. **Download the .unitypackage:** Make sure the Best WebSockets.unitypackage file is saved on your device.
2. **Import into Unity:** Open Unity and your project. Go to Assets > Import Package > Custom Package.
3. **Locate and Select the .unitypackage:** Find where you saved the Best WebSockets.unitypackage file, select it, and click Open.
4. **Review and Import:** Unity will show a list of all the package's assets. Ensure all assets are selected and click Import.
5. **Confirmation:** Post import, you'll see all the Best WebSockets assets in your project's Asset folder, indicating a successful setup.
!!! Note
Best WebSockets also supports other installation techniques as documented in Unity's manual for packages. For more advanced installation methods, please see the Unity Manual on [Sharing Packages](https://docs.unity3d.com/Manual/cus-share.html).
### Assembly Definitions and Runtime References
For developers familiar with Unity's development patterns, it's essential to understand how Best WebSockets incorporates Unity's systems:
- **Assembly Definition Files:** Best WebSockets incorporates [Unity's Assembly Definition files](https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html). It aids in organizing and managing the codebase efficiently.
- **Auto-Referencing of Runtime DLLs:** The runtime DLLs produced by Best WebSockets are [Auto Referenced](https://docs.unity3d.com/Manual/class-AssemblyDefinitionImporter.html), allowing Unity to automatically recognize and utilize them without manual intervention.
- **Manual Package Referencing:** Should you need to reference Best WebSockets manually in your project (for advanced setups or specific use cases), you can do so. Simply [reference the package](https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html#reference-another-assembly) by searching for `com.Tivadar.Best.WebSockets`.
Congratulations! You've successfully integrated Best WebSockets into your Unity project. Begin your WebSocket adventure with the [Getting Started guide](getting-started/index.md).
For any issues or additional assistance, please consult the [Community and Support page](../Shared/support.md).

14
README.md.meta Normal file
View File

@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 25179c491c36d554e89facefd79d266a
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/README.md
uploadId: 737284

8
Runtime.meta Normal file
View File

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

10
Runtime/AssemblyInfo.cs Normal file
View File

@ -0,0 +1,10 @@
using System.Reflection;
[assembly: AssemblyVersion("3.0.7")]
[assembly: AssemblyCompany("Tivadar György Nagy")]
[assembly: AssemblyCopyright("Copyright © 2024 Tivadar György Nagy")]
[assembly: AssemblyDescription("Best WebSockets is a premier networking library for Unity, tailored specifically for seamless WebSocket integration. It's perfect for applications that require real-time, bi-directional communication such as chat applications, multiplayer games, and live interactive systems.")]
#if WITH_BURST
[assembly: Unity.Burst.BurstCompile(CompileSynchronously = true, OptimizeFor = Unity.Burst.OptimizeFor.Performance)]
#endif

View File

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

8
Runtime/Extensions.meta Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1,368 @@
#if !UNITY_WEBGL || UNITY_EDITOR
using System;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Compression.Zlib;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
using Best.WebSockets.Implementations.Frames;
namespace Best.WebSockets.Extensions
{
/// <summary>
/// Compression Extensions for WebSocket implementation.
/// http://tools.ietf.org/html/rfc7692
/// </summary>
public sealed class PerMessageCompression : IExtension
{
public const int MinDataLengthToCompressDefault = 256;
private static readonly byte[] Trailer = new byte[] { 0x00, 0x00, 0xFF, 0xFF };
#region Public Properties
/// <summary>
/// By including this extension parameter in an extension negotiation offer, a client informs the peer server
/// of a hint that even if the server doesn't include the "client_no_context_takeover" extension parameter in
/// the corresponding extension negotiation response to the offer, the client is not going to use context takeover.
/// </summary>
public bool ClientNoContextTakeover { get; private set; }
/// <summary>
/// By including this extension parameter in an extension negotiation offer, a client prevents the peer server from using context takeover.
/// </summary>
public bool ServerNoContextTakeover { get; private set; }
/// <summary>
/// This parameter indicates the base-2 logarithm of the LZ77 sliding window size of the client context.
/// </summary>
public int ClientMaxWindowBits { get; private set; }
/// <summary>
/// This parameter indicates the base-2 logarithm of the LZ77 sliding window size of the server context.
/// </summary>
public int ServerMaxWindowBits { get; private set; }
/// <summary>
/// The compression level that the client will use to compress the frames.
/// </summary>
public CompressionLevel Level { get; private set; }
/// <summary>
/// What minimum data length will trigger the compression.
/// </summary>
public int MinimumDataLegthToCompress { get; set; }
#endregion
#region Private fields
/// <summary>
/// Cached object to support context takeover.
/// </summary>
private BufferPoolMemoryStream compressorOutputStream;
private DeflateStream compressorDeflateStream;
/// <summary>
/// Cached object to support context takeover.
/// </summary>
private BufferPoolMemoryStream decompressorInputStream;
private BufferPoolMemoryStream decompressorOutputStream;
private DeflateStream decompressorDeflateStream;
#endregion
public PerMessageCompression()
:this(CompressionLevel.Default, false, false, ZlibConstants.WindowBitsMax, ZlibConstants.WindowBitsMax, MinDataLengthToCompressDefault)
{ }
public PerMessageCompression(CompressionLevel level,
bool clientNoContextTakeover,
bool serverNoContextTakeover,
int desiredClientMaxWindowBits,
int desiredServerMaxWindowBits,
int minDatalengthToCompress)
{
this.Level = level;
this.ClientNoContextTakeover = clientNoContextTakeover;
this.ServerNoContextTakeover = serverNoContextTakeover;
this.ClientMaxWindowBits = desiredClientMaxWindowBits;
this.ServerMaxWindowBits = desiredServerMaxWindowBits;
this.MinimumDataLegthToCompress = minDatalengthToCompress;
}
#region IExtension Implementation
/// <summary>
/// This will start the permessage-deflate negotiation process.
/// <seealso href="http://tools.ietf.org/html/rfc7692#section-5.1"/>
/// </summary>
public void AddNegotiation(HTTP.HTTPRequest request)
{
// The default header value that we will send out minimum.
string headerValue = "permessage-deflate";
// http://tools.ietf.org/html/rfc7692#section-7.1.1.1
// A client MAY include the "server_no_context_takeover" extension parameter in an extension negotiation offer. This extension parameter has no value.
// By including this extension parameter in an extension negotiation offer, a client prevents the peer server from using context takeover.
// If the peer server doesn't use context takeover, the client doesn't need to reserve memory to retain the LZ77 sliding window between messages.
if (this.ServerNoContextTakeover)
headerValue += "; server_no_context_takeover";
// http://tools.ietf.org/html/rfc7692#section-7.1.1.2
// A client MAY include the "client_no_context_takeover" extension parameter in an extension negotiation offer.
// This extension parameter has no value. By including this extension parameter in an extension negotiation offer,
// a client informs the peer server of a hint that even if the server doesn't include the "client_no_context_takeover"
// extension parameter in the corresponding extension negotiation response to the offer, the client is not going to use context takeover.
if (this.ClientNoContextTakeover)
headerValue += "; client_no_context_takeover";
// http://tools.ietf.org/html/rfc7692#section-7.1.2.1
// By including this parameter in an extension negotiation offer, a client limits the LZ77 sliding window size that the server
// will use to compress messages.If the peer server uses a small LZ77 sliding window to compress messages, the client can reduce the memory needed for the LZ77 sliding window.
if (this.ServerMaxWindowBits != ZlibConstants.WindowBitsMax)
headerValue += "; server_max_window_bits=" + this.ServerMaxWindowBits.ToString();
else
// Absence of this parameter in an extension negotiation offer indicates that the client can receive messages compressed using an LZ77 sliding window of up to 32,768 bytes.
this.ServerMaxWindowBits = ZlibConstants.WindowBitsMax;
// http://tools.ietf.org/html/rfc7692#section-7.1.2.2
// By including this parameter in an offer, a client informs the peer server that the client supports the "client_max_window_bits"
// extension parameter in an extension negotiation response and, optionally, a hint by attaching a value to the parameter.
if (this.ClientMaxWindowBits != ZlibConstants.WindowBitsMax)
headerValue += "; client_max_window_bits=" + this.ClientMaxWindowBits.ToString();
else
{
headerValue += "; client_max_window_bits";
// If the "client_max_window_bits" extension parameter in an extension negotiation offer has a value, the parameter also informs the
// peer server of a hint that even if the server doesn't include the "client_max_window_bits" extension parameter in the corresponding
// extension negotiation response with a value greater than the one in the extension negotiation offer or if the server doesn't include
// the extension parameter at all, the client is not going to use an LZ77 sliding window size greater than the size specified
// by the value in the extension negotiation offer to compress messages.
this.ClientMaxWindowBits = ZlibConstants.WindowBitsMax;
}
// Add the new header to the request.
request.AddHeader("Sec-WebSocket-Extensions", headerValue);
}
public bool ParseNegotiation(HTTP.HTTPResponse resp)
{
// Search for any returned neogitation offer
var headerValues = resp.GetHeaderValues("Sec-WebSocket-Extensions");
if (headerValues == null)
return false;
for (int i = 0; i < headerValues.Count; ++i)
{
// If found, tokenize it
HeaderParser parser = new HeaderParser(headerValues[i]);
for (int cv = 0; cv < parser.Values.Count; ++cv)
{
HeaderValue value = parser.Values[i];
if (!string.IsNullOrEmpty(value.Key) && value.Key.StartsWith("permessage-deflate", StringComparison.OrdinalIgnoreCase))
{
HTTPManager.Logger.Information("PerMessageCompression", "Enabled with header: " + headerValues[i]);
HeaderValue option;
if (value.TryGetOption("client_no_context_takeover", out option))
this.ClientNoContextTakeover = true;
if (value.TryGetOption("server_no_context_takeover", out option))
this.ServerNoContextTakeover = true;
if (value.TryGetOption("client_max_window_bits", out option))
if (option.HasValue)
{
int windowBits;
if (int.TryParse(option.Value, out windowBits))
this.ClientMaxWindowBits = windowBits;
}
if (value.TryGetOption("server_max_window_bits", out option))
if (option.HasValue)
{
int windowBits;
if (int.TryParse(option.Value, out windowBits))
this.ServerMaxWindowBits = windowBits;
}
return true;
}
}
}
return false;
}
/// <summary>
/// IExtension implementation to set the Rsv1 flag in the header if we are we will want to compress the data
/// in the writer.
/// </summary>
public byte GetFrameHeader(WebSocketFrame writer, byte inFlag)
{
// http://tools.ietf.org/html/rfc7692#section-7.2.3.1
// the RSV1 bit is set only on the first frame.
if ((writer.Type == WebSocketFrameTypes.Binary || writer.Type == WebSocketFrameTypes.Text) &&
writer.Data != null && writer.Data.Count >= this.MinimumDataLegthToCompress)
return (byte)(inFlag | 0x40);
else
return inFlag;
}
/// <summary>
/// IExtension implementation to be able to compress the data hold in the writer.
/// </summary>
public BufferSegment Encode(WebSocketFrame writer)
{
if (writer.Data == null)
return BufferSegment.Empty;
// Is compressing enabled for this frame? If so, compress it.
if ((writer.Header & 0x40) != 0)
return Compress(writer.Data);
else
return writer.Data;
}
/// <summary>
/// IExtension implementation to possible decompress the data.
/// </summary>
public BufferSegment Decode(byte header, BufferSegment data)
{
// Is the server compressed the data? If so, decompress it.
if ((header & 0x40) != 0)
return Decompress(data);
else
return data;
}
#endregion
#region Private Helper Functions
/// <summary>
/// A function to compress and return the data parameter with possible context takeover support (reusing the DeflateStream).
/// </summary>
private BufferSegment Compress(BufferSegment data)
{
CheckDisposed();
if (compressorOutputStream == null)
compressorOutputStream = new BufferPoolMemoryStream();
compressorOutputStream.SetLength(0);
if (compressorDeflateStream == null)
{
compressorDeflateStream = new DeflateStream(compressorOutputStream, CompressionMode.Compress, this.Level, false, this.ClientMaxWindowBits);
compressorDeflateStream.FlushMode = FlushType.Sync;
}
BufferSegment result = BufferSegment.Empty;
try
{
compressorDeflateStream.Write(data.Data, data.Offset, data.Count);
compressorDeflateStream.Flush();
compressorOutputStream.Position = 0;
// http://tools.ietf.org/html/rfc7692#section-7.2.1
// Remove 4 octets (that are 0x00 0x00 0xff 0xff) from the tail end.
// After this step, the last octet of the compressed data contains (possibly part of)
// the DEFLATE header bits with the "BTYPE" bits set to 00.
compressorOutputStream.SetLength(compressorOutputStream.Length - 4);
result = compressorOutputStream.ToBufferSegment();
}
finally
{
if (this.ClientNoContextTakeover)
{
compressorDeflateStream.Dispose();
compressorDeflateStream = null;
compressorOutputStream = null;
}
}
return result;
}
/// <summary>
/// A function to decompress and return the data parameter with possible context takeover support (reusing the DeflateStream).
/// </summary>
private BufferSegment Decompress(BufferSegment data)
{
CheckDisposed();
if (decompressorInputStream == null)
decompressorInputStream = new BufferPoolMemoryStream(data.Count + 4);
decompressorInputStream.Write(data.Data, data.Offset, data.Count);
// http://tools.ietf.org/html/rfc7692#section-7.2.2
// Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the payload of the message.
decompressorInputStream.Write(PerMessageCompression.Trailer, 0, PerMessageCompression.Trailer.Length);
decompressorInputStream.Position = 0;
if (decompressorDeflateStream == null)
{
decompressorDeflateStream = new DeflateStream(decompressorInputStream, CompressionMode.Decompress, CompressionLevel.Default, false, this.ServerMaxWindowBits);
decompressorDeflateStream.FlushMode = FlushType.Sync;
}
if (decompressorOutputStream == null)
decompressorOutputStream = new BufferPoolMemoryStream();
decompressorOutputStream.SetLength(0);
byte[] copyBuffer = BufferPool.Get(1024, true);
int readCount;
while ((readCount = decompressorDeflateStream.Read(copyBuffer, 0, copyBuffer.Length)) != 0)
decompressorOutputStream.Write(copyBuffer, 0, readCount);
BufferPool.Release(copyBuffer);
decompressorDeflateStream.SetLength(0);
var result = decompressorOutputStream.ToBufferSegment();
if (this.ServerNoContextTakeover)
{
decompressorDeflateStream.Dispose();
decompressorDeflateStream = null;
decompressorInputStream = null;
}
return result;
}
#endregion
bool _disposed;
void CheckDisposed()
{
if (_disposed)
throw new ObjectDisposedException(nameof(PerMessageCompression));
}
void IDisposable.Dispose()
{
_disposed = true;
compressorDeflateStream?.Dispose();
compressorOutputStream?.Dispose();
decompressorDeflateStream?.Dispose();
decompressorInputStream?.Dispose();
decompressorOutputStream?.Dispose();
}
}
}
#endif

View File

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

View File

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

View File

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

View File

@ -0,0 +1,348 @@
#if !UNITY_WEBGL || UNITY_EDITOR
using System;
using System.Runtime.CompilerServices;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.IL2CPP;
using Best.HTTP.Shared.PlatformSupport.Memory;
#if WITH_BURST
using Unity.Burst;
using Unity.Burst.Intrinsics;
using static Unity.Burst.Intrinsics.X86.Avx2;
using static Unity.Burst.Intrinsics.X86.Sse2;
using static Unity.Burst.Intrinsics.Arm.Neon;
#endif
namespace Best.WebSockets.Implementations.Frames
{
/// <summary>
/// Denotes a binary frame. The "Payload data" is arbitrary binary data whose interpretation is solely up to the application layer.
/// This is the base class of all other frame writers, as all frame can be represented as a byte array.
/// </summary>
#if WITH_BURST
[BurstCompile]
#endif
[Il2CppEagerStaticClassConstruction]
public struct WebSocketFrame
{
public WebSocketFrameTypes Type { get; private set; }
public BufferSegment Data { get; private set; }
public WebSocket Websocket { get; private set; }
public byte Header;
public WebSocketFrame(WebSocket webSocket, WebSocketFrameTypes type, BufferSegment data)
:this(webSocket, type, data, copyData: true)
{
}
public WebSocketFrame(WebSocket webSocket, WebSocketFrameTypes type, BufferSegment data, bool copyData)
{
this.Type = type;
this.Websocket = webSocket;
this.Data = data;
if (this.Data.Data != null)
{
if (copyData)
{
var from = this.Data;
var buffer = BufferPool.Get(this.Data.Count, true);
this.Data = new BufferSegment(buffer, 0, this.Data.Count);
Array.Copy(from.Data, (int)from.Offset, this.Data.Data, this.Data.Offset, this.Data.Count);
}
}
else
this.Data = BufferSegment.Empty;
// We use the header only for storing extension flags only
this.Header = 0x00;
}
public override string ToString()
{
return $"[WebSocketFrame Type: {this.Type}, Header: {this.Header:X2}, Data: {this.Data}]";
}
public void WriteTo(Action<BufferSegment, BufferSegment> callback, uint maxFragmentSize, bool mask, LoggingContext context)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose("WebSocketFrame", "WriteTo - Frame: " + ToString(), context);
if ((this.Type == WebSocketFrameTypes.Binary || this.Type == WebSocketFrameTypes.Text))
{
DoExtensions();
if (this.Data.Count > maxFragmentSize)
FragmentAndSend(callback, maxFragmentSize, mask, context);
else
WriteFragment(callback, (byte)(0x80 | this.Header | (byte)this.Type), this.Data, mask, context);
}
else
{
WriteFragment(callback, (byte)(0x80 | this.Header | (byte)this.Type), this.Data, mask, context);
}
}
private void DoExtensions()
{
if (this.Websocket != null && this.Websocket.Extensions != null)
{
for (int i = 0; i < this.Websocket.Extensions.Length; ++i)
{
var ext = this.Websocket.Extensions[i];
if (ext != null)
{
this.Header |= ext.GetFrameHeader(this, this.Header);
BufferSegment newData = ext.Encode(this);
if (newData != this.Data)
{
BufferPool.Release(this.Data);
this.Data = newData;
}
}
}
}
}
private void FragmentAndSend(Action<BufferSegment, BufferSegment> callback, uint maxFragmentSize, bool mask, LoggingContext context)
{
int pos = this.Data.Offset;
int endPos = this.Data.Offset + this.Data.Count;
byte header = (byte)(0x00 | this.Header | (byte)this.Type);
while (pos < endPos)
{
int chunkLength = Math.Min((int)maxFragmentSize, endPos - pos);
WriteFragment(callback: callback,
Header: header,
Data: this.Data.Slice((int)pos, (int)chunkLength),
mask: mask,
context: context);
pos += chunkLength;
// set only the IsFinal flag, every other flags are zero
header = (byte)(pos + chunkLength >= this.Data.Count ? 0x80 : 0x00);
}
}
private static unsafe void WriteFragment(Action<BufferSegment, BufferSegment> callback, byte Header, BufferSegment Data, bool mask, LoggingContext context)
{
// For the complete documentation for this section see:
// http://tools.ietf.org/html/rfc6455#section-5.2
// Header(1) + Len(8) + Mask (4)
byte[] wsHeader = BufferPool.Get(13, true);
int pos = 0;
// Write the header
wsHeader[pos++] = Header;
// The length of the "Payload data", in bytes: if 0-125, that is the payload length. If 126, the following 2 bytes interpreted as a
// 16-bit unsigned integer are the payload length. If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the
// most significant bit MUST be 0) are the payload length. Multibyte length quantities are expressed in network byte order.
if (Data.Count < 126)
{
wsHeader[pos++] = (byte)(0x80 | (byte)Data.Count);
}
else if (Data.Count < UInt16.MaxValue)
{
wsHeader[pos++] = (byte)(0x80 | 126);
var count = (UInt16)Data.Count;
wsHeader[pos++] = (byte)(count >> 8);
wsHeader[pos++] = (byte)(count);
}
else
{
wsHeader[pos++] = (byte)(0x80 | 127);
var count = (UInt64)Data.Count;
wsHeader[pos++] = (byte)(count >> 56);
wsHeader[pos++] = (byte)(count >> 48);
wsHeader[pos++] = (byte)(count >> 40);
wsHeader[pos++] = (byte)(count >> 32);
wsHeader[pos++] = (byte)(count >> 24);
wsHeader[pos++] = (byte)(count >> 16);
wsHeader[pos++] = (byte)(count >> 8);
wsHeader[pos++] = (byte)(count);
}
if (Data != BufferSegment.Empty)
{
// All frames sent from the client to the server are masked by a 32-bit value that is contained within the frame. This field is
// present if the mask bit is set to 1 and is absent if the mask bit is set to 0.
// If the data is being sent by the client, the frame(s) MUST be masked.
uint hash = mask ? (uint)wsHeader.GetHashCode() : 0;
wsHeader[pos++] = (byte)(hash >> 24);
wsHeader[pos++] = (byte)(hash >> 16);
wsHeader[pos++] = (byte)(hash >> 8);
wsHeader[pos++] = (byte)(hash);
// Do the masking.
if (mask)
{
fixed (byte* pData = Data.Data/*, pmask = &wsHeader[pos - 4]*/)
{
byte* alignedMask = stackalloc byte[4];
alignedMask[0] = wsHeader[pos - 4];
alignedMask[1] = wsHeader[pos - 3];
alignedMask[2] = wsHeader[pos - 2];
alignedMask[3] = wsHeader[pos - 1];
ApplyMask(pData, Data.Offset, Data.Count, alignedMask);
}
}
}
else
{
wsHeader[pos++] = 0;
wsHeader[pos++] = 0;
wsHeader[pos++] = 0;
wsHeader[pos++] = 0;
}
var header = wsHeader.AsBuffer(pos);
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose("WebSocketFrame", string.Format("WriteFragment - Header: {0}, data chunk: {1}", header.ToString(), Data.ToString()), context);
callback(header, Data);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#if WITH_BURST
[BurstCompile(CompileSynchronously = true)]
#endif
public unsafe static void ApplyMask(
#if WITH_BURST
[NoAlias]
#endif
byte* pData,
int DataOffset,
int DataCount,
#if WITH_BURST
[NoAlias]
#endif
byte* pmask
)
{
int targetOffset = DataOffset + DataCount;
uint umask = *(uint*)pmask;
#if WITH_BURST
if (targetOffset - DataOffset >= 32)
{
if (IsAvx2Supported)
{
v256 mask = new v256(umask);
v256 ldstrMask = new v256((byte)0xFF);
while (targetOffset - DataOffset >= 32)
{
// load data
v256 data = mm256_maskload_epi32(pData + DataOffset, ldstrMask);
// xor
v256 result = mm256_xor_si256(data, mask);
// store
mm256_maskstore_epi32(pData + DataOffset, ldstrMask, result);
// advance
DataOffset += 32;
}
}
}
if (targetOffset - DataOffset >= 16)
{
v128 mask = new v128(umask);
#if !UNITY_ANDROID && !UNITY_IOS
if (IsSse2Supported)
{
while (targetOffset - DataOffset >= 16)
{
// load data
v128 data = loadu_si128(pData + DataOffset);
// xor
var result = xor_si128(data, mask);
// store
storeu_si128(pData + DataOffset, result);
// advance
DataOffset += 16;
}
}
else
#endif
if (IsNeonSupported)
{
while (targetOffset - DataOffset >= 16)
{
// load data
v128 data = vld1q_u8(pData + DataOffset);
// xor
v128 result = veorq_u8(data, mask);
// store
vst1q_u8(pData + DataOffset, result);
// advance
DataOffset += 16;
}
}
}
#endif
// fallback to calculate by reinterpret-casting to ulong
if (targetOffset - DataOffset >= 8)
{
ulong* ulpData = (ulong*)(pData + DataOffset);
#if UNITY_ANDROID && !UNITY_EDITOR
if ((long)ulpData % sizeof(ulong) == 0)
{
#endif
// duplicate the mask to fill up a whole ulong.
ulong ulmask = (((ulong)umask << 32) | umask);
while (targetOffset - DataOffset >= 8)
{
*ulpData = *ulpData ^ ulmask;
ulpData++;
DataOffset += 8;
}
#if UNITY_ANDROID && !UNITY_EDITOR
}
#endif
}
// process remaining bytes (0..7)
for (int i = DataOffset; i < targetOffset; ++i)
pData[i] = (byte)(pData[i] ^ pmask[(i - DataOffset) % 4]);
}
}
}
#endif

View File

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

View File

@ -0,0 +1,253 @@
#if !UNITY_WEBGL || UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
namespace Best.WebSockets.Implementations.Frames
{
/// <summary>
/// Represents an incoming WebSocket Frame.
/// </summary>
public struct WebSocketFrameReader
{
#region Properties
public byte Header { get; private set; }
/// <summary>
/// True if it's a final Frame in a sequence, or the only one.
/// </summary>
public bool IsFinal { get; private set; }
/// <summary>
/// The type of the Frame.
/// </summary>
public WebSocketFrameTypes Type { get; private set; }
/// <summary>
/// The decoded array of bytes.
/// </summary>
public BufferSegment Data { get; private set; }
/// <summary>
/// Textual representation of the received Data.
/// </summary>
public string DataAsText { get; private set; }
#endregion
#region Internal & Private Functions
private static bool HasEnoughBytesForAFrame(PeekableIncomingSegmentStream peekable)
{
// https://www.rfc-editor.org/rfc/rfc6455#section-5.2
// Minimum frame length is 2 bytes: header + zero length
if (peekable.Length < 2)
return false;
peekable.BeginPeek();
// header
peekable.PeekByte();
var maskAndLength = peekable.PeekByte();
var length = (ulong)(maskAndLength & 127);
if (length < 126)
{
return (ulong)peekable.Length >= (2 + length);
}
else if (length == 126)
{
return (ulong)peekable.Length >= (3 + length);
}
else if (length == 127)
{
return (ulong)peekable.Length >= (10 + length);
}
return true;
}
internal unsafe void Read(Stream stream)
{
// For the complete documentation for this section see:
// http://tools.ietf.org/html/rfc6455#section-5.2
this.Header = ReadByte(stream);
// The first byte is the Final Bit and the type of the frame
IsFinal = (this.Header & 0x80) != 0;
Type = (WebSocketFrameTypes)(this.Header & 0xF);
byte maskAndLength = ReadByte(stream);
// The second byte is the Mask Bit and the length of the payload data
if ((maskAndLength & 0x80) != 0)
throw new NotImplementedException($"Payload from the server is masked!");
// if 0-125, that is the payload length.
var length = (UInt64)(maskAndLength & 127);
// If 126, the following 2 bytes interpreted as a 16-bit unsigned integer are the payload length.
if (length == 126)
{
byte[] rawLen = BufferPool.Get(2, true);
stream.ReadBuffer(rawLen, 2);
if (BitConverter.IsLittleEndian)
Array.Reverse(rawLen, 0, 2);
length = (UInt64)BitConverter.ToUInt16(rawLen, 0);
BufferPool.Release(rawLen);
}
else if (length == 127)
{
// If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the
// most significant bit MUST be 0) are the payload length.
byte[] rawLen = BufferPool.Get(8, true);
stream.ReadBuffer(rawLen, 8);
if (BitConverter.IsLittleEndian)
Array.Reverse(rawLen, 0, 8);
length = (UInt64)BitConverter.ToUInt64(rawLen, 0);
BufferPool.Release(rawLen);
}
if (length == 0L)
{
Data = BufferSegment.Empty;
return;
}
var buffer = BufferPool.Get((long)length, true);
uint readLength = 0;
try
{
do
{
int read = stream.Read(buffer, (int)readLength, (int)(length - readLength));
if (read <= 0)
throw ExceptionHelper.ServerClosedTCPStream();
readLength += (uint)read;
} while (readLength < length);
}
catch
{
BufferPool.Release(buffer);
throw;
}
this.Data = new BufferSegment(buffer, 0, (int)length);
}
private static byte ReadByte(Stream stream)
{
int read = stream.ReadByte();
if (read < 0)
throw ExceptionHelper.ServerClosedTCPStream();
return (byte)read;
}
#endregion
#region Public Functions
/// <summary>
/// Assembles all fragments into a final frame. Call this on the last fragment of a frame.
/// </summary>
/// <param name="fragments">The list of previously downloaded and parsed fragments of the frame</param>
public void Assemble(List<WebSocketFrameReader> fragments)
{
// this way the following algorithms will handle this fragment's data too
fragments.Add(this);
UInt64 finalLength = 0;
for (int i = 0; i < fragments.Count; ++i)
finalLength += (UInt64)fragments[i].Data.Count;
byte[] buffer = BufferPool.Get((long)finalLength, true);
UInt64 pos = 0;
for (int i = 0; i < fragments.Count; ++i)
{
if (fragments[i].Data.Count > 0)
Array.Copy(fragments[i].Data.Data, fragments[i].Data.Offset, buffer, (int)pos, (int)fragments[i].Data.Count);
fragments[i].ReleaseData();
pos += (UInt64)fragments[i].Data.Count;
}
// All fragments of a message are of the same type, as set by the first fragment's opcode.
this.Type = fragments[0].Type;
// Reserver flags may be contained only in the first fragment
this.Header = fragments[0].Header;
this.Data = new BufferSegment(buffer, 0, (int)finalLength);
}
/// <summary>
/// This function will decode the received data incrementally with the associated websocket's extensions.
/// </summary>
public void DecodeWithExtensions(WebSocket webSocket)
{
if (webSocket.Extensions != null)
for (int i = 0; i < webSocket.Extensions.Length; ++i)
{
var ext = webSocket.Extensions[i];
if (ext != null)
{
var newData = ext.Decode(this.Header, this.Data);
if (this.Data != newData)
{
this.ReleaseData();
this.Data = newData;
}
}
}
if (this.Type == WebSocketFrameTypes.Text)
{
if (this.Data != BufferSegment.Empty)
{
this.DataAsText = System.Text.Encoding.UTF8.GetString(this.Data.Data, this.Data.Offset, this.Data.Count);
this.ReleaseData();
}
else
HTTPManager.Logger.Warning("WebSocketFrameReader", "Empty Text frame received!");
}
}
public void ReleaseData()
{
BufferPool.Release(this.Data);
this.Data = BufferSegment.Empty;
}
public override string ToString()
{
return string.Format("[{0} Header: {1:X2}, IsFinal: {2}, Data: {3}]", this.Type.ToString(), this.Header, this.IsFinal, this.Data);
}
#endregion
}
}
#endif

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,728 @@
#if !UNITY_WEBGL || UNITY_EDITOR
using Best.HTTP;
using Best.HTTP.Hosts.Connections;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
using Best.HTTP.Shared.PlatformSupport.Threading;
using Best.HTTP.Shared.Streams;
using Best.WebSockets.Implementations.Frames;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Text;
using System.Threading;
namespace Best.WebSockets.Implementations
{
/// <summary>
/// Implements WebSocket communication through an HTTP/1 connection.
/// </summary>
internal sealed class OverHTTP1 : WebSocketBaseImplementation, IContentConsumer, IHeartbeat
{
public PeekableContentProviderStream ContentProvider { get; private set; }
/// <summary>
/// Indicates whether we sent out the connection request to the server.
/// </summary>
private bool requestSent;
private volatile bool _closed;
private UInt16 _closeCode;
private string _closeMessage;
private ConcurrentQueue<WebSocketFrame> unsentFrames = new ConcurrentQueue<WebSocketFrame>();
private volatile AutoResetEvent newFrameSignal = new AutoResetEvent(false);
public OverHTTP1(WebSocket parent, Uri uri, string origin, string protocol) : base(parent, uri, origin, protocol)
{
string scheme = HTTPProtocolFactory.IsSecureProtocol(uri) ? "wss" : "ws";
int port = uri.Port != -1 ? uri.Port : (scheme.Equals("wss", StringComparison.OrdinalIgnoreCase) ? 443 : 80);
// Somehow if i use the UriBuilder it's not the same as if the uri is constructed from a string...
//uri = new UriBuilder(uri.Scheme, uri.Host, uri.Scheme.Equals("wss", StringComparison.OrdinalIgnoreCase) ? 443 : 80, uri.PathAndQuery).Uri;
base.Uri = new Uri(scheme + "://" + uri.Host + ":" + port + uri.GetRequestPathAndQueryURL());
}
protected override void CreateInternalRequest()
{
if (this._internalRequest != null)
return;
this._internalRequest = new HTTPRequest(base.Uri, OnInternalRequestCallback);
this._internalRequest.Context.Add("WebSocket", this.Parent.Context);
// Called when the regular GET request is successfully upgraded to WebSocket
this._internalRequest.DownloadSettings.OnUpgraded = OnInternalRequestUpgraded;
//http://tools.ietf.org/html/rfc6455#section-4
// The request MUST contain an |Upgrade| header field whose value MUST include the "websocket" keyword.
this._internalRequest.SetHeader("Upgrade", "websocket");
// The request MUST contain a |Connection| header field whose value MUST include the "Upgrade" token.
this._internalRequest.SetHeader("Connection", "Upgrade");
// The request MUST include a header field with the name |Sec-WebSocket-Key|. The value of this header field MUST be a nonce consisting of a
// randomly selected 16-byte value that has been base64-encoded (see Section 4 of [RFC4648]). The nonce MUST be selected randomly for each connection.
this._internalRequest.SetHeader("Sec-WebSocket-Key", WebSocket.GetSecKey(new object[] { this, InternalRequest, base.Uri, new object() }));
// The request MUST include a header field with the name |Origin| [RFC6454] if the request is coming from a browser client.
// If the connection is from a non-browser client, the request MAY include this header field if the semantics of that client match the use-case described here for browser clients.
// More on Origin Considerations: http://tools.ietf.org/html/rfc6455#section-10.2
if (!string.IsNullOrEmpty(Origin))
this._internalRequest.SetHeader("Origin", Origin);
// The request MUST include a header field with the name |Sec-WebSocket-Version|. The value of this header field MUST be 13.
this._internalRequest.SetHeader("Sec-WebSocket-Version", "13");
if (!string.IsNullOrEmpty(Protocol))
this._internalRequest.SetHeader("Sec-WebSocket-Protocol", Protocol);
// Disable caching
this._internalRequest.SetHeader("Cache-Control", "no-cache");
this._internalRequest.DownloadSettings.DisableCache = true;
#if !UNITY_WEBGL || UNITY_EDITOR
this._internalRequest.ProxySettings = this.Parent.GetProxy(this.Uri);
#endif
this._internalRequest.RedirectSettings.OnBeforeRedirection += InternalRequest_OnBeforeRedirection;
if (this.Parent.OnInternalRequestCreated != null)
{
try
{
this.Parent.OnInternalRequestCreated(this.Parent, this._internalRequest);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(OverHTTP1), "CreateInternalRequest", ex, this.Parent.Context);
}
}
this._internalRequest.OnCancellationRequested += OnCancellationRequested;
}
private void OnCancellationRequested(HTTPRequest req)
{
HTTPManager.Logger.Information(nameof(OverHTTP1), $"{nameof(OnCancellationRequested)}", this.Parent.Context);
this._internalRequest.OnCancellationRequested -= OnCancellationRequested;
this._closed = true;
this.newFrameSignal?.Set();
}
private bool InternalRequest_OnBeforeRedirection(HTTPRequest originalRequest, HTTPResponse response, Uri redirectUri)
{
HTTPManager.Logger.Information(nameof(OverHTTP1), $"{nameof(InternalRequest_OnBeforeRedirection)}", this.Parent.Context);
// We have to re-select/reset the implementation in the parent Websocket, as the redirected request might gets served over a HTTP/2 connection!
this.Parent.SelectImplementation(redirectUri, originalRequest.GetFirstHeaderValue("Origin"), originalRequest.GetFirstHeaderValue("Sec-WebSocket-Protocol"))
.StartOpen();
originalRequest.Callback = null;
return false;
}
private bool OnInternalRequestUpgraded(HTTPRequest req, HTTPResponse resp, PeekableContentProviderStream contentProvider)
{
HTTPManager.Logger.Information(nameof(OverHTTP1), $"{nameof(OnInternalRequestUpgraded)}", this.Parent.Context);
if (this.State == WebSocketStates.Closed)
return false;
if (!resp.HasHeader("sec-websocket-accept"))
throw new Exception("No Sec-Websocket-Accept header is sent by the server!");
base.ParseExtensionResponse(resp);
// Save the provider
this.ContentProvider = contentProvider;
// Websocket continously reading from the stream, but it could stuck with frames larger than the MaxBufferSize.
if (this.ContentProvider is ITCPStreamerContentConsumer consumer && consumer is not null)
consumer.MaxBufferSize = long.MaxValue;
// Switch the comsumer to this websocket implementation instead of the http1 consumer.
contentProvider.SetTwoWayBinding(this);
// Start send thread
Best.HTTP.Shared.PlatformSupport.Threading.ThreadedRunner.RunLongLiving(SendThread);
return true;
}
private void OnInternalRequestCallback(HTTPRequest req, HTTPResponse resp)
{
HTTPManager.Logger.Information(nameof(OverHTTP1), $"{nameof(OnInternalRequestCallback)}", this.Parent.Context);
Cleanup();
string reason = string.Empty;
switch (req.State)
{
case HTTPRequestStates.Finished:
HTTPManager.Logger.Information(nameof(OverHTTP1), string.Format("Request finished. Status Code: {0} Message: {1}", resp.StatusCode.ToString(), resp.Message), this.Parent.Context);
if (resp.IsUpgraded)
{
return;
}
else
reason = string.Format("Request Finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2}",
resp.StatusCode,
resp.Message,
resp.DataAsText);
break;
// The request finished with an unexpected error. The request's Exception property may contain more info about the error.
case HTTPRequestStates.Error:
reason = req.Exception != null ? req.Exception.Message : string.Empty;
break;
// The request aborted, initiated by the user.
case HTTPRequestStates.Aborted:
reason = "Request Aborted!";
break;
// Connecting to the server is timed out.
case HTTPRequestStates.ConnectionTimedOut:
reason = "Connection Timed Out!";
break;
// The request didn't finished in the given time.
case HTTPRequestStates.TimedOut:
reason = "Processing the request Timed Out!";
break;
default:
return;
}
/*if (this.State != WebSocketStates.Connecting || !string.IsNullOrEmpty(reason))
{
if (this.Parent.OnError != null)
this.Parent.OnError(this.Parent, reason);
else if (!HTTPManager.IsQuitting)
HTTPManager.Logger.Error(nameof(OverHTTP1), reason, this.Parent.Context);
}*/
if (this.Parent.OnClosed != null)
{
this.Parent.OnClosed(this.Parent,
!string.IsNullOrEmpty(reason) ? WebSocketStatusCodes.ClosedAbnormally : WebSocketStatusCodes.NormalClosure,
reason ?? "Closed while opening");
}
this._closed = true;
this.State = WebSocketStates.Closed;
this.newFrameSignal?.Set();
this.ContentProvider?.Unbind();
}
private void SendThread()
{
HTTPManager.Logger.Information(nameof(OverHTTP1), "SendThread - created", this.Parent.Context);
ThreadedRunner.SetThreadName("Best.WebSockets Send");
try
{
bool doMask = !HTTPProtocolFactory.IsSecureProtocol(this.Uri);
var pingFreq = this.Parent.SendPings ? this.Parent.PingFrequency : TimeSpan.Zero;
using (WriteOnlyBufferedStream bufferedStream = new WriteOnlyBufferedStream(this.ContentProvider as Stream, 16 * 1024, this.Parent.Context))
{
while (!this._closed)
{
//if (HTTPManager.Logger.Level <= Logger.Loglevels.All)
// HTTPManager.Logger.Information(nameof(OverHTTP1), "SendThread - Waiting...", this.Context);
TimeSpan waitTime = TimeSpan.FromMilliseconds(int.MaxValue);
if (pingFreq != TimeSpan.Zero)
{
DateTime now = DateTime.UtcNow;
DateTime pingTime = this.lastPing;
waitTime = pingTime + pingFreq - now;
if (waitTime <= TimeSpan.Zero)
{
if (!waitingForPong && now - pingTime >= pingFreq)
{
SendPing();
pingTime = this.lastPing;
}
waitTime = this.Parent.CloseAfterNoMessage;
}
if (waitingForPong && (now - pingTime > this.Parent.CloseAfterNoMessage) && (now - LastMessageReceived > this.Parent.CloseAfterNoMessage))
{
HTTPManager.Logger.Warning(nameof(OverHTTP1),
$"No pong received in the given time! LastPing: {pingTime}, PingFrequency: {pingFreq}, Close After: {this.Parent.CloseAfterNoMessage}, Now: {now}",
this.Parent.Context);
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this._internalRequest, HTTPRequestStates.Error, new Exception("No PONG received in the given time!")));
this._closed = true;
continue;
}
}
newFrameSignal.WaitOne(waitTime);
try
{
//if (HTTPManager.Logger.Level <= Logger.Loglevels.All)
// HTTPManager.Logger.Information(nameof(OverHTTP1), "SendThread - Wait is over, about " + this.unsentFrames.Count.ToString() + " new frames!", this.Context);
WebSocketFrame frame;
while (!this._closeSent && this.unsentFrames.TryDequeue(out frame))
{
// save data count as per-message deflate can compress, and it would be different after calling WriteTo
int originalFrameDataLength = frame.Data.Count;
frame.WriteTo((header, chunk) =>
{
bufferedStream.Write(header.Data, header.Offset, header.Count);
BufferPool.Release(header);
if (chunk != BufferSegment.Empty)
bufferedStream.Write(chunk.Data, chunk.Offset, chunk.Count);
}, WebSocket.MaxFragmentSize, doMask, this.Parent.Context);
BufferPool.Release(frame.Data);
if (frame.Type == WebSocketFrameTypes.ConnectionClose)
{
this._closeSent = true;
if (this._closeReceived)
{
this._closed = true;
this.State = WebSocketStates.Closed;
}
}
Interlocked.Add(ref this._bufferedAmount, -originalFrameDataLength);
}
bufferedStream.Flush();
}
catch (Exception ex)
{
//this._internalRequest.Timing.Finish(Timing_Name);
if (HTTPUpdateDelegator.IsCreated)
{
//this._internalRequest.Exception = ex;
//this._internalRequest.State = HTTPRequestStates.Error;
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this._internalRequest, HTTPRequestStates.Error, ex));
}
else
{
//this._internalRequest.State = HTTPRequestStates.Aborted;
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this._internalRequest, HTTPRequestStates.Aborted, null));
}
HTTPManager.Logger.Exception(nameof(OverHTTP1), "Frame sending", ex, this.Parent.Context);
this._closed = true;
this.State = WebSocketStates.Closed;
}
}
HTTPManager.Logger.Information(nameof(OverHTTP1), string.Format("Ending Send thread. Closed: {0}, closeSent: {1}", this._closed, this._closeSent), this.Parent.Context);
}
}
catch (Exception ex)
{
if (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Exception(nameof(OverHTTP1), "SendThread", ex, this.Parent.Context);
}
finally
{
HTTPManager.Logger.Information(nameof(OverHTTP1), "SendThread - Closed!", this.Parent.Context);
this.newFrameSignal?.Dispose();
this.newFrameSignal = null;
}
}
private void SendPing()
{
HTTPManager.Logger.Information(nameof(OverHTTP1), "Sending Ping frame, waiting for a pong...", this.Parent.Context);
lastPing = DateTime.UtcNow;
waitingForPong = true;
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Ping, BufferSegment.Empty));
}
public override void StartOpen()
{
HTTPManager.Logger.Information(nameof(OverHTTP1), $"{nameof(StartOpen)}", this.Parent.Context);
if (requestSent)
throw new InvalidOperationException("Open already called! You can't reuse this WebSocket instance!");
if (this.Parent.Extensions != null)
{
try
{
for (int i = 0; i < this.Parent.Extensions.Length; ++i)
{
var ext = this.Parent.Extensions[i];
if (ext != null)
ext.AddNegotiation(InternalRequest);
}
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(OverHTTP1), "Open", ex, this.Parent.Context);
}
}
InternalRequest.Send();
requestSent = true;
this.State = WebSocketStates.Connecting;
HTTPManager.Heartbeats.Subscribe(this);
}
public override void StartClose(WebSocketStatusCodes code, string message)
{
HTTPManager.Logger.Information(nameof(OverHTTP1), $"{nameof(StartClose)}({code}, {message})", this.Parent.Context);
if (this.State == WebSocketStates.Connecting)
{
if (this.InternalRequest != null)
this.InternalRequest.Abort();
this.State = WebSocketStates.Closed;
if (this.Parent.OnClosed != null)
this.Parent.OnClosed(this.Parent, WebSocketStatusCodes.NormalClosure, string.Empty);
}
else
{
this.State = WebSocketStates.Closing;
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.ConnectionClose, WebSocket.EncodeCloseData(code, message), false));
}
}
public override void Send(string message)
{
if (message == null)
throw new ArgumentNullException("message must not be null!");
int count = System.Text.Encoding.UTF8.GetByteCount(message);
byte[] data = BufferPool.Get(count, true);
System.Text.Encoding.UTF8.GetBytes(message, 0, message.Length, data, 0);
Send(WebSocketFrameTypes.Text, data.AsBuffer(count));
}
public override void Send(byte[] data)
{
if (data == null)
throw new ArgumentNullException("data must not be null!");
WebSocketFrame frame = new WebSocketFrame(this.Parent, WebSocketFrameTypes.Binary, new BufferSegment(data, 0, data.Length));
Send(frame);
}
public override void Send(byte[] data, ulong offset, ulong count)
{
if (data == null)
throw new ArgumentNullException("data must not be null!");
if (offset + count > (ulong)data.Length)
throw new ArgumentOutOfRangeException("offset + count >= data.Length");
WebSocketFrame frame = new WebSocketFrame(this.Parent, WebSocketFrameTypes.Binary, new BufferSegment(data, (int)offset, (int)count), true);
Send(frame);
}
public void Send(WebSocketFrameTypes type, BufferSegment data)
{
WebSocketFrame frame = new WebSocketFrame(this.Parent, type, data, false);
Send(frame);
}
public override void SendAsBinary(BufferSegment data)
{
Send(WebSocketFrameTypes.Binary, data);
}
public override void SendAsText(BufferSegment data)
{
Send(WebSocketFrameTypes.Text, data);
}
public override void Send(WebSocketFrame frame)
{
if (this._closed || this._closeSent)
return;
this.unsentFrames.Enqueue(frame);
Interlocked.Add(ref this._bufferedAmount, frame.Data.Count);
newFrameSignal.Set();
}
public void SetBinding(PeekableContentProviderStream stream)
{
this.ContentProvider = stream;
// Read any frames already in the buffers
OnContent();
}
public void UnsetBinding()
{
this.ContentProvider?.Dispose();
this.ContentProvider = null;
}
public void OnContent()
{
this.LastMessageReceived = DateTime.UtcNow;
if (this._closeReceived || this._closed)
return;
while (CanReadFullFrame(this.ContentProvider))
{
WebSocketFrameReader frame = new WebSocketFrameReader();
frame.Read(this.ContentProvider);
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(OverHTTP1), "Frame received: " + frame.ToString(), this.Parent.Context);
if (!frame.IsFinal)
{
IncompleteFrames.Add(frame);
continue;
}
switch (frame.Type)
{
// For a complete documentation and rules on fragmentation see http://tools.ietf.org/html/rfc6455#section-5.4
// A fragmented Frame's last fragment's opcode is 0 (Continuation) and the FIN bit is set to 1.
case WebSocketFrameTypes.Continuation:
// Do an assemble pass only if OnFragment is not set. Otherwise put it in the CompletedFrames, we will handle it in the HandleEvent phase.
frame.Assemble(IncompleteFrames);
// Remove all incomplete frames
IncompleteFrames.Clear();
// Control frames themselves MUST NOT be fragmented. So, its a normal text or binary frame. Go, handle it as usual.
goto case WebSocketFrameTypes.Binary;
case WebSocketFrameTypes.Text:
case WebSocketFrameTypes.Binary:
frame.DecodeWithExtensions(this.Parent);
CompletedFrames.Enqueue(frame);
break;
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in response, unless it already received a Close frame.
case WebSocketFrameTypes.Ping:
if (!_closeSent && this.State != WebSocketStates.Closed)
{
// copy data set to true here, as the frame's data is released back to the pool after the switch
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Pong, frame.Data, true));
}
break;
case WebSocketFrameTypes.Pong:
// https://tools.ietf.org/html/rfc6455#section-5.5
// A Pong frame MAY be sent unsolicited. This serves as a
// unidirectional heartbeat. A response to an unsolicited Pong frame is
// not expected.
if (!waitingForPong)
break;
waitingForPong = false;
// the difference between the current time and the time when the ping message is sent
TimeSpan diff = DateTime.UtcNow - lastPing;
// add it to the buffer
this.rtts.Add((int)diff.TotalMilliseconds);
// and calculate the new latency
base.Latency = CalculateLatency();
break;
// If an endpoint receives a Close frame and did not previously send a Close frame, the endpoint MUST send a Close frame in response.
case WebSocketFrameTypes.ConnectionClose:
this._closeReceived = true;
HTTPManager.Logger.Information(nameof(OverHTTP1), $"ConnectionClose packet received! ({this._closeReceived}, {this._closeSent})", this.Parent.Context);
CompletedFrames.Enqueue(frame);
break;
}
}
}
public void OnConnectionClosed()
{
if (this._closed)
return;
//this._internalRequest.Timing.Finish(Timing_Name);
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this._internalRequest, HTTPRequestStates.Error, new Exception("Connection closed unexpectedly!")));
}
public void OnError(Exception ex)
{
if (this._closed)
return;
//this._internalRequest.Timing.Finish(Timing_Name);
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this._internalRequest, HTTPRequestStates.Error, ex));
}
public void OnHeartbeatUpdate(DateTime now, TimeSpan dif)
{
if (HTTPManager.IsQuitting)
this.StartClose(WebSocketStatusCodes.GoingAway, "Editor closing");
switch (this.State)
{
case WebSocketStates.Connecting:
if (requestSent && this._internalRequest?.Response?.IsUpgraded is bool upgraded && upgraded)
{
this.State = WebSocketStates.Open;
// The request upgraded successfully.
if (this.Parent.OnOpen != null)
this.Parent.OnOpen(this.Parent);
OnHeartbeatUpdate(now, dif);
}
break;
case WebSocketStates.Closing:
// TODO: define and handle a timeout
HandleCompletedFrames();
break;
case WebSocketStates.Closed:
HandleCompletedFrames();
HTTPManager.Heartbeats.Unsubscribe(this);
this.ContentProvider?.Unbind();
if (this._internalRequest != null && this._internalRequest.State < HTTPRequestStates.Finished)
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this._internalRequest, HTTPRequestStates.Finished, null));
// TODO: go through any lists and queues to empty and recycle buffer segments
// this.unsentFrames.TryDequeue(out frame)
if (this.Parent.OnClosed != null)
{
try
{
this.Parent.OnClosed(this.Parent, (WebSocketStatusCodes)this._closeCode, this._closeMessage);
this.Parent.OnClosed = null;
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(OverHTTP1), "HandleEvents - OnClosed", ex, this.Parent.Context);
}
}
break;
default:
HandleCompletedFrames();
break;
}
}
private void HandleCompletedFrames()
{
while (CompletedFrames.TryDequeue(out var frame))
{
try
{
switch (frame.Type)
{
case WebSocketFrameTypes.Continuation:
if (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Verbose(nameof(OverHTTP1), "HandleEvents - OnIncompleteFrame", this.Parent.Context);
break;
case WebSocketFrameTypes.Text:
// Any not Final frame is handled as a fragment
if (!frame.IsFinal)
goto case WebSocketFrameTypes.Continuation;
if (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Verbose(nameof(OverHTTP1), $"HandleEvents - OnText(\"{frame.DataAsText}\")", this.Parent.Context);
if (this.Parent.OnMessage != null)
this.Parent.OnMessage(this.Parent, frame.DataAsText);
break;
case WebSocketFrameTypes.Binary:
// Any not Final frame is handled as a fragment
if (!frame.IsFinal)
goto case WebSocketFrameTypes.Continuation;
if (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Verbose(nameof(OverHTTP1), $"HandleEvents - OnBinary({frame.Data})", this.Parent.Context);
if (this.Parent.OnBinary != null)
this.Parent.OnBinary(this.Parent, frame.Data);
break;
case WebSocketFrameTypes.ConnectionClose:
HTTPManager.Logger.Verbose(nameof(OverHTTP1), "HandleEvents - Calling OnClosed", this.Parent.Context);
if (!this._closeSent)
{
this.State = WebSocketStates.Closing;
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.ConnectionClose, BufferSegment.Empty));
}
else
{
this._closed = true;
this.State = WebSocketStates.Closed;
this.newFrameSignal?.Set();
}
if (frame.Data != BufferSegment.Empty && frame.Data.Count >= 2)
{
if (BitConverter.IsLittleEndian)
Array.Reverse(frame.Data.Data, frame.Data.Offset, 2);
this._closeCode = BitConverter.ToUInt16(frame.Data.Data, frame.Data.Offset);
if (frame.Data.Count > 2)
this._closeMessage = Encoding.UTF8.GetString(frame.Data.Data, frame.Data.Offset + 2, frame.Data.Count - 2);
frame.ReleaseData();
}
break;
}
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(OverHTTP1), string.Format("HandleEvents({0})", frame.ToString()), ex, this.Parent.Context);
}
finally
{
frame.ReleaseData();
}
}
}
}
}
#endif

View File

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

View File

@ -0,0 +1,584 @@
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using Best.HTTP;
using Best.HTTP.Hosts.Connections;
using Best.HTTP.Hosts.Connections.HTTP2;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.WebSockets.Implementations.Frames;
namespace Best.WebSockets.Implementations
{
/// <summary>
/// Implements RFC 8441 (https://tools.ietf.org/html/rfc8441) to use Websocket over HTTP/2
/// </summary>
public sealed class OverHTTP2 : WebSocketBaseImplementation, IHeartbeat
{
public override int Latency { get { return base.Latency; } }
public int BufferedFramesCount { get => base.frames.Count; }
private IThreadSignaler threadSignaler;
public OverHTTP2(WebSocket parent, Uri uri, string origin, string protocol) : base(parent, uri, origin, protocol)
{
// use https scheme so it will be served over HTTP/2. The request's Tag will be set to this class' instance so HTTP2Handler will know it has to create a HTTP2WebSocketStream instance to
// process the request.
string scheme = "https";
int port = uri.Port != -1 ? uri.Port : 443;
base.Uri = new Uri(scheme + "://" + uri.Host + ":" + port + uri.GetRequestPathAndQueryURL());
}
internal void SetThreadSignaler(IThreadSignaler signaler) => this.threadSignaler = signaler;
protected override void CreateInternalRequest()
{
HTTPManager.Logger.Verbose("OverHTTP2", "CreateInternalRequest", this.Parent.Context);
base._internalRequest = new HTTPRequest(base.Uri, HTTPMethods.Connect, OnInternalRequestCallback);
base._internalRequest.Context.Add("WebSocket", this.Parent.Context);
base._internalRequest.SetHeader(":protocol", "websocket");
// The request MUST include a header field with the name |Sec-WebSocket-Key|. The value of this header field MUST be a nonce consisting of a
// randomly selected 16-byte value that has been base64-encoded (see Section 4 of [RFC4648]). The nonce MUST be selected randomly for each connection.
base._internalRequest.SetHeader("sec-webSocket-key", WebSocket.GetSecKey(new object[] { this, InternalRequest, base.Uri, new object() }));
// The request MUST include a header field with the name |Origin| [RFC6454] if the request is coming from a browser client.
// If the connection is from a non-browser client, the request MAY include this header field if the semantics of that client match the use-case described here for browser clients.
// More on Origin Considerations: http://tools.ietf.org/html/rfc6455#section-10.2
if (!string.IsNullOrEmpty(base.Origin))
base._internalRequest.SetHeader("origin", base.Origin);
// The request MUST include a header field with the name |Sec-WebSocket-Version|. The value of this header field MUST be 13.
base._internalRequest.SetHeader("sec-webSocket-version", "13");
if (!string.IsNullOrEmpty(base.Protocol))
base._internalRequest.SetHeader("sec-webSocket-protocol", base.Protocol);
// Disable caching
base._internalRequest.SetHeader("cache-control", "no-cache");
base._internalRequest.DownloadSettings.DisableCache = true;
base._internalRequest.DownloadSettings.OnHeadersReceived += OnHeadersReceived;
// set a fake upload stream, so HPACKEncoder will not set the END_STREAM flag
base._internalRequest.UploadSettings.UploadStream = new MemoryStream(0);
// TODO:
//base._internalRequest.UseUploadStreamLength = false;
this.LastMessageReceived = DateTime.UtcNow;
base._internalRequest.Tag = (CustomHTTP2StreamFactory)HTTP2WebSocketStreamFactory;
if (this.Parent.OnInternalRequestCreated != null)
{
try
{
this.Parent.OnInternalRequestCreated(this.Parent, base._internalRequest);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", "CreateInternalRequest", ex, this.Parent.Context);
}
}
}
private HTTP2WebSocketStream HTTP2WebSocketStreamFactory(HTTPRequest request, uint id, HTTP2ContentConsumer parentHandler, HTTP2SettingsManager registry, HPACKEncoder hpackEncoder)
{
request.Tag = this;
return new HTTP2WebSocketStream(id, parentHandler, registry, hpackEncoder);
}
private void OnHeadersReceived(HTTPRequest req, HTTPResponse resp, Dictionary<string, List<string>> newHeaders)
{
HTTPManager.Logger.Verbose("OverHTTP2", $"OnHeadersReceived - StatusCode: {resp?.StatusCode}", this.Parent.Context);
if (resp != null && resp.StatusCode == 200)
{
base.ParseExtensionResponse(resp);
this.State = WebSocketStates.Open;
if (this.Parent.OnOpen != null)
{
try
{
this.Parent.OnOpen(this.Parent);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", "OnOpen", ex, this.Parent.Context);
}
}
if (this.Parent.SendPings)
{
this.LastMessageReceived = DateTime.UtcNow;
SendPing();
}
}
else
req.Abort();
}
internal void OnReadThread(BufferSegment buffer)
{
this.LastMessageReceived = DateTime.UtcNow;
this.incomingSegmentStream.Write(buffer);
while (CanReadFullFrame(this.incomingSegmentStream))
{
WebSocketFrameReader frame = new WebSocketFrameReader();
frame.Read(this.incomingSegmentStream);
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose("OverHTTP2", "Frame received: " + frame.ToString(), this.Parent.Context);
if (!frame.IsFinal)
{
IncompleteFrames.Add(frame);
continue;
}
switch (frame.Type)
{
// For a complete documentation and rules on fragmentation see http://tools.ietf.org/html/rfc6455#section-5.4
// A fragmented Frame's last fragment's opcode is 0 (Continuation) and the FIN bit is set to 1.
case WebSocketFrameTypes.Continuation:
frame.Assemble(IncompleteFrames);
// Remove all incomplete frames
IncompleteFrames.Clear();
// Control frames themselves MUST NOT be fragmented. So, its a normal text or binary frame. Go, handle it as usual.
goto case WebSocketFrameTypes.Binary;
case WebSocketFrameTypes.Text:
case WebSocketFrameTypes.Binary:
frame.DecodeWithExtensions(this.Parent);
CompletedFrames.Enqueue(frame);
break;
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in response, unless it already received a Close frame.
case WebSocketFrameTypes.Ping:
if (!_closeSent && this.State != WebSocketStates.Closed)
{
// copy data set to true here, as the frame's data is released back to the pool after the switch
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Pong, frame.Data, true));
}
break;
case WebSocketFrameTypes.Pong:
// https://tools.ietf.org/html/rfc6455#section-5.5
// A Pong frame MAY be sent unsolicited. This serves as a
// unidirectional heartbeat. A response to an unsolicited Pong frame is
// not expected.
if (!waitingForPong)
break;
waitingForPong = false;
// the difference between the current time and the time when the ping message is sent
TimeSpan diff = DateTime.UtcNow - lastPing;
// add it to the buffer
this.rtts.Add((int)diff.TotalMilliseconds);
// and calculate the new latency
base.Latency = CalculateLatency();
break;
// If an endpoint receives a Close frame and did not previously send a Close frame, the endpoint MUST send a Close frame in response.
case WebSocketFrameTypes.ConnectionClose:
HTTPManager.Logger.Information("OverHTTP2", "ConnectionClose packet received!", this.Parent.Context);
CompletedFrames.Enqueue(frame);
if (!_closeSent)
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.ConnectionClose, BufferSegment.Empty));
this.State = WebSocketStates.Closed;
break;
}
}
}
private void OnInternalRequestCallback(HTTPRequest req, HTTPResponse resp)
{
HTTPManager.Logger.Verbose("OverHTTP2", $"OnInternalRequestCallback - this.State: {this.State}", this.Parent.Context);
Cleanup();
// If it's already closed, all events are called too.
if (this.State == WebSocketStates.Closed)
return;
if (this.State == WebSocketStates.Connecting && HTTPManager.PerHostSettings.Get(this.Uri).HTTP2ConnectionSettings.WebSocketOverHTTP2Settings.EnableImplementationFallback)
{
this.Parent.FallbackToHTTP1();
HTTPManager.Heartbeats.Unsubscribe(this);
return;
}
string reason = string.Empty;
switch (req.State)
{
case HTTPRequestStates.Finished:
HTTPManager.Logger.Information("OverHTTP2", string.Format("Request finished. Status Code: {0} Message: {1}", resp.StatusCode.ToString(), resp.Message), this.Parent.Context);
if (resp.StatusCode == 101)
{
// The request upgraded successfully.
return;
}
else
reason = string.Format("Request Finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2}",
resp.StatusCode,
resp.Message,
resp.DataAsText);
break;
// The request finished with an unexpected error. The request's Exception property may contain more info about the error.
case HTTPRequestStates.Error:
reason = "Request Finished with Error! " + (req.Exception != null ? ("Exception: " + req.Exception.Message + req.Exception.StackTrace) : string.Empty);
break;
// The request aborted, initiated by the user.
case HTTPRequestStates.Aborted:
reason = "Request Aborted!";
break;
// Connecting to the server is timed out.
case HTTPRequestStates.ConnectionTimedOut:
reason = "Connection Timed Out!";
break;
// The request didn't finished in the given time.
case HTTPRequestStates.TimedOut:
reason = "Processing the request Timed Out!";
break;
default:
return;
}
if (this.Parent.OnClosed != null)
{
try
{
this.Parent.OnClosed(this.Parent,
!string.IsNullOrEmpty(reason) ? WebSocketStatusCodes.ClosedAbnormally : WebSocketStatusCodes.NormalClosure,
reason ?? "Closed while opening");
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", "OnClosed", ex, this.Parent.Context);
}
}
this.State = WebSocketStates.Closed;
}
public override void StartOpen()
{
HTTPManager.Logger.Verbose("OverHTTP2", "StartOpen", this.Parent.Context);
if (this.Parent.Extensions != null)
{
try
{
for (int i = 0; i < this.Parent.Extensions.Length; ++i)
{
var ext = this.Parent.Extensions[i];
if (ext != null)
ext.AddNegotiation(base.InternalRequest);
}
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", "Open", ex, this.Parent.Context);
}
}
base.InternalRequest.Send();
HTTPManager.Heartbeats.Subscribe(this);
this.State = WebSocketStates.Connecting;
}
public override void StartClose(WebSocketStatusCodes code, string message)
{
HTTPManager.Logger.Verbose("OverHTTP2", "StartClose", this.Parent.Context);
if (this.State == WebSocketStates.Connecting)
{
if (this.InternalRequest != null)
this.InternalRequest.Abort();
this.State = WebSocketStates.Closed;
if (this.Parent.OnClosed != null)
this.Parent.OnClosed(this.Parent, WebSocketStatusCodes.NormalClosure, message);
}
else
{
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.ConnectionClose, WebSocket.EncodeCloseData(code, message), false));
this.State = WebSocketStates.Closing;
}
}
public override void Send(string message)
{
if (message == null)
throw new ArgumentNullException("message must not be null!");
int count = System.Text.Encoding.UTF8.GetByteCount(message);
byte[] data = BufferPool.Get(count, true);
System.Text.Encoding.UTF8.GetBytes(message, 0, message.Length, data, 0);
SendAsText(data.AsBuffer(0, count));
}
public override void Send(byte[] buffer)
{
if (buffer == null)
throw new ArgumentNullException("data must not be null!");
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Binary, new BufferSegment(buffer, 0, buffer.Length)));
}
public override void Send(byte[] data, ulong offset, ulong count)
{
if (data == null)
throw new ArgumentNullException("data must not be null!");
if (offset + count > (ulong)data.Length)
throw new ArgumentOutOfRangeException("offset + count >= data.Length");
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Binary, new BufferSegment(data, (int)offset, (int)count), true));
}
public override void Send(WebSocketFrame frame)
{
if (this.State == WebSocketStates.Closed || _closeSent)
return;
this.frames.Enqueue(frame);
this.threadSignaler.SignalThread();
Interlocked.Add(ref base._bufferedAmount, frame.Data.Count);
if (frame.Type == WebSocketFrameTypes.ConnectionClose)
this._closeSent = true;
}
public override void SendAsBinary(BufferSegment data)
{
Send(WebSocketFrameTypes.Binary, data);
}
public override void SendAsText(BufferSegment data)
{
Send(WebSocketFrameTypes.Text, data);
}
private void Send(WebSocketFrameTypes type, BufferSegment data)
{
Send(new WebSocketFrame(this.Parent, type, data, false));
}
internal void PreReadCallback()
{
if (this.Parent.SendPings)
{
DateTime now = DateTime.UtcNow;
if (!waitingForPong && now - LastMessageReceived >= this.Parent.PingFrequency)
SendPing();
if (waitingForPong && now - lastPing > this.Parent.CloseAfterNoMessage)
{
if (this.State != WebSocketStates.Closed)
{
HTTPManager.Logger.Warning("OverHTTP2",
string.Format("No message received in the given time! Closing WebSocket. LastPing: {0}, PingFrequency: {1}, Close After: {2}, Now: {3}",
this.lastPing, this.Parent.PingFrequency, this.Parent.CloseAfterNoMessage, now), this.Parent.Context);
CloseWithError("No message received in the given time!");
}
}
}
}
public void OnHeartbeatUpdate(DateTime now, TimeSpan dif)
{
switch (this.State)
{
case WebSocketStates.Connecting:
if (now - this.InternalRequest.Timing.Created >= this.Parent.CloseAfterNoMessage)
{
if (HTTPManager.PerHostSettings.Get(this.Uri).HTTP2ConnectionSettings.WebSocketOverHTTP2Settings.EnableImplementationFallback)
{
this.State = WebSocketStates.Closed;
this.InternalRequest.DownloadSettings.OnHeadersReceived = null;
this.InternalRequest.Callback = null;
this.Parent.FallbackToHTTP1();
HTTPManager.Heartbeats.Unsubscribe(this);
}
else
{
CloseWithError("WebSocket Over HTTP/2 Implementation failed to connect in the given time!");
}
}
break;
default:
while (CompletedFrames.TryDequeue(out var frame))
{
// Bugs in the clients shouldn't interrupt the code, so we need to try-catch and ignore any exception occurring here
try
{
switch (frame.Type)
{
case WebSocketFrameTypes.Continuation:
if (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Verbose("OverHTTP2", "HandleEvents - OnIncompleteFrame", this.Parent.Context);
break;
case WebSocketFrameTypes.Text:
// Any not Final frame is handled as a fragment
if (!frame.IsFinal)
goto case WebSocketFrameTypes.Continuation;
if (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Verbose("OverHTTP2", $"HandleEvents - OnText(\"{frame.DataAsText}\")", this.Parent.Context);
if (this.Parent.OnMessage != null)
this.Parent.OnMessage(this.Parent, frame.DataAsText);
break;
case WebSocketFrameTypes.Binary:
// Any not Final frame is handled as a fragment
if (!frame.IsFinal)
goto case WebSocketFrameTypes.Continuation;
if (HTTPManager.Logger.Level == Loglevels.All)
HTTPManager.Logger.Verbose("OverHTTP2", $"HandleEvents - OnBinary({frame.Data})", this.Parent.Context);
if (this.Parent.OnBinary != null)
this.Parent.OnBinary(this.Parent, frame.Data);
break;
case WebSocketFrameTypes.ConnectionClose:
HTTPManager.Logger.Verbose("OverHTTP2", "HandleEvents - Calling OnClosed", this.Parent.Context);
if (this.Parent.OnClosed != null)
{
try
{
UInt16 statusCode = 0;
string msg = string.Empty;
// If we received any data, we will get the status code and the message from it
if (/*CloseFrame != null && */ frame.Data != BufferSegment.Empty && frame.Data.Count >= 2)
{
if (BitConverter.IsLittleEndian)
Array.Reverse(frame.Data.Data, frame.Data.Offset, 2);
statusCode = BitConverter.ToUInt16(frame.Data.Data, frame.Data.Offset);
if (frame.Data.Count > 2)
msg = Encoding.UTF8.GetString(frame.Data.Data, frame.Data.Offset + 2, frame.Data.Count - 2);
frame.ReleaseData();
}
this.Parent.OnClosed(this.Parent, (WebSocketStatusCodes)statusCode, msg);
this.Parent.OnClosed = null;
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", "HandleEvents - OnClosed", ex, this.Parent.Context);
}
}
HTTPManager.Heartbeats.Unsubscribe(this);
break;
}
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", string.Format("HandleEvents({0})", frame.ToString()), ex, this.Parent.Context);
}
finally
{
frame.ReleaseData();
}
}
break;
}
}
/// <summary>
/// Next interaction relative to *now*.
/// </summary>
public TimeSpan GetNextInteraction()
{
if (waitingForPong)
return TimeSpan.MaxValue;
return (LastMessageReceived + this.Parent.PingFrequency) - DateTime.UtcNow;
}
private void SendPing()
{
HTTPManager.Logger.Information("OverHTTP2", "Sending Ping frame, waiting for a pong...", this.Parent.Context);
lastPing = DateTime.UtcNow;
waitingForPong = true;
Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Ping, BufferSegment.Empty));
}
private void CloseWithError(string message)
{
HTTPManager.Logger.Verbose("OverHTTP2", $"CloseWithError(\"{message}\")", this.Parent.Context);
this.State = WebSocketStates.Closed;
if (this.Parent.OnClosed != null)
{
try
{
this.Parent.OnClosed(this.Parent, WebSocketStatusCodes.ClosedAbnormally, message);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP2", "CloseWithError", ex, this.Parent.Context);
}
}
this.InternalRequest.Abort();
HTTPManager.Heartbeats.Unsubscribe(this);
}
internal bool TryDequeueFrame(out WebSocketFrame frame)
{
if (base.frames.TryDequeue(out frame))
{
Interlocked.Add(ref base._bufferedAmount, frame.Data.Count);
return true;
}
return false;
}
}
}
#endif

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,283 @@
#if UNITY_WEBGL && !UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Best.HTTP.Shared;
using Best.HTTP.Shared.PlatformSupport.Memory;
namespace Best.WebSockets.Implementations
{
delegate void OnWebGLWebSocketOpenDelegate(uint id);
delegate void OnWebGLWebSocketTextDelegate(uint id, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 2)] byte[] textBuffer, int allocatedLength, int length);
delegate void OnWebGLWebSocketBinaryDelegate(uint id, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 2)] byte[] buffer, int allocatedLength, int length);
delegate void OnWebGLWebSocketErrorDelegate(uint id, string error);
delegate void OnWebGLWebSocketCloseDelegate(uint id, int code, string reason);
delegate IntPtr OnWebGLAllocArray(int nativeId, int size);
internal sealed class WebGLBrowser : WebSocketBaseImplementation
{
public override WebSocketStates State => ImplementationId != 0 ? WS_GetState(ImplementationId) : WebSocketStates.Unknown;
public override bool IsOpen => ImplementationId != 0 && WS_GetState(ImplementationId) == WebSocketStates.Open;
public override int BufferedAmount => WS_GetBufferedAmount(ImplementationId);
internal static Dictionary<uint, WebSocket> WebSockets = new Dictionary<uint, WebSocket>();
private uint ImplementationId;
public WebGLBrowser(WebSocket parent, Uri uri, string origin, string protocol) : base(parent, uri, origin, protocol)
{
}
public override void StartOpen()
{
try
{
ImplementationId = WS_Create(this.Uri.OriginalString, this.Protocol, OnOpenCallback, OnTextCallback, OnBinaryCallback, OnErrorCallback, OnCloseCallback, Allocator);
WebSockets.Add(ImplementationId, this.Parent);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "Open", ex, this.Parent.Context);
}
}
public override void StartClose(WebSocketStatusCodes code, string message)
{
WS_Close(this.ImplementationId, (ushort)code, message);
}
public override void Send(string message)
{
var count = System.Text.Encoding.UTF8.GetByteCount(message);
var buffer = BufferPool.Get(count, true);
System.Text.Encoding.UTF8.GetBytes(message, 0, message.Length, buffer, 0);
WS_Send_String(this.ImplementationId, buffer, 0, count);
BufferPool.Release(buffer);
}
public override void Send(byte[] buffer)
{
WS_Send_Binary(this.ImplementationId, buffer, 0, buffer.Length);
}
public override void Send(byte[] buffer, ulong offset, ulong count)
{
WS_Send_Binary(this.ImplementationId, buffer, (int)offset, (int)count);
}
public override void SendAsBinary(BufferSegment data)
{
WS_Send_Binary(this.ImplementationId, data.Data, data.Offset, data.Count);
BufferPool.Release(data);
}
public override void SendAsText(BufferSegment data)
{
WS_Send_String(this.ImplementationId, data.Data, data.Offset, data.Count);
BufferPool.Release(data);
}
[DllImport("__Internal")]
static extern uint WS_Create(string url,
string protocol,
OnWebGLWebSocketOpenDelegate onOpen,
OnWebGLWebSocketTextDelegate onText,
OnWebGLWebSocketBinaryDelegate onBinary,
OnWebGLWebSocketErrorDelegate onError,
OnWebGLWebSocketCloseDelegate onClose,
OnWebGLAllocArray allocator);
[DllImport("__Internal")]
static extern WebSocketStates WS_GetState(uint id);
[DllImport("__Internal")]
static extern int WS_GetBufferedAmount(uint id);
[DllImport("__Internal")]
static extern int WS_Send_String(uint id, byte[] strData, int pos, int length);
[DllImport("__Internal")]
static extern int WS_Send_Binary(uint id, byte[] buffer, int pos, int length);
[DllImport("__Internal")]
static extern void WS_Close(uint id, ushort code, string reason);
[DllImport("__Internal")]
static extern void WS_Release(uint id);
[AOT.MonoPInvokeCallback(typeof(OnWebGLAllocArray))]
static unsafe IntPtr Allocator(int nativeId, int length)
{
byte[] buffer = BufferPool.Get(length, true);
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(WebGLBrowser), $"Allocator - allocated: {buffer.Length}");
buffer[0] = (byte)(buffer.Length >> 24);
buffer[1] = (byte)(buffer.Length >> 16);
buffer[2] = (byte)(buffer.Length >> 8);
buffer[3] = (byte)(buffer.Length);
fixed (byte* ptr = buffer)
{
var p = (IntPtr)ptr;
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(WebGLBrowser), $"({p}) <= Allocator({nativeId}, {length})");
return p;
}
}
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketOpenDelegate))]
static void OnOpenCallback(uint id)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
if (ws.OnOpen != null)
{
try
{
ws.OnOpen(ws);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnOpen", ex, ws.Context);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnOpenCallback - No WebSocket found for id: " + id.ToString(), ws.Context);
}
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketTextDelegate))]
static void OnTextCallback(uint id, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 2)] byte[] textBuffer, int allocatedLength, int length)
{
try
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
if (ws.OnMessage != null)
{
try
{
var text = System.Text.Encoding.UTF8.GetString(textBuffer, 0, length);
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(WebGLBrowser), $"{id}, {textBuffer}, {length} => {text}", ws.Context);
ws.OnMessage(ws, text);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnMessage", ex, ws.Context);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnTextCallback - No WebSocket found for id: " + id.ToString());
}
finally
{
BufferPool.Release(textBuffer);
}
}
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketBinaryDelegate))]
static void OnBinaryCallback(uint id, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 2)] byte[] buffer, int allocatedLength, int length)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
if (ws.OnBinary != null)
{
try
{
ws.OnBinary(ws, new BufferSegment(buffer, 0, length));
BufferPool.Release(buffer);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnBinary", ex, ws.Context);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnBinaryCallback - No WebSocket found for id: " + id.ToString());
}
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketErrorDelegate))]
static void OnErrorCallback(uint id, string error)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
WebSockets.Remove(id);
if (ws.OnClosed != null)
{
try
{
ws.OnClosed(ws, WebSocketStatusCodes.ClosedAbnormally, error);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnError", ex, ws.Context);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnErrorCallback - No WebSocket found for id: " + id.ToString());
try
{
WS_Release(id);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "WS_Release", ex);
}
}
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketCloseDelegate))]
static void OnCloseCallback(uint id, int code, string reason)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
WebSockets.Remove(id);
if (ws.OnClosed != null)
{
try
{
ws.OnClosed(ws, (WebSocketStatusCodes)code, reason);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnClosed", ex, ws.Context);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnCloseCallback - No WebSocket found for id: " + id.ToString());
try
{
WS_Release(id);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "WS_Release", ex);
}
}
}
}
#endif

View File

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

View File

@ -0,0 +1,262 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
#if !UNITY_WEBGL || UNITY_EDITOR
using Best.WebSockets.Implementations.Frames;
#endif
namespace Best.WebSockets.Implementations
{
/// <summary>
/// States of the underlying implementation's state.
/// </summary>
public enum WebSocketStates : byte
{
Connecting = 0,
Open = 1,
Closing = 2,
Closed = 3,
Unknown
};
public delegate void OnWebSocketOpenDelegate(WebSocket webSocket);
public delegate void OnWebSocketMessageDelegate(WebSocket webSocket, string message);
public delegate void OnWebSocketBinaryNoAllocDelegate(WebSocket webSocket, BufferSegment data);
public delegate void OnWebSocketClosedDelegate(WebSocket webSocket, WebSocketStatusCodes code, string message);
#if !UNITY_WEBGL || UNITY_EDITOR
public delegate void OnWebSocketIncompleteFrameDelegate(WebSocket webSocket, WebSocketFrameReader frame);
#endif
/// <summary>
/// Abstract class for concrete websocket communication implementations.
/// </summary>
public abstract class WebSocketBaseImplementation
{
/// <summary>
/// Capacity of the RTT buffer where the latencies are kept.
/// </summary>
public static int RTTBufferCapacity = 5;
public const string Timing_Name = "Websocket";
public virtual WebSocketStates State { get; protected set; }
#if UNITY_WEBGL && !UNITY_EDITOR
public virtual bool IsOpen { get; protected set; }
public virtual int BufferedAmount { get; protected set; }
#else
public bool IsOpen => this.State == WebSocketStates.Open;
public int BufferedAmount { get => this._bufferedAmount; }
protected volatile int _bufferedAmount;
public HTTP.HTTPRequest InternalRequest
{
get
{
if (this._internalRequest == null)
CreateInternalRequest();
return this._internalRequest;
}
}
protected HTTP.HTTPRequest _internalRequest;
public virtual int Latency { get; protected set; }
public virtual DateTime LastMessageReceived { get; protected set; }
/// <summary>
/// A circular buffer to store the last N rtt times calculated by the pong messages.
/// </summary>
protected CircularBuffer<int> rtts = new CircularBuffer<int>(WebSocketBaseImplementation.RTTBufferCapacity);
/// <summary>
/// When we sent out the last ping.
/// </summary>
protected DateTime lastPing = DateTime.MinValue;
protected bool waitingForPong = false;
protected List<WebSocketFrameReader> IncompleteFrames = new List<WebSocketFrameReader>();
protected PeekableIncomingSegmentStream incomingSegmentStream = new PeekableIncomingSegmentStream();
protected ConcurrentQueue<WebSocketFrameReader> CompletedFrames = new ConcurrentQueue<WebSocketFrameReader>();
protected ConcurrentQueue<WebSocketFrame> frames = new ConcurrentQueue<WebSocketFrame>();
/// <summary>
/// True if we sent out a Close message to the server
/// </summary>
internal volatile bool _closeSent;
internal volatile bool _closeReceived;
#endif
public WebSocket Parent { get; }
public Uri Uri { get; protected set; }
public string Origin { get; }
public string Protocol { get; }
public WebSocketBaseImplementation(WebSocket parent, Uri uri, string origin, string protocol)
{
this.Parent = parent;
this.Uri = uri;
this.Origin = origin;
this.Protocol = protocol;
#if !UNITY_WEBGL || UNITY_EDITOR
this.LastMessageReceived = DateTime.MinValue;
// Set up some default values.
this.Parent.PingFrequency = TimeSpan.FromMilliseconds(10_000);
this.Parent.CloseAfterNoMessage = TimeSpan.FromSeconds(2);
#endif
}
public abstract void StartOpen();
public abstract void StartClose(WebSocketStatusCodes code, string message);
public abstract void Send(string message);
public abstract void Send(byte[] buffer);
public abstract void Send(byte[] buffer, ulong offset, ulong count);
public abstract void SendAsBinary(BufferSegment data);
public abstract void SendAsText(BufferSegment data);
#if !UNITY_WEBGL || UNITY_EDITOR
protected void ParseExtensionResponse(HTTP.HTTPResponse resp)
{
if (this.Parent.Extensions != null)
{
for (int i = 0; i < this.Parent.Extensions.Length; ++i)
{
var ext = this.Parent.Extensions[i];
try
{
if (ext != null && !ext.ParseNegotiation(resp))
this.Parent.Extensions[i] = null; // Keep extensions only that successfully negotiated
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocketBaseImplementation", "ParseNegotiation", ex, this.Parent.Context);
// Do not try to use a defective extension in the future
this.Parent.Extensions[i] = null;
}
}
}
}
protected abstract void CreateInternalRequest();
/// <summary>
/// It will send the given frame to the server.
/// </summary>
public abstract void Send(WebSocketFrame frame);
protected virtual void Cleanup()
{
for (int i = 0; i < this.IncompleteFrames.Count; ++i)
{
var frame = this.IncompleteFrames[i];
BufferPool.Release(frame.Data);
}
this.IncompleteFrames.Clear();
this.Parent.DisposeExtensions();
}
protected int CalculateLatency()
{
if (this.rtts.Count == 0)
return 0;
int sumLatency = 0;
for (int i = 0; i < this.rtts.Count; ++i)
sumLatency += this.rtts[i];
return sumLatency / this.rtts.Count;
}
public static bool CanReadFullFrame(PeekableStream stream)
{
if (stream.Length < 2)
return false;
stream.BeginPeek();
int headerLength = 2;
int header = stream.PeekByte();
if (header == -1)
return false;
int maskAndLength = stream.PeekByte();
if (maskAndLength == -1)
return false;
// The second byte is the Mask Bit and the length of the payload data
var HasMask = (maskAndLength & 0x80) != 0;
if (HasMask)
throw new NotSupportedException("Server-sent frames must not be masked!");
// if 0-125, that is the payload length.
int payloadLength = (int)(maskAndLength & 127);
// If 126, the following 2 bytes interpreted as a 16-bit unsigned integer are the payload length.
if (payloadLength == 126)
{
byte[] rawLen = BufferPool.Get(2, true);
for (int i = 0; i < 2; i++)
{
int data = stream.PeekByte();
if (data < 0)
return false;
rawLen[i] = (byte)data;
}
if (BitConverter.IsLittleEndian)
Array.Reverse(rawLen, 0, 2);
payloadLength = (int)BitConverter.ToUInt16(rawLen, 0);
headerLength += 2;
BufferPool.Release(rawLen);
}
else if (payloadLength == 127)
{
// If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the
// most significant bit MUST be 0) are the payload length.
byte[] rawLen = BufferPool.Get(8, true);
for (int i = 0; i < 8; i++)
{
int data = stream.PeekByte();
if (data < 0)
return false;
rawLen[i] = (byte)data;
}
if (BitConverter.IsLittleEndian)
Array.Reverse(rawLen, 0, 8);
payloadLength = (int)BitConverter.ToUInt64(rawLen, 0);
headerLength += 8;
BufferPool.Release(rawLen);
}
return stream.Length >= (headerLength + payloadLength);
}
#endif
}
}

View File

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

8
Runtime/Plugins.meta Normal file
View File

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

View File

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

View File

@ -0,0 +1,191 @@
// https://docs.unity3d.com/6000.0/Documentation/Manual/web-interacting-browser-deprecated.html
// Deprecated code | Replacement code
// -----------------+-----------------
// dynCall() | makeDynCall()
var Lib_BEST_HTTP_WebGL_WS_Bridge =
{
$ws: {
webSocketInstances: {},
nextInstanceId : 1,
/*UTF8Decoder: new TextDecoder('utf8'),*/
Set : function(socket) {
ws.webSocketInstances[ws.nextInstanceId] = socket;
return ws.nextInstanceId++;
},
Get : function(id) {
return ws.webSocketInstances[id];
},
Remove: function(id) {
delete ws.webSocketInstances[id];
},
_callOnClose: function(onClose, id, code, reason)
{
var length = lengthBytesUTF8(reason) + 1;
var buffer = _malloc(length);
stringToUTF8Array(reason, HEAPU8, buffer, length);
{{{ makeDynCall('viii', 'onClose') }}}(id, code, buffer);
_free(buffer);
},
_callOnError: function(errCallback, id, reason)
{
var length = lengthBytesUTF8(reason) + 1;
var buffer = _malloc(length);
stringToUTF8Array(reason, HEAPU8, buffer, length);
{{{ makeDynCall('vii', 'errCallback') }}}(id, buffer);
_free(buffer);
}
},
WS_Create: function(url, protocol, onOpen, onText, onBinary, onError, onClose, allocator)
{
var urlStr = new URL(UTF8ToString(url)); ///*encodeURI*/(UTF8ToString(url)).replace(/\+/g, '%2B').replace(/%252[fF]/ig, '%2F');
var proto = UTF8ToString(protocol);
console.log('WS_Create(' + urlStr + ', "' + proto + '")');
var socket = {
onError: onError,
onClose: onClose
};
if (proto == '')
socket.socketImpl = new WebSocket(urlStr);
else
socket.socketImpl = new WebSocket(urlStr, proto.trim().split(","));
var id = ws.nextInstanceId;
socket.socketImpl.binaryType = "arraybuffer";
socket.socketImpl.onopen = function(e) {
console.log(id + ' WS_Create - onOpen');
{{{ makeDynCall('vi', 'onOpen') }}}(id);
};
const encoder = new TextEncoder();
socket.socketImpl.onmessage = function (e)
{
// Binary?
if (e.data instanceof ArrayBuffer)
{
var byteArray = new Uint8Array(e.data);
const array = {{{ makeDynCall('iii', 'allocator') }}}(id, byteArray.length);
const numArr = HEAPU8.subarray(array, array + 4);
const originalLength = (numArr[0] << 24) | (numArr[1] << 16) | (numArr[2] << 8) | (numArr[3]);
HEAPU8.set(byteArray, array);
{{{ makeDynCall('viiii', 'onBinary') }}}(id, array, originalLength, byteArray.length);
}
else // Text
{
// https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encodeInto#buffer_sizing
// "If the output allocation (typically within Wasm heap) is expected to be short-lived, it makes sense to allocate s.length * 3 bytes for the output,
// in which case the first conversion attempt is guaranteed to convert the whole string."
const length = e.data.length * 3;
const array = {{{ makeDynCall('iii', 'allocator') }}}(id, length);
const numArr = HEAPU8.subarray(array, array + 4);
const originalLength = (numArr[0] << 24) | (numArr[1] << 16) | (numArr[2] << 8) | (numArr[3]);
const ret = encoder.encodeInto(e.data, HEAPU8.subarray(array));
{{{ makeDynCall('viiii', 'onText') }}}(id, array, originalLength, ret.written);
}
};
socket.socketImpl.onclose = function (e) {
console.log(id + ' WS_Create - onClose ' + e.code + ' ' + e.reason);
ws._callOnClose(onClose, id, e.code, e.reason);
};
return ws.Set(socket);
},
WS_GetState: function (id)
{
var socket = ws.Get(id);
if (typeof socket === 'undefined' ||
socket == null ||
typeof socket.socketImpl === 'undefined' ||
socket.socketImpl == null)
return 3; // closed
return socket.socketImpl.readyState;
},
WS_GetBufferedAmount: function (id)
{
var socket = ws.Get(id);
return socket.socketImpl.bufferedAmount;
},
WS_Send_String: function (id, ptr, pos, length)
{
var socket = ws.Get(id);
var startPtr = ptr + pos;
var endPtr = startPtr + length;
var UTF8Decoder = new TextDecoder('utf8');
var str = UTF8Decoder.decode(HEAPU8.subarray ? HEAPU8.subarray(startPtr, endPtr) : new Uint8Array(HEAPU8.slice(startPtr, endPtr)));
try
{
socket.socketImpl.send(str);
}
catch(e) {
ws._callOnError(socket.onError, id, ' ' + e.name + ': ' + e.message);
}
return socket.socketImpl.bufferedAmount;
},
WS_Send_Binary: function(id, ptr, pos, length)
{
var socket = ws.Get(id);
try
{
var buff = HEAPU8.subarray(ptr + pos, ptr + pos + length);
socket.socketImpl.send(buff /*HEAPU8.buffer.slice(ptr + pos, ptr + pos + length)*/);
}
catch(e) {
ws._callOnError(socket.onError, id, ' ' + e.name + ': ' + e.message);
}
return socket.socketImpl.bufferedAmount;
},
WS_Close: function (id, code, reason)
{
var socket = ws.Get(id);
var reasonStr = UTF8ToString(reason);
console.log(id + ' WS_Close(' + code + ', ' + reasonStr + ')');
socket.socketImpl.close(/*ulong*/code, reasonStr);
},
WS_Release: function(id)
{
console.log(id + ' WS_Release');
ws.Remove(id);
}
};
autoAddDeps(Lib_BEST_HTTP_WebGL_WS_Bridge, '$ws');
mergeInto(LibraryManager.library, Lib_BEST_HTTP_WebGL_WS_Bridge);

View File

@ -0,0 +1,39 @@
fileFormatVersion: 2
guid: 8efe6cedf2832a647a33465b85e4f311
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
WebGL: WebGL
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/Runtime/Plugins/WebGL/BestHTTP_WebSocket.jslib
uploadId: 737284

427
Runtime/WebSocket.cs Normal file
View File

@ -0,0 +1,427 @@
using System;
using System.Text;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
using Best.HTTP.HostSetting;
using Best.HTTP.Request.Settings;
using Best.HTTP.Shared;
using Best.HTTP.Hosts.Connections;
using Best.WebSockets.Implementations;
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
using Best.HTTP.Hosts.Connections.HTTP2;
#endif
#if !UNITY_WEBGL || UNITY_EDITOR
using Best.WebSockets.Extensions;
#endif
using Best.HTTP.Shared.Compression.Zlib;
/*
* This is a wrapper class organizing where and how the websocket request is sent out. If there's an already open HTTP/2 connection with
* an ENABLE_CONNECT_PROTOCOL flag, it tries using the HTTP/2 connection first and if it fails, a new HTTP/1 connection is created.
* */
namespace Best.WebSockets
{
/// <summary>
/// Implements the WebSocket standard for duplex, two-way communications.
/// </summary>
public sealed class WebSocket
{
/// <summary>
/// Maximum payload size of a websocket frame. Its default value is 32 KiB.
/// </summary>
public static uint MaxFragmentSize = UInt16.MaxValue / 2;
#if !UNITY_WEBGL || UNITY_EDITOR
public static IExtension[] GetDefaultExtensions()
{
return new IExtension[] { new PerMessageCompression(/*compression level: */ CompressionLevel.Default,
/*clientNoContextTakeover: */ false,
/*serverNoContextTakeover: */ false,
/*clientMaxWindowBits: */ ZlibConstants.WindowBitsMax,
/*desiredServerMaxWindowBits: */ ZlibConstants.WindowBitsMax,
/*minDatalengthToCompress: */ PerMessageCompression.MinDataLengthToCompressDefault) };
}
#endif
public WebSocketStates State { get { return this.implementation.State; } }
/// <summary>
/// The connection to the WebSocket server is open.
/// </summary>
public bool IsOpen { get { return this.implementation.IsOpen; } }
/// <summary>
/// Data waiting to be written to the wire.
/// </summary>
public int BufferedAmount { get { return this.implementation.BufferedAmount; } }
#if !UNITY_WEBGL || UNITY_EDITOR
/// <summary>
/// Set to <c>true</c> to start sending Ping frames to the WebSocket server.
/// </summary>
public bool SendPings { get; set; }
/// <summary>
/// The delay between two Pings in milliseconds. Minimum value is 100ms, default is 10 seconds.
/// </summary>
public TimeSpan PingFrequency { get; set; }
/// <summary>
/// If <see cref="SendPings"/> set to <c>true</c>, the plugin will close the connection and emit an <see cref="OnClosed"/> event if no
/// message is received from the server in the given time. Its default value is 2 sec.
/// </summary>
public TimeSpan CloseAfterNoMessage { get; set; }
/// <summary>
/// The internal <see cref="Best.HTTP.HTTPRequest"/> object.
/// </summary>
public HTTP.HTTPRequest InternalRequest { get { return this.implementation.InternalRequest; } }
/// <summary>
/// <see cref="IExtension"/> implementations the plugin will negotiate with the server to use.
/// </summary>
public IExtension[] Extensions { get; private set; }
/// <summary>
/// Latency calculated from ping-pong message round-trip times.
/// </summary>
public int Latency { get { return this.implementation.Latency; } }
/// <summary>
/// When the WebSocket instance received the last message from the server.
/// </summary>
public DateTime LastMessageReceived { get { return this.implementation.LastMessageReceived; } }
/// <summary>
/// When the <c>Websocket Over HTTP/2</c> implementation fails to connect and <see cref="WebSocketOverHTTP2Settings"/><c>.</c><see cref="WebSocketOverHTTP2Settings.EnableImplementationFallback"/> is <c>true</c>, the plugin tries to fall back to the HTTP/1 implementation.
/// When this happens a new <see cref="InternalRequest"/> is created and all previous custom modifications (like added headers) are lost. With OnInternalRequestCreated these modifications can be reapplied.
/// </summary>
public Action<WebSocket, HTTP.HTTPRequest> OnInternalRequestCreated;
#endif
/// <summary>
/// Called when the connection to the WebSocket server is established.
/// </summary>
public OnWebSocketOpenDelegate OnOpen;
/// <summary>
/// Called when a new textual message is received from the server.
/// </summary>
public OnWebSocketMessageDelegate OnMessage;
/// <summary>
/// Called when a Binary message received.
/// The content of the <see cref="BufferSegment"/> must be used or copied to a new array in the callbacks because the plugin reuses the memory immediately after the callback by placing it back to the <see cref="BufferPool"/>!
/// </summary>
/// <remarks>Note that the memory will be reused when this event returns. Either process it in this call or make a copy from the received data.</remarks>
public OnWebSocketBinaryNoAllocDelegate OnBinary;
/// <summary>
/// Called when the WebSocket connection is closed.
/// </summary>
public OnWebSocketClosedDelegate OnClosed;
/// <summary>
/// Logging context of this websocket instance.
/// </summary>
public LoggingContext Context { get; private set; }
/// <summary>
/// The underlying, real implementation.
/// </summary>
private WebSocketBaseImplementation implementation;
/// <summary>
/// Creates a WebSocket instance from the given uri.
/// </summary>
/// <param name="uri">The uri of the WebSocket server</param>
public WebSocket(Uri uri)
:this(uri, string.Empty, string.Empty)
{
#if (!UNITY_WEBGL || UNITY_EDITOR)
this.Extensions = WebSocket.GetDefaultExtensions();
#endif
}
#if !UNITY_WEBGL || UNITY_EDITOR
/// <summary>
/// Creates a WebSocket instance from the given uri.
/// </summary>
/// <param name="uri">Uri of the WebSocket endpoint.</param>
/// <param name="origin">Where the WebSocket originating from.</param>
/// <param name="protocol">The application-level protocol that the client want to use(eg. "chat", "leaderboard", etc.). Can be null or empty string if not used.</param>
public WebSocket(Uri uri, string origin, string protocol)
:this(uri, origin, protocol, null)
{
#if (!UNITY_WEBGL || UNITY_EDITOR)
this.Extensions = WebSocket.GetDefaultExtensions();
#endif
}
#endif
/// <summary>
/// Creates a WebSocket instance from the given uri, protocol and origin.
/// </summary>
/// <param name="uri">The uri of the WebSocket server</param>
/// <param name="origin">Servers that are not intended to process input from any web page but only for certain sites SHOULD verify the |Origin| field is an origin they expect.
/// If the origin indicated is unacceptable to the server, then it SHOULD respond to the WebSocket handshake with a reply containing HTTP 403 Forbidden status code.</param>
/// <param name="protocol">The application-level protocol that the client want to use(eg. "chat", "leaderboard", etc.). Can be null or empty string if not used.</param>
/// <param name="extensions">Optional <see cref="IExtension"/> implementations</param>
public WebSocket(Uri uri, string origin, string protocol
#if !UNITY_WEBGL || UNITY_EDITOR
, params IExtension[] extensions
#endif
)
{
this.Context = new LoggingContext(this);
#if !UNITY_WEBGL || UNITY_EDITOR
this.Extensions = extensions;
#endif
SelectImplementation(uri, origin, protocol);
// Under WebGL when only the WebSocket protocol is used Setup() isn't called, so we have to call it here.
HTTPManager.Setup();
}
internal WebSocketBaseImplementation SelectImplementation(Uri uri, string origin, string protocol)
{
#if !UNITY_WEBGL || UNITY_EDITOR
#if !BESTHTTP_DISABLE_ALTERNATE_SSL
if (HTTPProtocolFactory.IsSecureProtocol(uri) &&
HTTPManager.PerHostSettings.Get(uri).HTTP2ConnectionSettings.WebSocketOverHTTP2Settings.EnableWebSocketOverHTTP2)
{
// Try to find a HTTP/2 connection that supports the connect protocol.
var connectionKey = HostKey.From(new UriBuilder("https", uri.Host, uri.Port).Uri, GetProxy(uri));
var con = HostManager.GetHostVariant(connectionKey).Find(c => {
var httpConnection = c as HTTPOverTCPConnection;
var http2Handler = httpConnection?.requestHandler as HTTP2ContentConsumer;
return http2Handler != null && http2Handler.settings.RemoteSettings[HTTP2Settings.ENABLE_CONNECT_PROTOCOL] != 0;
});
if (con != null)
{
HTTPManager.Logger.Information("WebSocket", "Connection with enabled Connect Protocol found!", this.Context);
var httpConnection = con as HTTPOverTCPConnection;
var http2Handler = httpConnection?.requestHandler as HTTP2ContentConsumer;
this.implementation = new OverHTTP2(this, uri, origin, protocol);
}
}
#endif
if (this.implementation == null)
this.implementation = new OverHTTP1(this, uri, origin, protocol);
#else
this.implementation = new WebGLBrowser(this, uri, origin, protocol);
#endif
return this.implementation;
}
#if !UNITY_WEBGL || UNITY_EDITOR
internal void FallbackToHTTP1()
{
HTTPManager.Logger.Verbose("WebSocket", "FallbackToHTTP1", this.Context);
if (this.implementation == null)
return;
this.implementation = new OverHTTP1(this, this.implementation.Uri, this.implementation.Origin, this.implementation.Protocol);
this.implementation.StartOpen();
}
#endif
/// <summary>
/// Start the opening process.
/// </summary>
/// <remarks>It's a non-blocking call. To get notified when the WebSocket instance is considered open and can send/receive, use the <see cref="OnOpen"/> event.</remarks>
public void Open()
{
this.implementation.StartOpen();
}
/// <summary>
/// It will send the given textual message to the remote server.
/// </summary>
public void Send(string message)
{
if (!IsOpen)
return;
this.implementation.Send(message);
}
/// <summary>
/// It will send the given binary message to the remote server.
/// </summary>
public void Send(byte[] buffer)
{
if (!IsOpen)
return;
this.implementation.Send(buffer);
}
/// <summary>
/// It will send the given binary message to the remote server.
/// </summary>
public void Send(byte[] buffer, ulong offset, ulong count)
{
if (!IsOpen)
return;
this.implementation.Send(buffer, offset, count);
}
/// <summary>
/// Will send the data in one or more binary frame and takes ownership over it calling BufferPool.Release when the data sent.
/// </summary>
public void SendAsBinary(BufferSegment data)
{
if (!IsOpen)
{
BufferPool.Release(data);
return;
}
this.implementation.SendAsBinary(data);
}
/// <summary>
/// Will send data as a text frame and takes owenership over the memory region releasing it to the BufferPool as soon as possible.
/// </summary>
public void SendAsText(BufferSegment data)
{
if (!IsOpen)
{
BufferPool.Release(data);
return;
}
this.implementation.SendAsText(data);
}
/// <summary>
/// It will initiate the closing of the connection to the server.
/// </summary>
public void Close()
{
if (State >= WebSocketStates.Closing)
return;
this.implementation.StartClose(WebSocketStatusCodes.NormalClosure, "Bye!");
}
/// <summary>
/// It will initiate the closing of the connection to the server sending the given code and message.
/// </summary>
public void Close(WebSocketStatusCodes code, string message)
{
if (!IsOpen)
return;
this.implementation.StartClose(code, message);
}
#if !UNITY_WEBGL || UNITY_EDITOR
internal ProxySettings GetProxy(Uri uri)
{
// WebSocket is not a request-response based protocol, so we need a 'tunnel' through the proxy
var proxy = HTTPManager.Proxy as HTTP.Proxies.HTTPProxy;
if (proxy != null && proxy.UseProxyForAddress(uri))
proxy = new HTTP.Proxies.HTTPProxy(proxy.Address,
proxy.Credentials,
false, /*turn on 'tunneling'*/
false, /*sendWholeUri*/
proxy.NonTransparentForHTTPS);
return new ProxySettings { Proxy = proxy };
}
internal void DisposeExtensions()
{
if (this.Extensions != null)
{
for (int i = 0; i < this.Extensions.Length; ++i)
{
var ext = this.Extensions[i];
try
{
ext?.Dispose();
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("OverHTTP1", "OnInternalRequestCallback - extension dispose", ex, this.Context);
}
this.Extensions[i] = null;
}
this.Extensions = null;
}
}
#endif
#if !UNITY_WEBGL || UNITY_EDITOR
internal static BufferSegment EncodeCloseData(WebSocketStatusCodes code, string message)
{
//If there is a body, the first two bytes of the body MUST be a 2-byte unsigned integer
// (in network byte order) representing a status code with value /code/ defined in Section 7.4 (http://tools.ietf.org/html/rfc6455#section-7.4). Following the 2-byte integer,
// the body MAY contain UTF-8-encoded data with value /reason/, the interpretation of which is not defined by this specification.
// This data is not necessarily human readable but may be useful for debugging or passing information relevant to the script that opened the connection.
int msgLen = Encoding.UTF8.GetByteCount(message);
using (var ms = new BufferPoolMemoryStream(2 + msgLen))
{
byte[] buff = BitConverter.GetBytes((ushort)code);
if (BitConverter.IsLittleEndian)
Array.Reverse(buff, 0, buff.Length);
ms.Write(buff, 0, buff.Length);
buff = Encoding.UTF8.GetBytes(message);
ms.Write(buff, 0, buff.Length);
buff = ms.ToArray();
return buff.AsBuffer(buff.Length);
}
}
internal static string GetSecKey(object[] from)
{
const int keysLength = 16;
byte[] keys = BufferPool.Get(keysLength, true);
int pos = 0;
for (int i = 0; i < from.Length; ++i)
{
byte[] hash = BitConverter.GetBytes((Int32)from[i].GetHashCode());
for (int cv = 0; cv < hash.Length && pos < keysLength; ++cv)
keys[pos++] = hash[cv];
}
var result = Convert.ToBase64String(keys, 0, keysLength);
BufferPool.Release(keys);
return result;
}
#endif
}
}

18
Runtime/WebSocket.cs.meta Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1,22 @@
{
"name": "com.Tivadar.Best.WebSockets",
"rootNamespace": "Best.WebSockets",
"references": [
"Unity.Burst",
"com.Tivadar.Best.HTTP"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [ {
"name": "com.unity.burst",
"expression": "1.8.7",
"define": "WITH_BURST"
}
],
"noEngineReferences": false
}

View File

@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: bba0e49b0b1b9924ab06cd58867f661c
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/Runtime/com.Tivadar.Best.WebSockets.asmdef
uploadId: 737284

5
Runtime/csc.rsp Normal file
View File

@ -0,0 +1,5 @@
#https://forum.unity.com/threads/rsp-file-per-assembly-or-folder.572758/
#https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/errors-warnings
-warnaserror
-nowarn:CS1701,CS0612,CS1591,CS1571,CS1572,CS1573,CS1574,CS1587

14
Runtime/csc.rsp.meta Normal file
View File

@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 2a3b358778e405d448e3de83204258d4
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/Runtime/csc.rsp
uploadId: 737284

39
package.json Normal file
View File

@ -0,0 +1,39 @@
{
"name": "com.tivadar.best.websockets",
"displayName": "Best WebSockets",
"description": "Best WebSockets is a premier networking library for Unity, tailored specifically for seamless WebSocket integration. It's perfect for applications that require real-time, bi-directional communication such as chat applications, multiplayer games, and live interactive systems.",
"version": "3.0.7",
"unity": "2021.1",
"documentationUrl": "https://bestdocshub.pages.dev/WebSockets/",
"changelogUrl": "https://bestdocshub.pages.dev/WebSockets/changelog/",
"licensesUrl": "https://bestdocshub.pages.dev/HTTP/third-party-notices/",
"author": {
"name": "Tivadar György Nagy",
"email": "besthttp@gmail.com",
"url": "https://bestdocshub.pages.dev/"
},
"keywords": [
"best",
"besthttp",
"http",
"tls",
"websocket",
"ws",
"wss",
"dns",
"tcp",
"http2"
],
"dependencies": {
"com.unity.burst": "1.8.10",
"com.tivadar.best.http": "3.0.10"
},
"samples": [
{
"displayName": "Simple Sample",
"description": "Contains sample codes for various WebSocket related scenarios.",
"path": "Samples~/With UI"
}
],
"_fingerprint": "3a85df99e0e81cf6effff7ba2e690276f2aee437"
}

14
package.json.meta Normal file
View File

@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: f957ac0c1e625e54d8ba12e2a5121954
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 268757
packageName: Best WebSockets
packageVersion: 3.0.7
assetPath: Packages/com.tivadar.best.websockets/package.json
uploadId: 737284