Servidor TCP Multithread e Cliente C #

Sep 22 2020

Eu criei um Servidor e Cliente TCP e realmente gostaria de saber se é bom em relação a desempenho e qualidade / segurança de código. Destaco a classe de servidor e cliente aqui. Se você precisar de mais informações sobre as aulas do Helper, eu também poderia compartilhá-las. Uma coisa que sei é que criar threads para cada cliente não é muito bom e gostaria de saber em que número de soquetes conectados isso realmente faz diferença.

TCPServer.cs

/// <summary>
/// Multithreaded TCP Server
/// </summary>
public class TCPServer : TCPBase {

    /// <summary>
    /// Max Package length, default 100MB
    /// </summary>
    public static int MaxPackageLength { get; set; } = 107374182;

    /// <summary>
    /// List of all clients currently connected to the server
    /// </summary>
    public List<TCPServerClient> ClientsList { get; protected set; } = new List<TCPServerClient>();

    /// <summary>
    /// Backlog to use, only change before start server
    /// </summary>
    public int Backlog { get; set; } = 500;

    /// <summary>
    /// Size of the uid of a client
    /// </summary>
    public uint UIDLength { get; set; } = 12;

    /// <summary>
    /// Whether clients need to make a initial handshake
    /// </summary>
    public bool RequireHandshake { get; set; } = true;

    /// <summary>
    /// Is logging enabled
    /// </summary>
    public bool Logging { get; set; } = true;

    /// <summary>
    /// Whether pinging is enabled
    /// </summary>
    public bool Pinging { get; set; } = true;

    /// <summary>
    /// Thread to handle management such as kick clients with no handshake
    /// </summary>
    public Thread ManagementThread { get; private set; }

    /// <summary>
    /// Thread to handle pinging and rtt
    /// </summary>
    public Thread PingThread { get; private set; }

    /// <summary>
    /// Management sleep time in ms
    /// </summary>
    public int ManagementSleep { get; set; } = 20000;

    /// <summary>
    /// Ping sleep time in ms
    /// </summary>
    public int PingSleep { get; set; } = 5000;

    /// <summary>
    /// Time span until clients are kicked without handshake
    /// </summary>
    public TimeSpan HandshakeTimeout { get; set; } = new TimeSpan(0, 0, 40);

    /// <summary>
    /// Dictionary containing all clients identified by their uid
    /// </summary>
    public Dictionary<string, TCPServerClient> ClientsDict { get; protected set; } = new Dictionary<string, TCPServerClient>();

    /// <summary>
    /// Message Event Handler
    /// </summary>
    /// <param name="client"></param>
    /// <param name="message"></param>
    public delegate void MessageEventHandler(TCPServerClient client, TCPMessage message);

    /// <summary>
    /// Message Event, called if a client sent a message
    /// </summary>
    public event MessageEventHandler OnMessage;

    /// <summary>
    /// Connected Event Handler
    /// </summary>
    /// <param name="client"></param>
    public delegate void ConnectedEventHandler(TCPServerClient client);

    /// <summary>
    /// Connected Event, called if a new client is successfully connected
    /// </summary>
    public event ConnectedEventHandler OnConnected;

    /// <summary>
    /// Disconnected Event Handler
    /// </summary>
    /// <param name="client"></param>
    public delegate void DisconnectedEventHandler(TCPServerClient client);

    /// <summary>
    /// Disconnected Event, called if a client is disconnected
    /// </summary>
    public event DisconnectedEventHandler OnDisconnected;

    /// <summary>
    /// No Handshaked Event Handler
    /// </summary>
    /// <param name="client"></param>
    public delegate void NoHandshakeEventHandler(TCPServerClient client);

    /// <summary>
    /// No Handshake Event, called if client fails to provide correct init package
    /// </summary>
    public event NoHandshakeEventHandler OnNoHandshake;

    /// <summary>
    /// Timeout Event Handler
    /// </summary>
    /// <param name="client"></param>
    public delegate void TimeoutEventHandler(TCPServerClient client);

    /// <summary>
    /// Timeout Event, called if client is timed out
    /// </summary>
    public event TimeoutEventHandler OnTimeout;

    /// <summary>
    /// Kick Event Handler
    /// </summary>
    /// <param name="client"></param>
    public delegate void KickEventHandler(TCPServerClient client);

    /// <summary>
    /// Kick Event, called if client was kicked
    /// </summary>
    public event KickEventHandler OnKick;

