A while ago, I wanted to make a multiplayer game, so I wrote some TCPClient and TCPListener code, and it worked, but it was messy. I looked around for a way to use the TCPClient with events, but I couldn't find any good APIs for it. So I made my own.
This library successfully turns TCP networking (sending messages to a server, server responding, etc.) into an event-based system. You can attach the MessageReceived event from either Client or Server, making it much easier to make programs using this, along with a variety of other options and events on each class.
Within the GitHub page, there's the solution for it, and within that, 3 projects. The main DLL project, and a test client and test server showing example usage.
This also has a ResponseEvent system, where you can add a ResponseEvent to the server, so that when a message has certain text, you can trigger an action or method.
Client
public class Client
{
    TcpClient _client;
    NetworkStream _stream => _client.GetStream();
    Thread _listenThread;
    /// <summary>
    /// Whether the Client has been disconnected and disposed or not.
    /// </summary>
    public bool IsDisposed { get; private set; }
    /// <summary>
    /// When a message is received from the server this client is connected to.
    /// </summary>
    public event EventHandler<MessageReceivedEventArgs> MessageReceived;
    /// <summary>
    /// When the server stops, disposing the client automatically.
    /// </summary>
    public event EventHandler ServerStopped;
    /// <summary>
    /// When the client is disposed.
    /// </summary>
    public event EventHandler Disposed;
    /// <summary>
    /// When the client has begun listening for messages from the server.
    /// </summary>
    public event EventHandler StartedListening;
    /// <summary>
    /// If this is the client end creating the connected, this is false.
    /// If this is the client the sever creates on the server end after a client has connected, this is true.
    /// </summary>
    public bool IsServerClient { get; }
    /// <summary>
    /// If the instantiation, and inheritly the connection, failed.
    /// </summary>
    public bool FailedConnect { get; }
    /// <summary>
    /// The tag attached to this object.
    /// </summary>
    public object Tag { get; set; }
    /// <summary>
    /// The endpoint of the client. If <see cref="IsServerClient"/>, this returns the originating client's IP endpoint. 
    /// If not true, returns the address of the server.
    /// </summary>
    public string ConnectAddress => IPAddress.Parse(((IPEndPoint)_client.Client.RemoteEndPoint).Address.ToString()).ToString();
    /// <summary>
    /// The port this client is connected to the server on.
    /// </summary>
    public int Port { get; set; }
    /// <summary>
    /// If it's server-side.
    /// </summary>
    /// <param name="client"></param>
    public Client(TcpClient client)
    {
        _client = client;
        IsServerClient = true;
        Port = ((IPEndPoint)client.Client.RemoteEndPoint).Port;
        StartListening();
    }
    /// <summary>
    /// If it's client side.
    /// </summary>
    /// <param name="address"></param>
    /// <param name="port"></param>
    public Client(string address, int port)
    {
        try
        {
            Port = port;
            _client = new TcpClient(address, port);
            StartListening();
        }
        catch
        {
            FailedConnect = true;
        }
    }
    /// <summary>
    /// Starts the client listening for messages.
    /// </summary>
    private void StartListening()
    {
        _listenThread = new Thread(ListenForMessages);
        _listenThread.Start();
        StartedListening?.Invoke(this, null);
    }
    /// <summary>
    /// Sends a message to the endpoint of this client.
    /// </summary>
    /// <param name="content"></param>
    public void SendMessage(object content)
    {
        var outContent = content.ToString()
            .Replace(TcpOptions.EndConnectionCode.ToString(), "")
            .Replace(TcpOptions.EndMessageCode.ToString(), "");
        outContent += TcpOptions.EndMessageCode.ToString();
        var data = outContent.GetBytes();
        _stream.Write(data, 0, data.Length);
    }
    /// <summary>
    /// Sends a message to the endpoint of this client, not replacing a <see cref="TcpOptions.EndConnectionCode"/>.
    /// </summary>
    /// <param name="content"></param>
    /// <param name="a"></param>
    private void SendMessage(object content, bool a)
    {
        var outContent = content.ToString()
            .Replace(TcpOptions.EndMessageCode.ToString(), "");
        outContent += TcpOptions.EndMessageCode.ToString();
        var data = outContent.GetBytes();
        _stream.Write(data, 0, data.Length);
    }
    /// <summary>
    /// The thread method where the client listens for new messages and handles them accordingly.
    /// </summary>
    private void ListenForMessages()
    {
        var bytes = new List<byte>();
        while (!IsDisposed)
        {
            var i = -1;
            try
            {
                i = _stream.ReadByte();
            }
            catch (SocketException e)
            {
                if (e.SocketErrorCode == SocketError.Interrupted)
                {
                    break;
                }
            }
            catch
            {
                break;
            }
            if (i == -1)
            {
                break;
            }
            else if (i == TcpOptions.EndMessageCode)
            {
                if (bytes.Count > 0)
                {
                    var message = bytes.ToArray().GetString();
                    var eventargs = new MessageReceivedEventArgs
                    {
                        Message = message,
                        Time = DateTime.Now,
                        Client = this
                    };
                    MessageReceived?.Invoke(this, eventargs);
                    bytes.Clear();
                }
            }
            else if (i == TcpOptions.EndConnectionCode && !IsServerClient)
            {
                ServerStopped?.Invoke(this, null);
                Dispose(true);
                break;
            }
            else
            {
                bytes.Add(Convert.ToByte(i));
            }
        }
    }
    /// <summary>
    /// Stops the client from listening, sending an end connection code to the server, and disposing.
    /// </summary>
    /// <param name="fromServer"></param>
    public void Dispose(bool fromServer)
    {
        if (!IsDisposed)
        {
            IsDisposed = true;
            if (!fromServer)
            {
                SendMessage(TcpOptions.EndConnectionCode.ToString(), true);
            }
            _client.Close();
            _client.Dispose();
            _listenThread.Abort();
            Disposed?.Invoke(this, null);
        }
    }
}
Server
public class Server
{
    TcpListener _listener;
    Thread _clientListenerThread;
    /// <summary>
    /// A list of all the clients connected to this server.
    /// </summary>
    public List<Client> ConnectedClients { get; set; }
    /// <summary>
    /// A list of all the responses set up on this server.
    /// </summary>
    public List<ResponseEvent> Responses { get; set; }
    /// <summary>
    /// The address the server is listening on.
    /// </summary>
    public string Address { get; }
    /// <summary>
    /// The port the server is listening on.
    /// </summary>
    public int Port { get; }
    /// <summary>
    /// Whether the server has been stopped and disposed.
    /// </summary>
    public bool IsDisposed { get; private set; }
    /// <summary>
    /// Occurs when a client connects to the server.
    /// </summary>
    public event EventHandler<ClientToggleEventArgs> ClientConnected;
    /// <summary>
    /// Occurs when a client disconnected from the server.
    /// </summary>
    public event EventHandler<ClientToggleEventArgs> ClientDisconnected;
    /// <summary>
    /// Occurs when any client sends a message to the server.
    /// </summary>
    public event EventHandler<MessageReceivedEventArgs> MessageReceived;
    /// <summary>
    /// Occurs when the server is disconnected and disposed.
    /// </summary>
    public event EventHandler Disposed;
    /// <summary>
    /// Whether the client is listening or not.
    /// </summary>
    public bool HasStartedListening { get; private set; }
    /// <summary>
    /// If it's listening and has not been disposed.
    /// </summary>
    public bool IsReady => HasStartedListening && !IsDisposed;
    /// <summary>
    /// The tag attached to this object.
    /// </summary>
    public object Tag { get; set; }
    /// <summary>
    /// Constructor to instantiate and start the server for listening.
    /// </summary>
    /// <param name="address">The IP address the listen on.</param>
    /// <param name="port">The port to listen on.</param>
    public Server(string address, int port)
    {
        _listener = new TcpListener(IPAddress.Parse(address), port);
        _listener.Start();
        Address = address;
        Port = port;
        StartClientListening();
    }
    /// <summary>
    /// Starts the listening thread for the server. After this, the server has begun listening for connected from clients. 
    /// Private so that users do not call more than once.
    /// </summary>
    private void StartClientListening()
    {
        ConnectedClients = new List<Client>();
        Responses = new List<ResponseEvent>();
        _clientListenerThread = new Thread(ListenForClients);
        _clientListenerThread.Start();
        HasStartedListening = true;
    }
    /// <summary>
    /// The threaded method where the server listens for client connections. Only called from <see cref="StartClientListening"/>
    /// </summary>
    private void ListenForClients()
    {
        while (!IsDisposed)
        {
            try
            {
                var connectedTCPClient = _listener.AcceptTcpClient();
                var connectedClient = new Client(connectedTCPClient);
                connectedClient.MessageReceived += ConnectedClient_MessageReceived;
                ConnectedClients.Add(connectedClient);
                var eventargs = new ClientToggleEventArgs
                {
                    ConnectedClient = connectedClient,
                    Time = DateTime.Now
                };
                ClientConnected?.Invoke(this, eventargs);
            }
            catch (SocketException e)
            {
                if (e.SocketErrorCode == SocketError.Interrupted)
                {
                    break;
                }
                else
                {
                    throw e;
                }
            }
        }
    }
    /// <summary>
    /// This is the event handler attached to every client that is connected's MessageReceive event.
    /// This is where it checks if a client has sent the disconnetion code, and if so, disposes of them.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void ConnectedClient_MessageReceived(object sender, MessageReceivedEventArgs e)
    {
        if (e.Message == TcpOptions.EndConnectionCode.ToString())
        {
            ConnectedClients.Remove(sender as Client);
            var eventargs = new ClientToggleEventArgs
            {
                ConnectedClient = sender as Client,
                Time = DateTime.Now
            };
            ClientDisconnected?.Invoke(this, eventargs);
        }
        else
        {
            foreach (var response in Responses)
            {
                var willTrigger = false;
                switch (response.Mode)
                {
                    case ContentMode.Contains:
                        if (e.Message.Contains(response.Content))
                        {
                            willTrigger = true;
                        }
                        break;
                    case ContentMode.EndsWish:
                        if (e.Message.EndsWith(response.Content))
                        {
                            willTrigger = true;
                        }
                        break;
                    case ContentMode.StartsWith:
                        if (e.Message.StartsWith(response.Content))
                        {
                            willTrigger = true;
                        }
                        break;
                    case ContentMode.Equals:
                        if (e.Message == response.Content)
                        {
                            willTrigger = true;
                        }
                        break;
                }
                if (willTrigger)
                {
                    response.Event?.Invoke(e);
                }
                else
                {
                    MessageReceived?.Invoke(sender, e);
                }
            }
        }
    }
    /// <summary>
    /// This disposes the server, also stopping the listening thread, and sending an
    /// <see cref="TcpOptions.EndConnectionCode"/> to every client connected.
    /// </summary>
    public void Dispose()
    {
        if (!IsDisposed)
        {
            IsDisposed = true;
            foreach (var client in ConnectedClients)
            {
                client.SendMessage(TcpOptions.EndConnectionCode);
                client.Dispose(false);
            }
            ConnectedClients = null;
            _listener.Stop();
            Disposed?.Invoke(this, null);
        }
    }
    /// <summary>
    /// Returns this machine's intranetwork IPv4 address. 
    /// Throws an exception if there are no connected network adapters on the system.
    /// </summary>
    /// <returns>The IPv4 address of this machine.</returns>
    public static string GetLocalIPAddress()
    {
        var host = Dns.GetHostEntry(Dns.GetHostName());
        foreach (var ip in host.AddressList)
        {
            if (ip.AddressFamily == AddressFamily.InterNetwork)
            {
                return ip.ToString();
            }
        }
        throw new Exception("No network adapters with an IPv4 address in the system!");
    }
}
Other
public class ResponseEvent
{
    public string Content { get; set; }
    public ContentMode Mode { get; set; }
    public Action<MessageReceivedEventArgs> Event { get; set; }
}
public enum ContentMode
{
    Contains,
    StartsWith,
    EndsWish,
    Equals,
}
Example Usage Server-Side
public class Program
{
    static Server server;
    static void Main(string[] args)
    {
        Console.WriteLine("Initializing Server...");
        server = new Server(Server.GetLocalIPAddress(), 13001);
        server.ClientConnected += Server_ClientConnected;
        server.MessageReceived += Server_MessageReceived;
        server.ClientDisconnected += Server_ClientDisconnected;
        var rickroll = new ResponseEvent()
        {
            Content = "never gunna give you up",
            Mode = ContentMode.Contains,
            Event = Rickroll,
        };
        server.Responses.Add(rickroll);
        Console.WriteLine("Server started.");
        Console.WriteLine("Listing on IP address: " + server.Address);
        Console.WriteLine("On port: " + server.Port);
        while (!server.IsDisposed)
        {
            Console.Write("> ");
            var input = Console.ReadLine();
            switch (input)
            {
                case "listclients":
                    Console.WriteLine(server.ConnectedClients.Count + " Client(s) Connected\n-----------------------");
                    foreach (var client in server.ConnectedClients)
                    {
                        Console.WriteLine(client.ConnectAddress);
                    }
                    Console.WriteLine("-----------------------");
                    break;
                case "stop":
                    Console.WriteLine("Disposing Server...");
                    server.Dispose();
                    Console.WriteLine("Server closed. Press any key to exit.");
                    Console.Read();
                    break;
                default:
                    Console.WriteLine("Invalid Command: " + input);
                    break;
            }
            Console.WriteLine();
        }
    }
    public static void Rickroll(MessageReceivedEventArgs e)
    {
        e.Client.SendMessage("never gunna let you down");
    }
    private static void Server_ClientDisconnected(object sender, ClientToggleEventArgs e)
    {
        Console.WriteLine("Client Disconnected: " + e.ConnectedClient.ConnectAddress);
    }
    private static void Server_MessageReceived(object sender, MessageReceivedEventArgs e)
    {
        Console.WriteLine("Received Message: " + e.Client.ConnectAddress + " : " + e.Message);
        var toRespond = Reverse(e.Message);
        Console.WriteLine("Returning Message: " + toRespond);
        e.Client.SendMessage(toRespond);
    }
    public static string Reverse(string s)
    {
        var charArray = s.ToCharArray();
        Array.Reverse(charArray);
        return new string(charArray);
    }
    private static void Server_ClientConnected(object sender, ClientToggleEventArgs e)
    {
        Console.WriteLine("Client Connected: " + e.ConnectedClient.ConnectAddress);
    }
}
Example Usage Client-Side
public class Program
{
    static Client client;
    static void Main(string[] args)
    {
        failedToConnect:;
        Console.Write("Enter the IP address to connect to:\n> ");
        var ip = Console.ReadLine();
        invalidPort:;
        Console.Write("Enter the port to connect to:\n> ");
        var portString = Console.ReadLine();
        Console.WriteLine();
        if (int.TryParse(portString, out var port))
        {
            try
            {
                client = new Client(ip, port);
                if (client.FailedConnect)
                {
                    Console.WriteLine("Failed to connect!");
                    goto failedToConnect;
                }
                client.MessageReceived += Client_MessageReceived;
                Console.WriteLine("Client connected.");
                while (!client.IsDisposed)
                {
                    Console.Write("> ");
                    var input = Console.ReadLine();
                    if (client.IsDisposed)
                    {
                        Console.WriteLine("The server closed. This client has disposed. Press any key to close...");
                        Console.ReadLine();
                    }
                    else
                    {
                        if (input.ToLower().StartsWith("send "))
                        {
                            var toSend = input.Substring(5, input.Length - 5);
                            Console.WriteLine("Sending message: \n     " + toSend + "\n");
                            client.SendMessage(toSend);
                            Console.WriteLine("Sent message.");
                        }
                        else if (input.ToLower() == "stop")
                        {
                            Console.WriteLine("Disconnecting...");
                            client.Dispose(false);
                            Console.WriteLine("Disconnected. Press any key to continue.");
                            Console.ReadLine();
                        }
                    }
                    Console.WriteLine();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("Error: " + e.Message);
                Console.ReadLine();
            }
        }
        else
        {
            Console.WriteLine("Invalid Port. ");
            goto invalidPort;
        }
    }
    private static void Client_MessageReceived(object sender, MessageReceivedEventArgs e)
    {
        Console.WriteLine("Received message from server: " + e.Message);
    }
}
My questions:
- Are event-based TCP protocols usable or feasible in real-world applications?
- Is this the best way I could have done this?
- Is there any code that should be added? Removed? Changed?
