dependency: added bayou transport layer for WebGL + WSS + FishNet

This commit is contained in:
Jakob Feldmann 2024-05-05 17:34:06 +02:00
parent 1815d51d68
commit b13e3f617e
93 changed files with 4667 additions and 0 deletions

View File

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

View File

@ -0,0 +1,17 @@
{
"name": "Bayou",
"rootNamespace": "",
"references": [
"GUID:3b5390adca4e2bb4791cb930316d6f3e",
"GUID:7c88a4a7926ee5145ad2dfa06f454c67"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7bf834368d865d943b0569a8ab0aa1f7
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,460 @@
using FishNet.Managing;
using FishNet.Managing.Logging;
using FishNet.Managing.Transporting;
using JamesFrowen.SimpleWeb;
using System;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace FishNet.Transporting.Bayou
{
[DisallowMultipleComponent]
public class Bayou : Transport
{
#region Serialized.
[Header("Security")]
/// <summary>
/// True to connect using WSS.
/// </summary>
[Tooltip("True to connect using WSS.")]
[SerializeField]
private bool _useWss = false;
#if UNITY_SERVER || UNITY_EDITOR
/// <summary>
/// Configuration to use for SSL.
/// </summary>
[Tooltip("Configuration to use for SSL.")]
[SerializeField]
private SslConfiguration _sslConfiguration;
#endif
[Header("Channels")]
/// <summary>
/// Maximum transmission unit for this transport.
/// </summary>
[Tooltip("Maximum transmission unit for the unreliable channel.")]
[Range(MINIMUM_MTU, MAXIMUM_MTU)]
[SerializeField]
private int _mtu = 1023;
[Header("Server")]
/// <summary>
/// Port to use.
/// </summary>
[Tooltip("Port to use.")]
[SerializeField]
private ushort _port = 7770;
/// <summary>
/// Maximum number of players which may be connected at once.
/// </summary>
[Tooltip("Maximum number of players which may be connected at once.")]
[Range(1, 9999)]
[SerializeField]
private int _maximumClients = 2000;
[Header("Client")]
/// <summary>
/// Address to connect.
/// </summary>
[Tooltip("Address to connect.")]
[SerializeField]
private string _clientAddress = "localhost";
#endregion
#region Private.
/// <summary>
/// Server socket and handler.
/// </summary>
private Server.ServerSocket _server = new Server.ServerSocket();
/// <summary>
/// Client socket and handler.
/// </summary>
private Client.ClientSocket _client = new Client.ClientSocket();
#endregion
#region Const.
/// <summary>
/// Minimum UDP packet size allowed.
/// </summary>
private const int MINIMUM_MTU = 576;
/// <summary>
/// Maximum UDP packet size allowed.
/// </summary>
private const int MAXIMUM_MTU = ushort.MaxValue;
#endregion
#region Initialization and unity.
protected void OnDestroy()
{
Shutdown();
}
#endregion
#region ConnectionStates.
/// <summary>
/// Gets the address of a remote connection Id.
/// </summary>
/// <param name="connectionId"></param>
/// <returns></returns>
public override string GetConnectionAddress(int connectionId)
{
return _server.GetConnectionAddress(connectionId);
}
/// <summary>
/// Called when a connection state changes for the local client.
/// </summary>
public override event Action<ClientConnectionStateArgs> OnClientConnectionState;
/// <summary>
/// Called when a connection state changes for the local server.
/// </summary>
public override event Action<ServerConnectionStateArgs> OnServerConnectionState;
/// <summary>
/// Called when a connection state changes for a remote client.
/// </summary>
public override event Action<RemoteConnectionStateArgs> OnRemoteConnectionState;
/// <summary>
/// Gets the current local ConnectionState.
/// </summary>
/// <param name="server">True if getting ConnectionState for the server.</param>
public override LocalConnectionState GetConnectionState(bool server)
{
if (server)
return _server.GetConnectionState();
else
return _client.GetConnectionState();
}
/// <summary>
/// Gets the current ConnectionState of a remote client on the server.
/// </summary>
/// <param name="connectionId">ConnectionId to get ConnectionState for.</param>
public override RemoteConnectionState GetConnectionState(int connectionId)
{
return _server.GetConnectionState(connectionId);
}
/// <summary>
/// Handles a ConnectionStateArgs for the local client.
/// </summary>
/// <param name="connectionStateArgs"></param>
public override void HandleClientConnectionState(ClientConnectionStateArgs connectionStateArgs)
{
OnClientConnectionState?.Invoke(connectionStateArgs);
}
/// <summary>
/// Handles a ConnectionStateArgs for the local server.
/// </summary>
/// <param name="connectionStateArgs"></param>
public override void HandleServerConnectionState(ServerConnectionStateArgs connectionStateArgs)
{
OnServerConnectionState?.Invoke(connectionStateArgs);
}
/// <summary>
/// Handles a ConnectionStateArgs for a remote client.
/// </summary>
/// <param name="connectionStateArgs"></param>
public override void HandleRemoteConnectionState(RemoteConnectionStateArgs connectionStateArgs)
{
OnRemoteConnectionState?.Invoke(connectionStateArgs);
}
#endregion
#region Iterating.
/// <summary>
/// Processes data received by the socket.
/// </summary>
/// <param name="server">True to process data received on the server.</param>
public override void IterateIncoming(bool server)
{
if (server)
_server.IterateIncoming();
else
_client.IterateIncoming();
}
/// <summary>
/// Processes data to be sent by the socket.
/// </summary>
/// <param name="server">True to process data received on the server.</param>
public override void IterateOutgoing(bool server)
{
if (server)
_server.IterateOutgoing();
else
_client.IterateOutgoing();
}
#endregion
#region ReceivedData.
/// <summary>
/// Called when client receives data.
/// </summary>
public override event Action<ClientReceivedDataArgs> OnClientReceivedData;
/// <summary>
/// Handles a ClientReceivedDataArgs.
/// </summary>
/// <param name="receivedDataArgs"></param>
public override void HandleClientReceivedDataArgs(ClientReceivedDataArgs receivedDataArgs)
{
OnClientReceivedData?.Invoke(receivedDataArgs);
}
/// <summary>
/// Called when server receives data.
/// </summary>
public override event Action<ServerReceivedDataArgs> OnServerReceivedData;
/// <summary>
/// Handles a ClientReceivedDataArgs.
/// </summary>
/// <param name="receivedDataArgs"></param>
public override void HandleServerReceivedDataArgs(ServerReceivedDataArgs receivedDataArgs)
{
OnServerReceivedData?.Invoke(receivedDataArgs);
}
#endregion
#region Sending.
/// <summary>
/// Sends to the server or all clients.
/// </summary>
/// <param name="channelId">Channel to use.</param>
/// <param name="segment">Data to send.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void SendToServer(byte channelId, ArraySegment<byte> segment)
{
SanitizeChannel(ref channelId);
_client.SendToServer(channelId, segment);
}
/// <summary>
/// Sends data to a client.
/// </summary>
/// <param name="channelId"></param>
/// <param name="segment"></param>
/// <param name="connectionId"></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void SendToClient(byte channelId, ArraySegment<byte> segment, int connectionId)
{
SanitizeChannel(ref channelId);
_server.SendToClient(channelId, segment, connectionId);
}
#endregion
#region Configuration.
/// <summary>
/// Sets UseWSS value.
/// </summary>
/// <param name="useWss"></param>
public void SetUseWSS(bool useWss)
{
_useWss = useWss;
}
/// <summary>
/// How long in seconds until either the server or client socket must go without data before being timed out.
/// </summary>
/// <param name="asServer">True to get the timeout for the server socket, false for the client socket.</param>
/// <returns></returns>
public override float GetTimeout(bool asServer)
{
return -1f;
}
/// <summary>
/// Returns the maximum number of clients allowed to connect to the server. If the transport does not support this method the value -1 is returned.
/// </summary>
/// <returns></returns>
public override int GetMaximumClients()
{
return _server.GetMaximumClients();
}
/// <summary>
/// Sets maximum number of clients allowed to connect to the server. If applied at runtime and clients exceed this value existing clients will stay connected but new clients may not connect.
/// </summary>
/// <param name="value"></param>
public override void SetMaximumClients(int value)
{
if (_server.GetConnectionState() != LocalConnectionState.Stopped)
base.NetworkManager.LogWarning($"Cannot set maximum clients when server is running.");
else
_maximumClients = value;
}
/// <summary>
/// Sets which address the client will connect to.
/// </summary>
/// <param name="address"></param>
public override void SetClientAddress(string address)
{
_clientAddress = address;
}
/// <summary>
/// Gets which address the client will connect to.
/// </summary>
public override string GetClientAddress()
{
return _clientAddress;
}
/// <summary>
/// Sets which address the server will bind to.
/// </summary>
/// <param name="address"></param>
public override void SetServerBindAddress(string address, IPAddressType addressType) { }
/// <summary>
/// Gets which address the server will bind to.
/// </summary>
/// <param name="address"></param>
public override string GetServerBindAddress(IPAddressType addressType)
{
return "localhost";
}
/// <summary>
/// Sets which port to use.
/// </summary>
/// <param name="port"></param>
public override void SetPort(ushort port)
{
_port = port;
}
/// <summary>
/// Gets which port to use.
/// </summary>
/// <param name="port"></param>
public override ushort GetPort()
{
return _port;
}
#endregion
#region Start and stop.
/// <summary>
/// Starts the local server or client using configured settings.
/// </summary>
/// <param name="server">True to start server.</param>
public override bool StartConnection(bool server)
{
if (server)
return StartServer();
else
return StartClient(_clientAddress);
}
/// <summary>
/// Stops the local server or client.
/// </summary>
/// <param name="server">True to stop server.</param>
public override bool StopConnection(bool server)
{
if (server)
return StopServer();
else
return StopClient();
}
/// <summary>
/// Stops a remote client from the server, disconnecting the client.
/// </summary>
/// <param name="connectionId">ConnectionId of the client to disconnect.</param>
/// <param name="immediately">True to abrutly stp the client socket without waiting socket thread.</param>
public override bool StopConnection(int connectionId, bool immediately)
{
return StopClient(connectionId, immediately);
}
/// <summary>
/// Stops both client and server.
/// </summary>
public override void Shutdown()
{
//Stops client then server connections.
StopConnection(false);
StopConnection(true);
}
#region Privates.
/// <summary>
/// Starts server.
/// </summary>
private bool StartServer()
{
SslConfiguration config;
#if UNITY_SERVER
config = _sslConfiguration;
#else
config = new SslConfiguration();
#endif
_server.Initialize(this, _mtu, config);
return _server.StartConnection(_port, _maximumClients);
}
/// <summary>
/// Stops server.
/// </summary>
private bool StopServer()
{
return _server.StopConnection();
}
/// <summary>
/// Starts the client.
/// </summary>
/// <param name="address"></param>
private bool StartClient(string address)
{
_client.Initialize(this, _mtu);
return _client.StartConnection(address, _port, _useWss);
}
/// <summary>
/// Stops the client.
/// </summary>
private bool StopClient()
{
return _client.StopConnection();
}
/// <summary>
/// Stops a remote client on the server.
/// </summary>
/// <param name="connectionId"></param>
/// <param name="immediately">True to abrutly stp the client socket without waiting socket thread.</param>
private bool StopClient(int connectionId, bool immediately)
{
return _server.StopConnection(connectionId, immediately);
}
#endregion
#endregion
#region Channels.
/// <summary>
/// If channelId is invalid then channelId becomes forced to reliable.
/// </summary>
/// <param name="channelId"></param>
private void SanitizeChannel(ref byte channelId)
{
if (channelId < 0 || channelId >= TransportManager.CHANNEL_COUNT)
{
base.NetworkManager.LogWarning($"Channel of {channelId} is out of range of supported channels. Channel will be defaulted to reliable.");
channelId = 0;
}
}
/// <summary>
/// Gets the MTU for a channel. This should take header size into consideration.
/// For example, if MTU is 1200 and a packet header for this channel is 10 in size, this method should return 1190.
/// </summary>
/// <param name="channel"></param>
/// <returns></returns>
public override int GetMTU(byte channel)
{
return _mtu;
}
#endregion
#region Editor.
#if UNITY_EDITOR
private void OnValidate()
{
if (_mtu < 0)
_mtu = MINIMUM_MTU;
else if (_mtu > MAXIMUM_MTU)
_mtu = MAXIMUM_MTU;
}
#endif
#endregion
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 362dfd7e6009fb44c90beec3df0ad853
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: bf9191e2e07d29749bca3a1ae44e4bc8, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,38 @@
4.1.0
- Fish-Networking 4.1.0 support. Not compatible with V3.
4.0.0
- Fish-Networking 4.0.0 support. Backwards compatible to V3.
- Added an assembly definition.
- Moved Bayou folder to FishNet/Plugins.
2.0.0
- Fish-Networking 2.0.0 support.
1.2.5
- Added SSL support.
1.2.4
- Fixed maximum clients allowing 1 additional client to connect.
- UseWSS is now false by default.
1.2.3
- Really fixed index issue in AddChannel.
- Updated namespaces to reflect other Fish-Networking namespaces.
1.2.2
- Fixed rare index out of range error in AddChannel.
1.2.1
- More fixes to support Multipass transport.
- Removed ServerBindAddress as it was not being used.
1.2.0
- WSS can now be toggled in the inspector and set through Bayou.SetUseWss.
1.1.0
- FishNetworking 1.3.1 Multipass transport support.
- SimpleWebTransport fix (Runtime is not defined for unity 2021) https://github.com/James-Frowen/SimpleWebTransport/commit/945b50dbad5b71c43e2bdaa4033f87d3f62c5572
1.0.0
- Initial release.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 48f07f72fa094094bb70b7a69debae89
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@ -0,0 +1,115 @@
//using System.Collections;
//using System.Collections.Generic;
//namespace FishNet.Transporting.Bayou
//{
// internal class BidirectionalDictionary<T1, T2> : IEnumerable
// {
// private Dictionary<T1, T2> t1ToT2Dict = new Dictionary<T1, T2>();
// private Dictionary<T2, T1> t2ToT1Dict = new Dictionary<T2, T1>();
// public IEnumerable<T1> FirstTypes => t1ToT2Dict.Keys;
// public IEnumerable<T2> SecondTypes => t2ToT1Dict.Keys;
// public IEnumerator GetEnumerator() => t1ToT2Dict.GetEnumerator();
// public int Count => t1ToT2Dict.Count;
// public Dictionary<T1, T2> First => t1ToT2Dict;
// public Dictionary<T2, T1> Second => t2ToT1Dict;
// public void Add(T1 key, T2 value)
// {
// if (t1ToT2Dict.ContainsKey(key))
// {
// Remove(key);
// }
// t1ToT2Dict[key] = value;
// t2ToT1Dict[value] = key;
// }
// public void Add(T2 key, T1 value)
// {
// if (t2ToT1Dict.ContainsKey(key))
// {
// Remove(key);
// }
// t2ToT1Dict[key] = value;
// t1ToT2Dict[value] = key;
// }
// public T2 Get(T1 key) => t1ToT2Dict[key];
// public T1 Get(T2 key) => t2ToT1Dict[key];
// public bool TryGetValue(T1 key, out T2 value) => t1ToT2Dict.TryGetValue(key, out value);
// public bool TryGetValue(T2 key, out T1 value) => t2ToT1Dict.TryGetValue(key, out value);
// public bool Contains(T1 key) => t1ToT2Dict.ContainsKey(key);
// public bool Contains(T2 key) => t2ToT1Dict.ContainsKey(key);
// public void Remove(T1 key)
// {
// if (First.TryGetValue(key, out T2 value))
// {
// t1ToT2Dict.Remove(key);
// t2ToT1Dict.Remove(value);
// }
// }
// public void Clear()
// {
// First.Clear();
// Second.Clear();
// }
// public void Remove(T2 key)
// {
// if (Second.TryGetValue(key, out T1 value))
// {
// t1ToT2Dict.Remove(value);
// t2ToT1Dict.Remove(key);
// }
// }
// public void RemoveFirst(T1 key)
// {
// if (First.TryGetValue(key, out T2 value))
// {
// Second.Remove(value);
// First.Remove(key);
// }
// }
// public void RemoveSecond(T2 key)
// {
// if (Second.TryGetValue(key, out T1 value))
// {
// First.Remove(value);
// Second.Remove(key);
// }
// }
// public T1 this[T2 key]
// {
// get => t2ToT1Dict[key];
// set
// {
// Add(key, value);
// }
// }
// public T2 this[T1 key]
// {
// get => t1ToT2Dict[key];
// set
// {
// Add(key, value);
// }
// }
// }
//}

View File

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

View File

@ -0,0 +1,202 @@
using JamesFrowen.SimpleWeb;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace FishNet.Transporting.Bayou.Client
{
public class ClientSocket : CommonSocket
{
~ClientSocket()
{
StopConnection();
}
#region Private.
#region Configuration.
/// <summary>
/// Address to bind server to.
/// </summary>
private string _address = string.Empty;
/// <summary>
/// Port used by server.
/// </summary>
private ushort _port;
/// <summary>
/// MTU sizes for each channel.
/// </summary>
private int _mtu;
#endregion
#region Queues.
/// <summary>
/// Outbound messages which need to be handled.
/// </summary>
private Queue<Packet> _outgoing = new Queue<Packet>();
#endregion
/// <summary>
/// Client socket manager.
/// </summary>
private SimpleWebClient _client;
#endregion
/// <summary>
/// Initializes this for use.
/// </summary>
/// <param name="t"></param>
internal void Initialize(Transport t, int mtu)
{
base.Transport = t;
_mtu = mtu;
}
/// <summary>
/// Threaded operation to process client actions.
/// </summary>
private void Socket(bool useWss)
{
TcpConfig tcpConfig = new TcpConfig(false, 5000, 20000);
_client = SimpleWebClient.Create(ushort.MaxValue, 5000, tcpConfig);
_client.onConnect += _client_onConnect;
_client.onDisconnect += _client_onDisconnect;
_client.onData += _client_onData;
_client.onError += _client_onError;
string scheme = (useWss) ? "wss" : "ws";
UriBuilder builder = new UriBuilder
{
Scheme = scheme,
Host = _address,
Port = _port
};
base.SetConnectionState(LocalConnectionState.Starting, false);
_client.Connect(builder.Uri);
}
private void _client_onError(Exception obj)
{
StopConnection();
}
private void _client_onData(ArraySegment<byte> data)
{
if (_client == null || _client.ConnectionState != ClientState.Connected)
return;
Channel channel;
data = base.RemoveChannel(data, out channel);
ClientReceivedDataArgs dataArgs = new ClientReceivedDataArgs(data, channel, base.Transport.Index);
base.Transport.HandleClientReceivedDataArgs(dataArgs);
}
private void _client_onDisconnect()
{
StopConnection();
}
private void _client_onConnect()
{
base.SetConnectionState(LocalConnectionState.Started, false);
}
/// <summary>
/// Starts the client connection.
/// </summary>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="channelsCount"></param>
/// <param name="pollTime"></param>
internal bool StartConnection(string address, ushort port, bool useWss)
{
if (base.GetConnectionState() != LocalConnectionState.Stopped)
return false;
base.SetConnectionState(LocalConnectionState.Starting, false);
//Assign properties.
_port = port;
_address = address;
ResetQueues();
Socket(useWss);
return true;
}
/// <summary>
/// Stops the local socket.
/// </summary>
internal bool StopConnection()
{
if (base.GetConnectionState() == LocalConnectionState.Stopped || base.GetConnectionState() == LocalConnectionState.Stopping)
return false;
base.SetConnectionState(LocalConnectionState.Stopping, false);
_client.Disconnect();
base.SetConnectionState(LocalConnectionState.Stopped, false);
return true;
}
/// <summary>
/// Resets queues.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ResetQueues()
{
base.ClearPacketQueue(ref _outgoing);
}
/// <summary>
/// Dequeues and processes outgoing.
/// </summary>
private void DequeueOutgoing()
{
int count = _outgoing.Count;
for (int i = 0; i < count; i++)
{
Packet outgoing = _outgoing.Dequeue();
base.AddChannel(ref outgoing);
_client.Send(outgoing.GetArraySegment());
outgoing.Dispose();
}
}
/// <summary>
/// Allows for Outgoing queue to be iterated.
/// </summary>
internal void IterateOutgoing()
{
DequeueOutgoing();
}
/// <summary>
/// Iterates the Incoming queue.
/// </summary>
internal void IterateIncoming()
{
if (_client == null)
return;
/* This has to be called even if not connected because it will also poll events such as
* Connected, or Disconnected, ect. */
_client.ProcessMessageQueue();
}
/// <summary>
/// Sends a packet to the server.
/// </summary>
internal void SendToServer(byte channelId, ArraySegment<byte> segment)
{
//Not started, cannot send.
if (base.GetConnectionState() != LocalConnectionState.Started)
return;
base.Send(ref _outgoing, channelId, segment, -1);
}
}
}

View File

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

View File

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
namespace FishNet.Transporting.Bayou
{
public abstract class CommonSocket
{
#region Public.
/// <summary>
/// Current ConnectionState.
/// </summary>
private LocalConnectionState _connectionState = LocalConnectionState.Stopped;
/// <summary>
/// Returns the current ConnectionState.
/// </summary>
/// <returns></returns>
internal LocalConnectionState GetConnectionState()
{
return _connectionState;
}
/// <summary>
/// Sets a new connection state.
/// </summary>
/// <param name="connectionState"></param>
protected void SetConnectionState(LocalConnectionState connectionState, bool asServer)
{
//If state hasn't changed.
if (connectionState == _connectionState)
return;
_connectionState = connectionState;
if (asServer)
Transport.HandleServerConnectionState(new ServerConnectionStateArgs(connectionState, Transport.Index));
else
Transport.HandleClientConnectionState(new ClientConnectionStateArgs(connectionState, Transport.Index));
}
#endregion
#region Protected.
/// <summary>
/// Transport controlling this socket.
/// </summary>
protected Transport Transport = null;
#endregion
/// <summary>
/// Sends data to connectionId.
/// </summary>
internal void Send(ref Queue<Packet> queue, byte channelId, ArraySegment<byte> segment, int connectionId)
{
if (GetConnectionState() != LocalConnectionState.Started)
return;
//ConnectionId isn't used from client to server.
Packet outgoing = new Packet(connectionId, segment, channelId);
queue.Enqueue(outgoing);
}
/// <summary>
/// Clears a queue using Packet type.
/// </summary>
/// <param name="queue"></param>
internal void ClearPacketQueue(ref Queue<Packet> queue)
{
int count = queue.Count;
for (int i = 0; i < count; i++)
{
Packet p = queue.Dequeue();
p.Dispose();
}
}
/// <summary>
/// Adds channel to the end of the data.
/// </summary>
internal void AddChannel(ref Packet packet)
{
int writePosition = packet.Length;
packet.AddLength(1);
packet.Data[writePosition] = (byte)packet.Channel;
}
/// <summary>
/// Removes the channel, outputting it and returning a new ArraySegment.
/// </summary>
internal ArraySegment<byte> RemoveChannel(ArraySegment<byte> segment, out Channel channel)
{
byte[] array = segment.Array;
int count = segment.Count;
channel = (Channel)array[count - 1];
return new ArraySegment<byte>(array, 0, count - 1);
}
}
}

View File

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

View File

@ -0,0 +1,355 @@
using JamesFrowen.SimpleWeb;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace FishNet.Transporting.Bayou.Server
{
public class ServerSocket : CommonSocket
{
#region Public.
/// <summary>
/// Gets the current ConnectionState of a remote client on the server.
/// </summary>
/// <param name="connectionId">ConnectionId to get ConnectionState for.</param>
internal RemoteConnectionState GetConnectionState(int connectionId)
{
RemoteConnectionState state = _clients.Contains(connectionId) ? RemoteConnectionState.Started : RemoteConnectionState.Stopped;
return state;
}
#endregion
#region Private.
#region Configuration.
/// <summary>
/// Port used by server.
/// </summary>
private ushort _port;
/// <summary>
/// Maximum number of allowed clients.
/// </summary>
private int _maximumClients;
/// <summary>
/// MTU sizes for each channel.
/// </summary>
private int _mtu;
#endregion
#region Queues.
/// <summary>
/// Outbound messages which need to be handled.
/// </summary>
private Queue<Packet> _outgoing = new Queue<Packet>();
/// <summary>
/// Ids to disconnect next iteration. This ensures data goes through to disconnecting remote connections. This may be removed in a later release.
/// </summary>
private List<int> _disconnectingNext = new List<int>();
/// <summary>
/// Ids to disconnect immediately.
/// </summary>
private List<int> _disconnectingNow = new List<int>();
/// <summary>
/// ConnectionEvents which need to be handled.
/// </summary>
private Queue<RemoteConnectionEvent> _remoteConnectionEvents = new Queue<RemoteConnectionEvent>();
#endregion
/// <summary>
/// Currently connected clients.
/// </summary>
private HashSet<int> _clients = new HashSet<int>();
/// <summary>
/// Server socket manager.
/// </summary>
private SimpleWebServer _server;
/// <summary>
/// SslConfiguration to use.
/// </summary>
private SslConfiguration _sslConfiguration;
#endregion
~ServerSocket()
{
StopConnection();
}
/// <summary>
/// Initializes this for use.
/// </summary>
/// <param name="t"></param>
internal void Initialize(Transport t, int unreliableMTU, SslConfiguration config)
{
_sslConfiguration = config;
base.Transport = t;
_mtu = unreliableMTU;
}
/// <summary>
/// Threaded operation to process server actions.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Socket()
{
TcpConfig tcpConfig = new TcpConfig(false, 5000, 20000);
SslConfig config;
if (!_sslConfiguration.Enabled)
config = new SslConfig();
else
config = new SslConfig(_sslConfiguration.Enabled, _sslConfiguration.CertificatePath, _sslConfiguration.CertificatePassword,
_sslConfiguration.SslProtocol);
_server = new SimpleWebServer(5000, tcpConfig, _mtu, 5000, config);
_server.onConnect += _server_onConnect;
_server.onDisconnect += _server_onDisconnect;
_server.onData += _server_onData;
_server.onError += _server_onError;
base.SetConnectionState(LocalConnectionState.Starting, true);
_server.Start(_port);
base.SetConnectionState(LocalConnectionState.Started, true);
}
/// <summary>
/// Called when a client connection errors.
/// </summary>
private void _server_onError(int clientId, Exception arg2)
{
StopConnection(clientId, true);
}
/// <summary>
/// Called when receiving data.
/// </summary>
private void _server_onData(int clientId, ArraySegment<byte> data)
{
if (_server == null || !_server.Active)
return;
Channel channel;
ArraySegment<byte> segment = base.RemoveChannel(data, out channel);
ServerReceivedDataArgs dataArgs = new ServerReceivedDataArgs(segment, channel, clientId, base.Transport.Index);
base.Transport.HandleServerReceivedDataArgs(dataArgs);
}
/// <summary>
/// Called when a client connects.
/// </summary>
private void _server_onConnect(int clientId)
{
if (_server == null || !_server.Active)
return;
if (_clients.Count >= _maximumClients)
_server.KickClient(clientId);
else
_remoteConnectionEvents.Enqueue(new RemoteConnectionEvent(true, clientId));
}
/// <summary>
/// Called when a client disconnects.
/// </summary>
private void _server_onDisconnect(int clientId)
{
StopConnection(clientId, true);
}
/// <summary>
/// Gets the address of a remote connection Id.
/// </summary>
/// <param name="connectionId"></param>
/// <returns>Returns string.empty if Id is not found.</returns>
internal string GetConnectionAddress(int connectionId)
{
if (_server == null || !_server.Active)
return string.Empty;
return _server.GetClientAddress(connectionId);
}
/// <summary>
/// Starts the server.
/// </summary>
internal bool StartConnection(ushort port, int maximumClients)
{
if (base.GetConnectionState() != LocalConnectionState.Stopped)
return false;
base.SetConnectionState(LocalConnectionState.Starting, true);
//Assign properties.
_port = port;
_maximumClients = maximumClients;
ResetQueues();
Socket();
return true;
}
/// <summary>
/// Stops the local socket.
/// </summary>
internal bool StopConnection()
{
if (_server == null || base.GetConnectionState() == LocalConnectionState.Stopped || base.GetConnectionState() == LocalConnectionState.Stopping)
return false;
ResetQueues();
base.SetConnectionState(LocalConnectionState.Stopping, true);
_server.Stop();
base.SetConnectionState(LocalConnectionState.Stopped, true);
return true;
}
/// <summary>
/// Stops a remote client disconnecting the client from the server.
/// </summary>
/// <param name="connectionId">ConnectionId of the client to disconnect.</param>
internal bool StopConnection(int connectionId, bool immediately)
{
if (_server == null || base.GetConnectionState() != LocalConnectionState.Started)
return false;
//Don't disconnect immediately, wait until next command iteration.
if (!immediately)
{
_disconnectingNext.Add(connectionId);
}
//Disconnect immediately.
else
{
_server.KickClient(connectionId);
_clients.Remove(connectionId);
base.Transport.HandleRemoteConnectionState(new RemoteConnectionStateArgs(RemoteConnectionState.Stopped, connectionId, base.Transport.Index));
}
return true;
}
/// <summary>
/// Resets queues.
/// </summary>
private void ResetQueues()
{
_clients.Clear();
base.ClearPacketQueue(ref _outgoing);
_disconnectingNext.Clear();
_disconnectingNow.Clear();
_remoteConnectionEvents.Clear();
}
/// <summary>
/// Dequeues and processes commands.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DequeueDisconnects()
{
int count;
count = _disconnectingNow.Count;
//If there are disconnect nows.
if (count > 0)
{
for (int i = 0; i < count; i++)
StopConnection(_disconnectingNow[i], true);
_disconnectingNow.Clear();
}
count = _disconnectingNext.Count;
//If there are disconnect next.
if (count > 0)
{
for (int i = 0; i < count; i++)
_disconnectingNow.Add(_disconnectingNext[i]);
_disconnectingNext.Clear();
}
}
/// <summary>
/// Dequeues and processes outgoing.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DequeueOutgoing()
{
if (base.GetConnectionState() != LocalConnectionState.Started || _server == null)
{
//Not started, clear outgoing.
base.ClearPacketQueue(ref _outgoing);
}
else
{
int count = _outgoing.Count;
for (int i = 0; i < count; i++)
{
Packet outgoing = _outgoing.Dequeue();
int connectionId = outgoing.ConnectionId;
AddChannel(ref outgoing);
ArraySegment<byte> segment = outgoing.GetArraySegment();
//Send to all clients.
if (connectionId == -1)
_server.SendAll(_clients, segment);
//Send to one client.
else
_server.SendOne(connectionId, segment);
outgoing.Dispose();
}
}
}
/// <summary>
/// Allows for Outgoing queue to be iterated.
/// </summary>
internal void IterateOutgoing()
{
if (_server == null)
return;
DequeueOutgoing();
DequeueDisconnects();
}
/// <summary>
/// Iterates the Incoming queue.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void IterateIncoming()
{
if (_server == null)
return;
//Handle connection and disconnection events.
while (_remoteConnectionEvents.Count > 0)
{
RemoteConnectionEvent connectionEvent = _remoteConnectionEvents.Dequeue();
if (connectionEvent.Connected)
_clients.Add(connectionEvent.ConnectionId);
RemoteConnectionState state = (connectionEvent.Connected) ? RemoteConnectionState.Started : RemoteConnectionState.Stopped;
base.Transport.HandleRemoteConnectionState(new RemoteConnectionStateArgs(state, connectionEvent.ConnectionId, base.Transport.Index));
}
//Read data from clients.
_server.ProcessMessageQueue();
}
/// <summary>
/// Sends a packet to a single, or all clients.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void SendToClient(byte channelId, ArraySegment<byte> segment, int connectionId)
{
Send(ref _outgoing, channelId, segment, connectionId);
}
/// <summary>
/// Returns the maximum number of clients allowed to connect to the server. If the transport does not support this method the value -1 is returned.
/// </summary>
/// <returns></returns>
internal int GetMaximumClients()
{
return _maximumClients;
}
}
}

View File

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

View File

@ -0,0 +1,23 @@
using System.Security.Authentication;
namespace FishNet.Transporting.Bayou
{
[System.Serializable]
public struct SslConfiguration
{
public bool Enabled;
public string CertificatePath;
public string CertificatePassword;
public SslProtocols SslProtocol;
public SslConfiguration(bool enabled, string certPath, string certPassword, SslProtocols sslProtocols)
{
Enabled = enabled;
CertificatePath = certPath;
CertificatePassword = certPassword;
SslProtocol = sslProtocols;
}
}
}

View File

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

View File

@ -0,0 +1,78 @@
using FishNet.Utility.Performance;
using System;
namespace FishNet.Transporting.Bayou
{
internal struct Packet
{
public readonly int ConnectionId;
public byte[] Data;
public int Length;
public readonly byte Channel;
public Packet(int connectionId, byte[] data, int length, byte channel)
{
ConnectionId = connectionId;
Data = data;
Length = length;
Channel = channel;
}
public Packet(int sender, ArraySegment<byte> segment, byte channel)
{
Data = ByteArrayPool.Retrieve(segment.Count);
Buffer.BlockCopy(segment.Array, segment.Offset, Data, 0, segment.Count);
ConnectionId = sender;
Length = segment.Count;
Channel = channel;
}
public ArraySegment<byte> GetArraySegment()
{
return new ArraySegment<byte>(Data, 0, Length);
}
/// <summary>
/// Adds on length and resizes Data if needed.
/// </summary>
/// <param name="length"></param>
public void AddLength(int length)
{
int totalNeeded = (Length + length);
if (Data.Length < totalNeeded)
Array.Resize(ref Data, totalNeeded);
Length += length;
}
public void Dispose()
{
ByteArrayPool.Store(Data);
}
}
}
namespace FishNet.Transporting.Bayou.Server
{
internal struct RemoteConnectionEvent
{
public readonly bool Connected;
public readonly int ConnectionId;
public RemoteConnectionEvent(bool connected, int connectionId)
{
Connected = connected;
ConnectionId = connectionId;
}
}
}

View File

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

View File

@ -0,0 +1,23 @@
MIT License
Copyright (c) 2020 Mirror Networking
Copyright (c) 2020 James Frowen
Copyright (c) 2022, Benjamin Berwick of FirstGearGames LLC, registered 2018, North Carolina.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 179e84ac4879de74fb51966e6c161a51
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@ -0,0 +1,7 @@
using System.Reflection;
using System.Runtime.CompilerServices;
[assembly: AssemblyVersion("1.2.4")]
[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Runtime")]
[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Editor")]

View File

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

View File

@ -0,0 +1,20 @@
## [1.2.4](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.3...v1.2.4) (2021-12-16)
### Bug Fixes
* adding meta file for changelog ([ba5b164](https://github.com/James-Frowen/SimpleWebTransport/commit/ba5b1647aa5cc69ca80f5b52c542a9b5ee749c7f))
## [1.2.3](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.2...v1.2.3) (2021-12-16)
### Bug Fixes
* fixing compile error in assemblyInfo ([7ee8380](https://github.com/James-Frowen/SimpleWebTransport/commit/7ee8380b4daf34d4e12017de55d8be481690046f))
## [1.2.2](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.1...v1.2.2) (2021-12-16)
### Bug Fixes
* fixing release with empty commit ([068af74](https://github.com/James-Frowen/SimpleWebTransport/commit/068af74f7399354081f25181f90fb060b0fa1524))

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b0ef23ac1c6a62546bbad5529b3bfdad
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@ -0,0 +1,99 @@
using System;
using System.Collections.Concurrent;
using UnityEngine;
namespace JamesFrowen.SimpleWeb
{
public enum ClientState
{
NotConnected = 0,
Connecting = 1,
Connected = 2,
Disconnecting = 3,
}
/// <summary>
/// Client used to control websockets
/// <para>Base class used by WebSocketClientWebGl and WebSocketClientStandAlone</para>
/// </summary>
public abstract class SimpleWebClient
{
public static SimpleWebClient Create(int maxMessageSize, int maxMessagesPerTick, TcpConfig tcpConfig)
{
#if UNITY_WEBGL && !UNITY_EDITOR
return new WebSocketClientWebGl(maxMessageSize, maxMessagesPerTick);
#else
return new WebSocketClientStandAlone(maxMessageSize, maxMessagesPerTick, tcpConfig);
#endif
}
readonly int maxMessagesPerTick;
protected readonly int maxMessageSize;
public readonly ConcurrentQueue<Message> receiveQueue = new ConcurrentQueue<Message>();
protected readonly BufferPool bufferPool;
protected ClientState state;
protected SimpleWebClient(int maxMessageSize, int maxMessagesPerTick)
{
this.maxMessageSize = maxMessageSize;
this.maxMessagesPerTick = maxMessagesPerTick;
bufferPool = new BufferPool(5, 20, maxMessageSize);
}
public ClientState ConnectionState => state;
public event Action onConnect;
public event Action onDisconnect;
public event Action<ArraySegment<byte>> onData;
public event Action<Exception> onError;
/// <summary>
/// Processes all new messages
/// </summary>
public void ProcessMessageQueue()
{
ProcessMessageQueue(null);
}
/// <summary>
/// Processes all messages while <paramref name="behaviour"/> is enabled
/// </summary>
/// <param name="behaviour"></param>
public void ProcessMessageQueue(MonoBehaviour behaviour)
{
int processedCount = 0;
bool skipEnabled = behaviour == null;
// check enabled every time incase behaviour was disabled after data
while (
(skipEnabled || behaviour.enabled) &&
processedCount < maxMessagesPerTick &&
// Dequeue last
receiveQueue.TryDequeue(out Message next)
)
{
processedCount++;
switch (next.type)
{
case EventType.Connected:
onConnect?.Invoke();
break;
case EventType.Data:
onData?.Invoke(next.data.ToSegment());
next.data.Release();
break;
case EventType.Disconnected:
onDisconnect?.Invoke();
break;
case EventType.Error:
onError?.Invoke(next.exception);
break;
}
}
}
public abstract void Connect(Uri serverAddress);
public abstract void Disconnect();
public abstract void Send(ArraySegment<byte> segment);
}
}

View File

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

View File

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

View File

@ -0,0 +1,77 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace JamesFrowen.SimpleWeb
{
/// <summary>
/// Handles Handshake to the server when it first connects
/// <para>The client handshake does not need buffers to reduce allocations since it only happens once</para>
/// </summary>
internal class ClientHandshake
{
public bool TryHandshake(Connection conn, Uri uri)
{
try
{
Stream stream = conn.stream;
byte[] keyBuffer = new byte[16];
using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(keyBuffer);
}
string key = Convert.ToBase64String(keyBuffer);
string keySum = key + Constants.HandshakeGUID;
byte[] keySumBytes = Encoding.ASCII.GetBytes(keySum);
Log.Verbose($"Handshake Hashing {Encoding.ASCII.GetString(keySumBytes)}");
byte[] keySumHash = SHA1.Create().ComputeHash(keySumBytes);
string expectedResponse = Convert.ToBase64String(keySumHash);
string handshake =
$"GET {uri.PathAndQuery} HTTP/1.1\r\n" +
$"Host: {uri.Host}:{uri.Port}\r\n" +
$"Upgrade: websocket\r\n" +
$"Connection: Upgrade\r\n" +
$"Sec-WebSocket-Key: {key}\r\n" +
$"Sec-WebSocket-Version: 13\r\n" +
"\r\n";
byte[] encoded = Encoding.ASCII.GetBytes(handshake);
stream.Write(encoded, 0, encoded.Length);
byte[] responseBuffer = new byte[1000];
int? lengthOrNull = ReadHelper.SafeReadTillMatch(stream, responseBuffer, 0, responseBuffer.Length, Constants.endOfHandshake);
if (!lengthOrNull.HasValue)
{
Log.Error("Connected closed before handshake");
return false;
}
string responseString = Encoding.ASCII.GetString(responseBuffer, 0, lengthOrNull.Value);
string acceptHeader = "Sec-WebSocket-Accept: ";
int startIndex = responseString.IndexOf(acceptHeader, StringComparison.InvariantCultureIgnoreCase) + acceptHeader.Length;
int endIndex = responseString.IndexOf("\r\n", startIndex);
string responseKey = responseString.Substring(startIndex, endIndex - startIndex);
if (responseKey != expectedResponse)
{
Log.Error($"Response key incorrect, Response:{responseKey} Expected:{expectedResponse}");
return false;
}
return true;
}
catch (Exception e)
{
Log.Exception(e);
return false;
}
}
}
}

View File

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

View File

@ -0,0 +1,47 @@
using System;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
namespace JamesFrowen.SimpleWeb
{
internal class ClientSslHelper
{
internal bool TryCreateStream(Connection conn, Uri uri)
{
NetworkStream stream = conn.client.GetStream();
if (uri.Scheme != "wss")
{
conn.stream = stream;
return true;
}
try
{
conn.stream = CreateStream(stream, uri);
return true;
}
catch (Exception e)
{
Log.Error($"Create SSLStream Failed: {e}", false);
return false;
}
}
Stream CreateStream(NetworkStream stream, Uri uri)
{
SslStream sslStream = new SslStream(stream, true, ValidateServerCertificate);
sslStream.AuthenticateAsClient(uri.Host);
return sslStream;
}
static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
// Do not allow this client to communicate with unauthenticated servers.
// only accept if no errors
return sslPolicyErrors == SslPolicyErrors.None;
}
}
}

View File

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

View File

@ -0,0 +1,149 @@
using System;
using System.Net.Sockets;
using System.Threading;
namespace JamesFrowen.SimpleWeb
{
public class WebSocketClientStandAlone : SimpleWebClient
{
readonly ClientSslHelper sslHelper;
readonly ClientHandshake handshake;
readonly TcpConfig tcpConfig;
Connection conn;
internal WebSocketClientStandAlone(int maxMessageSize, int maxMessagesPerTick, TcpConfig tcpConfig) : base(maxMessageSize, maxMessagesPerTick)
{
#if UNITY_WEBGL && !UNITY_EDITOR
throw new NotSupportedException();
#else
sslHelper = new ClientSslHelper();
handshake = new ClientHandshake();
this.tcpConfig = tcpConfig;
#endif
}
public override void Connect(Uri serverAddress)
{
state = ClientState.Connecting;
// create connection here before thread so that send queue exist for MiragePeer to send to
var client = new TcpClient();
tcpConfig.ApplyTo(client);
// create connection object here so dispose correctly disconnects on failed connect
conn = new Connection(client, AfterConnectionDisposed);
var receiveThread = new Thread(() => ConnectAndReceiveLoop(serverAddress));
receiveThread.IsBackground = true;
receiveThread.Start();
}
void ConnectAndReceiveLoop(Uri serverAddress)
{
try
{
// connection created above
TcpClient client = conn.client;
//TcpClient client = new TcpClient();
//tcpConfig.ApplyTo(client);
//// create connection object here so dispose correctly disconnects on failed connect
//conn = new Connection(client, AfterConnectionDisposed);
conn.receiveThread = Thread.CurrentThread;
try
{
client.Connect(serverAddress.Host, serverAddress.Port);
}
catch (SocketException)
{
client.Dispose();
throw;
}
bool success = sslHelper.TryCreateStream(conn, serverAddress);
if (!success)
{
Log.Warn("Failed to create Stream");
conn.Dispose();
return;
}
success = handshake.TryHandshake(conn, serverAddress);
if (!success)
{
Log.Warn("Failed Handshake");
conn.Dispose();
return;
}
Log.Info("HandShake Successful");
state = ClientState.Connected;
receiveQueue.Enqueue(new Message(EventType.Connected));
var sendThread = new Thread(() =>
{
var sendConfig = new SendLoop.Config(
conn,
bufferSize: Constants.HeaderSize + Constants.MaskSize + maxMessageSize,
setMask: true);
SendLoop.Loop(sendConfig);
});
conn.sendThread = sendThread;
sendThread.IsBackground = true;
sendThread.Start();
var config = new ReceiveLoop.Config(conn,
maxMessageSize,
false,
receiveQueue,
bufferPool);
ReceiveLoop.Loop(config);
}
catch (ThreadInterruptedException e) { Log.InfoException(e); }
catch (ThreadAbortException e) { Log.InfoException(e); }
catch (Exception e) { Log.Exception(e); }
finally
{
// close here incase connect fails
conn?.Dispose();
}
}
void AfterConnectionDisposed(Connection conn)
{
state = ClientState.NotConnected;
// make sure Disconnected event is only called once
receiveQueue.Enqueue(new Message(EventType.Disconnected));
}
public override void Disconnect()
{
state = ClientState.Disconnecting;
Log.Info("Disconnect Called");
if (conn == null)
{
state = ClientState.NotConnected;
}
else
{
conn?.Dispose();
}
}
public override void Send(ArraySegment<byte> segment)
{
ArrayBuffer buffer = bufferPool.Take(segment.Count);
buffer.CopyFrom(segment);
conn.sendQueue.Enqueue(buffer);
conn.sendPending.Set();
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,34 @@
using System;
#if UNITY_WEBGL
using System.Runtime.InteropServices;
#endif
namespace JamesFrowen.SimpleWeb
{
internal static class SimpleWebJSLib
{
#if UNITY_WEBGL
[DllImport("__Internal")]
internal static extern bool IsConnected(int index);
#pragma warning disable CA2101 // Specify marshaling for P/Invoke string arguments
[DllImport("__Internal")]
#pragma warning restore CA2101 // Specify marshaling for P/Invoke string arguments
internal static extern int Connect(string address, Action<int> openCallback, Action<int> closeCallBack, Action<int, IntPtr, int> messageCallback, Action<int> errorCallback);
[DllImport("__Internal")]
internal static extern void Disconnect(int index);
[DllImport("__Internal")]
internal static extern bool Send(int index, byte[] array, int offset, int length);
#else
internal static bool IsConnected(int index) => throw new NotSupportedException();
internal static int Connect(string address, Action<int> openCallback, Action<int> closeCallBack, Action<int, IntPtr, int> messageCallback, Action<int> errorCallback) => throw new NotSupportedException();
internal static void Disconnect(int index) => throw new NotSupportedException();
internal static bool Send(int index, byte[] array, int offset, int length) => throw new NotSupportedException();
#endif
}
}

View File

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

View File

@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AOT;
namespace JamesFrowen.SimpleWeb
{
public class WebSocketClientWebGl : SimpleWebClient
{
static readonly Dictionary<int, WebSocketClientWebGl> instances = new Dictionary<int, WebSocketClientWebGl>();
/// <summary>
/// key for instances sent between c# and js
/// </summary>
int index;
/// <summary>
/// Message sent by high level while still connecting, they will be send after onOpen is called
/// <para>this is a workaround for mirage where send it called right after Connect</para>
/// </summary>
Queue<byte[]> ConnectingSendQueue;
internal WebSocketClientWebGl(int maxMessageSize, int maxMessagesPerTick) : base(maxMessageSize, maxMessagesPerTick)
{
#if !UNITY_WEBGL || UNITY_EDITOR
throw new NotSupportedException();
#endif
}
public bool CheckJsConnected() => SimpleWebJSLib.IsConnected(index);
public override void Connect(Uri serverAddress)
{
index = SimpleWebJSLib.Connect(serverAddress.ToString(), OpenCallback, CloseCallBack, MessageCallback, ErrorCallback);
instances.Add(index, this);
state = ClientState.Connecting;
}
public override void Disconnect()
{
state = ClientState.Disconnecting;
// disconnect should cause closeCallback and OnDisconnect to be called
SimpleWebJSLib.Disconnect(index);
}
public override void Send(ArraySegment<byte> segment)
{
if (segment.Count > maxMessageSize)
{
Log.Error($"Cant send message with length {segment.Count} because it is over the max size of {maxMessageSize}");
return;
}
if (state == ClientState.Connected)
{
SimpleWebJSLib.Send(index, segment.Array, segment.Offset, segment.Count);
}
else
{
if (ConnectingSendQueue == null)
ConnectingSendQueue = new Queue<byte[]>();
ConnectingSendQueue.Enqueue(segment.ToArray());
}
}
void onOpen()
{
receiveQueue.Enqueue(new Message(EventType.Connected));
state = ClientState.Connected;
if (ConnectingSendQueue != null)
{
while (ConnectingSendQueue.Count > 0)
{
byte[] next = ConnectingSendQueue.Dequeue();
SimpleWebJSLib.Send(index, next, 0, next.Length);
}
ConnectingSendQueue = null;
}
}
void onClose()
{
// this code should be last in this class
receiveQueue.Enqueue(new Message(EventType.Disconnected));
state = ClientState.NotConnected;
instances.Remove(index);
}
void onMessage(IntPtr bufferPtr, int count)
{
try
{
ArrayBuffer buffer = bufferPool.Take(count);
buffer.CopyFrom(bufferPtr, count);
receiveQueue.Enqueue(new Message(buffer));
}
catch (Exception e)
{
Log.Error($"onData {e.GetType()}: {e.Message}\n{e.StackTrace}");
receiveQueue.Enqueue(new Message(e));
}
}
void onErr()
{
receiveQueue.Enqueue(new Message(new Exception("Javascript Websocket error")));
Disconnect();
}
[MonoPInvokeCallback(typeof(Action<int>))]
static void OpenCallback(int index) => instances[index].onOpen();
[MonoPInvokeCallback(typeof(Action<int>))]
static void CloseCallBack(int index) => instances[index].onClose();
[MonoPInvokeCallback(typeof(Action<int, IntPtr, int>))]
static void MessageCallback(int index, IntPtr bufferPtr, int count) => instances[index].onMessage(bufferPtr, count);
[MonoPInvokeCallback(typeof(Action<int>))]
static void ErrorCallback(int index) => instances[index].onErr();
}
}

View File

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

View File

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

View File

@ -0,0 +1,115 @@
// this will create a global object
const SimpleWeb = {
webSockets: [],
next: 1,
GetWebSocket: function (index) {
return SimpleWeb.webSockets[index]
},
AddNextSocket: function (webSocket) {
var index = SimpleWeb.next;
SimpleWeb.next++;
SimpleWeb.webSockets[index] = webSocket;
return index;
},
RemoveSocket: function (index) {
SimpleWeb.webSockets[index] = undefined;
},
};
function IsConnected(index) {
var webSocket = SimpleWeb.GetWebSocket(index);
if (webSocket) {
return webSocket.readyState === webSocket.OPEN;
}
else {
return false;
}
}
function Connect(addressPtr, openCallbackPtr, closeCallBackPtr, messageCallbackPtr, errorCallbackPtr) {
// fix for unity 2021 because unity bug in .jslib
if (typeof Runtime === "undefined") {
// if unity doesn't create Runtime, then make it here
// dont ask why this works, just be happy that it does
Runtime = {
dynCall: dynCall
}
}
const address = UTF8ToString(addressPtr);
console.log("Connecting to " + address);
// Create webSocket connection.
webSocket = new WebSocket(address);
webSocket.binaryType = 'arraybuffer';
const index = SimpleWeb.AddNextSocket(webSocket);
// Connection opened
webSocket.addEventListener('open', function (event) {
console.log("Connected to " + address);
Runtime.dynCall('vi', openCallbackPtr, [index]);
});
webSocket.addEventListener('close', function (event) {
console.log("Disconnected from " + address);
Runtime.dynCall('vi', closeCallBackPtr, [index]);
});
// Listen for messages
webSocket.addEventListener('message', function (event) {
if (event.data instanceof ArrayBuffer) {
// TODO dont alloc each time
var array = new Uint8Array(event.data);
var arrayLength = array.length;
var bufferPtr = _malloc(arrayLength);
var dataBuffer = new Uint8Array(HEAPU8.buffer, bufferPtr, arrayLength);
dataBuffer.set(array);
Runtime.dynCall('viii', messageCallbackPtr, [index, bufferPtr, arrayLength]);
_free(bufferPtr);
}
else {
console.error("message type not supported")
}
});
webSocket.addEventListener('error', function (event) {
console.error('Socket Error', event);
Runtime.dynCall('vi', errorCallbackPtr, [index]);
});
return index;
}
function Disconnect(index) {
var webSocket = SimpleWeb.GetWebSocket(index);
if (webSocket) {
webSocket.close(1000, "Disconnect Called by Mirror");
}
SimpleWeb.RemoveSocket(index);
}
function Send(index, arrayPtr, offset, length) {
var webSocket = SimpleWeb.GetWebSocket(index);
if (webSocket) {
const start = arrayPtr + offset;
const end = start + length;
const data = HEAPU8.buffer.slice(start, end);
webSocket.send(data);
return true;
}
return false;
}
const SimpleWebLib = {
$SimpleWeb: SimpleWeb,
IsConnected,
Connect,
Disconnect,
Send
};
autoAddDeps(SimpleWebLib, '$SimpleWeb');
mergeInto(LibraryManager.library, SimpleWebLib);

View File

@ -0,0 +1,37 @@
fileFormatVersion: 2
guid: 54452a8c6d2ca9b49a8c79f81b50305c
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:
Facebook: WebGL
second:
enabled: 1
settings: {}
- first:
WebGL: WebGL
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@ -0,0 +1,265 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
namespace JamesFrowen.SimpleWeb
{
public interface IBufferOwner
{
void Return(ArrayBuffer buffer);
}
public sealed class ArrayBuffer : IDisposable
{
readonly IBufferOwner owner;
public readonly byte[] array;
/// <summary>
/// number of bytes writen to buffer
/// </summary>
public int count { get; internal set; }
/// <summary>
/// How many times release needs to be called before buffer is returned to pool
/// <para>This allows the buffer to be used in multiple places at the same time</para>
/// </summary>
public void SetReleasesRequired(int required)
{
releasesRequired = required;
}
/// <summary>
/// How many times release needs to be called before buffer is returned to pool
/// <para>This allows the buffer to be used in multiple places at the same time</para>
/// </summary>
/// <remarks>
/// This value is normally 0, but can be changed to require release to be called multiple times
/// </remarks>
int releasesRequired;
public ArrayBuffer(IBufferOwner owner, int size)
{
this.owner = owner;
array = new byte[size];
}
public void Release()
{
int newValue = Interlocked.Decrement(ref releasesRequired);
if (newValue <= 0)
{
count = 0;
owner.Return(this);
}
}
public void Dispose()
{
Release();
}
public void CopyTo(byte[] target, int offset)
{
if (count > (target.Length + offset)) throw new ArgumentException($"{nameof(count)} was greater than {nameof(target)}.length", nameof(target));
Buffer.BlockCopy(array, 0, target, offset, count);
}
public void CopyFrom(ArraySegment<byte> segment)
{
CopyFrom(segment.Array, segment.Offset, segment.Count);
}
public void CopyFrom(byte[] source, int offset, int length)
{
if (length > array.Length) throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length));
count = length;
Buffer.BlockCopy(source, offset, array, 0, length);
}
public void CopyFrom(IntPtr bufferPtr, int length)
{
if (length > array.Length) throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length));
count = length;
Marshal.Copy(bufferPtr, array, 0, length);
}
public ArraySegment<byte> ToSegment()
{
return new ArraySegment<byte>(array, 0, count);
}
[Conditional("UNITY_ASSERTIONS")]
internal void Validate(int arraySize)
{
if (array.Length != arraySize)
{
Log.Error("Buffer that was returned had an array of the wrong size");
}
}
}
internal class BufferBucket : IBufferOwner
{
public readonly int arraySize;
readonly ConcurrentQueue<ArrayBuffer> buffers;
/// <summary>
/// keeps track of how many arrays are taken vs returned
/// </summary>
internal int _current = 0;
public BufferBucket(int arraySize)
{
this.arraySize = arraySize;
buffers = new ConcurrentQueue<ArrayBuffer>();
}
public ArrayBuffer Take()
{
IncrementCreated();
if (buffers.TryDequeue(out ArrayBuffer buffer))
{
return buffer;
}
else
{
//Log.Verbose($"BufferBucket({arraySize}) create new");
return new ArrayBuffer(this, arraySize);
}
}
public void Return(ArrayBuffer buffer)
{
DecrementCreated();
buffer.Validate(arraySize);
buffers.Enqueue(buffer);
}
[Conditional("DEBUG")]
void IncrementCreated()
{
int next = Interlocked.Increment(ref _current);
//Log.Verbose($"BufferBucket({arraySize}) count:{next}");
}
[Conditional("DEBUG")]
void DecrementCreated()
{
int next = Interlocked.Decrement(ref _current);
//Log.Verbose($"BufferBucket({arraySize}) count:{next}");
}
}
/// <summary>
/// Collection of different sized buffers
/// </summary>
/// <remarks>
/// <para>
/// Problem: <br/>
/// * Need to cached byte[] so that new ones arn't created each time <br/>
/// * Arrays sent are multiple different sizes <br/>
/// * Some message might be big so need buffers to cover that size <br/>
/// * Most messages will be small compared to max message size <br/>
/// </para>
/// <br/>
/// <para>
/// Solution: <br/>
/// * Create multiple groups of buffers covering the range of allowed sizes <br/>
/// * Split range exponentially (using math.log) so that there are more groups for small buffers <br/>
/// </para>
/// </remarks>
public class BufferPool
{
internal readonly BufferBucket[] buckets;
readonly int bucketCount;
readonly int smallest;
readonly int largest;
public BufferPool(int bucketCount, int smallest, int largest)
{
if (bucketCount < 2) throw new ArgumentException("Count must be atleast 2");
if (smallest < 1) throw new ArgumentException("Smallest must be atleast 1");
if (largest < smallest) throw new ArgumentException("Largest must be greater than smallest");
this.bucketCount = bucketCount;
this.smallest = smallest;
this.largest = largest;
// split range over log scale (more buckets for smaller sizes)
double minLog = Math.Log(this.smallest);
double maxLog = Math.Log(this.largest);
double range = maxLog - minLog;
double each = range / (bucketCount - 1);
buckets = new BufferBucket[bucketCount];
for (int i = 0; i < bucketCount; i++)
{
double size = smallest * Math.Pow(Math.E, each * i);
buckets[i] = new BufferBucket((int)Math.Ceiling(size));
}
Validate();
// Example
// 5 count
// 20 smallest
// 16400 largest
// 3.0 log 20
// 9.7 log 16400
// 6.7 range 9.7 - 3
// 1.675 each 6.7 / (5-1)
// 20 e^ (3 + 1.675 * 0)
// 107 e^ (3 + 1.675 * 1)
// 572 e^ (3 + 1.675 * 2)
// 3056 e^ (3 + 1.675 * 3)
// 16,317 e^ (3 + 1.675 * 4)
// perceision wont be lose when using doubles
}
[Conditional("UNITY_ASSERTIONS")]
void Validate()
{
if (buckets[0].arraySize != smallest)
{
Log.Error($"BufferPool Failed to create bucket for smallest. bucket:{buckets[0].arraySize} smallest{smallest}");
}
int largestBucket = buckets[bucketCount - 1].arraySize;
// rounded using Ceiling, so allowed to be 1 more that largest
if (largestBucket != largest && largestBucket != largest + 1)
{
Log.Error($"BufferPool Failed to create bucket for largest. bucket:{largestBucket} smallest{largest}");
}
}
public ArrayBuffer Take(int size)
{
if (size > largest) { throw new ArgumentException($"Size ({size}) is greatest that largest ({largest})"); }
for (int i = 0; i < bucketCount; i++)
{
if (size <= buckets[i].arraySize)
{
return buckets[i].Take();
}
}
throw new ArgumentException($"Size ({size}) is greatest that largest ({largest})");
}
}
}

View File

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

View File

@ -0,0 +1,90 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net.Sockets;
using System.Threading;
namespace JamesFrowen.SimpleWeb
{
internal sealed class Connection : IDisposable
{
public const int IdNotSet = -1;
readonly object disposedLock = new object();
public TcpClient client;
public int connId = IdNotSet;
public Stream stream;
public Thread receiveThread;
public Thread sendThread;
public ManualResetEventSlim sendPending = new ManualResetEventSlim(false);
public ConcurrentQueue<ArrayBuffer> sendQueue = new ConcurrentQueue<ArrayBuffer>();
public Action<Connection> onDispose;
volatile bool hasDisposed;
public Connection(TcpClient client, Action<Connection> onDispose)
{
this.client = client ?? throw new ArgumentNullException(nameof(client));
this.onDispose = onDispose;
}
/// <summary>
/// disposes client and stops threads
/// </summary>
public void Dispose()
{
Log.Verbose($"Dispose {ToString()}");
// check hasDisposed first to stop ThreadInterruptedException on lock
if (hasDisposed) { return; }
Log.Info($"Connection Close: {ToString()}");
lock (disposedLock)
{
// check hasDisposed again inside lock to make sure no other object has called this
if (hasDisposed) { return; }
hasDisposed = true;
// stop threads first so they dont try to use disposed objects
receiveThread.Interrupt();
sendThread?.Interrupt();
try
{
// stream
stream?.Dispose();
stream = null;
client.Dispose();
client = null;
}
catch (Exception e)
{
Log.Exception(e);
}
sendPending.Dispose();
// release all buffers in send queue
while (sendQueue.TryDequeue(out ArrayBuffer buffer))
{
buffer.Release();
}
onDispose.Invoke(this);
}
}
public override string ToString()
{
System.Net.EndPoint endpoint = client?.Client?.RemoteEndPoint;
return $"[Conn:{connId}, endPoint:{endpoint}]";
}
}
}

View File

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

View File

@ -0,0 +1,72 @@
using System.Text;
namespace JamesFrowen.SimpleWeb
{
/// <summary>
/// Constant values that should never change
/// <para>
/// Some values are from https://tools.ietf.org/html/rfc6455
/// </para>
/// </summary>
internal static class Constants
{
/// <summary>
/// Header is at most 4 bytes
/// <para>
/// If message is less than 125 then header is 2 bytes, else header is 4 bytes
/// </para>
/// </summary>
public const int HeaderSize = 4;
/// <summary>
/// Smallest size of header
/// <para>
/// If message is less than 125 then header is 2 bytes, else header is 4 bytes
/// </para>
/// </summary>
public const int HeaderMinSize = 2;
/// <summary>
/// bytes for short length
/// </summary>
public const int ShortLength = 2;
/// <summary>
/// Message mask is always 4 bytes
/// </summary>
public const int MaskSize = 4;
/// <summary>
/// Max size of a message for length to be 1 byte long
/// <para>
/// payload length between 0-125
/// </para>
/// </summary>
public const int BytePayloadLength = 125;
/// <summary>
/// if payload length is 126 when next 2 bytes will be the length
/// </summary>
public const int UshortPayloadLength = 126;
/// <summary>
/// if payload length is 127 when next 8 bytes will be the length
/// </summary>
public const int UlongPayloadLength = 127;
/// <summary>
/// Guid used for WebSocket Protocol
/// </summary>
public const string HandshakeGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
public static readonly int HandshakeGUIDLength = HandshakeGUID.Length;
public static readonly byte[] HandshakeGUIDBytes = Encoding.ASCII.GetBytes(HandshakeGUID);
/// <summary>
/// Handshake messages will end with \r\n\r\n
/// </summary>
public static readonly byte[] endOfHandshake = new byte[4] { (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' };
}
}

View File

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

View File

@ -0,0 +1,10 @@
namespace JamesFrowen.SimpleWeb
{
public enum EventType
{
Connected,
Data,
Disconnected,
Error
}
}

View File

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

View File

@ -0,0 +1,116 @@
using System;
using UnityEngine;
using Conditional = System.Diagnostics.ConditionalAttribute;
namespace JamesFrowen.SimpleWeb
{
public static class Log
{
// used for Conditional
const string SIMPLEWEB_LOG_ENABLED = nameof(SIMPLEWEB_LOG_ENABLED);
const string DEBUG = nameof(DEBUG);
public enum Levels
{
none = 0,
error = 1,
warn = 2,
info = 3,
verbose = 4,
}
public static ILogger logger = Debug.unityLogger;
public static Levels level = Levels.none;
public static string BufferToString(byte[] buffer, int offset = 0, int? length = null)
{
return BitConverter.ToString(buffer, offset, length ?? buffer.Length);
}
[Conditional(SIMPLEWEB_LOG_ENABLED)]
public static void DumpBuffer(string label, byte[] buffer, int offset, int length)
{
if (level < Levels.verbose)
return;
logger.Log(LogType.Log, $"VERBOSE: <color=blue>{label}: {BufferToString(buffer, offset, length)}</color>");
}
[Conditional(SIMPLEWEB_LOG_ENABLED)]
public static void DumpBuffer(string label, ArrayBuffer arrayBuffer)
{
if (level < Levels.verbose)
return;
logger.Log(LogType.Log, $"VERBOSE: <color=blue>{label}: {BufferToString(arrayBuffer.array, 0, arrayBuffer.count)}</color>");
}
[Conditional(SIMPLEWEB_LOG_ENABLED)]
public static void Verbose(string msg, bool showColor = true)
{
if (level < Levels.verbose)
return;
if (showColor)
logger.Log(LogType.Log, $"VERBOSE: <color=blue>{msg}</color>");
else
logger.Log(LogType.Log, $"VERBOSE: {msg}");
}
[Conditional(SIMPLEWEB_LOG_ENABLED)]
public static void Info(string msg, bool showColor = true)
{
if (level < Levels.info)
return;
if (showColor)
logger.Log(LogType.Log, $"INFO: <color=blue>{msg}</color>");
else
logger.Log(LogType.Log, $"INFO: {msg}");
}
/// <summary>
/// An expected Exception was caught, useful for debugging but not important
/// </summary>
/// <param name="msg"></param>
/// <param name="showColor"></param>
[Conditional(SIMPLEWEB_LOG_ENABLED)]
public static void InfoException(Exception e)
{
if (level < Levels.info)
return;
logger.Log(LogType.Log, $"INFO_EXCEPTION: <color=blue>{e.GetType().Name}</color> Message: {e.Message}\n{e.StackTrace}\n\n");
}
[Conditional(SIMPLEWEB_LOG_ENABLED), Conditional(DEBUG)]
public static void Warn(string msg, bool showColor = true)
{
if (level < Levels.warn)
return;
if (showColor)
logger.Log(LogType.Warning, $"WARN: <color=orange>{msg}</color>");
else
logger.Log(LogType.Warning, $"WARN: {msg}");
}
[Conditional(SIMPLEWEB_LOG_ENABLED), Conditional(DEBUG)]
public static void Error(string msg, bool showColor = true)
{
if (level < Levels.error)
return;
if (showColor)
logger.Log(LogType.Error, $"ERROR: <color=red>{msg}</color>");
else
logger.Log(LogType.Error, $"ERROR: {msg}");
}
public static void Exception(Exception e)
{
// always log Exceptions
logger.Log(LogType.Error, $"EXCEPTION: <color=red>{e.GetType().Name}</color> Message: {e.Message}\n{e.StackTrace}\n\n");
}
}
}

View File

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

View File

@ -0,0 +1,49 @@
using System;
namespace JamesFrowen.SimpleWeb
{
public struct Message
{
public readonly int connId;
public readonly EventType type;
public readonly ArrayBuffer data;
public readonly Exception exception;
public Message(EventType type) : this()
{
this.type = type;
}
public Message(ArrayBuffer data) : this()
{
type = EventType.Data;
this.data = data;
}
public Message(Exception exception) : this()
{
type = EventType.Error;
this.exception = exception;
}
public Message(int connId, EventType type) : this()
{
this.connId = connId;
this.type = type;
}
public Message(int connId, ArrayBuffer data) : this()
{
this.connId = connId;
type = EventType.Data;
this.data = data;
}
public Message(int connId, Exception exception) : this()
{
this.connId = connId;
type = EventType.Error;
this.exception = exception;
}
}
}

View File

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

View File

@ -0,0 +1,140 @@
using System.IO;
using System.Runtime.CompilerServices;
namespace JamesFrowen.SimpleWeb
{
public static class MessageProcessor
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static byte FirstLengthByte(byte[] buffer) => (byte)(buffer[1] & 0b0111_1111);
public static bool NeedToReadShortLength(byte[] buffer)
{
byte lenByte = FirstLengthByte(buffer);
return lenByte >= Constants.UshortPayloadLength;
}
public static int GetOpcode(byte[] buffer)
{
return buffer[0] & 0b0000_1111;
}
public static int GetPayloadLength(byte[] buffer)
{
byte lenByte = FirstLengthByte(buffer);
return GetMessageLength(buffer, 0, lenByte);
}
public static void ValidateHeader(byte[] buffer, int maxLength, bool expectMask)
{
bool finished = (buffer[0] & 0b1000_0000) != 0; // has full message been sent
bool hasMask = (buffer[1] & 0b1000_0000) != 0; // true from clients, false from server, "All messages from the client to the server have this bit set"
int opcode = buffer[0] & 0b0000_1111; // expecting 1 - text message
byte lenByte = FirstLengthByte(buffer);
ThrowIfNotFinished(finished);
ThrowIfMaskNotExpected(hasMask, expectMask);
ThrowIfBadOpCode(opcode);
int msglen = GetMessageLength(buffer, 0, lenByte);
ThrowIfLengthZero(msglen);
ThrowIfMsgLengthTooLong(msglen, maxLength);
}
public static void ToggleMask(byte[] src, int sourceOffset, int messageLength, byte[] maskBuffer, int maskOffset)
{
ToggleMask(src, sourceOffset, src, sourceOffset, messageLength, maskBuffer, maskOffset);
}
public static void ToggleMask(byte[] src, int sourceOffset, ArrayBuffer dst, int messageLength, byte[] maskBuffer, int maskOffset)
{
ToggleMask(src, sourceOffset, dst.array, 0, messageLength, maskBuffer, maskOffset);
dst.count = messageLength;
}
public static void ToggleMask(byte[] src, int srcOffset, byte[] dst, int dstOffset, int messageLength, byte[] maskBuffer, int maskOffset)
{
for (int i = 0; i < messageLength; i++)
{
byte maskByte = maskBuffer[maskOffset + i % Constants.MaskSize];
dst[dstOffset + i] = (byte)(src[srcOffset + i] ^ maskByte);
}
}
/// <exception cref="InvalidDataException"></exception>
static int GetMessageLength(byte[] buffer, int offset, byte lenByte)
{
if (lenByte == Constants.UshortPayloadLength)
{
// header is 4 bytes long
ushort value = 0;
value |= (ushort)(buffer[offset + 2] << 8);
value |= buffer[offset + 3];
return value;
}
else if (lenByte == Constants.UlongPayloadLength)
{
throw new InvalidDataException("Max length is longer than allowed in a single message");
}
else // is less than 126
{
// header is 2 bytes long
return lenByte;
}
}
/// <exception cref="InvalidDataException"></exception>
static void ThrowIfNotFinished(bool finished)
{
if (!finished)
{
throw new InvalidDataException("Full message should have been sent, if the full message wasn't sent it wasn't sent from this trasnport");
}
}
/// <exception cref="InvalidDataException"></exception>
static void ThrowIfMaskNotExpected(bool hasMask, bool expectMask)
{
if (hasMask != expectMask)
{
throw new InvalidDataException($"Message expected mask to be {expectMask} but was {hasMask}");
}
}
/// <exception cref="InvalidDataException"></exception>
static void ThrowIfBadOpCode(int opcode)
{
// 2 = binary
// 8 = close
if (opcode != 2 && opcode != 8)
{
throw new InvalidDataException("Expected opcode to be binary or close");
}
}
/// <exception cref="InvalidDataException"></exception>
static void ThrowIfLengthZero(int msglen)
{
if (msglen == 0)
{
throw new InvalidDataException("Message length was zero");
}
}
/// <summary>
/// need to check this so that data from previous buffer isnt used
/// </summary>
/// <exception cref="InvalidDataException"></exception>
static void ThrowIfMsgLengthTooLong(int msglen, int maxLength)
{
if (msglen > maxLength)
{
throw new InvalidDataException("Message length is greater than max length");
}
}
}
}

View File

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

View File

@ -0,0 +1,132 @@
using System;
using System.IO;
using System.Runtime.Serialization;
namespace JamesFrowen.SimpleWeb
{
public static class ReadHelper
{
/// <summary>
/// Reads exactly length from stream
/// </summary>
/// <returns>outOffset + length</returns>
/// <exception cref="ReadHelperException"></exception>
public static int Read(Stream stream, byte[] outBuffer, int outOffset, int length)
{
int received = 0;
try
{
while (received < length)
{
int read = stream.Read(outBuffer, outOffset + received, length - received);
if (read == 0)
{
throw new ReadHelperException("returned 0");
}
received += read;
}
}
catch (AggregateException ae)
{
// if interupt is called we dont care about Exceptions
Utils.CheckForInterupt();
// rethrow
ae.Handle(e => false);
}
if (received != length)
{
throw new ReadHelperException("returned not equal to length");
}
return outOffset + received;
}
/// <summary>
/// Reads and returns results. This should never throw an exception
/// </summary>
public static bool TryRead(Stream stream, byte[] outBuffer, int outOffset, int length)
{
try
{
Read(stream, outBuffer, outOffset, length);
return true;
}
catch (ReadHelperException)
{
return false;
}
catch (IOException)
{
return false;
}
catch (Exception e)
{
Log.Exception(e);
return false;
}
}
public static int? SafeReadTillMatch(Stream stream, byte[] outBuffer, int outOffset, int maxLength, byte[] endOfHeader)
{
try
{
int read = 0;
int endIndex = 0;
int endLength = endOfHeader.Length;
while (true)
{
int next = stream.ReadByte();
if (next == -1) // closed
return null;
if (read >= maxLength)
{
Log.Error("SafeReadTillMatch exceeded maxLength");
return null;
}
outBuffer[outOffset + read] = (byte)next;
read++;
// if n is match, check n+1 next
if (endOfHeader[endIndex] == next)
{
endIndex++;
// when all is match return with read length
if (endIndex >= endLength)
{
return read;
}
}
// if n not match reset to 0
else
{
endIndex = 0;
}
}
}
catch (IOException e)
{
Log.InfoException(e);
return null;
}
catch (Exception e)
{
Log.Exception(e);
return null;
}
}
}
[Serializable]
public class ReadHelperException : Exception
{
public ReadHelperException(string message) : base(message) { }
protected ReadHelperException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
}

View File

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

View File

@ -0,0 +1,198 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine.Profiling;
namespace JamesFrowen.SimpleWeb
{
internal static class ReceiveLoop
{
public struct Config
{
public readonly Connection conn;
public readonly int maxMessageSize;
public readonly bool expectMask;
public readonly ConcurrentQueue<Message> queue;
public readonly BufferPool bufferPool;
public Config(Connection conn, int maxMessageSize, bool expectMask, ConcurrentQueue<Message> queue, BufferPool bufferPool)
{
this.conn = conn ?? throw new ArgumentNullException(nameof(conn));
this.maxMessageSize = maxMessageSize;
this.expectMask = expectMask;
this.queue = queue ?? throw new ArgumentNullException(nameof(queue));
this.bufferPool = bufferPool ?? throw new ArgumentNullException(nameof(bufferPool));
}
public void Deconstruct(out Connection conn, out int maxMessageSize, out bool expectMask, out ConcurrentQueue<Message> queue, out BufferPool bufferPool)
{
conn = this.conn;
maxMessageSize = this.maxMessageSize;
expectMask = this.expectMask;
queue = this.queue;
bufferPool = this.bufferPool;
}
}
public static void Loop(Config config)
{
(Connection conn, int maxMessageSize, bool expectMask, ConcurrentQueue<Message> queue, BufferPool _) = config;
//Profiler.BeginThreadProfiling("SimpleWeb", $"ReceiveLoop {conn.connId}");
byte[] readBuffer = new byte[Constants.HeaderSize + (expectMask ? Constants.MaskSize : 0) + maxMessageSize];
try
{
try
{
TcpClient client = conn.client;
while (client.Connected)
{
ReadOneMessage(config, readBuffer);
}
Log.Info($"{conn} Not Connected");
}
catch (Exception)
{
// if interupted we dont care about other execptions
Utils.CheckForInterupt();
throw;
}
}
catch (ThreadInterruptedException e) { Log.InfoException(e); }
catch (ThreadAbortException e) { Log.InfoException(e); }
catch (ObjectDisposedException e) { Log.InfoException(e); }
catch (ReadHelperException e)
{
Log.InfoException(e);
}
catch (SocketException e)
{
// this could happen if wss client closes stream
Log.Warn($"ReceiveLoop SocketException\n{e.Message}", false);
queue.Enqueue(new Message(conn.connId, e));
}
catch (IOException e)
{
// this could happen if client disconnects
Log.Warn($"ReceiveLoop IOException\n{e.Message}", false);
queue.Enqueue(new Message(conn.connId, e));
}
catch (InvalidDataException e)
{
Log.Error($"Invalid data from {conn}: {e.Message}");
queue.Enqueue(new Message(conn.connId, e));
}
catch (Exception e)
{
Log.Exception(e);
queue.Enqueue(new Message(conn.connId, e));
}
finally
{
//Profiler.EndThreadProfiling();
conn.Dispose();
}
}
static void ReadOneMessage(Config config, byte[] buffer)
{
(Connection conn, int maxMessageSize, bool expectMask, ConcurrentQueue<Message> queue, BufferPool bufferPool) = config;
Stream stream = conn.stream;
int offset = 0;
// read 2
offset = ReadHelper.Read(stream, buffer, offset, Constants.HeaderMinSize);
// log after first blocking call
Log.Verbose($"Message From {conn}");
if (MessageProcessor.NeedToReadShortLength(buffer))
{
offset = ReadHelper.Read(stream, buffer, offset, Constants.ShortLength);
}
MessageProcessor.ValidateHeader(buffer, maxMessageSize, expectMask);
if (expectMask)
{
offset = ReadHelper.Read(stream, buffer, offset, Constants.MaskSize);
}
int opcode = MessageProcessor.GetOpcode(buffer);
int payloadLength = MessageProcessor.GetPayloadLength(buffer);
Log.Verbose($"Header ln:{payloadLength} op:{opcode} mask:{expectMask}");
Log.DumpBuffer($"Raw Header", buffer, 0, offset);
int msgOffset = offset;
offset = ReadHelper.Read(stream, buffer, offset, payloadLength);
switch (opcode)
{
case 2:
HandleArrayMessage(config, buffer, msgOffset, payloadLength);
break;
case 8:
HandleCloseMessage(config, buffer, msgOffset, payloadLength);
break;
}
}
static void HandleArrayMessage(Config config, byte[] buffer, int msgOffset, int payloadLength)
{
(Connection conn, int _, bool expectMask, ConcurrentQueue<Message> queue, BufferPool bufferPool) = config;
ArrayBuffer arrayBuffer = bufferPool.Take(payloadLength);
if (expectMask)
{
int maskOffset = msgOffset - Constants.MaskSize;
// write the result of toggle directly into arrayBuffer to avoid 2nd copy call
MessageProcessor.ToggleMask(buffer, msgOffset, arrayBuffer, payloadLength, buffer, maskOffset);
}
else
{
arrayBuffer.CopyFrom(buffer, msgOffset, payloadLength);
}
// dump after mask off
Log.DumpBuffer($"Message", arrayBuffer);
queue.Enqueue(new Message(conn.connId, arrayBuffer));
}
static void HandleCloseMessage(Config config, byte[] buffer, int msgOffset, int payloadLength)
{
(Connection conn, int _, bool expectMask, ConcurrentQueue<Message> _, BufferPool _) = config;
if (expectMask)
{
int maskOffset = msgOffset - Constants.MaskSize;
MessageProcessor.ToggleMask(buffer, msgOffset, payloadLength, buffer, maskOffset);
}
// dump after mask off
Log.DumpBuffer($"Message", buffer, msgOffset, payloadLength);
Log.Info($"Close: {GetCloseCode(buffer, msgOffset)} message:{GetCloseMessage(buffer, msgOffset, payloadLength)}");
conn.Dispose();
}
static string GetCloseMessage(byte[] buffer, int msgOffset, int payloadLength)
{
return Encoding.UTF8.GetString(buffer, msgOffset + 2, payloadLength - 2);
}
static int GetCloseCode(byte[] buffer, int msgOffset)
{
return buffer[msgOffset + 0] << 8 | buffer[msgOffset + 1];
}
}
}

View File

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

View File

@ -0,0 +1,221 @@
using System;
using System.IO;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Threading;
using UnityEngine.Profiling;
namespace JamesFrowen.SimpleWeb
{
public static class SendLoopConfig
{
public static volatile bool batchSend = false;
public static volatile bool sleepBeforeSend = false;
}
internal static class SendLoop
{
public struct Config
{
public readonly Connection conn;
public readonly int bufferSize;
public readonly bool setMask;
public Config(Connection conn, int bufferSize, bool setMask)
{
this.conn = conn ?? throw new ArgumentNullException(nameof(conn));
this.bufferSize = bufferSize;
this.setMask = setMask;
}
public void Deconstruct(out Connection conn, out int bufferSize, out bool setMask)
{
conn = this.conn;
bufferSize = this.bufferSize;
setMask = this.setMask;
}
}
public static void Loop(Config config)
{
(Connection conn, int bufferSize, bool setMask) = config;
//Profiler.BeginThreadProfiling("SimpleWeb", $"SendLoop {conn.connId}");
// create write buffer for this thread
byte[] writeBuffer = new byte[bufferSize];
MaskHelper maskHelper = setMask ? new MaskHelper() : null;
try
{
TcpClient client = conn.client;
Stream stream = conn.stream;
// null check incase disconnect while send thread is starting
if (client == null)
return;
while (client.Connected)
{
// wait for message
conn.sendPending.Wait();
// wait for 1ms for mirror to send other messages
if (SendLoopConfig.sleepBeforeSend)
{
Thread.Sleep(1);
}
conn.sendPending.Reset();
if (SendLoopConfig.batchSend)
{
int offset = 0;
while (conn.sendQueue.TryDequeue(out ArrayBuffer msg))
{
// check if connected before sending message
if (!client.Connected)
{
//Log.Info($"SendLoop {conn} not connected");
return;
}
int maxLength = msg.count + Constants.HeaderSize + Constants.MaskSize;
// if next writer could overflow, write to stream and clear buffer
if (offset + maxLength > bufferSize)
{
stream.Write(writeBuffer, 0, offset);
offset = 0;
}
offset = SendMessage(writeBuffer, offset, msg, setMask, maskHelper);
msg.Release();
}
// after no message in queue, send remaining messages
// dont need to check offset > 0 because last message in queue will always be sent here
stream.Write(writeBuffer, 0, offset);
}
else
{
while (conn.sendQueue.TryDequeue(out ArrayBuffer msg))
{
// check if connected before sending message
if (!client.Connected)
{
//Log.Info($"SendLoop {conn} not connected");
return;
}
int length = SendMessage(writeBuffer, 0, msg, setMask, maskHelper);
stream.Write(writeBuffer, 0, length);
msg.Release();
}
}
}
Log.Info($"{conn} Not Connected");
}
catch (ThreadInterruptedException e)
{
Log.InfoException(e);
}
catch (ThreadAbortException e)
{
Log.InfoException(e);
}
catch (Exception e)
{
Log.Exception(e);
}
finally
{
Profiler.EndThreadProfiling();
conn.Dispose();
maskHelper?.Dispose();
}
}
/// <returns>new offset in buffer</returns>
static int SendMessage(byte[] buffer, int startOffset, ArrayBuffer msg, bool setMask, MaskHelper maskHelper)
{
int msgLength = msg.count;
int offset = WriteHeader(buffer, startOffset, msgLength, setMask);
if (setMask)
{
offset = maskHelper.WriteMask(buffer, offset);
}
msg.CopyTo(buffer, offset);
offset += msgLength;
// dump before mask on
//Log.DumpBuffer("Send", buffer, startOffset, offset);
if (setMask)
{
int messageOffset = offset - msgLength;
MessageProcessor.ToggleMask(buffer, messageOffset, msgLength, buffer, messageOffset - Constants.MaskSize);
}
return offset;
}
static int WriteHeader(byte[] buffer, int startOffset, int msgLength, bool setMask)
{
int sendLength = 0;
const byte finished = 128;
const byte byteOpCode = 2;
buffer[startOffset + 0] = finished | byteOpCode;
sendLength++;
if (msgLength <= Constants.BytePayloadLength)
{
buffer[startOffset + 1] = (byte)msgLength;
sendLength++;
}
else if (msgLength <= ushort.MaxValue)
{
buffer[startOffset + 1] = 126;
buffer[startOffset + 2] = (byte)(msgLength >> 8);
buffer[startOffset + 3] = (byte)msgLength;
sendLength += 3;
}
else
{
throw new InvalidDataException($"Trying to send a message larger than {ushort.MaxValue} bytes");
}
if (setMask)
{
buffer[startOffset + 1] |= 0b1000_0000;
}
return sendLength + startOffset;
}
sealed class MaskHelper : IDisposable
{
readonly byte[] maskBuffer;
readonly RNGCryptoServiceProvider random;
public MaskHelper()
{
maskBuffer = new byte[4];
random = new RNGCryptoServiceProvider();
}
public void Dispose()
{
random.Dispose();
}
public int WriteMask(byte[] buffer, int offset)
{
random.GetBytes(maskBuffer);
Buffer.BlockCopy(maskBuffer, 0, buffer, offset, 4);
return offset + 4;
}
}
}
}

View File

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

View File

@ -0,0 +1,26 @@
using System.Net.Sockets;
namespace JamesFrowen.SimpleWeb
{
[System.Serializable]
public struct TcpConfig
{
public readonly bool noDelay;
public readonly int sendTimeout;
public readonly int receiveTimeout;
public TcpConfig(bool noDelay, int sendTimeout, int receiveTimeout)
{
this.noDelay = noDelay;
this.sendTimeout = sendTimeout;
this.receiveTimeout = receiveTimeout;
}
public void ApplyTo(TcpClient client)
{
client.SendTimeout = sendTimeout;
client.ReceiveTimeout = receiveTimeout;
client.NoDelay = noDelay;
}
}
}

View File

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

View File

@ -0,0 +1,13 @@
using System.Threading;
namespace JamesFrowen.SimpleWeb
{
internal static class Utils
{
public static void CheckForInterupt()
{
// sleep in order to check for ThreadInterruptedException
Thread.Sleep(1);
}
}
}

View File

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

View File

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2020 Mirror Networking
Copyright (c) 2020 James Frowen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a7fc222182536244785aa0082d2eb06a
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,18 @@
SimpleWebTransport is a Transport that implements websocket for Webgl
Can be used in High level networking solution like Mirror or Mirage
This transport can also work on standalone builds and has support for
encryption with websocket secure.
Requirements:
Unity 2019.4 LTS
Documentation:
https://github.com/James-Frowen/SimpleWebTransport/blob/master/README.md
Support:
Discord: https://discord.gg/yp6W73Xs68
Bug Reports: https://github.com/James-Frowen/SimpleWebTransport/issues
**To get most recent updates and fixes download from github**
https://github.com/James-Frowen/SimpleWebTransport/releases

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0e3971d5783109f4d9ce93c7a689d701
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@ -0,0 +1,149 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace JamesFrowen.SimpleWeb
{
/// <summary>
/// Handles Handshakes from new clients on the server
/// <para>The server handshake has buffers to reduce allocations when clients connect</para>
/// </summary>
internal class ServerHandshake
{
const int GetSize = 3;
const int ResponseLength = 129;
const int KeyLength = 24;
const int MergedKeyLength = 60;
const string KeyHeaderString = "Sec-WebSocket-Key: ";
// this isnt an offical max, just a reasonable size for a websocket handshake
readonly int maxHttpHeaderSize = 3000;
readonly SHA1 sha1 = SHA1.Create();
readonly BufferPool bufferPool;
public ServerHandshake(BufferPool bufferPool, int handshakeMaxSize)
{
this.bufferPool = bufferPool;
this.maxHttpHeaderSize = handshakeMaxSize;
}
~ServerHandshake()
{
sha1.Dispose();
}
public bool TryHandshake(Connection conn)
{
Stream stream = conn.stream;
using (ArrayBuffer getHeader = bufferPool.Take(GetSize))
{
if (!ReadHelper.TryRead(stream, getHeader.array, 0, GetSize))
return false;
getHeader.count = GetSize;
if (!IsGet(getHeader.array))
{
//Log.Warn($"First bytes from client was not 'GET' for handshake, instead was {Log.BufferToString(getHeader.array, 0, GetSize)}");
return false;
}
}
string msg = ReadToEndForHandshake(stream);
if (string.IsNullOrEmpty(msg))
return false;
try
{
AcceptHandshake(stream, msg);
return true;
}
catch (ArgumentException e)
{
Log.InfoException(e);
return false;
}
}
string ReadToEndForHandshake(Stream stream)
{
using (ArrayBuffer readBuffer = bufferPool.Take(maxHttpHeaderSize))
{
int? readCountOrFail = ReadHelper.SafeReadTillMatch(stream, readBuffer.array, 0, maxHttpHeaderSize, Constants.endOfHandshake);
if (!readCountOrFail.HasValue)
return null;
int readCount = readCountOrFail.Value;
string msg = Encoding.ASCII.GetString(readBuffer.array, 0, readCount);
Log.Verbose(msg);
return msg;
}
}
static bool IsGet(byte[] getHeader)
{
// just check bytes here instead of using Encoding.ASCII
return getHeader[0] == 71 && // G
getHeader[1] == 69 && // E
getHeader[2] == 84; // T
}
void AcceptHandshake(Stream stream, string msg)
{
using (
ArrayBuffer keyBuffer = bufferPool.Take(KeyLength + Constants.HandshakeGUIDLength),
responseBuffer = bufferPool.Take(ResponseLength))
{
GetKey(msg, keyBuffer.array);
AppendGuid(keyBuffer.array);
byte[] keyHash = CreateHash(keyBuffer.array);
CreateResponse(keyHash, responseBuffer.array);
stream.Write(responseBuffer.array, 0, ResponseLength);
}
}
static void GetKey(string msg, byte[] keyBuffer)
{
int start = msg.IndexOf(KeyHeaderString) + KeyHeaderString.Length;
Log.Verbose($"Handshake Key: {msg.Substring(start, KeyLength)}");
Encoding.ASCII.GetBytes(msg, start, KeyLength, keyBuffer, 0);
}
static void AppendGuid(byte[] keyBuffer)
{
Buffer.BlockCopy(Constants.HandshakeGUIDBytes, 0, keyBuffer, KeyLength, Constants.HandshakeGUIDLength);
}
byte[] CreateHash(byte[] keyBuffer)
{
Log.Verbose($"Handshake Hashing {Encoding.ASCII.GetString(keyBuffer, 0, MergedKeyLength)}");
return sha1.ComputeHash(keyBuffer, 0, MergedKeyLength);
}
static void CreateResponse(byte[] keyHash, byte[] responseBuffer)
{
string keyHashString = Convert.ToBase64String(keyHash);
// compiler should merge these strings into 1 string before format
string message = string.Format(
"HTTP/1.1 101 Switching Protocols\r\n" +
"Connection: Upgrade\r\n" +
"Upgrade: websocket\r\n" +
"Sec-WebSocket-Accept: {0}\r\n\r\n",
keyHashString);
Log.Verbose($"Handshake Response length {message.Length}, IsExpected {message.Length == ResponseLength}");
Encoding.ASCII.GetBytes(message, 0, ResponseLength, responseBuffer, 0);
}
}
}

View File

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

View File

@ -0,0 +1,74 @@
using System;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
namespace JamesFrowen.SimpleWeb
{
public struct SslConfig
{
public readonly bool enabled;
public readonly string certPath;
public readonly string certPassword;
public readonly SslProtocols sslProtocols;
public SslConfig(bool enabled, string certPath, string certPassword, SslProtocols sslProtocols)
{
this.enabled = enabled;
this.certPath = certPath;
this.certPassword = certPassword;
this.sslProtocols = sslProtocols;
}
}
internal class ServerSslHelper
{
readonly SslConfig config;
readonly X509Certificate2 certificate;
public ServerSslHelper(SslConfig sslConfig)
{
config = sslConfig;
if (config.enabled)
certificate = new X509Certificate2(config.certPath, config.certPassword);
}
internal bool TryCreateStream(Connection conn)
{
NetworkStream stream = conn.client.GetStream();
if (config.enabled)
{
try
{
conn.stream = CreateStream(stream);
return true;
}
catch (Exception e)
{
Log.Error($"Create SSLStream Failed: {e}", false);
return false;
}
}
else
{
conn.stream = stream;
return true;
}
}
Stream CreateStream(NetworkStream stream)
{
SslStream sslStream = new SslStream(stream, true, acceptClient);
sslStream.AuthenticateAsServer(certificate, false, config.sslProtocols, false);
return sslStream;
}
bool acceptClient(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
// always accept client
return true;
}
}
}

View File

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

View File

@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace JamesFrowen.SimpleWeb
{
public class SimpleWebServer
{
readonly int maxMessagesPerTick;
public readonly WebSocketServer server;
readonly BufferPool bufferPool;
public SimpleWebServer(int maxMessagesPerTick, TcpConfig tcpConfig, int maxMessageSize, int handshakeMaxSize, SslConfig sslConfig)
{
this.maxMessagesPerTick = maxMessagesPerTick;
// use max because bufferpool is used for both messages and handshake
int max = Math.Max(maxMessageSize, handshakeMaxSize);
bufferPool = new BufferPool(5, 20, max);
server = new WebSocketServer(tcpConfig, maxMessageSize, handshakeMaxSize, sslConfig, bufferPool);
}
public bool Active { get; private set; }
public event Action<int> onConnect;
public event Action<int> onDisconnect;
public event Action<int, ArraySegment<byte>> onData;
public event Action<int, Exception> onError;
public void Start(ushort port)
{
server.Listen(port);
Active = true;
}
public void Stop()
{
server.Stop();
Active = false;
}
public void SendAll(Dictionary<int, short> connections, ArraySegment<byte> source)
{
ArrayBuffer buffer = bufferPool.Take(source.Count);
buffer.CopyFrom(source);
buffer.SetReleasesRequired(connections.Count);
// make copy of array before for each, data sent to each client is the same
foreach (short id in connections.Values)
{
server.Send(id, buffer);
}
}
public void SendAll(HashSet<int> connectionIds, ArraySegment<byte> source)
{
ArrayBuffer buffer = bufferPool.Take(source.Count);
buffer.CopyFrom(source);
buffer.SetReleasesRequired(connectionIds.Count);
// make copy of array before for each, data sent to each client is the same
foreach (int id in connectionIds)
server.Send(id, buffer);
}
public void SendOne(int connectionId, ArraySegment<byte> source)
{
ArrayBuffer buffer = bufferPool.Take(source.Count);
buffer.CopyFrom(source);
server.Send(connectionId, buffer);
}
public bool KickClient(int connectionId)
{
return server.CloseConnection(connectionId);
}
public string GetClientAddress(int connectionId)
{
return server.GetClientAddress(connectionId);
}
/// <summary>
/// Processes all messages.
/// </summary>
public void ProcessMessageQueue()
{
int processedCount = 0;
// check enabled every time incase behaviour was disabled after data
while (
processedCount < maxMessagesPerTick &&
// Dequeue last
server.receiveQueue.TryDequeue(out Message next)
)
{
processedCount++;
switch (next.type)
{
case EventType.Connected:
onConnect?.Invoke(next.connId);
break;
case EventType.Data:
onData?.Invoke(next.connId, next.data.ToSegment());
next.data.Release();
break;
case EventType.Disconnected:
onDisconnect?.Invoke(next.connId);
break;
case EventType.Error:
onError?.Invoke(next.connId, next.exception);
break;
}
}
}
}
}

View File

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

View File

@ -0,0 +1,268 @@
using FishNet.Connection;
using FishNet.Managing;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
namespace JamesFrowen.SimpleWeb
{
public class WebSocketServer
{
public readonly ConcurrentQueue<Message> receiveQueue = new ConcurrentQueue<Message>();
readonly TcpConfig tcpConfig;
readonly int maxMessageSize;
public TcpListener listener;
Thread acceptThread;
bool serverStopped;
readonly ServerHandshake handShake;
readonly ServerSslHelper sslHelper;
readonly BufferPool bufferPool;
readonly ConcurrentDictionary<int, Connection> connections = new ConcurrentDictionary<int, Connection>();
private ConcurrentQueue<int> _idCache = new ConcurrentQueue<int>();
private int _nextId = 0;
private int GetNextId()
{
if (_idCache.Count == 0)
GrowIdCache(1000);
int result = NetworkConnection.UNSET_CLIENTID_VALUE;
_idCache.TryDequeue(out result);
return result;
}
/// <summary>
/// Grows IdCache by value.
/// </summary>
private void GrowIdCache(int value)
{
int over = (_nextId + value) - NetworkConnection.MAXIMUM_CLIENTID_VALUE;
//Prevent overflow.
if (over > 0)
value -= over;
for (int i = _nextId; i < value; i++)
_idCache.Enqueue(i);
}
public WebSocketServer(TcpConfig tcpConfig, int maxMessageSize, int handshakeMaxSize, SslConfig sslConfig, BufferPool bufferPool)
{
//Make a small queue to start.
GrowIdCache(1000);
this.tcpConfig = tcpConfig;
this.maxMessageSize = maxMessageSize;
sslHelper = new ServerSslHelper(sslConfig);
this.bufferPool = bufferPool;
handShake = new ServerHandshake(this.bufferPool, handshakeMaxSize);
}
public void Listen(int port)
{
listener = TcpListener.Create(port);
listener.Start();
Log.Info($"Server has started on port {port}");
acceptThread = new Thread(acceptLoop);
acceptThread.IsBackground = true;
acceptThread.Start();
}
public void Stop()
{
serverStopped = true;
// Interrupt then stop so that Exception is handled correctly
acceptThread?.Interrupt();
listener?.Stop();
acceptThread = null;
Log.Info("Server stoped, Closing all connections...");
// make copy so that foreach doesn't break if values are removed
Connection[] connectionsCopy = connections.Values.ToArray();
foreach (Connection conn in connectionsCopy)
{
conn.Dispose();
}
connections.Clear();
}
void acceptLoop()
{
try
{
try
{
while (true)
{
TcpClient client = listener.AcceptTcpClient();
tcpConfig.ApplyTo(client);
// TODO keep track of connections before they are in connections dictionary
// this might not be a problem as HandshakeAndReceiveLoop checks for stop
// and returns/disposes before sending message to queue
Connection conn = new Connection(client, AfterConnectionDisposed);
//Log.Info($"A client connected {conn}");
// handshake needs its own thread as it needs to wait for message from client
Thread receiveThread = new Thread(() => HandshakeAndReceiveLoop(conn));
conn.receiveThread = receiveThread;
receiveThread.IsBackground = true;
receiveThread.Start();
}
}
catch (SocketException)
{
// check for Interrupted/Abort
Utils.CheckForInterupt();
throw;
}
}
catch (ThreadInterruptedException e) { Log.InfoException(e); }
catch (ThreadAbortException e) { Log.InfoException(e); }
catch (Exception e) { Log.Exception(e); }
}
void HandshakeAndReceiveLoop(Connection conn)
{
try
{
bool success = sslHelper.TryCreateStream(conn);
if (!success)
{
//Log.Error($"Failed to create SSL Stream {conn}");
conn.Dispose();
return;
}
success = handShake.TryHandshake(conn);
if (success)
{
//Log.Info($"Sent Handshake {conn}");
}
else
{
//Log.Error($"Handshake Failed {conn}");
conn.Dispose();
return;
}
// check if Stop has been called since accepting this client
if (serverStopped)
{
Log.Info("Server stops after successful handshake");
return;
}
conn.connId = GetNextId();
if (conn.connId == NetworkConnection.UNSET_CLIENTID_VALUE)
{
NetworkManagerExtensions.LogWarning($"At maximum connections. A client attempting to connect to be rejected.");
conn.Dispose();
return;
}
connections.TryAdd(conn.connId, conn);
receiveQueue.Enqueue(new Message(conn.connId, EventType.Connected));
Thread sendThread = new Thread(() =>
{
SendLoop.Config sendConfig = new SendLoop.Config(
conn,
bufferSize: Constants.HeaderSize + maxMessageSize,
setMask: false);
SendLoop.Loop(sendConfig);
});
conn.sendThread = sendThread;
sendThread.IsBackground = true;
sendThread.Name = $"SendLoop {conn.connId}";
sendThread.Start();
ReceiveLoop.Config receiveConfig = new ReceiveLoop.Config(
conn,
maxMessageSize,
expectMask: true,
receiveQueue,
bufferPool);
ReceiveLoop.Loop(receiveConfig);
}
catch (ThreadInterruptedException e) { Log.InfoException(e); }
catch (ThreadAbortException e) { Log.InfoException(e); }
catch (Exception e) { Log.Exception(e); }
finally
{
// close here incase connect fails
conn.Dispose();
}
}
void AfterConnectionDisposed(Connection conn)
{
if (conn.connId != Connection.IdNotSet)
{
receiveQueue.Enqueue(new Message(conn.connId, EventType.Disconnected));
connections.TryRemove(conn.connId, out Connection _);
_idCache.Enqueue(conn.connId);
}
}
public void Send(int id, ArrayBuffer buffer)
{
if (connections.TryGetValue(id, out Connection conn))
{
conn.sendQueue.Enqueue(buffer);
conn.sendPending.Set();
}
else
{
Log.Warn($"Cant send message to {id} because connection was not found in dictionary. Maybe it disconnected.");
}
}
public bool CloseConnection(int id)
{
if (connections.TryGetValue(id, out Connection conn))
{
Log.Info($"Kicking connection {id}");
conn.Dispose();
return true;
}
else
{
Log.Warn($"Failed to kick {id} because id not found");
return false;
}
}
public string GetClientAddress(int id)
{
if (connections.TryGetValue(id, out Connection conn))
{
return conn.client.Client.RemoteEndPoint.ToString();
}
else
{
Log.Error($"Cant close connection to {id} because connection was not found in dictionary");
return null;
}
}
}
}

View File

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

View File

@ -0,0 +1,16 @@
{
"name": "SimpleWebTransport",
"rootNamespace": "",
"references": [
"GUID:7c88a4a7926ee5145ad2dfa06f454c67"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3b5390adca4e2bb4791cb930316d6f3e
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,51 @@
using System.IO;
using System.Security.Authentication;
using UnityEngine;
namespace JamesFrowen.SimpleWeb
{
public class SslConfigLoader
{
internal struct Cert
{
public string path;
public string password;
}
public static SslConfig Load(bool sslEnabled, string sslCertJson, SslProtocols sslProtocols)
{
// dont need to load anything if ssl is not enabled
if (!sslEnabled)
return default;
string certJsonPath = sslCertJson;
Cert cert = LoadCertJson(certJsonPath);
return new SslConfig(
enabled: sslEnabled,
sslProtocols: sslProtocols,
certPath: cert.path,
certPassword: cert.password
);
}
internal static Cert LoadCertJson(string certJsonPath)
{
string json = File.ReadAllText(certJsonPath);
Cert cert = JsonUtility.FromJson<Cert>(json);
if (string.IsNullOrEmpty(cert.path))
{
throw new InvalidDataException("Cert Json didnt not contain \"path\"");
}
if (string.IsNullOrEmpty(cert.password))
{
// password can be empty
cert.password = string.Empty;
}
return cert;
}
}
}

View File

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

View File

@ -0,0 +1,14 @@
{
"name": "com.james-frowen.simplewebtransport",
"displayName": "Simple Web Transport",
"version": "1.2.4",
"unity": "2019.4",
"description": "Low level transport for Unity using the websocket Protocol. Includes a Websocket server, standalone client, and a WebGL client so that both your Server and Client can be build with Unity.",
"author": "James Frowen",
"repository": {
"type": "git",
"url": "https://github.com/James-Frowen/SimpleWebTransport"
},
"dependencies": {},
"samples": []
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 916a27871ab0b794f9c6582d12ca0d40
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1 @@
4.1.0

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1896dffa49f580d4ca3b7d169e596cc4
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,6 +1,7 @@
{
"dependencies": {
"com.kyrylokuzyk.primetween": "file:../Assets/Plugins/PrimeTween/internal/com.kyrylokuzyk.primetween.tgz",
"com.unity.2d.sprite": "1.0.0",
"com.unity.adaptiveperformance": "5.0.2",
"com.unity.cloud.gltfast": "6.0.1",
"com.unity.collab-proxy": "2.3.1",

View File

@ -6,6 +6,12 @@
"source": "local-tarball",
"dependencies": {}
},
"com.unity.2d.sprite": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.adaptiveperformance": {
"version": "5.0.2",
"depth": 0,