    /// <summary>
    /// Handshake Event Handler
    /// </summary>
    /// <param name="client"></param>
    public delegate void HandshakeHandler(TCPServerClient client);

    /// <summary>
    /// Handshake Event
    /// </summary>
    public event HandshakeHandler OnHandshake;

    /// <summary>
    /// Default constructor, default uses ipv4 address
    /// </summary>
    /// <param name="port"></param>
    /// <param name="ssl"></param>
    public TCPServer(ushort port = 27789, X509Certificate2 ssl = null, IPAddress address = null) {

        if (Logging)
            Logger.Write("REGION", "TCP Server Constructor");

        SSL = ssl;
        Port = port;

        if (Logging) {
            Logger.Write("INIT", "Setting Port: " + Port);
            Logger.Write("INIT", "Setting SSL: " + SSL);
        }

        if (address == null) {

            var host = Dns.GetHostEntry(Dns.GetHostName());

            foreach (IPAddress adr in host.AddressList) {
                if (adr.AddressFamily == AddressFamily.InterNetwork) {
                    Address = adr;
                    break;
                }
            }

            if (Address == null) {
                Address = host.AddressList[0];
            }

        } else {
            Address = address;
        }

        if (Logging) {
            Logger.Write("INIT", "Using Address: " + Enum.GetName(typeof(AddressFamily), Address.AddressFamily) + "//" + Address.ToString());
        }

        Socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
        Socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false);
        Socket.Bind(new IPEndPoint(Address, Port));

