public void Disconnect() { // only if started if (Connecting || Connected) { // close client client.Close(); // kill the receive thread // => AbortAndJoin is the safest way and avoids race conditions! // this way we can guarantee that when Disconnect() returns, // we are 100% ready for the next Connect! receiveThread?.AbortAndJoin(); // we interrupted the receive Thread, so we can't guarantee that // connecting was reset. let's do it manually. _Connecting = false; // clear send queues. no need to hold on to them. // (unlike receiveQueue, which is still needed to process the // latest Disconnected message, etc.) sendQueue.Clear(); // let go of this one completely. the thread ended, no one uses // it anymore and this way Connected is false again immediately. client = null; } }
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. // => AbortAndJoin is the safest way and avoids race conditions! listenerThread?.AbortAndJoin(); // wait until thread is TRULY finished. this is the only way // to guarantee that everything was properly cleaned up before // returning. // => this means that calling Stop() may sometimes block // for a while, but there is no other way to guarantee that // everything is cleaned up properly by the time Stop() returns. // we have to live with the wait time. listenerThread?.Join(); listenerThread = null; // close all client connections foreach (KeyValuePair <int, ClientToken> 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(); // reset the counter in case we start up again so // clients get connection ID's starting from 1 counter = 0; }
// 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. // => AbortAndJoin is the safest way and avoids race conditions! sendThread.AbortAndJoin(); } 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); } }
// the thread function void ReceiveThreadFunction(string ip, int port) { // absolutely must wrap with try/catch, otherwise thread // exceptions are silent try { // connect (blocking) client.Connect(ip, port); _Connecting = false; // set socket options after the socket was created in Connect() // (not after the constructor because we clear the socket there) client.NoDelay = NoDelay; client.SendTimeout = SendTimeout; // start send thread only after connected sendThread = new Thread(() => { SendLoop(0, client, sendQueue, sendPending); }); sendThread.IsBackground = true; sendThread.Start(); // run the receive loop ReceiveLoop(0, client, receiveQueue, MaxMessageSize); } catch (SocketException exception) { // this happens if (for example) the ip address is correct // but there is no server running on that ip/port Logger.Log("Client Recv: failed to connect to ip=" + ip + " port=" + port + " reason=" + exception); // add 'Disconnected' event to message queue so that the caller // knows that the Connect failed. otherwise they will never know receiveQueue.Enqueue(new Message(0, EventType.Disconnected, null)); } catch (ThreadInterruptedException) { // expected if Disconnect() aborts it } catch (ThreadAbortException) { // expected if Disconnect() aborts it } catch (Exception exception) { // something went wrong. probably important. Logger.LogError("Client Recv Exception: " + exception); } // 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. // => AbortAndJoin is the safest way and avoids race conditions! sendThread?.AbortAndJoin(); // Connect might have failed. thread might have been closed. // let's reset connecting state no matter what. _Connecting = false; // if we got here then we are done. ReceiveLoop cleans up already, // but we may never get there if connect fails. so let's clean up // here too. client?.Close(); }