Added multiplayer plugin

This commit is contained in:
Anthony Berg
2020-11-30 08:12:07 +00:00
parent 9cb342dd42
commit f64cf54803
450 changed files with 33131 additions and 10 deletions

View File

@@ -0,0 +1,189 @@
// uses the first available transport for server and client.
// example: to use Apathy if on Windows/Mac/Linux and fall back to Telepathy
// otherwise.
using System;
using UnityEngine;
namespace Mirror
{
[HelpURL("https://mirror-networking.com/docs/Transports/Fallback.html")]
public class FallbackTransport : Transport
{
public Transport[] transports;
// the first transport that is available on this platform
Transport available;
public void Awake()
{
if (transports == null || transports.Length == 0)
{
throw new Exception("FallbackTransport requires at least 1 underlying transport");
}
InitClient();
InitServer();
available = GetAvailableTransport();
Debug.Log("FallbackTransport available: " + available.GetType());
}
void OnEnable()
{
available.enabled = true;
}
void OnDisable()
{
available.enabled = false;
}
// The client just uses the first transport available
Transport GetAvailableTransport()
{
foreach (Transport transport in transports)
{
if (transport.Available())
{
return transport;
}
}
throw new Exception("No transport suitable for this platform");
}
public override bool Available()
{
return available.Available();
}
// clients always pick the first transport
void InitClient()
{
// wire all the base transports to our events
foreach (Transport transport in transports)
{
transport.OnClientConnected.AddListener(OnClientConnected.Invoke);
transport.OnClientDataReceived.AddListener(OnClientDataReceived.Invoke);
transport.OnClientError.AddListener(OnClientError.Invoke);
transport.OnClientDisconnected.AddListener(OnClientDisconnected.Invoke);
}
}
public override void ClientConnect(string address)
{
available.ClientConnect(address);
}
public override void ClientConnect(Uri uri)
{
foreach (Transport transport in transports)
{
if (transport.Available())
{
try
{
transport.ClientConnect(uri);
available = transport;
}
catch (ArgumentException)
{
// transport does not support the schema, just move on to the next one
}
}
}
throw new Exception("No transport suitable for this platform");
}
public override bool ClientConnected()
{
return available.ClientConnected();
}
public override void ClientDisconnect()
{
available.ClientDisconnect();
}
public override void ClientSend(int channelId, ArraySegment<byte> segment)
{
available.ClientSend(channelId, segment);
}
void InitServer()
{
// wire all the base transports to our events
foreach (Transport transport in transports)
{
transport.OnServerConnected.AddListener(OnServerConnected.Invoke);
transport.OnServerDataReceived.AddListener(OnServerDataReceived.Invoke);
transport.OnServerError.AddListener(OnServerError.Invoke);
transport.OnServerDisconnected.AddListener(OnServerDisconnected.Invoke);
}
}
// right now this just returns the first available uri,
// should we return the list of all available uri?
public override Uri ServerUri() => available.ServerUri();
public override bool ServerActive()
{
return available.ServerActive();
}
public override string ServerGetClientAddress(int connectionId)
{
return available.ServerGetClientAddress(connectionId);
}
public override bool ServerDisconnect(int connectionId)
{
return available.ServerDisconnect(connectionId);
}
public override void ServerSend(int connectionId, int channelId, ArraySegment<byte> segment)
{
available.ServerSend(connectionId, channelId, segment);
}
public override void ServerStart()
{
available.ServerStart();
}
public override void ServerStop()
{
available.ServerStop();
}
public override void Shutdown()
{
available.Shutdown();
}
public override int GetMaxPacketSize(int channelId = 0)
{
// finding the max packet size in a fallback environment has to be
// done very carefully:
// * servers and clients might run different transports depending on
// which platform they are on.
// * there should only ever be ONE true max packet size for everyone,
// otherwise a spawn message might be sent to all tcp sockets, but
// be too big for some udp sockets. that would be a debugging
// nightmare and allow for possible exploits and players on
// different platforms seeing a different game state.
// => the safest solution is to use the smallest max size for all
// transports. that will never fail.
int mininumAllowedSize = int.MaxValue;
foreach (Transport transport in transports)
{
int size = transport.GetMaxPacketSize(channelId);
mininumAllowedSize = Mathf.Min(size, mininumAllowedSize);
}
return mininumAllowedSize;
}
public override string ToString()
{
return available.ToString();
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,172 @@
//#if MIRROR <- commented out because MIRROR isn't defined on first import yet
using System;
using System.Linq;
using System.Net;
using Mirror;
using UnityEngine;
namespace kcp2k
{
public class KcpTransport : Transport
{
// scheme used by this transport
public const string Scheme = "kcp";
// common
[Header("Transport Configuration")]
public ushort Port = 7777;
[Tooltip("NoDelay is recommended to reduce latency. This also scales better without buffers getting full.")]
public bool NoDelay = true;
[Tooltip("KCP internal update interval. 100ms is KCP default, but a lower interval is recommended to minimize latency and to scale to more networked entities.")]
public uint Interval = 10;
[Header("Advanced")]
[Tooltip("KCP fastresend parameter. Faster resend for the cost of higher bandwidth.")]
public int FastResend = 0;
[Tooltip("KCP congestion window can be disabled. This is necessary to Mirror 10k Benchmark. Disable this for high scale games if connections get chocked regularly.")]
public bool CongestionWindow = true; // KCP 'NoCongestionWindow' is false by default. here we negate it for ease of use.
[Tooltip("KCP window size can be modified to support higher loads. For example, Mirror Benchmark requires 128 for 4k monsters, 256 for 10k monsters (if CongestionWindow is disabled.)")]
public uint SendWindowSize = 128; //Kcp.WND_SND; 32 by default. 128 is better for 4k Benchmark etc.
[Tooltip("KCP window size can be modified to support higher loads. For example, Mirror Benchmark requires 128 for 4k monsters, 256 for 10k monsters (if CongestionWindow is disabled.)")]
public uint ReceiveWindowSize = Kcp.WND_RCV;
// server & client
KcpServer server;
KcpClient client;
// debugging
[Header("Debug")]
public bool debugGUI;
void Awake()
{
// TODO simplify after converting Mirror Transport events to Action
client = new KcpClient(
() => OnClientConnected.Invoke(),
(message) => OnClientDataReceived.Invoke(message, Channels.DefaultReliable),
() => OnClientDisconnected.Invoke()
);
// TODO simplify after converting Mirror Transport events to Action
server = new KcpServer(
(connectionId) => OnServerConnected.Invoke(connectionId),
(connectionId, message) => OnServerDataReceived.Invoke(connectionId, message, Channels.DefaultReliable),
(connectionId) => OnServerDisconnected.Invoke(connectionId),
NoDelay,
Interval,
FastResend,
CongestionWindow,
SendWindowSize,
ReceiveWindowSize
);
Debug.Log("KcpTransport initialized!");
}
// all except WebGL
public override bool Available() =>
Application.platform != RuntimePlatform.WebGLPlayer;
// client
public override bool ClientConnected() => client.connected;
public override void ClientConnect(string address)
{
client.Connect(address, Port, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize);
}
public override void ClientSend(int channelId, ArraySegment<byte> segment)
{
client.Send(segment);
}
public override void ClientDisconnect() => client.Disconnect();
// IMPORTANT: set script execution order to >1000 to call Transport's
// LateUpdate after all others. Fixes race condition where
// e.g. in uSurvival Transport would apply Cmds before
// ShoulderRotation.LateUpdate, resulting in projectile
// spawns at the point before shoulder rotation.
public void LateUpdate()
{
// note: we need to check enabled in case we set it to false
// when LateUpdate already started.
// (https://github.com/vis2k/Mirror/pull/379)
if (!enabled)
return;
server.Tick();
client.Tick();
}
// server
public override Uri ServerUri()
{
UriBuilder builder = new UriBuilder();
builder.Scheme = Scheme;
builder.Host = Dns.GetHostName();
builder.Port = Port;
return builder.Uri;
}
public override bool ServerActive() => server.IsActive();
public override void ServerStart() => server.Start(Port);
public override void ServerSend(int connectionId, int channelId, ArraySegment<byte> segment)
{
server.Send(connectionId, segment);
}
public override bool ServerDisconnect(int connectionId)
{
server.Disconnect(connectionId);
return true;
}
public override string ServerGetClientAddress(int connectionId) => server.GetClientAddress(connectionId);
public override void ServerStop() => server.Stop();
// common
public override void Shutdown() {}
// MTU
public override int GetMaxPacketSize(int channelId = Channels.DefaultReliable) => Kcp.MTU_DEF;
public override string ToString()
{
return "KCP";
}
int GetTotalSendQueue() =>
server.connections.Values.Sum(conn => conn.SendQueueCount);
int GetTotalReceiveQueue() =>
server.connections.Values.Sum(conn => conn.ReceiveQueueCount);
int GetTotalSendBuffer() =>
server.connections.Values.Sum(conn => conn.SendBufferCount);
int GetTotalReceiveBuffer() =>
server.connections.Values.Sum(conn => conn.ReceiveBufferCount);
void OnGUI()
{
if (!debugGUI) return;
GUILayout.BeginArea(new Rect(5, 100, 300, 300));
if (ServerActive())
{
GUILayout.BeginVertical("Box");
GUILayout.Label("SERVER");
GUILayout.Label(" connections: " + server.connections.Count);
GUILayout.Label(" SendQueue: " + GetTotalSendQueue());
GUILayout.Label(" ReceiveQueue: " + GetTotalReceiveQueue());
GUILayout.Label(" SendBuffer: " + GetTotalSendBuffer());
GUILayout.Label(" ReceiveBuffer: " + GetTotalReceiveBuffer());
GUILayout.EndVertical();
}
if (ClientConnected())
{
GUILayout.BeginVertical("Box");
GUILayout.Label("CLIENT");
GUILayout.Label(" SendQueue: " + client.connection.SendQueueCount);
GUILayout.Label(" ReceiveQueue: " + client.connection.ReceiveQueueCount);
GUILayout.Label(" SendBuffer: " + client.connection.SendBufferCount);
GUILayout.Label(" ReceiveBuffer: " + client.connection.ReceiveBufferCount);
GUILayout.EndVertical();
}
GUILayout.EndArea();
}
}
}
//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
MIT License
Copyright (c) 2016 limpo1989
Copyright (c) 2020 Paul Pacheco
Copyright (c) 2020 Lymdun
Copyright (c) 2020 vis2k
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: 9a3e8369060cf4e94ac117603de47aa6
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
V1.1
- high level cleanup, fixes, improvements
V1.0
- Kcp.cs now mirrors original Kcp.c behaviour
(this fixes dozens of bugs)
V0.1
- initial kcp-csharp based version

View File

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

View File

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

View File

@@ -0,0 +1,95 @@
// kcp client logic abstracted into a class.
// for use in Mirror, DOTSNET, testing, etc.
using System;
using UnityEngine;
namespace kcp2k
{
public class KcpClient
{
// events
public Action OnConnected;
public Action<ArraySegment<byte>> OnData;
public Action OnDisconnected;
// state
public KcpClientConnection connection;
public bool connected;
public KcpClient(Action OnConnected, Action<ArraySegment<byte>> OnData, Action OnDisconnected)
{
this.OnConnected = OnConnected;
this.OnData = OnData;
this.OnDisconnected = OnDisconnected;
}
public void Connect(string address, ushort port, bool noDelay, uint interval, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV)
{
if (connected)
{
Debug.LogWarning("KCP: client already connected!");
return;
}
connection = new KcpClientConnection();
// setup events
connection.OnAuthenticated = () =>
{
Debug.Log($"KCP: OnClientConnected");
connected = true;
OnConnected.Invoke();
};
connection.OnData = (message) =>
{
//Debug.Log($"KCP: OnClientData({BitConverter.ToString(message.Array, message.Offset, message.Count)})");
OnData.Invoke(message);
};
connection.OnDisconnected = () =>
{
Debug.Log($"KCP: OnClientDisconnected");
connected = false;
connection = null;
OnDisconnected.Invoke();
};
// connect
connection.Connect(address, port, noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize);
}
public void Send(ArraySegment<byte> segment)
{
if (connected)
{
connection.Send(segment);
}
else Debug.LogWarning("KCP: can't send because client not connected!");
}
public void Disconnect()
{
// only if connected
// otherwise we end up in a deadlock because of an open Mirror bug:
// https://github.com/vis2k/Mirror/issues/2353
if (connected)
{
// call Disconnect and let the connection handle it.
// DO NOT set it to null yet. it needs to be updated a few more
// times first. let the connection handle it!
connection?.Disconnect();
}
}
public void Tick()
{
// tick client connection
if (connection != null)
{
// recv on socket first
connection.RawReceive();
// then update
connection.Tick();
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6aa069a28ed24fedb533c102d9742b36
timeCreated: 1603786960

View File

@@ -0,0 +1,59 @@
using UnityEngine;
using System.Net;
using System.Net.Sockets;
namespace kcp2k
{
public class KcpClientConnection : KcpConnection
{
readonly byte[] buffer = new byte[1500];
public void Connect(string host, ushort port, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV)
{
Debug.Log($"KcpClient: connect to {host}:{port}");
IPAddress[] ipAddress = Dns.GetHostAddresses(host);
if (ipAddress.Length < 1)
throw new SocketException((int)SocketError.HostNotFound);
remoteEndpoint = new IPEndPoint(ipAddress[0], port);
socket = new Socket(remoteEndpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
socket.Connect(remoteEndpoint);
SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize);
// client should send handshake to server as very first message
SendHandshake();
RawReceive();
}
// call from transport update
public void RawReceive()
{
try
{
if (socket != null)
{
while (socket.Poll(0, SelectMode.SelectRead))
{
int msgLength = socket.ReceiveFrom(buffer, ref remoteEndpoint);
//Debug.Log($"KCP: client raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
RawInput(buffer, msgLength);
}
}
}
// this is fine, the socket might have been closed in the other end
catch (SocketException) {}
}
protected override void Dispose()
{
socket.Close();
socket = null;
}
protected override void RawSend(byte[] data, int length)
{
socket.Send(data, length, SocketFlags.None);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 96512e74aa8214a6faa8a412a7a07877
timeCreated: 1602601237

View File

@@ -0,0 +1,366 @@
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using Debug = UnityEngine.Debug;
namespace kcp2k
{
enum KcpState { Connected, Authenticated, Disconnected }
public abstract class KcpConnection
{
protected Socket socket;
protected EndPoint remoteEndpoint;
internal Kcp kcp;
// kcp can have several different states, let's use a state machine
KcpState state = KcpState.Disconnected;
public Action OnAuthenticated;
public Action<ArraySegment<byte>> OnData;
public Action OnDisconnected;
// If we don't receive anything these many milliseconds
// then consider us disconnected
public const int TIMEOUT = 10000;
uint lastReceiveTime;
// internal time.
// StopWatch offers ElapsedMilliSeconds and should be more precise than
// Unity's time.deltaTime over long periods.
readonly Stopwatch refTime = new Stopwatch();
// recv buffer to avoid allocations
byte[] buffer = new byte[Kcp.MTU_DEF];
internal static readonly ArraySegment<byte> Hello = new ArraySegment<byte>(new byte[] { 0 });
static readonly ArraySegment<byte> Goodbye = new ArraySegment<byte>(new byte[] { 1 });
static readonly ArraySegment<byte> Ping = new ArraySegment<byte>(new byte[] { 2 });
// send a ping occasionally so we don't time out on the other end.
// for example, creating a character in an MMO could easily take a
// minute of no data being sent. which doesn't mean we want to time out.
// same goes for slow paced card games etc.
public const int PING_INTERVAL = 1000;
uint lastPingTime;
// if we send more than kcp can handle, we will get ever growing
// send/recv buffers and queues and minutes of latency.
// => if a connection can't keep up, it should be disconnected instead
// to protect the server under heavy load, and because there is no
// point in growing to gigabytes of memory or minutes of latency!
// => 2k isn't enough. we reach 2k when spawning 4k monsters at once
// easily, but it does recover over time.
// => 10k seems safe.
//
// note: we have a ChokeConnectionAutoDisconnects test for this too!
internal const int QueueDisconnectThreshold = 10000;
// getters for queue and buffer counts, used for debug info
public int SendQueueCount => kcp.snd_queue.Count;
public int ReceiveQueueCount => kcp.rcv_queue.Count;
public int SendBufferCount => kcp.snd_buf.Count;
public int ReceiveBufferCount => kcp.rcv_buf.Count;
// NoDelay, interval, window size are the most important configurations.
// let's force require the parameters so we don't forget it anywhere.
protected void SetupKcp(bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV)
{
kcp = new Kcp(0, RawSend);
// set nodelay.
// note that kcp uses 'nocwnd' internally so we negate the parameter
kcp.SetNoDelay(noDelay ? 1u : 0u, interval, fastResend, !congestionWindow);
kcp.SetWindowSize(sendWindowSize, receiveWindowSize);
refTime.Start();
state = KcpState.Connected;
Tick();
}
void HandleTimeout(uint time)
{
// note: we are also sending a ping regularly, so timeout should
// only ever happen if the connection is truly gone.
if (time >= lastReceiveTime + TIMEOUT)
{
Debug.LogWarning($"KCP: Connection timed out after {TIMEOUT}ms. Disconnecting.");
Disconnect();
}
}
void HandleDeadLink()
{
// kcp has 'dead_link' detection. might as well use it.
if (kcp.state == -1)
{
Debug.LogWarning("KCP Connection dead_link detected. Disconnecting.");
Disconnect();
}
}
// send a ping occasionally in order to not time out on the other end.
void HandlePing(uint time)
{
// enough time elapsed since last ping?
if (time >= lastPingTime + PING_INTERVAL)
{
// ping again and reset time
//Debug.Log("KCP: sending ping...");
Send(Ping);
lastPingTime = time;
}
}
void HandleChoked()
{
// disconnect connections that can't process the load.
// see QueueSizeDisconnect comments.
int total = kcp.rcv_queue.Count + kcp.snd_queue.Count +
kcp.rcv_buf.Count + kcp.snd_buf.Count;
if (total >= QueueDisconnectThreshold)
{
Debug.LogWarning($"KCP: disconnecting connection because it can't process data fast enough.\n" +
$"Queue total {total}>{QueueDisconnectThreshold}. rcv_queue={kcp.rcv_queue.Count} snd_queue={kcp.snd_queue.Count} rcv_buf={kcp.rcv_buf.Count} snd_buf={kcp.snd_buf.Count}\n" +
$"* Try to Enable NoDelay, decrease INTERVAL, disable Congestion Window (= enable NOCWND!), increase SEND/RECV WINDOW or compress data.\n" +
$"* Or perhaps the network is simply too slow on our end, or on the other end.\n");
// let's clear all pending sends before disconnting with 'Bye'.
// otherwise a single Flush in Disconnect() won't be enough to
// flush thousands of messages to finally deliver 'Bye'.
// this is just faster and more robust.
kcp.snd_queue.Clear();
Disconnect();
}
}
// reads the next message from connection.
bool ReceiveNext(out ArraySegment<byte> message)
{
// read only one message
int msgSize = kcp.PeekSize();
if (msgSize > 0)
{
// only allow receiving up to MaxMessageSize sized messages.
// otherwise we would get BlockCopy ArgumentException anyway.
if (msgSize <= Kcp.MTU_DEF)
{
int received = kcp.Receive(buffer, msgSize);
if (received >= 0)
{
message = new ArraySegment<byte>(buffer, 0, msgSize);
lastReceiveTime = (uint)refTime.ElapsedMilliseconds;
// return false if it was a ping message. true otherwise.
if (Utils.SegmentsEqual(message, Ping))
{
//Debug.Log("KCP: received ping.");
return false;
}
return true;
}
else
{
// if receive failed, close everything
Debug.LogWarning($"Receive failed with error={received}. closing connection.");
Disconnect();
}
}
// we don't allow sending messages > Max, so this must be an
// attacker. let's disconnect to avoid allocation attacks etc.
else
{
Debug.LogWarning($"KCP: possible allocation attack for msgSize {msgSize} > max {Kcp.MTU_DEF}. Disconnecting the connection.");
Disconnect();
}
}
return false;
}
void TickConnected(uint time)
{
// detect common events & ping
HandleTimeout(time);
HandleDeadLink();
HandlePing(time);
HandleChoked();
kcp.Update(time);
// any message received?
if (ReceiveNext(out ArraySegment<byte> message))
{
// handshake message?
if (Utils.SegmentsEqual(message, Hello))
{
Debug.Log("KCP: received handshake");
state = KcpState.Authenticated;
OnAuthenticated?.Invoke();
}
// otherwise it's random data from the internet, not
// from a legitimate player. disconnect.
else
{
Debug.LogWarning("KCP: received random data before handshake. Disconnecting the connection.");
Disconnect();
}
}
}
void TickAuthenticated(uint time)
{
// detect common events & ping
HandleTimeout(time);
HandleDeadLink();
HandlePing(time);
HandleChoked();
kcp.Update(time);
// process all received messages
while (ReceiveNext(out ArraySegment<byte> message))
{
// disconnect message?
if (Utils.SegmentsEqual(message, Goodbye))
{
Debug.Log("KCP: received disconnect message");
Disconnect();
break;
}
// otherwise regular message
else
{
// only accept regular messages
//Debug.LogWarning($"Kcp recv msg: {BitConverter.ToString(buffer, 0, msgSize)}");
OnData?.Invoke(message);
}
}
}
public void Tick()
{
uint time = (uint)refTime.ElapsedMilliseconds;
try
{
switch (state)
{
case KcpState.Connected:
{
TickConnected(time);
break;
}
case KcpState.Authenticated:
{
TickAuthenticated(time);
break;
}
case KcpState.Disconnected:
{
// do nothing while disconnected
break;
}
}
}
catch (SocketException exception)
{
// this is ok, the connection was closed
Debug.Log($"KCP Connection: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (ObjectDisposedException exception)
{
// fine, socket was closed
Debug.Log($"KCP Connection: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (Exception ex)
{
// unexpected
Debug.LogException(ex);
Disconnect();
}
}
public void RawInput(byte[] buffer, int msgLength)
{
int input = kcp.Input(buffer, msgLength);
if (input != 0)
{
Debug.LogWarning($"Input failed with error={input} for buffer with length={msgLength}");
}
}
protected abstract void RawSend(byte[] data, int length);
public void Send(ArraySegment<byte> data)
{
// only allow sending up to MaxMessageSize sized messages.
// other end won't process bigger messages anyway.
if (data.Count <= Kcp.MTU_DEF)
{
int sent = kcp.Send(data.Array, data.Offset, data.Count);
if (sent < 0)
{
Debug.LogWarning($"Send failed with error={sent} for segment with length={data.Count}");
}
}
else Debug.LogError($"Failed to send message of size {data.Count} because it's larger than MaxMessageSize={Kcp.MTU_DEF}");
}
// server & client need to send handshake at different times, so we need
// to expose the function.
// * client should send it immediately.
// * server should send it as reply to client's handshake, not before
// (server should not reply to random internet messages with handshake)
public void SendHandshake()
{
Debug.Log("KcpConnection: sending Handshake to other end!");
Send(Hello);
}
protected virtual void Dispose()
{
}
// disconnect this connection
public void Disconnect()
{
// only if not disconnected yet
if (state == KcpState.Disconnected)
return;
// send a disconnect message
if (socket.Connected)
{
try
{
Send(Goodbye);
kcp.Flush();
}
catch (SocketException)
{
// this is ok, the connection was already closed
}
catch (ObjectDisposedException)
{
// this is normal when we stop the server
// the socket is stopped so we can't send anything anymore
// to the clients
// the clients will eventually timeout and realize they
// were disconnected
}
}
// set as Disconnected, call event
Debug.Log("KCP Connection: Disconnected.");
state = KcpState.Disconnected;
OnDisconnected?.Invoke();
}
// get remote endpoint
public EndPoint GetRemoteEndPoint() => remoteEndpoint;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3915c7c62b72d4dc2a9e4e76c94fc484
timeCreated: 1602600432

View File

@@ -0,0 +1,229 @@
// kcp server logic abstracted into a class.
// for use in Mirror, DOTSNET, testing, etc.
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
namespace kcp2k
{
public class KcpServer
{
// events
public Action<int> OnConnected;
public Action<int, ArraySegment<byte>> OnData;
public Action<int> OnDisconnected;
// configuration
// NoDelay is recommended to reduce latency. This also scales better
// without buffers getting full.
public bool NoDelay;
// KCP internal update interval. 100ms is KCP default, but a lower
// interval is recommended to minimize latency and to scale to more
// networked entities.
public uint Interval;
// KCP fastresend parameter. Faster resend for the cost of higher
// bandwidth.
public int FastResend;
// KCP 'NoCongestionWindow' is false by default. here we negate it for
// ease of use. This can be disabled for high scale games if connections
// choke regularly.
public bool CongestionWindow;
// KCP window size can be modified to support higher loads.
// for example, Mirror Benchmark requires:
// 128, 128 for 4k monsters
// 512, 512 for 10k monsters
// 8192, 8192 for 20k monsters
public uint SendWindowSize;
public uint ReceiveWindowSize;
// state
Socket socket;
EndPoint newClientEP = new IPEndPoint(IPAddress.IPv6Any, 0);
readonly byte[] buffer = new byte[Kcp.MTU_DEF];
// connections <connectionId, connection> where connectionId is EndPoint.GetHashCode
public Dictionary<int, KcpServerConnection> connections = new Dictionary<int, KcpServerConnection>();
public KcpServer(Action<int> OnConnected,
Action<int, ArraySegment<byte>> OnData,
Action<int> OnDisconnected,
bool NoDelay,
uint Interval,
int FastResend = 0,
bool CongestionWindow = true,
uint SendWindowSize = Kcp.WND_SND,
uint ReceiveWindowSize = Kcp.WND_RCV)
{
this.OnConnected = OnConnected;
this.OnData = OnData;
this.OnDisconnected = OnDisconnected;
this.NoDelay = NoDelay;
this.Interval = Interval;
this.FastResend = FastResend;
this.CongestionWindow = CongestionWindow;
this.SendWindowSize = SendWindowSize;
this.ReceiveWindowSize = ReceiveWindowSize;
}
public bool IsActive() => socket != null;
public void Start(ushort port)
{
// only start once
if (socket != null)
{
Debug.LogWarning("KCP: server already started!");
}
// listen
socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
socket.DualMode = true;
socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port));
}
public void Send(int connectionId, ArraySegment<byte> segment)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
connection.Send(segment);
}
}
public void Disconnect(int connectionId)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
connection.Disconnect();
}
}
public string GetClientAddress(int connectionId)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
return (connection.GetRemoteEndPoint() as IPEndPoint).Address.ToString();
}
return "";
}
HashSet<int> connectionsToRemove = new HashSet<int>();
public void Tick()
{
while (socket != null && socket.Poll(0, SelectMode.SelectRead))
{
int msgLength = socket.ReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref newClientEP);
//Debug.Log($"KCP: server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
// calculate connectionId from endpoint
int connectionId = newClientEP.GetHashCode();
// is this a new connection?
if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
// create a new KcpConnection
connection = new KcpServerConnection(socket, newClientEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize);
// DO NOT add to connections yet. only if the first message
// is actually the kcp handshake. otherwise it's either:
// * random data from the internet
// * or from a client connection that we just disconnected
// but that hasn't realized it yet, still sending data
// from last session that we should absolutely ignore.
//
//
// TODO this allocates a new KcpConnection for each new
// internet connection. not ideal, but C# UDP Receive
// already allocated anyway.
//
// expecting a MAGIC byte[] would work, but sending the raw
// UDP message without kcp's reliability will have low
// probability of being received.
//
// for now, this is fine.
// setup authenticated event that also adds to connections
connection.OnAuthenticated = () =>
{
// only send handshake to client AFTER we received his
// handshake in OnAuthenticated.
// we don't want to reply to random internet messages
// with handshakes each time.
connection.SendHandshake();
// add to connections dict after being authenticated.
connections.Add(connectionId, connection);
Debug.Log($"KCP: server added connection({connectionId}): {newClientEP}");
// setup Data + Disconnected events only AFTER the
// handshake. we don't want to fire OnServerDisconnected
// every time we receive invalid random data from the
// internet.
// setup data event
connection.OnData = (message) =>
{
// call mirror event
//Debug.Log($"KCP: OnServerDataReceived({connectionId}, {BitConverter.ToString(message.Array, message.Offset, message.Count)})");
OnData.Invoke(connectionId, message);
};
// setup disconnected event
connection.OnDisconnected = () =>
{
// flag for removal
// (can't remove directly because connection is updated
// and event is called while iterating all connections)
connectionsToRemove.Add(connectionId);
// call mirror event
Debug.Log($"KCP: OnServerDisconnected({connectionId})");
OnDisconnected.Invoke(connectionId);
};
// finally, call mirror OnConnected event
Debug.Log($"KCP: OnServerConnected({connectionId})");
OnConnected.Invoke(connectionId);
};
// now input the message & tick
// connected event was set up.
// tick will process the first message and adds the
// connection if it was the handshake.
connection.RawInput(buffer, msgLength);
connection.Tick();
// again, do not add to connections.
// if the first message wasn't the kcp handshake then
// connection will simply be garbage collected.
}
// existing connection: simply input the message into kcp
else
{
connection.RawInput(buffer, msgLength);
}
}
// tick all server connections
foreach (KcpServerConnection connection in connections.Values)
{
connection.Tick();
}
// remove disconnected connections
// (can't do it in connection.OnDisconnected because Tick is called
// while iterating connections)
foreach (int connectionId in connectionsToRemove)
{
connections.Remove(connectionId);
}
connectionsToRemove.Clear();
}
public void Stop()
{
socket?.Close();
socket = null;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9759159c6589494a9037f5e130a867ed
timeCreated: 1603787747

View File

@@ -0,0 +1,20 @@
using System.Net;
using System.Net.Sockets;
namespace kcp2k
{
public class KcpServerConnection : KcpConnection
{
public KcpServerConnection(Socket socket, EndPoint remoteEndpoint, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV)
{
this.socket = socket;
this.remoteEndpoint = remoteEndpoint;
SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize);
}
protected override void RawSend(byte[] data, int length)
{
socket.SendTo(data, 0, length, SocketFlags.None, remoteEndpoint);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 80a9b1ce9a6f14abeb32bfa9921d097b
timeCreated: 1602601483

View File

@@ -0,0 +1,23 @@
using System;
using Unity.Collections.LowLevel.Unsafe;
namespace kcp2k
{
public static partial class Utils
{
// ArraySegment content comparison without Linq
public static unsafe bool SegmentsEqual(ArraySegment<byte> a, ArraySegment<byte> b)
{
if (a.Count == b.Count)
{
fixed (byte* aPtr = &a.Array[a.Offset],
bPtr = &b.Array[b.Offset])
{
return UnsafeUtility.MemCmp(aPtr, bPtr, a.Count) == 0;
}
}
return false;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f3a2f1efc7864cb2b01af9d99470613a
timeCreated: 1603833478

View File

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

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("kcp2k.Tests")]

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: aec6a15ac7bd43129317ea1f01f19782
timeCreated: 1602665988

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,81 @@
using System.Collections.Generic;
using System.IO;
namespace kcp2k
{
// KCP Segment Definition
internal class Segment
{
internal uint conv; // conversation
internal uint cmd; // command, e.g. Kcp.CMD_ACK etc.
internal uint frg; // fragment
internal uint wnd; // window
internal uint ts; // timestamp
internal uint sn; // serial number
internal uint una;
internal uint resendts; // resend timestamp
internal int rto;
internal uint fastack;
internal uint xmit;
// we need a auto scaling byte[] with a WriteBytes function.
// MemoryStream does that perfectly, no need to reinvent the wheel.
// note: no need to pool it, because Segment is already pooled.
internal MemoryStream data = new MemoryStream();
// pool ////////////////////////////////////////////////////////////////
internal static readonly Stack<Segment> Pool = new Stack<Segment>(32);
public static Segment Take()
{
if (Pool.Count > 0)
{
Segment seg = Pool.Pop();
return seg;
}
return new Segment();
}
public static void Return(Segment seg)
{
seg.Reset();
Pool.Push(seg);
}
////////////////////////////////////////////////////////////////////////
// ikcp_encode_seg
// encode a segment into buffer
internal int Encode(byte[] ptr, int offset)
{
int offset_ = offset;
offset += Utils.Encode32U(ptr, offset, conv);
offset += Utils.Encode8u(ptr, offset, (byte)cmd);
offset += Utils.Encode8u(ptr, offset, (byte)frg);
offset += Utils.Encode16U(ptr, offset, (ushort)wnd);
offset += Utils.Encode32U(ptr, offset, ts);
offset += Utils.Encode32U(ptr, offset, sn);
offset += Utils.Encode32U(ptr, offset, una);
offset += Utils.Encode32U(ptr, offset, (uint)data.Position);
return offset - offset_;
}
// reset to return a fresh segment to the pool
internal void Reset()
{
conv = 0;
cmd = 0;
frg = 0;
wnd = 0;
ts = 0;
sn = 0;
una = 0;
rto = 0;
xmit = 0;
resendts = 0;
fastack = 0;
// keep buffer for next pool usage, but reset length (= bytes written)
data.SetLength(0);
}
}
}

View File

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

View File

@@ -0,0 +1,68 @@
using System.Runtime.CompilerServices;
namespace kcp2k
{
public static partial class Utils
{
// encode 8 bits unsigned int
public static int Encode8u(byte[] p, int offset, byte c)
{
p[0 + offset] = c;
return 1;
}
// decode 8 bits unsigned int
public static int Decode8u(byte[] p, int offset, ref byte c)
{
c = p[0 + offset];
return 1;
}
// encode 16 bits unsigned int (lsb)
public static int Encode16U(byte[] p, int offset, ushort w)
{
p[0 + offset] = (byte)(w >> 0);
p[1 + offset] = (byte)(w >> 8);
return 2;
}
// decode 16 bits unsigned int (lsb)
public static int Decode16U(byte[] p, int offset, ref ushort c)
{
ushort result = 0;
result |= p[0 + offset];
result |= (ushort)(p[1 + offset] << 8);
c = result;
return 2;
}
// encode 32 bits unsigned int (lsb)
public static int Encode32U(byte[] p, int offset, uint l)
{
p[0 + offset] = (byte)(l >> 0);
p[1 + offset] = (byte)(l >> 8);
p[2 + offset] = (byte)(l >> 16);
p[3 + offset] = (byte)(l >> 24);
return 4;
}
// decode 32 bits unsigned int (lsb)
public static int Decode32U(byte[] p, int offset, ref uint c)
{
uint result = 0;
result |= p[0 + offset];
result |= (uint)(p[1 + offset] << 8);
result |= (uint)(p[2 + offset] << 16);
result |= (uint)(p[3 + offset] << 24);
c = result;
return 4;
}
// timediff was a macro in original Kcp. let's inline it if possible.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int TimeDiff(uint later, uint earlier)
{
return (int)(later - earlier);
}
}
}

View File

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

View File

@@ -0,0 +1,12 @@
{
"name": "kcp2k",
"references": [],
"optionalUnityReferences": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": []
}

View File

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

View File

@@ -0,0 +1,384 @@
// Coburn: LLAPI is not available on UWP. There are a lot of compile directives here that we're checking against.
// Checking all of them may be overkill, but it's better to cover all the possible UWP directives. Sourced from
// https://docs.unity3d.com/Manual/PlatformDependentCompilation.html
// TODO: Check if LLAPI is supported on Xbox One?
// LLAPITransport wraps UNET's LLAPI for use as a HLAPI TransportLayer, only if you're not on a UWP platform.
#if !(UNITY_WSA || UNITY_WSA_10_0 || UNITY_WINRT || UNITY_WINRT_10_0 || NETFX_CORE)
using System;
using System.Net;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.Networking.Types;
namespace Mirror
{
[Obsolete("LLAPI is obsolete and will be removed from future versions of Unity")]
public class LLAPITransport : Transport
{
public const string Scheme = "unet";
public ushort port = 7777;
[Tooltip("Enable for WebGL games. Can only do either WebSockets or regular Sockets, not both (yet).")]
public bool useWebsockets;
// settings copied from uMMORPG configuration for best results
public ConnectionConfig connectionConfig = new ConnectionConfig
{
PacketSize = 1500,
FragmentSize = 500,
ResendTimeout = 1200,
DisconnectTimeout = 6000,
ConnectTimeout = 6000,
MinUpdateTimeout = 1,
PingTimeout = 2000,
ReducedPingTimeout = 100,
AllCostTimeout = 20,
NetworkDropThreshold = 80,
OverflowDropThreshold = 80,
MaxConnectionAttempt = 10,
AckDelay = 33,
SendDelay = 10,
MaxCombinedReliableMessageSize = 100,
MaxCombinedReliableMessageCount = 10,
MaxSentMessageQueueSize = 512,
AcksType = ConnectionAcksType.Acks128,
InitialBandwidth = 0,
BandwidthPeakFactor = 2,
WebSocketReceiveBufferMaxSize = 0,
UdpSocketReceiveBufferMaxSize = 0
};
// settings copied from uMMORPG configuration for best results
public GlobalConfig globalConfig = new GlobalConfig
{
ReactorModel = ReactorModel.SelectReactor,
ThreadAwakeTimeout = 1,
ReactorMaximumSentMessages = 4096,
ReactorMaximumReceivedMessages = 4096,
MaxPacketSize = 2000,
MaxHosts = 16,
ThreadPoolSize = 3,
MinTimerTimeout = 1,
MaxTimerTimeout = 12000
};
// always use first channel
readonly int channelId;
byte error;
int clientId = -1;
int clientConnectionId = -1;
readonly byte[] clientReceiveBuffer = new byte[4096];
byte[] clientSendBuffer;
int serverHostId = -1;
readonly byte[] serverReceiveBuffer = new byte[4096];
byte[] serverSendBuffer;
void OnValidate()
{
// add connectionconfig channels if none
if (connectionConfig.Channels.Count == 0)
{
// channel 0 is reliable fragmented sequenced
connectionConfig.AddChannel(QosType.ReliableFragmentedSequenced);
// channel 1 is unreliable
connectionConfig.AddChannel(QosType.Unreliable);
}
}
void Awake()
{
NetworkTransport.Init(globalConfig);
Debug.Log("LLAPITransport initialized!");
// initialize send buffers
clientSendBuffer = new byte[globalConfig.MaxPacketSize];
serverSendBuffer = new byte[globalConfig.MaxPacketSize];
}
public override bool Available()
{
// LLAPI runs on all platforms, including webgl
return true;
}
#region client
public override bool ClientConnected()
{
return clientConnectionId != -1;
}
void ClientConnect(string address, int port)
{
// LLAPI can't handle 'localhost'
if (address.ToLower() == "localhost") address = "127.0.0.1";
HostTopology hostTopology = new HostTopology(connectionConfig, 1);
// important:
// AddHost(topology) doesn't work in WebGL.
// AddHost(topology, port) works in standalone and webgl if port=0
clientId = NetworkTransport.AddHost(hostTopology, 0);
clientConnectionId = NetworkTransport.Connect(clientId, address, port, 0, out error);
NetworkError networkError = (NetworkError)error;
if (networkError != NetworkError.Ok)
{
Debug.LogWarning("NetworkTransport.Connect failed: clientId=" + clientId + " address= " + address + " port=" + port + " error=" + error);
clientConnectionId = -1;
}
}
public override void ClientConnect(string address)
{
ClientConnect(address, port);
}
public override void ClientConnect(Uri uri)
{
if (uri.Scheme != Scheme)
throw new ArgumentException($"Invalid url {uri}, use {Scheme}://host:port instead", nameof(uri));
int serverPort = uri.IsDefaultPort ? port : uri.Port;
ClientConnect(uri.Host, serverPort);
}
public override void ClientSend(int channelId, ArraySegment<byte> segment)
{
// Send buffer is copied internally, so we can get rid of segment
// immediately after returning and it still works.
// -> BUT segment has an offset, Send doesn't. we need to manually
// copy it into a 0-offset array
if (segment.Count <= clientSendBuffer.Length)
{
Array.Copy(segment.Array, segment.Offset, clientSendBuffer, 0, segment.Count);
NetworkTransport.Send(clientId, clientConnectionId, channelId, clientSendBuffer, segment.Count, out error);
}
else Debug.LogError("LLAPI.ClientSend: buffer( " + clientSendBuffer.Length + ") too small for: " + segment.Count);
}
public bool ProcessClientMessage()
{
if (clientId == -1)
return false;
NetworkEventType networkEvent = NetworkTransport.ReceiveFromHost(clientId, out int connectionId, out int channel, clientReceiveBuffer, clientReceiveBuffer.Length, out int receivedSize, out error);
// note: 'error' is used for extra information, e.g. the reason for
// a disconnect. we don't necessarily have to throw an error if
// error != 0. but let's log it for easier debugging.
//
// DO NOT return after error != 0. otherwise Disconnect won't be
// registered.
NetworkError networkError = (NetworkError)error;
if (networkError != NetworkError.Ok)
{
string message = "NetworkTransport.Receive failed: hostid=" + clientId + " connId=" + connectionId + " channelId=" + channel + " error=" + networkError;
OnClientError.Invoke(new Exception(message));
}
// raise events
switch (networkEvent)
{
case NetworkEventType.ConnectEvent:
OnClientConnected.Invoke();
break;
case NetworkEventType.DataEvent:
ArraySegment<byte> data = new ArraySegment<byte>(clientReceiveBuffer, 0, receivedSize);
OnClientDataReceived.Invoke(data, channel);
break;
case NetworkEventType.DisconnectEvent:
OnClientDisconnected.Invoke();
break;
default:
return false;
}
return true;
}
public string ClientGetAddress()
{
NetworkTransport.GetConnectionInfo(serverHostId, clientId, out string address, out int port, out NetworkID networkId, out NodeID node, out error);
return address;
}
public override void ClientDisconnect()
{
if (clientId != -1)
{
NetworkTransport.RemoveHost(clientId);
clientId = -1;
}
}
#endregion
#region server
// right now this just returns the first available uri,
// should we return the list of all available uri?
public override Uri ServerUri()
{
UriBuilder builder = new UriBuilder();
builder.Scheme = Scheme;
builder.Host = Dns.GetHostName();
builder.Port = port;
return builder.Uri;
}
public override bool ServerActive()
{
return serverHostId != -1;
}
public override void ServerStart()
{
if (useWebsockets)
{
HostTopology topology = new HostTopology(connectionConfig, ushort.MaxValue - 1);
serverHostId = NetworkTransport.AddWebsocketHost(topology, port);
//Debug.Log("LLAPITransport.ServerStartWebsockets port=" + port + " max=" + maxConnections + " hostid=" + serverHostId);
}
else
{
HostTopology topology = new HostTopology(connectionConfig, ushort.MaxValue - 1);
serverHostId = NetworkTransport.AddHost(topology, port);
//Debug.Log("LLAPITransport.ServerStart port=" + port + " max=" + maxConnections + " hostid=" + serverHostId);
}
}
public override void ServerSend(int connectionId, int channelId, ArraySegment<byte> segment)
{
// Send buffer is copied internally, so we can get rid of segment
// immediately after returning and it still works.
// -> BUT segment has an offset, Send doesn't. we need to manually
// copy it into a 0-offset array
if (segment.Count <= serverSendBuffer.Length)
{
// copy to 0-offset
Array.Copy(segment.Array, segment.Offset, serverSendBuffer, 0, segment.Count);
// send
NetworkTransport.Send(serverHostId, connectionId, channelId, serverSendBuffer, segment.Count, out error);
}
else Debug.LogError("LLAPI.ServerSend: buffer( " + serverSendBuffer.Length + ") too small for: " + segment.Count);
}
public bool ProcessServerMessage()
{
if (serverHostId == -1)
return false;
NetworkEventType networkEvent = NetworkTransport.ReceiveFromHost(serverHostId, out int connectionId, out int channel, serverReceiveBuffer, serverReceiveBuffer.Length, out int receivedSize, out error);
// note: 'error' is used for extra information, e.g. the reason for
// a disconnect. we don't necessarily have to throw an error if
// error != 0. but let's log it for easier debugging.
//
// DO NOT return after error != 0. otherwise Disconnect won't be
// registered.
NetworkError networkError = (NetworkError)error;
if (networkError != NetworkError.Ok)
{
string message = "NetworkTransport.Receive failed: hostid=" + serverHostId + " connId=" + connectionId + " channelId=" + channel + " error=" + networkError;
// TODO write a TransportException or better
OnServerError.Invoke(connectionId, new Exception(message));
}
// LLAPI client sends keep alive messages (75-6C-6C) on channel=110.
// ignore all messages that aren't for our selected channel.
/*if (channel != channelId)
{
return false;
}*/
switch (networkEvent)
{
case NetworkEventType.ConnectEvent:
OnServerConnected.Invoke(connectionId);
break;
case NetworkEventType.DataEvent:
ArraySegment<byte> data = new ArraySegment<byte>(serverReceiveBuffer, 0, receivedSize);
OnServerDataReceived.Invoke(connectionId, data, channel);
break;
case NetworkEventType.DisconnectEvent:
OnServerDisconnected.Invoke(connectionId);
break;
default:
// nothing or a message we don't recognize
return false;
}
return true;
}
public override bool ServerDisconnect(int connectionId)
{
return NetworkTransport.Disconnect(serverHostId, connectionId, out error);
}
public override string ServerGetClientAddress(int connectionId)
{
NetworkTransport.GetConnectionInfo(serverHostId, connectionId, out string address, out int port, out NetworkID networkId, out NodeID node, out error);
return address;
}
public override void ServerStop()
{
NetworkTransport.RemoveHost(serverHostId);
serverHostId = -1;
Debug.Log("LLAPITransport.ServerStop");
}
#endregion
#region common
// IMPORTANT: set script execution order to >1000 to call Transport's
// LateUpdate after all others. Fixes race condition where
// e.g. in uSurvival Transport would apply Cmds before
// ShoulderRotation.LateUpdate, resulting in projectile
// spawns at the point before shoulder rotation.
public void LateUpdate()
{
// process all messages
while (ProcessClientMessage()) { }
while (ProcessServerMessage()) { }
}
public override void Shutdown()
{
NetworkTransport.Shutdown();
serverHostId = -1;
clientConnectionId = -1;
Debug.Log("LLAPITransport.Shutdown");
}
public override int GetMaxPacketSize(int channelId)
{
return globalConfig.MaxPacketSize;
}
public override string ToString()
{
if (ServerActive())
{
return "LLAPI Server port: " + port;
}
else if (ClientConnected())
{
string ip = ClientGetAddress();
return "LLAPI Client ip: " + ip + " port: " + port;
}
return "LLAPI (inactive/disconnected)";
}
#endregion
}
}
#endif

View File

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

View File

@@ -0,0 +1,36 @@
using System;
namespace Mirror
{
/// <summary>
/// Allows Middleware to override some of the transport methods or let the inner transport handle them.
/// </summary>
public abstract class MiddlewareTransport : Transport
{
/// <summary>
/// Transport to call to after middleware
/// </summary>
public Transport inner;
public override bool Available() => inner.Available();
public override int GetMaxPacketSize(int channelId = 0) => inner.GetMaxPacketSize(channelId);
public override void Shutdown() => inner.Shutdown();
#region Client
public override void ClientConnect(string address) => inner.ClientConnect(address);
public override bool ClientConnected() => inner.ClientConnected();
public override void ClientDisconnect() => inner.ClientDisconnect();
public override void ClientSend(int channelId, ArraySegment<byte> segment) => inner.ClientSend(channelId, segment);
#endregion
#region Server
public override bool ServerActive() => inner.ServerActive();
public override void ServerStart() => inner.ServerStart();
public override void ServerStop() => inner.ServerStop();
public override void ServerSend(int connectionId, int channelId, ArraySegment<byte> segment) => inner.ServerSend(connectionId, channelId, segment);
public override bool ServerDisconnect(int connectionId) => inner.ServerDisconnect(connectionId);
public override string ServerGetClientAddress(int connectionId) => inner.ServerGetClientAddress(connectionId);
public override Uri ServerUri() => inner.ServerUri();
#endregion
}
}

View File

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

View File

@@ -0,0 +1,278 @@
using System;
using System.Text;
using UnityEngine;
namespace Mirror
{
// a transport that can listen to multiple underlying transport at the same time
public class MultiplexTransport : Transport
{
public Transport[] transports;
Transport available;
public void Awake()
{
if (transports == null || transports.Length == 0)
{
Debug.LogError("Multiplex transport requires at least 1 underlying transport");
}
InitClient();
InitServer();
}
void OnEnable()
{
foreach (Transport transport in transports)
{
transport.enabled = true;
}
}
void OnDisable()
{
foreach (Transport transport in transports)
{
transport.enabled = false;
}
}
public override bool Available()
{
// available if any of the transports is available
foreach (Transport transport in transports)
{
if (transport.Available())
{
return true;
}
}
return false;
}
#region Client
// clients always pick the first transport
void InitClient()
{
// wire all the base transports to my events
foreach (Transport transport in transports)
{
transport.OnClientConnected.AddListener(OnClientConnected.Invoke);
transport.OnClientDataReceived.AddListener(OnClientDataReceived.Invoke);
transport.OnClientError.AddListener(OnClientError.Invoke);
transport.OnClientDisconnected.AddListener(OnClientDisconnected.Invoke);
}
}
public override void ClientConnect(string address)
{
foreach (Transport transport in transports)
{
if (transport.Available())
{
available = transport;
transport.ClientConnect(address);
return;
}
}
throw new ArgumentException("No transport suitable for this platform");
}
public override void ClientConnect(Uri uri)
{
foreach (Transport transport in transports)
{
if (transport.Available())
{
try
{
transport.ClientConnect(uri);
available = transport;
return;
}
catch (ArgumentException)
{
// transport does not support the schema, just move on to the next one
}
}
}
throw new ArgumentException("No transport suitable for this platform");
}
public override bool ClientConnected()
{
return (object)available != null && available.ClientConnected();
}
public override void ClientDisconnect()
{
if ((object)available != null)
available.ClientDisconnect();
}
public override void ClientSend(int channelId, ArraySegment<byte> segment)
{
available.ClientSend(channelId, segment);
}
#endregion
#region Server
// connection ids get mapped to base transports
// if we have 3 transports, then
// transport 0 will produce connection ids [0, 3, 6, 9, ...]
// transport 1 will produce connection ids [1, 4, 7, 10, ...]
// transport 2 will produce connection ids [2, 5, 8, 11, ...]
int FromBaseId(int transportId, int connectionId)
{
return connectionId * transports.Length + transportId;
}
int ToBaseId(int connectionId)
{
return connectionId / transports.Length;
}
int ToTransportId(int connectionId)
{
return connectionId % transports.Length;
}
void InitServer()
{
// wire all the base transports to my events
for (int i = 0; i < transports.Length; i++)
{
// this is required for the handlers, if I use i directly
// then all the handlers will use the last i
int locali = i;
Transport transport = transports[i];
transport.OnServerConnected.AddListener(baseConnectionId =>
{
OnServerConnected.Invoke(FromBaseId(locali, baseConnectionId));
});
transport.OnServerDataReceived.AddListener((baseConnectionId, data, channel) =>
{
OnServerDataReceived.Invoke(FromBaseId(locali, baseConnectionId), data, channel);
});
transport.OnServerError.AddListener((baseConnectionId, error) =>
{
OnServerError.Invoke(FromBaseId(locali, baseConnectionId), error);
});
transport.OnServerDisconnected.AddListener(baseConnectionId =>
{
OnServerDisconnected.Invoke(FromBaseId(locali, baseConnectionId));
});
}
}
// for now returns the first uri,
// should we return all available uris?
public override Uri ServerUri()
{
return transports[0].ServerUri();
}
public override bool ServerActive()
{
// avoid Linq.All allocations
foreach (Transport transport in transports)
{
if (!transport.ServerActive())
{
return false;
}
}
return true;
}
public override string ServerGetClientAddress(int connectionId)
{
int baseConnectionId = ToBaseId(connectionId);
int transportId = ToTransportId(connectionId);
return transports[transportId].ServerGetClientAddress(baseConnectionId);
}
public override bool ServerDisconnect(int connectionId)
{
int baseConnectionId = ToBaseId(connectionId);
int transportId = ToTransportId(connectionId);
return transports[transportId].ServerDisconnect(baseConnectionId);
}
public override void ServerSend(int connectionId, int channelId, ArraySegment<byte> segment)
{
int baseConnectionId = ToBaseId(connectionId);
int transportId = ToTransportId(connectionId);
for (int i = 0; i < transports.Length; ++i)
{
if (i == transportId)
{
transports[i].ServerSend(baseConnectionId, channelId, segment);
}
}
}
public override void ServerStart()
{
foreach (Transport transport in transports)
{
transport.ServerStart();
}
}
public override void ServerStop()
{
foreach (Transport transport in transports)
{
transport.ServerStop();
}
}
#endregion
public override int GetMaxPacketSize(int channelId = 0)
{
// finding the max packet size in a multiplex environment has to be
// done very carefully:
// * servers run multiple transports at the same time
// * different clients run different transports
// * there should only ever be ONE true max packet size for everyone,
// otherwise a spawn message might be sent to all tcp sockets, but
// be too big for some udp sockets. that would be a debugging
// nightmare and allow for possible exploits and players on
// different platforms seeing a different game state.
// => the safest solution is to use the smallest max size for all
// transports. that will never fail.
int mininumAllowedSize = int.MaxValue;
foreach (Transport transport in transports)
{
int size = transport.GetMaxPacketSize(channelId);
mininumAllowedSize = Mathf.Min(size, mininumAllowedSize);
}
return mininumAllowedSize;
}
public override void Shutdown()
{
foreach (Transport transport in transports)
{
transport.Shutdown();
}
}
public override string ToString()
{
StringBuilder builder = new StringBuilder();
foreach (Transport transport in transports)
{
builder.AppendLine(transport.ToString());
}
return builder.ToString().Trim();
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[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,8 @@
fileFormatVersion: 2
guid: 5faa957b8d9fc314ab7596ccf14750d9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,86 @@
using System;
using System.Collections.Concurrent;
using UnityEngine;
namespace Mirror.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;
protected 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;
public void ProcessMessageQueue(MonoBehaviour behaviour)
{
int processedCount = 0;
// check enabled every time incase behaviour was disabled after data
while (
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 Mirror.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 /chat 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) + 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 Mirror.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,139 @@
using System;
using System.Net.Sockets;
using System.Threading;
namespace Mirror.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;
Thread receiveThread = new Thread(() => ConnectAndReceiveLoop(serverAddress));
receiveThread.IsBackground = true;
receiveThread.Start();
}
void ConnectAndReceiveLoop(Uri serverAddress)
{
try
{
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));
Thread sendThread = new Thread(() =>
{
SendLoop.Config sendConfig = new SendLoop.Config(
conn,
bufferSize: Constants.HeaderSize + Constants.MaskSize + maxMessageSize,
setMask: true);
SendLoop.Loop(sendConfig);
});
conn.sendThread = sendThread;
sendThread.IsBackground = true;
sendThread.Start();
ReceiveLoop.Config 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 Mirror.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,99 @@
using System;
using System.Collections.Generic;
using AOT;
namespace Mirror.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;
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;
}
SimpleWebJSLib.Send(index, segment.Array, 0, segment.Count);
}
void onOpen()
{
receiveQueue.Enqueue(new Message(EventType.Connected));
state = ClientState.Connected;
}
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,105 @@
// 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) {
const address = Pointer_stringify(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 Mirror.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>
internal int count;
/// <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 Mirror.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 Mirror.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 Mirror.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 Mirror.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}");
}
[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}");
}
}
}

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 Mirror.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 Mirror.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 Mirror.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,195 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace Mirror.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;
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)
{
// this could happen if client sends bad message
Log.InfoException(e);
queue.Enqueue(new Message(conn.connId, 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
{
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,163 @@
using System;
using System.IO;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Threading;
namespace Mirror.SimpleWeb
{
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;
// 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();
conn.sendPending.Reset();
while (conn.sendQueue.TryDequeue(out ArrayBuffer msg))
{
// check if connected before sending message
if (!client.Connected) { Log.Info($"SendLoop {conn} not connected"); return; }
SendMessage(stream, writeBuffer, msg, setMask, maskHelper);
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
{
conn.Dispose();
maskHelper?.Dispose();
}
}
static void SendMessage(Stream stream, byte[] buffer, ArrayBuffer msg, bool setMask, MaskHelper maskHelper)
{
int msgLength = msg.count;
int sendLength = WriteHeader(buffer, msgLength, setMask);
if (setMask)
{
sendLength = maskHelper.WriteMask(buffer, sendLength);
}
msg.CopyTo(buffer, sendLength);
sendLength += msgLength;
// dump before mask on
Log.DumpBuffer("Send", buffer, 0, sendLength);
if (setMask)
{
int messageOffset = sendLength - msgLength;
MessageProcessor.ToggleMask(buffer, messageOffset, msgLength, buffer, messageOffset - Constants.MaskSize);
}
stream.Write(buffer, 0, sendLength);
}
static int WriteHeader(byte[] buffer, int msgLength, bool setMask)
{
int sendLength = 0;
const byte finished = 128;
const byte byteOpCode = 2;
buffer[0] = finished | byteOpCode;
sendLength++;
if (msgLength <= Constants.BytePayloadLength)
{
buffer[1] = (byte)msgLength;
sendLength++;
}
else if (msgLength <= ushort.MaxValue)
{
buffer[1] = 126;
buffer[2] = (byte)(msgLength >> 8);
buffer[3] = (byte)msgLength;
sendLength += 3;
}
else
{
throw new InvalidDataException($"Trying to send a message larger than {ushort.MaxValue} bytes");
}
if (setMask)
{
buffer[1] |= 0b1000_0000;
}
return sendLength;
}
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,25 @@
using System.Net.Sockets;
namespace Mirror.SimpleWeb
{
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 Mirror.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 @@
SimpleWebTransport is a Transport that implements websocket for Webgl builds of
mirror. This transport can also work on standalone builds and has support for
encryption with websocket secure.
How to use:
Replace your existing Transport with SimpleWebTransport on your NetworkManager
Requirements:
Unity 2018.4 LTS
Mirror v18.0.0
Documentation:
https://mirror-networking.com/docs/
https://github.com/MirrorNetworking/SimpleWebTransport/blob/master/README.md
Support:
Discord: https://discordapp.com/invite/N9QVxbM
Bug Reports: https://github.com/MirrorNetworking/SimpleWebTransport/issues
**To get most recent updates and fixes download from github**
https://github.com/MirrorNetworking/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 Mirror.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),
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.HandshakeGUID.Length);
}
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 Mirror.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,105 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror.SimpleWeb
{
public class SimpleWebServer
{
readonly int maxMessagesPerTick;
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(List<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);
}
public void ProcessMessageQueue(MonoBehaviour behaviour)
{
int processedCount = 0;
// check enabled every time incase behaviour was disabled after data
while (
behaviour.enabled &&
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,230 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
namespace Mirror.SimpleWeb
{
public class WebSocketServer
{
public readonly ConcurrentQueue<Message> receiveQueue = new ConcurrentQueue<Message>();
readonly TcpConfig tcpConfig;
readonly int maxMessageSize;
TcpListener listener;
Thread acceptThread;
bool serverStopped;
readonly ServerHandshake handShake;
readonly ServerSslHelper sslHelper;
readonly BufferPool bufferPool;
readonly ConcurrentDictionary<int, Connection> connections = new ConcurrentDictionary<int, Connection>();
int _idCounter = 0;
public WebSocketServer(TcpConfig tcpConfig, int maxMessageSize, int handshakeMaxSize, SslConfig sslConfig, BufferPool bufferPool)
{
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 = Interlocked.Increment(ref _idCounter);
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 _);
}
}
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,14 @@
{
"name": "SimpleWebTransport",
"references": [
"Mirror"
],
"optionalUnityReferences": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": []
}

View File

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

Some files were not shown because too many files have changed in this diff Show More