        Running = false;

    }
    
    /// <summary>
    /// Kicks user
    /// </summary>
    /// <param name="client"></param>
    public void Kick(TCPServerClient client) {

        RemoveClient(client, TCPDisconnectType.Kick);

    }

    /// <summary>
    /// Start the server
    /// </summary>
    /// <returns></returns>
    public bool Start() {

        if (Logging) {
            Logger.Write("REGION", "Method [Start]");
        }

        if ((ListenThread == null || !ListenThread.IsAlive) && !Running) {

            if (Logging) {
                Logger.Write("SUCCESS", "Starting server");
            }

            ListenThread = new Thread(() => Listen());
            ManagementThread = new Thread(Management);
            PingThread = new Thread(Ping);

            Running = true;

            ListenThread.Start();
            ManagementThread.Start();
            PingThread.Start();

            return true;

        }

        if (Logging) {
            Logger.Write("FAILED", "Starting server");
        }

        return false;

    }

    /// <summary>
    /// Stop the server
    /// </summary>
    public void Stop() {

        if (Logging) {
            Logger.Write("REGION", "Method [Stop]");
        }

        if (Logging) {
            Logger.Write("INFO", "Stopping server");
        }

        Running = false;
        
        lock (ClientsList)
        lock(ClientsDict) {

            for(int e = ClientsList.Count - 1; e >= 0; e--) {

                TCPServerClient client = ClientsList[e];

                RemoveClient(client, TCPDisconnectType.Disconnect, e);

            }

        }

        try {

            Socket.Shutdown(SocketShutdown.Both);
            Socket.Close();

        } catch(Exception er) {

        }

        ManagementThread.Join();
        ListenThread.Join();

        ListenThread = new Thread(() => Listen());
        ManagementThread = new Thread(Management);

        Socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
        Socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false);
        Socket.Bind(new IPEndPoint(Address, Port));
        
        if (Logging) {
            Logger.Write("INFO", "Stopped server");
        }

    }

    /// <summary>
    /// Removes a client from the server
    /// </summary>
    /// <param name="client"></param>
    /// <param name="type"></param>
    public void RemoveClient(TCPServerClient client, TCPDisconnectType type = TCPDisconnectType.Disconnect, int pos = -1) {

        if (Logging) {
            Logger.Write("REGION", "Method [RemoveClient]");
        }

        if (type == TCPDisconnectType.NoHandshake) {

            if (Logging) {
                Logger.Write("INFO", "Client no handshake: " + client.UID);
            }
            OnNoHandshake?.Invoke(client);

        } else if (type == TCPDisconnectType.Disconnect) {

            if (Logging) {
                Logger.Write("INFO", "Client disconnect: " + client.UID);
            }
            OnDisconnected?.Invoke(client);

        } else if (type == TCPDisconnectType.Timeout) {

            if (Logging) {
                Logger.Write("INFO", "Client timeout: " + client.UID);
            }
            OnTimeout?.Invoke(client);

        } else if (type == TCPDisconnectType.Kick) {

            if (Logging) {
                Logger.Write("INFO", "Client kick: " + client.UID);
            }
            OnKick?.Invoke(client);

        }

        lock (ClientsDict) ClientsDict.Remove(client.UID);
        lock (ClientsList) {

            if(pos >= 0) {

                ClientsList.RemoveAt(pos);

            } else {

                for (int e = ClientsList.Count - 1; e >= 0; e--) {

                    if (ClientsList[e].UID == client.UID) {
                        if (Logging) {
                            Logger.Write("INFO", "Client found in ClientsList: " + client.UID);
                        }
                        ClientsList.RemoveAt(e);
                        break;
                    }

                }

            }

        }

        try {

            client.Socket.Shutdown(SocketShutdown.Both);
            client.Socket.Close();

        } catch (Exception e) {

            if (Logging) {
                Logger.Write("FAILED", "Socket shutdown/close", e);
            }

        }

    }

    protected void Ping() {

        while(Running && Pinging) {

            Thread.Sleep(PingSleep);

            lock(ClientsList) {
                lock(ClientsDict) {

                     for (int e = ClientsList.Count - 1; e >= 0; e--) {

                        TCPServerClient client = ClientsList[e];

                        try {

                            using (IOStream stream = new IOStream()) {

                                stream.WriteDouble(client.RTT);
                                stream.WriteString(DateTime.UtcNow.ToString("O"));

                                byte[] arr = stream.ToArray();

                                client.Send(new TCPMessage() {
                                    Code = TCPMessageCode.Ping,
                                    Content = arr
                                });

                            }

                        } catch (Exception er) {

                            RemoveClient(client, TCPDisconnectType.Timeout);

                        }

                    }

                }
            }

        }

    }

    /// <summary>
    /// Management to handle automated kicks etc
    /// </summary>
    protected void Management() {

        while (Running) {

            Thread.Sleep(ManagementSleep);

            lock (ClientsList) {
                lock (ClientsDict) {

                    for (int e = ClientsList.Count - 1; e >= 0; e--) {

                        TCPServerClient c = ClientsList[e];

                        if ((DateTime.Now - c.Joined) > HandshakeTimeout
                            && RequireHandshake && !c.DoneHandshake) {

                            RemoveClient(c, TCPDisconnectType.NoHandshake);

                        }

                    }
                }
            }

        }

    }

    /// <summary>
    /// Listen for new connections
    /// </summary>
    protected void Listen() {

        if (Logging) {
            Logger.Write("REGION", "Method [Listen]");
        }

        Socket.Listen(Backlog);

        if (Logging) {
            Logger.Write("INFO", "Start listening for clients");
        }

        while (Running) {

            Socket socket = Socket.Accept();

            if (Logging) {
                Logger.Write("INFO", "New socket connected");
            }

            TCPServerClient client = new TCPServerClient(
                socket, RandomGen.GenRandomUID(ClientsDict, UIDLength));

            client.Joined = DateTime.Now;

            Thread clientThread = new Thread(() => ListenClient(client));
            client.Thread = clientThread;

            clientThread.Start();

            if (Logging) {
                Logger.Write("INFO", "Created client and started thread");
            }

        }

    }

    /// <summary>
    /// Listen for new messages of individual clients
    /// </summary>
    /// <param name="client"></param>
    protected void ListenClient(TCPServerClient client) {

        if (Logging) {
            Logger.Write("REGION", "Method [ListenClient]");
        }

        using (Stream ns = GetStream(client)) {

            client.Stream = ns;

            client.Writer = new TCPWriter(ns);
            client.Reader = new TCPReader(ns);

            if (Logging) {
                Logger.Write("INFO", "Created stream, writer and reader for client: " + client.UID);
            }

            lock (ClientsList) ClientsList.Add(client);
            lock(ClientsDict) ClientsDict.Add(client.UID, client);
            OnConnected?.Invoke(client);

            if (RequireHandshake) {

                TCPMessage message = client.Reader.Read(client);

                if (message == null || message.Code != TCPMessageCode.Init
                    || message.Content.Length > 10) {
                    RemoveClient(client, TCPDisconnectType.NoHandshake);
                    return;
                }

                if (Logging) {
                    Logger.Write("SUCCESS", "Handshake: " + client.UID);
                }

                client.DoneHandshake = true;

                client.Send(new TCPMessage() {
                    Code = TCPMessageCode.Init,
                    Content = new byte[] { 0, 1, 0 }
                });

                OnHandshake?.Invoke(client);

            }

            while (Running && ClientsDict.ContainsKey(client.UID)) {

                TCPMessage message = client.Reader.Read(client);

                if(message == null) {
                    RemoveClient(client, TCPDisconnectType.Timeout);
                    return;
                }

                if (Logging) {
                    Logger.Write("INFO", "New message " + Enum.GetName(typeof(TCPMessageCode), message.Code) + " from user: " + client.UID);
                }

                if (message.Code == TCPMessageCode.Close) {
                    RemoveClient(client, TCPDisconnectType.Disconnect);
                }

                if(message.Code == TCPMessageCode.Pong) {
                    HandlePong(message);
                    continue;
                }

                if(message.Code == TCPMessageCode.Message)
                    OnMessage?.Invoke(client, message);

            }

        }

    }

    /// <summary>
    /// Handle pong and rtt
    /// </summary>
    /// <param name="message"></param>
    protected void HandlePong(TCPMessage message) {

        try {

            string strDate = Encoding.UTF8.GetString(message.Content);
            DateTime time = DateTime.Parse(strDate, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);

            message.Client.RTT = ((DateTime.UtcNow.Ticks - time.Ticks) / 10000);

        } catch(Exception er) {

            if (Logging) {
                Logger.Write("FAILED", "Socket RTT failed", er);
            }

        }

    }

    /// <summary>
    /// Get appropiate stream of socket
    /// </summary>
    /// <param name="client"></param>
    /// <returns></returns>
    protected Stream GetStream(TCPServerClient client) {

        Stream stream = new NetworkStream(client.Socket);

        if (SSL == null) {

            return stream;

        }

        try {

            SslStream sslStream = new SslStream(stream, false);
            var task = sslStream.AuthenticateAsServerAsync(SSL, false, SSLProtocol, true);
            task.Start();
            task.Wait();

            return sslStream;

        } catch (Exception e) {

            return null;

        }

    }

}

