I've been building a simple C# server-client chat-style app as a test of my C#. I've picked up code from a few tutorials, and extended what's there to come up with my own server spec.
In this post (the second will be the client), I'd like to get some feedback on the server. To me, the code seems bulky and as if it could be brought down by some judicious use of functions or a utility class (I've spotted a doubled-up function (SendToClient
) that I guess I could just make public
to save on LOC, but what else is there?)
1 - Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MessengerServer
{
class Program
{
static void Main(string[] args)
{
int port = 1100;
if (args.Length == 2 && args[0] == "--port")
{
try
{
port = Int32.Parse(args[1]);
}
catch (Exception e)
{
Output.Log("Not a valid port number. Defaulting to 1100.", LogType.Error);
}
}
Output.Log("Starting server on port " + port, LogType.Info);
new Server(port);
}
}
}
2 - Server.cs
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text;
namespace MessengerServer
{
class Server
{
private TcpListener tcpListener;
private Thread listenThread;
private ASCIIEncoding encoder = new ASCIIEncoding();
public Dictionary<DateTime, string> Messages = new Dictionary<DateTime, string>();
private List<string> MessagesAfter(string time)
{
List<string> matches = new List<string>();
DateTime sinceTime = DateTime.Parse(time);
foreach (KeyValuePair<DateTime, string> pair in Messages)
{
if (pair.Key.CompareTo(sinceTime) > 0)
{
matches.Add(pair.Value);
}
}
return matches;
}
private List<TcpClient> connectedClients = new List<TcpClient>();
public Server(int port)
{
Output.Log("Starting server: all network interfaces, port " + port, LogType.Info);
this.tcpListener = new TcpListener(IPAddress.Any, port);
this.listenThread = new Thread(new ThreadStart(ListenForClients));
this.listenThread.Start();
}
private void ListenForClients()
{
Output.Log("Listener thread spawned", LogType.Info);
this.tcpListener.Start();
Output.Log("TCP listener started, ready to accept clients.", LogType.Info);
while (true)
{
TcpClient client = this.tcpListener.AcceptTcpClient();
Output.Log("Client connected, starting thread...", LogType.Info);
Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClientComms));
clientThread.Start(client);
connectedClients.Add((TcpClient) client);
}
}
private void HandleClientComms(object client)
{
int clientId = new Random().Next(0, int.MaxValue - 1);
TcpClient tcpClient = (TcpClient) client;
NetworkStream clientStream = tcpClient.GetStream();
Output.Log("Communication thread started with client at " + tcpClient.Client.RemoteEndPoint.ToString(), LogType.Info);
Output.Log("Client identifier is " + clientId, LogType.Info);
SendToClient(tcpClient, clientId.ToString());
byte[] message = new byte[4096];
int bytesRead;
string data = "";
while (true)
{
bytesRead = 0;
data = "";
try
{
bytesRead = clientStream.Read(message, 0, 4096);
Output.Log("Read " + bytesRead + " bytes from " + clientId, LogType.Info);
}
catch(Exception e)
{
Output.Log("Could not read from client: " + e.Message, LogType.Error);
if (e.GetType() == Type.GetType("System.IO.IOException"))
{
connectedClients.Remove(tcpClient);
Thread.CurrentThread.Abort();
}
break;
}
if (bytesRead == 0)
{
Output.Log("Client " + clientId + " disconnected", LogType.Info);
connectedClients.Remove(tcpClient);
Thread.CurrentThread.Abort();
break;
}
string received = encoder.GetString(message, 0, bytesRead);
data += received;
Output.Log("Message: " + received, LogType.Info);
try
{
HandleMessage(tcpClient, clientId, data);
}
catch (ThreadAbortException tae)
{
Output.Log("Client thread disconnect complete: " + tae.Message, LogType.Info);
}
catch (Exception e)
{
Output.Log("Could not handle message: " + e.Message, LogType.Warn);
}
}
}
public void SendToClient(TcpClient client, string message)
{
try
{
NetworkStream stream = client.GetStream();
byte[] msg = new byte[message.Length];
msg = encoder.GetBytes(message);
stream.Write(msg, 0, message.Length);
stream.Flush();
Output.Log("Sent to client: " + message, LogType.Info);
}
catch (Exception e)
{
Output.Log("Unexpected exception sending: " + e.Message, LogType.Error);
}
}
private void HandleMessage(TcpClient client, int clientId, string message)
{
Output.Log("Attempting to handle message " + message, LogType.Info);
if (message.StartsWith("[AllSince]"))
{
try
{
string date = message.Split(']')[1];
List<string> messages = MessagesAfter(date);
string data = "";
foreach (string msg in messages)
{
data += msg + "|&|";
}
SendToClient(client, data);
}
catch (IndexOutOfRangeException e)
{
SendToClient(client, "[200]");
throw new Exception("No date was found for [AllSince]: " + e.Message);
}
catch (FormatException e)
{
SendToClient(client, "[201]");
throw new Exception("Date was not formatted correctly: " + e.Message);
}
catch (Exception e)
{
SendToClient(client, "[100]");
throw new Exception("Unexpected exception: " + e.Message);
}
}
else if (message.StartsWith("[Send]"))
{
try
{
string text = message.Split(']')[1];
Messages.Add(DateTime.Now, "<" + clientId + ">" + text);
Output.Log("Added to message list: " + text, LogType.Info);
NotifyAllClients("<" + clientId + ">" + text);
SendToClient(client, "[600]");
}
catch (Exception e)
{
SendToClient(client, "[300]");
throw new Exception("No message could be found for [Send]: " + e.Message);
}
}
else if (message.StartsWith("[Disconnect]"))
{
Output.Log("Client " + clientId + "'s client thread disconnected", LogType.Warn);
connectedClients.Remove(client);
Thread.CurrentThread.Abort();
}
else if (message.StartsWith("[Command]"))
{
string command = message.Substring(9);
Commands.HandleCommand(client, command);
}
else
{
SendToClient(client, "[400]");
throw new Exception("No handling protocol specified.");
}
}
public void NotifyAllClients(string message)
{
Output.Log("Notifying all clients of message " + message, LogType.Info);
foreach (TcpClient client in connectedClients)
{
Output.Log("Notify: client " + client.Client.RemoteEndPoint.ToString(), LogType.Info);
SendToClient(client, "[Message]" + message);
}
}
}
}
3 - Commands.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace MessengerServer
{
class Commands
{
public static void HandleCommand(TcpClient client, string command)
{
string[] args = command.Split(' ');
switch (args[0].ToLower().Trim())
{
case "force":
if (args.Length == 2)
{
SendToClient(client, "[" + args[1] + "]");
}
else
{
SendToClient(client, "[CommandInvalid]");
}
break;
default:
SendToClient(client, "[CommandInvalid]");
break;
}
}
public static void SendToClient(TcpClient client, string message)
{
try
{
NetworkStream stream = client.GetStream();
byte[] msg = new byte[message.Length];
msg = Encoding.ASCII.GetBytes(message);
stream.Write(msg, 0, message.Length);
stream.Flush();
Output.Log("Sent to client: " + message, LogType.Info);
}
catch (Exception e)
{
Output.Log("Unexpected exception sending: " + e.Message, LogType.Error);
}
}
}
}
The other class, Output
(in Output.cs
) is one I'm happy with - I use it widely and it's pretty solid, so I've decided not to put it up for review.
Note: I've also got XML documentation comments in my code, but have excluded them here for succinctness.