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?