TCPClient.cs

/// <summary>
/// TCPClient used to conenct to and communicate with tcp server
/// </summary>
public class TCPClient : TCPBase {

    /// <summary>
    /// Is logging enabled
    /// </summary>
    public bool Logging { get; set; } = true;

    /// <summary>
    /// Time until next reconnect try in ms
    /// </summary>
    public int ReconnectSleep { get; set; } = 2000;

    /// <summary>
    /// Whether or not a handshake is required
    /// </summary>
    public bool RequireHandshake { get; set; } = true;

    /// <summary>
    /// Enabled raw communcation, only use if you know what you are doing
    /// </summary>
    public bool RawSocket { get; set; } = false;

    /// <summary>
    /// Stream of the socket
    /// </summary>
    public Stream Stream { get; private set; }

    /// <summary>
    /// Writer to the stream
    /// </summary>
    public TCPWriter Writer { get; private set; }

    /// <summary>
    /// Reader to the stream
    /// </summary>
    public TCPReader Reader { get; private set; }

    /// <summary>
    /// RTT of connection
    /// </summary>
    public double RTT { get; private set; }

    /// <summary>
    /// Message Event Handler
    /// </summary>
    /// <param name="message"></param>
    public delegate void MessageEventHandler(TCPMessage message);

    /// <summary>
    /// Message Event, called if the server sent a message
    /// </summary>
    public event MessageEventHandler OnMessage;

    /// <summary>
    /// Connected Event Handler
    /// </summary>
    /// <param name="client"></param>
    public delegate void ConnectedEventHandler();

    /// <summary>
    /// Connected Event, called if the client connected to the server
    /// </summary>
    public event ConnectedEventHandler OnConnected;

    /// <summary>
    /// Handshake Event Handler
    /// </summary>
    /// <param name="client"></param>
    public delegate void HandshakeEventHandler();

    /// <summary>
    /// Handshake Event, called if the client successfully done the handshake
    /// </summary>
    public event HandshakeEventHandler OnHandshake;

    /// <summary>
    /// Disconnected Event Handler
    /// </summary>
    public delegate void DisconnectedEventHandler();

    /// <summary>
    /// Disconnected Event, called if client was disconnected 
    /// </summary>
    public event DisconnectedEventHandler OnDisconnected;

