// 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); } // create receive pipe with max message size for pooling // => create new pipes every time! // if an old receive thread is still finishing up, it might still // be using the old pipes. we don't want to risk any old data for // our new start here. receivePipe = new MagnificentReceivePipe(MaxMessageSize); // start the listener thread // (on low priority. if main thread is too busy then there is not // much value in accepting even more clients) Log.Info("Server: Start port=" + port); listenerThread = new Thread(() => { Listen(port); }); listenerThread.IsBackground = true; listenerThread.Priority = ThreadPriority.BelowNormal; listenerThread.Start(); return(true); }
public ConnectionState(TcpClient client, int MaxMessageSize) { this.client = client; // create send pipe with max message size for pooling receivePipe = new MagnificentReceivePipe(MaxMessageSize); sendPipe = new MagnificentSendPipe(MaxMessageSize); }
// thread receive function is the same for client and server's clients public static void ReceiveLoop(int connectionId, TcpClient client, int MaxMessageSize, MagnificentReceivePipe receivePipe, int queueLimit) { // get NetworkStream from client NetworkStream stream = client.GetStream(); // every receive loop needs it's own receive buffer of // HeaderSize + MaxMessageSize // to avoid runtime allocations. // // IMPORTANT: DO NOT make this a member, otherwise every connection // on the server would use the same buffer simulatenously byte[] receiveBuffer = new byte[4 + MaxMessageSize]; // avoid header[4] allocations // // IMPORTANT: DO NOT make this a member, otherwise every connection // on the server would use the same buffer simulatenously byte[] headerBuffer = new byte[4]; // absolutely must wrap with try/catch, otherwise thread exceptions // are silent try { // set connected event in pipe receivePipe.SetConnected(); // 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) { // read the next message (blocking) or stop if stream closed if (!ReadMessageBlocking(stream, MaxMessageSize, headerBuffer, receiveBuffer, out int size)) { // break instead of return so stream close still happens! break; } // create arraysegment for the read message ArraySegment <byte> message = new ArraySegment <byte>(receiveBuffer, 0, size); // send to main thread via pipe // -> it'll copy the message internally so we can reuse the // receive buffer for next read! receivePipe.Enqueue(message); // disconnect if receive pipe gets too big. // -> avoids ever growing queue memory if network is slower // than input // -> disconnecting is great for load balancing. better to // disconnect one connection than risking every // connection / the whole server if (receivePipe.Count >= queueLimit) { // log the reason Log.Warning($"receivePipe reached limit of {queueLimit}. This can happen if network messages come in way faster than we manage to process them. Disconnecting this connection for load balancing."); // clear queue so the final disconnect message will be // processed immediately. no need to process thousands // of pending messages before disconnecting. // it would just delay it for quite some time. receivePipe.Clear(); // just break. the finally{} will close everything. break; } } } 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 Log.Info("ReceiveLoop: finished receive function for connectionId=" + connectionId + " reason: " + exception); } finally { // clean up no matter what stream.Close(); client.Close(); // set 'Disconnected' event in pipe after disconnecting properly // -> always AFTER closing the streams to avoid a race condition // where Disconnected -> Reconnect wouldn't work because // Connected is still true for a short moment before the stream // would be closed. receivePipe.SetDisconnected(); } }
// constructor always creates new TcpClient for client connection! public ClientConnectionState(int MaxMessageSize) : base(new TcpClient(), MaxMessageSize) { // create receive pipe with max message size for pooling receivePipe = new MagnificentReceivePipe(MaxMessageSize); }
public int Tick(int processLimit, Func <bool> checkEnabled = null) { int remaining = 0; // for each connection // checks enabled in case a Mirror scene message arrived foreach (KeyValuePair <int, ConnectionState> kvp in clients) { MagnificentReceivePipe receivePipe = kvp.Value.receivePipe; // need a processLimit copy just for this connection so that // we can count the Connected message as a processed one. // => otherwise decreasing the limit in Connected event would // decrease the limit for everyone! int connectionProcessLimit = processLimit; // always process connect FIRST before anything else if (connectionProcessLimit > 0) { if (receivePipe.CheckConnected()) { OnConnected?.Invoke(kvp.Key); // it counts as a processed message --connectionProcessLimit; } } // process up to 'processLimit' messages for this connection // checks enabled in case a Mirror scene message arrived for (int i = 0; i < connectionProcessLimit; ++i) { // check enabled in case a Mirror scene message arrived if (checkEnabled != null && !checkEnabled()) { break; } // peek first. allows us to process the first queued entry while // still keeping the pooled byte[] alive by not removing anything. if (receivePipe.TryPeek(out ArraySegment <byte> message)) { OnData?.Invoke(kvp.Key, message); // IMPORTANT: now dequeue and return it to pool AFTER we are // done processing the event. receivePipe.TryDequeue(); } // AFTER PROCESSING, add remaining ones to our counter remaining += receivePipe.Count; } // always process disconnect AFTER anything else // (should never process data messages after disconnect message) if (connectionProcessLimit > 0) { if (receivePipe.CheckDisconnected()) { OnDisconnected?.Invoke(kvp.Key); connectionsToRemove.Add(kvp.Key); } } } // remove all disconnected connections now that we processed the // final disconnect message. foreach (int connectionId in connectionsToRemove) { clients.TryRemove(connectionId, out ConnectionState _); } connectionsToRemove.Clear(); return(remaining); }