mirror of
https://github.com/smyalygames/monopoly.git
synced 2025-12-29 07:48:48 +01:00
Added multiplayer plugin
This commit is contained in:
189
Assets/Mirror/Runtime/Transport/FallbackTransport.cs
Normal file
189
Assets/Mirror/Runtime/Transport/FallbackTransport.cs
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
11
Assets/Mirror/Runtime/Transport/FallbackTransport.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/FallbackTransport.cs.meta
Normal 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:
|
||||
8
Assets/Mirror/Runtime/Transport/KCP.meta
Normal file
8
Assets/Mirror/Runtime/Transport/KCP.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 953bb5ec5ab2346a092f58061e01ba65
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Mirror/Runtime/Transport/KCP/MirrorTransport.meta
Normal file
8
Assets/Mirror/Runtime/Transport/KCP/MirrorTransport.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7bdb797750d0a490684410110bf48192
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -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:
|
||||
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k.meta
Normal file
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71a1c8e8c022d4731a481c1808f37e5d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
24
Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE
Normal file
24
Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE
Normal 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.
|
||||
7
Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE.meta
Normal file
7
Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a3e8369060cf4e94ac117603de47aa6
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
9
Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION
Normal file
9
Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION
Normal 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
|
||||
7
Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION.meta
Normal file
7
Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed3f2cf1bbf1b4d53a6f2c103d311f71
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel.meta
Normal file
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a54d18b954cb4407a28b633fc32ea6d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6aa069a28ed24fedb533c102d9742b36
|
||||
timeCreated: 1603786960
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96512e74aa8214a6faa8a412a7a07877
|
||||
timeCreated: 1602601237
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3915c7c62b72d4dc2a9e4e76c94fc484
|
||||
timeCreated: 1602600432
|
||||
229
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs
Normal file
229
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9759159c6589494a9037f5e130a867ed
|
||||
timeCreated: 1603787747
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 80a9b1ce9a6f14abeb32bfa9921d097b
|
||||
timeCreated: 1602601483
|
||||
23
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/Utils.cs
Normal file
23
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/Utils.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f3a2f1efc7864cb2b01af9d99470613a
|
||||
timeCreated: 1603833478
|
||||
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp.meta
Normal file
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cafb8851a0084f3e94a580c207b3923
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("kcp2k.Tests")]
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aec6a15ac7bd43129317ea1f01f19782
|
||||
timeCreated: 1602665988
|
||||
1025
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs
Normal file
1025
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a59b1cae10a334faf807432ab472f212
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
81
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Segment.cs
Normal file
81
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Segment.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc58706a05dd3442c8fde858d5266855
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
68
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs
Normal file
68
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef959eb716205bd48b050f010a9a35ae
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
12
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp2k.asmdef
Normal file
12
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp2k.asmdef
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "kcp2k",
|
||||
"references": [],
|
||||
"optionalUnityReferences": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": true,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6806a62c384838046a3c66c44f06d75f
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
384
Assets/Mirror/Runtime/Transport/LLAPITransport.cs
Normal file
384
Assets/Mirror/Runtime/Transport/LLAPITransport.cs
Normal 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
|
||||
11
Assets/Mirror/Runtime/Transport/LLAPITransport.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/LLAPITransport.cs.meta
Normal 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:
|
||||
36
Assets/Mirror/Runtime/Transport/MiddlewareTransport.cs
Normal file
36
Assets/Mirror/Runtime/Transport/MiddlewareTransport.cs
Normal 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
|
||||
}
|
||||
}
|
||||
11
Assets/Mirror/Runtime/Transport/MiddlewareTransport.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/MiddlewareTransport.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46f20ede74658e147a1af57172710de2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
278
Assets/Mirror/Runtime/Transport/MultiplexTransport.cs
Normal file
278
Assets/Mirror/Runtime/Transport/MultiplexTransport.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Mirror/Runtime/Transport/MultiplexTransport.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/MultiplexTransport.cs.meta
Normal 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:
|
||||
8
Assets/Mirror/Runtime/Transport/SimpleWebTransport.meta
Normal file
8
Assets/Mirror/Runtime/Transport/SimpleWebTransport.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3ba68af305d809418d6c6a804939290
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,4 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Runtime")]
|
||||
[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Editor")]
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee9e76201f7665244bd6ab8ea343a83f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5faa957b8d9fc314ab7596ccf14750d9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13131761a0bf5a64dadeccd700fe26e5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9c19d05220a87c4cbbe4d1e422da0aa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ffdcabc9e28f764a94fc4efc82d3e8b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46055a75559a79849a750f39a766db61
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 05a9c87dea309e241a9185e5aa0d72ab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7142349d566213c4abc763afaf4d91a1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97b96a0b65c104443977473323c2ff35
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 015c5b1915fd1a64cbe36444d16b2f7d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1999985791b91b9458059e88404885a7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 564d2cd3eee5b21419553c0528739d1b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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})");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94ae50f3ec35667469b861b12cd72f92
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a13073c2b49d39943888df45174851bd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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' };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85d110a089d6ad348abf2d073ebce7cd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
public enum EventType
|
||||
{
|
||||
Connected,
|
||||
Data,
|
||||
Disconnected,
|
||||
Error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d9cd7d2b5229ab42a12e82ae17d0347
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
116
Assets/Mirror/Runtime/Transport/SimpleWebTransport/Common/Log.cs
Normal file
116
Assets/Mirror/Runtime/Transport/SimpleWebTransport/Common/Log.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3cf1521098e04f74fbea0fe2aa0439f8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5d05d71b09d2714b96ffe80bc3d2a77
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c1f218a2b16ca846aaf23260078e549
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f4fa5d324e708c46a55810a97de75bc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a26c2815f58431c4a98c158c8b655ffd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f87dd81736d9c824db67f808ac71841d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81ac8d35f28fab14b9edda5cd9d4fc86
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4643ffb4cb0562847b1ae925d07e15b6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e3971d5783109f4d9ce93c7a689d701
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e599e92544d43344a9a9060052add28
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6268509ac4fb48141b9944c03295da11
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11061fee528ebdd43817a275b1e4a317
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd51d7896f55a5e48b41a4b526562b0e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c434db044777d2439bae5a57d4e8ee7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "SimpleWebTransport",
|
||||
"references": [
|
||||
"Mirror"
|
||||
],
|
||||
"optionalUnityReferences": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": []
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user