    /// <summary>
    /// Default constructor
    /// </summary>
    /// <param name="address"></param>
    /// <param name="port"></param>
    /// <param name="ssl"></param>
    public TCPClient(string address = "localhost", ushort port = 27789, bool logging = true) {

        Logging = logging;

        if(Logging) {
            Logger.Write("REGION", "TCP Client Constructor");
        }

        IPAddress adr = null;

        if(!IPAddress.TryParse(address, out adr)) {
            throw new Exception("IPAddress not recognizable");
        }

        Address = adr;
        AddressString = address;
        Port = port;

        if (Logging) {
            Logger.Write("INIT", "Setting Port: " + Port);
            Logger.Write("INIT", "Setting SSL: " + SSL);
            Logger.Write("INIT", "Using Address: " + Enum.GetName(typeof(AddressFamily), Address.AddressFamily) + "//" + Address.ToString());
        }

        Running = false;
        InitHandlers();

    }

    /// <summary>
    /// Sends a message to the server
    /// </summary>
    /// <param name="message"></param>
    public void Send(TCPMessage message) {

        Writer.Write(message);

    }

    /// <summary>
    /// Listen for incomming messages
    /// </summary>
    protected void Listen() {

        using(Stream = GetStream()) {

            Writer = new TCPWriter(Stream);
            Reader = new TCPReader(Stream);

            if (Logging) {
                Logger.Write("SUCCESS", "Connected to the server");
            }

            OnConnected?.Invoke();

            if (RequireHandshake) {

                byte[] rand = new byte[10];
                RandomGen.Random.NextBytes(rand);

                TCPMessage init = new TCPMessage() {
                    Code = TCPMessageCode.Init,
                    Content = rand
                };

                if (Logging) {
                    Logger.Write("INFO", "Sending handshake");
                }

                Send(init);

            }

            while (Running) {

                TCPMessage message = Reader.Read(Socket);

                if(message == null) {
                    Running = false;
                    OnDisconnected?.Invoke();
                    continue;
                }

                if (message.Code == TCPMessageCode.Init) {
                    if (Logging) {
                        Logger.Write("SUCCESS", "Successful handshake");
                    }
                    OnHandshake?.Invoke();
                } else if (message.Code == TCPMessageCode.Ping) {

                    OnPingMessage(message);

                } else if (message.Code == TCPMessageCode.Message) {

                    OnMessage?.Invoke(message);

                }

            }

        }
    }

    protected void OnPingMessage(TCPMessage message) {

        using (IOStream stream = new IOStream(message.Content)) {

            double rtt = 0;
            bool error = stream.ReadDouble(out rtt);

            string dateStr = null;
            error = stream.ReadString(out dateStr);

            RTT = rtt;

            try {

                if(error) {
                    return;
                }

                DateTime sent = DateTime.Parse(dateStr, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);

                Send(new TCPMessage() {
                    Code = TCPMessageCode.Pong,
                    Content = Encoding.UTF8.GetBytes(sent.ToString("O"))
                });

            } catch(Exception er) {
                Console.WriteLine("Error");
            }

        }

    }

    /// <summary>
    /// Initialize the handlers
    /// </summary>
    protected void InitHandlers() {

        OnMessage += (message) => {
            
        };

    }

    /// <summary>
    /// Get appropiate stream of socket
    /// </summary>
    /// <param name="client"></param>
    /// <returns></returns>
    protected Stream GetStream() {

        Stream stream = new NetworkStream(Socket);

        if (SSL == null) {

            return stream;

        }

        try {

            SslStream sslStream = new SslStream(stream, false);
            sslStream.AuthenticateAsClient(Address.ToString());

            return sslStream;

        } catch (Exception e) {

            return null;

        }

    }

    /// <summary>
    /// Init new Socket instance
    /// </summary>
    protected void InitSocket() {

        Socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
        Socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false);

    }

    /// <summary>
    /// Disconnectes from Server
    /// </summary>
    public void Disconnect() {

        if(Running) {

            Send(new TCPMessage() {
                Code = TCPMessageCode.Close,
                Content = new byte[2] { 0, 1 }
            });

            try {

                Socket.Shutdown(SocketShutdown.Both);
                Socket.Disconnect(true);

            } catch(Exception er) {

            }

        }

    }

    /// <summary>
    /// Connect to server
    /// </summary>
    public void Connect() {

        if (Logging) {
            Logger.Write("REGION", "Method [Connect]");
        }

        bool connected = false;

        while(!connected) {

            if(Logging) {
                Logger.Write("INFO", "Trying to connect...");
            }

            InitSocket();

            try {

                Socket.Connect(new IPEndPoint(Address, Port));
                
                Running = connected = true;

                ListenThread = new Thread(Listen);
                ListenThread.Start();

            } catch (Exception e) {

                if (Logging) {
                    Logger.Write("FAILED", "Failed to connect");
                }

                Running = connected = false;

                if (Logging) {
                    Logger.Write("INFO", "Sleep for " + ReconnectSleep + "ms");
                }

                Thread.Sleep(ReconnectSleep);

            }

        }

    }

}

