using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Threading; namespace Telepathy { public class Server : Common { // listener public TcpListener listener; Thread listenerThread; // class with all the client's data. let's call it Token for consistency // with the async socket methods. class ClientToken { public TcpClient client; // send queue // SafeQueue is twice as fast as ConcurrentQueue, see SafeQueue.cs! public SafeQueue sendQueue = new SafeQueue(); // ManualResetEvent to wake up the send thread. better than Thread.Sleep // -> call Set() if everything was sent // -> call Reset() if there is something to send again // -> call WaitOne() to block until Reset was called public ManualResetEvent sendPending = new ManualResetEvent(false); public ClientToken(TcpClient client) { this.client = client; } } // clients with readonly ConcurrentDictionary clients = new ConcurrentDictionary(); // connectionId counter int counter; // public next id function in case someone needs to reserve an id // (e.g. if hostMode should always have 0 connection and external // connections should start at 1, etc.) public int NextConnectionId() { int id = Interlocked.Increment(ref counter); // it's very unlikely that we reach the uint limit of 2 billion. // even with 1 new connection per second, this would take 68 years. // -> but if it happens, then we should throw an exception because // the caller probably should stop accepting clients. // -> it's hardly worth using 'bool Next(out id)' for that case // because it's just so unlikely. if (id == int.MaxValue) { throw new Exception("connection id limit reached: " + id); } return id; } // check if the server is running public bool Active => listenerThread != null && listenerThread.IsAlive; // the listener thread's listen function // note: no maxConnections parameter. high level API should handle that. // (Transport can't send a 'too full' message anyway) void Listen(int port) { // absolutely must wrap with try/catch, otherwise thread // exceptions are silent try { // start listener on all IPv4 and IPv6 address via .Create listener = TcpListener.Create(port); listener.Server.NoDelay = NoDelay; listener.Server.SendTimeout = SendTimeout; listener.Start(); Logger.Log("Server: listening port=" + port); // keep accepting new clients while (true) { // wait and accept new client // note: 'using' sucks here because it will try to // dispose after thread was started but we still need it // in the thread TcpClient client = listener.AcceptTcpClient(); // set socket options client.NoDelay = NoDelay; client.SendTimeout = SendTimeout; // generate the next connection id (thread safely) int connectionId = NextConnectionId(); // add to dict immediately ClientToken token = new ClientToken(client); clients[connectionId] = token; // spawn a send thread for each client Thread sendThread = new Thread(() => { // wrap in try-catch, otherwise Thread exceptions // are silent try { // run the send loop SendLoop(connectionId, client, token.sendQueue, token.sendPending); } catch (ThreadAbortException) { // happens on stop. don't log anything. // (we catch it in SendLoop too, but it still gets // through to here when aborting. don't show an // error.) } catch (Exception exception) { Logger.LogError("Server send thread exception: " + exception); } }); sendThread.IsBackground = true; sendThread.Start(); // spawn a receive thread for each client Thread receiveThread = new Thread(() => { // wrap in try-catch, otherwise Thread exceptions // are silent try { // run the receive loop ReceiveLoop(connectionId, client, receiveQueue, MaxMessageSize); // remove client from clients dict afterwards clients.TryRemove(connectionId, out ClientToken _); // sendthread might be waiting on ManualResetEvent, // so let's make sure to end it if the connection // closed. // otherwise the send thread would only end if it's // actually sending data while the connection is // closed. sendThread.Interrupt(); } catch (Exception exception) { Logger.LogError("Server client thread exception: " + exception); } }); receiveThread.IsBackground = true; receiveThread.Start(); } } catch (ThreadAbortException exception) { // UnityEditor causes AbortException if thread is still // running when we press Play again next time. that's okay. Logger.Log("Server thread aborted. That's okay. " + exception); } catch (SocketException exception) { // calling StopServer will interrupt this thread with a // 'SocketException: interrupted'. that's okay. Logger.Log("Server Thread stopped. That's okay. " + exception); } catch (Exception exception) { // something went wrong. probably important. Logger.LogError("Server Exception: " + exception); } } // start listening for new connections in a background thread and spawn // a new thread for each one. public bool Start(int port) { // not if already started if (Active) return false; // clear old messages in queue, just to be sure that the caller // doesn't receive data from last time and gets out of sync. // -> calling this in Stop isn't smart because the caller may // still want to process all the latest messages afterwards receiveQueue = new ConcurrentQueue(); // start the listener thread // (on low priority. if main thread is too busy then there is not // much value in accepting even more clients) Logger.Log("Server: Start port=" + port); listenerThread = new Thread(() => { Listen(port); }); listenerThread.IsBackground = true; listenerThread.Priority = ThreadPriority.BelowNormal; listenerThread.Start(); return true; } public void Stop() { // only if started if (!Active) return; Logger.Log("Server: stopping..."); // stop listening to connections so that no one can connect while we // close the client connections // (might be null if we call Stop so quickly after Start that the // thread was interrupted before even creating the listener) listener?.Stop(); // kill listener thread at all costs. only way to guarantee that // .Active is immediately false after Stop. // -> calling .Join would sometimes wait forever listenerThread?.Interrupt(); listenerThread = null; // close all client connections foreach (KeyValuePair kvp in clients) { TcpClient client = kvp.Value.client; // close the stream if not closed yet. it may have been closed // by a disconnect already, so use try/catch try { client.GetStream().Close(); } catch {} client.Close(); } // clear clients list clients.Clear(); } // send message to client using socket connection. public bool Send(int connectionId, byte[] data) { // respect max message size to avoid allocation attacks. if (data.Length <= MaxMessageSize) { // find the connection ClientToken token; if (clients.TryGetValue(connectionId, out token)) { // add to send queue and return immediately. // calling Send here would be blocking (sometimes for long times // if other side lags or wire was disconnected) token.sendQueue.Enqueue(data); token.sendPending.Set(); // interrupt SendThread WaitOne() return true; } Logger.Log("Server.Send: invalid connectionId: " + connectionId); return false; } Logger.LogError("Client.Send: message too big: " + data.Length + ". Limit: " + MaxMessageSize); return false; } // client's ip is sometimes needed by the server, e.g. for bans public string GetClientAddress(int connectionId) { // find the connection ClientToken token; if (clients.TryGetValue(connectionId, out token)) { return ((IPEndPoint)token.client.Client.RemoteEndPoint).Address.ToString(); } return ""; } // disconnect (kick) a client public bool Disconnect(int connectionId) { // find the connection ClientToken token; if (clients.TryGetValue(connectionId, out token)) { // just close it. client thread will take care of the rest. token.client.Close(); Logger.Log("Server.Disconnect connectionId:" + connectionId); return true; } return false; } } }