Space-Smash-Out/Assets/FishNet/Plugins/Bayou/SimpleWebTransport/Server/WebSocketServer.cs

269 lines
8.7 KiB
C#

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