Exemplo de uso

class Program {

    static void Main(string[] args) {

        new Thread(() => {

            Random rand = new Random();

            TCPServer server = new TCPServer(54545, null, IPAddress.Any);
            server.Start();

            server.OnMessage += (cl, mess) => {

                using (IOStream stream = new IOStream(mess.Content)) {

                    float fl;
                    stream.ReadFloat(out fl);

                    Console.WriteLine("Received from Client: " + fl);

                }

                using (IOStream stream = new IOStream()) {

                    float numb = (float)rand.NextDouble() * 999;
                    Console.WriteLine("Server sending number: " + numb);

                    stream.WriteFloat(numb);

                    cl.Send(new TCPMessage() {
                        Content = stream.ToArray()
                    });

                }

            };

        }).Start();

        new Thread(() => {

            Random rand = new Random();

            TCPClient client = new TCPClient("127.0.0.1", 54545);
            client.Connect();

            client.OnHandshake += () => {

                using (IOStream stream = new IOStream()) {

                    float numb = (float)rand.NextDouble() * 999;
                    Console.WriteLine("Client sending number: " + numb);

                    stream.WriteFloat(numb);

                    client.Send(new TCPMessage() {
                        Content = stream.ToArray()
                    });

                }

            };

            client.OnMessage += (mes) => {

                using (IOStream stream = new IOStream(mes.Content)) {

                    float fl;
                    stream.ReadFloat(out fl);

                    Console.WriteLine("Received from Server: " + fl);

                }

                using (IOStream stream = new IOStream()) {

                    float numb = (float)rand.NextDouble() * 999;
                    Console.WriteLine("Client sending number: " + numb);

                    stream.WriteFloat(numb);

                    client.Send(new TCPMessage() {
                        Content = stream.ToArray()
                    });

                }

            };

        }).Start();

    }
}

Respostas

1 CharlesNRice Sep 24 2020 at 20:30

Propriedades

Parece que há muitas propriedades públicas e, se elas precisarem ser públicas, use a interface somente leitura. Por exemplo, no tcpserver existe uma propriedade chamada ClientsList que é uma lista. Parece anormal que uma classe externa possa limpar a lista ou adicionar / remover clientes da lista. Se a propriedade precisa ser pública, retornar IReadOnlyList ou IEnumerable seria uma maneira melhor e mais protetora. O mesmo com ClientsDict que parece ser um funcionamento interno e não deve ser uma propriedade pública e, mais provavelmente, um campo somente leitura privado.

Eventos

Em vez de fazer delegados, deve seguir o padrão de evento padrão de usar EventHandler <> e passar EventArgs. Embora você precise criar mais classes EventArgs, será mais fácil para outras pessoas que entram em seu projeto ou que desejam adicionar outras bibliotecas.

Exceções

GetStream está apenas comendo exceções e retornando null, mas o código não parece estar procurando especificamente por null sendo ruim. Além disso, deve capturar apenas exceções que você pode manipular, e não a classe de exceção genérica.

Exploração madeireira

Eu recomendaria usar o Microsoft.Extensions.Logging.Abstractions para registro. Muitos aplicativos de registro já se adaptam à interface MS ILogger. Em seguida, pode registrar diferentes tipos. Por exemplo, ao capturar uma exceção, chame o logger.LogError com a exceção. Em seguida, ter outro registro para depuração ou informações. Em seguida, pode configurar o nível que você deseja registrar. Por exemplo, no modo dev provavelmente deseja registrar a depuração ou superior, mas quando for liberar o aviso ou superior.

TPL

MS tem um Async Socket Server e Client esses exemplos estão usando o antigo APM e agora podem usar o TPL. Por exemplo, agora pode apenas fazer em var client = await socket.AcceptAsync()vez das chamadas BeginAccept e EndAccept.

Para fazer ping em um cronômetro, eu usaria Task.Delay ou System.Threading.Timer. Quanto mais você puder transferir para o TPL, melhor.