// the thread function // (static to reduce state for maximum reliability) static void ThreadFunction(TcpClient client, string ip, int port, SafeQueue <Message> messageQueue) { // absolutely must wrap with try/catch, otherwise thread // exceptions are silent try { // connect (blocking) // (NoDelay disables nagle algorithm. lowers CPU% and latency) client.NoDelay = true; client.Connect(ip, port); // run the receive loop ReceiveLoop(0, client, messageQueue); } 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: 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 messageQueue.Enqueue(new Message(0, EventType.Disconnected, null, 0)); } catch (Exception exception) { // something went wrong. probably important. Logger.LogError("Client Exception: " + exception); } // 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(); }
protected static void SendLoop(int connectionId, TcpClient client, SafeQueue <byte[]> sendQueue, ManualResetEvent sendPending) { NetworkStream stream = client.GetStream(); try { while (client.Connected) { sendPending.Reset(); byte[][] messages; if (sendQueue.TryDequeueAll(out messages)) { if (!SendMessagesBlocking(stream, messages)) { return; } } sendPending.WaitOne(); } } catch (ThreadAbortException) { } catch (ThreadInterruptedException) { } catch (Exception exception) { Logger.Log("SendLoop Exception: connectionId=" + connectionId + " reason: " + exception); } }
// thread send function // note: we really do need one per connection, so that if one connection // blocks, the rest will still continue to get sends protected static void SendLoop(int connectionId, TcpClient client, SafeQueue <byte[]> sendQueue, ManualResetEvent sendPending) { // get NetworkStream from client NetworkStream stream = client.GetStream(); try { // (Yurgis) while (client.Connected) // try this. client will get closed eventually. while (true) { // reset ManualResetEvent before we do anything else. this // way there is no race condition. if Send() is called again // while in here then it will be properly detected next time // -> otherwise Send might be called right after dequeue but // before .Reset, which would completely ignore it until // the next Send call. sendPending.Reset(); // WaitOne() blocks until .Set() again // dequeue all // SafeQueue.TryDequeueAll is twice as fast as // ConcurrentQueue, see SafeQueue.cs! byte[][] messages; if (sendQueue.TryDequeueAll(out messages)) { // send message (blocking) or stop if stream is closed if (!SendMessagesBlocking(stream, messages)) { // (Yurgis) if (stream.CanWrite == false) { Logger.LogWarning("SendLoop(): EXIT because...."); return; } } } // don't choke up the CPU: wait until queue not empty anymore sendPending.WaitOne(); } } catch (ThreadAbortException) { // happens on stop. don't log anything. Logger.LogWarning($"SendLoop() Conn #{connectionId}: ThreadAbortException"); } catch (ThreadInterruptedException) { // happens if receive thread interrupts send thread. Logger.LogWarning($"SendLoop() Conn #{connectionId}: ThreadInterruptedException"); } catch (Exception exception) { // something went wrong. the thread was interrupted or the // connection closed or we closed our own connection or ... // -> either way we should stop gracefully Logger.Log("SendLoop Exception: connectionId=" + connectionId + " reason: " + exception); } }
// thread send function // note: we really do need one per connection, so that if one connection // blocks, the rest will still continue to get sends protected static void SendLoop(int connectionId, TcpClient client, SafeQueue <byte[]> sendQueue, ManualResetEvent sendPending) { // get NetworkStream from client NetworkStream stream = client.GetStream(); try { // try this. client will get closed eventually. while (client.Connected) { // reset ManualResetEvent before we do anything else. this // way there is no race condition. if Send() is called again // while in here then it will be properly detected next time // -> otherwise Send might be called right after dequeue but // before .Reset, which would completely ignore it until // the next Send call. // WaitOne() blocks until .Set() again sendPending.Reset(); // dequeue all // SafeQueue.TryDequeueAll is twice as fast as // ConcurrentQueue, see SafeQueue.cs! byte[][] messages; if (sendQueue.TryDequeueAll(out messages)) { // send message (blocking) or stop if stream is closed if (!SendMessagesBlocking(stream, messages)) { // break instead of return so stream close still happens! break; } } // don't choke up the CPU: wait until queue not empty anymore sendPending.WaitOne(); } } catch (ThreadAbortException) { // happens on stop. don't log anything. } catch (ThreadInterruptedException) { // happens if receive thread interrupts send thread. } catch (Exception exception) { // something went wrong. the thread was interrupted or the // connection closed or we closed our own connection or ... // -> either way we should stop gracefully Logger.Log("SendLoop Exception: connectionId=" + connectionId + " reason: " + exception); } finally { // clean up no matter what // we might get SocketExceptions when sending if the 'host has // failed to respond' - in which case we should close the connection // which causes the ReceiveLoop to end and fire the Disconnected // message. otherwise the connection would stay alive forever even // though we can't send anymore. stream.Close(); client.Close(); } }
// 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; // create send queue for this client SafeQueue <byte[]> sendQueue = new SafeQueue <byte[]>(); sendQueues.Add(0, sendQueue); // start send thread only after connected sendThread = new Thread(() => { SendLoop(0, client, sendQueue); }); sendThread.IsBackground = true; sendThread.Start(); // run the receive loop ReceiveLoop(0, client, receiveQueue); } 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 (Exception exception) { // something went wrong. probably important. Logger.LogError("Client Recv Exception: " + exception); } // try interrupting send thread after receive thread // ends, just to be sure if (sendThread != null) { sendThread.Interrupt(); } // 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(); }
// thread send function // note: we really do need one per connection, so that if one connection // blocks, the rest will still continue to get sends protected static void SendLoop(int connectionId, TcpClient client, SafeQueue <byte[]> sendQueue) { // get NetworkStream from client NetworkStream stream = client.GetStream(); try { while (client.Connected) // try this. client will get closed eventually. { // dequeue all byte[][] messages; if (sendQueue.TryDequeueAll(out messages)) { // send message (blocking) or stop if stream is closed if (!SendMessagesBlocking(stream, messages)) { return; } } // don't choke up the CPU: wait until queue not empty anymore sendQueue.notEmpty.WaitOne(); } } catch (ThreadAbortException) { // happens on stop. don't log anything. } catch (Exception exception) { // something went wrong. the thread was interrupted or the // connection closed or we closed our own connection or ... // -> either way we should stop gracefully Logger.Log("SendLoop Exception: connectionId=" + connectionId + " reason: " + exception); } }
// thread receive function is the same for client and server's clients // (static to reduce state for maximum reliability) protected static void ReceiveLoop(int connectionId, TcpClient client, SafeQueue <Message> messageQueue) { // get NetworkStream from client NetworkStream stream = client.GetStream(); // keep track of last message queue warning DateTime messageQueueLastWarning = DateTime.Now; // absolutely must wrap with try/catch, otherwise thread exceptions // are silent try { // add connected event to queue with ip address as data in case // it's needed messageQueue.Enqueue(new Message(connectionId, EventType.Connected, null, 0)); // let's talk about reading data. // -> normally we would read as much as possible and then // extract as many <size,content>,<size,content> messages // as we received this time. this is really complicated // and expensive to do though // -> instead we use a trick: // Read(2) -> size // Read(size) -> content // repeat // Read is blocking, but it doesn't matter since the // best thing to do until the full message arrives, // is to wait. // => this is the most elegant AND fast solution. // + no resizing // + no extra allocations, just one for the content // + no crazy extraction logic while (true) { byte[] content = ByteArrayPool.Take(); // read the next message (blocking) or stop if stream closed if (!ReadMessageBlocking(stream, content, out int size)) { break; } // queue it messageQueue.Enqueue(new Message(connectionId, EventType.Data, content, size)); // it is now up to the developer to Dispose the message to return the // buffer to the pool. // and show a warning if the queue gets too big // -> we don't want to show a warning every single time, // because then a lot of processing power gets wasted on // logging, which will make the queue pile up even more. // -> instead we show it every 10s, so that the system can // use most it's processing power to hopefully process it. if (messageQueue.Count > MessageQueueSizeWarning) { TimeSpan elapsed = DateTime.Now - messageQueueLastWarning; if (elapsed.TotalSeconds > 10) { Logger.LogWarning("ReceiveLoop: messageQueue is getting big(" + messageQueue.Count + "), try calling GetNextMessage more often. You can call it more than once per frame!"); messageQueueLastWarning = DateTime.Now; } } } } catch (Exception exception) { // something went wrong. the thread was interrupted or the // connection closed or we closed our own connection or ... // -> either way we should stop gracefully Logger.Log("ReceiveLoop: finished receive function for connectionId=" + connectionId + " reason: " + exception); } // if we got here then either the client while loop ended, or an // exception happened. disconnect messageQueue.Enqueue(new Message(connectionId, EventType.Disconnected, null, 0)); // clean up no matter what stream.Close(); client.Close(); }
// 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 listener = new TcpListener(new IPEndPoint(IPAddress.Any, 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(); // generate the next connection id (thread safely) int connectionId = NextConnectionId(); // spawn a send thread for each client Thread sendThread = new Thread(() => { // wrap in try-catch, otherwise Thread exceptions // are silent try { // create send queue immediately SafeQueue <byte[]> sendQueue = new SafeQueue <byte[]>(); sendQueues[connectionId] = sendQueue; // run the send loop SendLoop(connectionId, client, sendQueue); // remove queue from queues afterwards sendQueues.TryRemove(connectionId, out SafeQueue <byte[]> _); } 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 { // add to dict immediately clients[connectionId] = client; // run the receive loop ReceiveLoop(connectionId, client, receiveQueue); // remove client from clients dict afterwards clients.TryRemove(connectionId, out TcpClient _); } 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); } }