Creating a chat room

tcpdemopic

Learning network programming has been on my backburner for quite a while now, so I decided that it was about time I got around to it.

Way back when I made a program called NetChat that was written in, of all things, a language called Blitz Basic, which was a language focused on making games. NetChat was what my friends and I used at school to chat with each other between classrooms (instead of paying attention in class…) It was single threaded and used Blitz Basic’s implementation of networking to communicate. Nonetheless it had support for multiple concurrent users, colors for users and their messages, an admin user, and even the ability for all users to draw on a picture at the same time using colored circles of various sizes.

Unfortunately the source code and program has been lost for years now, which is disappointing because it was probably the most full featured, and definitely most used, program I had made around that time. Since then I’ve been wanting to recreate it in a more common language. This post is about the first steps toward making it, simply figuring out how to send and receive pieces of data through the network.

I found a tutorial on the subject here. However it seemed to have an issue that it had to alternate between sending and receiving messages. For every message it sends it had to receive one, back and forth. Here’s the send/receive code from the client:

NetworkStream serverStream = clientSocket.GetStream();
byte[] outStream = System.Text.Encoding.ASCII.GetBytes("Message from Client$");
serverStream.Write(outStream, 0, outStream.Length);
serverStream.Flush();

byte[] inStream = new byte[10025];
serverStream.Read(inStream, 0, (int)clientSocket.ReceiveBufferSize);
string returndata = System.Text.Encoding.ASCII.GetString(inStream);
msg("Data from Server : " + returndata);

Line 7, serverStream.Read() appears to block until something is in the stream to be read. That means that until the server sends something to the client, the client will be stuck on that line, which prevents the user from sending any more information. Despite this issue that example has a ton of useful information that has helped me learn socket programming in C#.

Finding that issue made me decide to try taking a stab at creating my own client/server chat program. I decided on a similar setup with a thread on the server for each client connected. However the client connection threads on the server only listen for incoming messages, they don’t send messages.

chatflow

Server Program

The server program consists of two parts.

  1. The actual server class.
  2. Client connections, one per client.
Server Class
public class ChatServer
{
private readonly IList<ClientConnection> _clientConnections = new List<ClientConnection>();
private readonly TcpListener _serverListener;

public ChatServer(IPAddress addr, int port)
{
Console.WriteLine(">> Starting server on {0}:{1}", addr, port);
this._serverListener = new TcpListener(addr, port);
this._serverListener.Start();

//start threads
Thread clientConnectThread = new Thread(this.ConnectToClientsThread);
clientConnectThread.Start();
}

public void BroadcastMessage(string msg)
{
//don't need a mutex because we're just reading
Console.WriteLine(msg);
foreach (ClientConnection clientConnection in this._clientConnections)
clientConnection.SendMessage(msg);
}

private void ConnectToClientsThread()
{
int counter = 0;
while (true)
{
//AcceptTcpClient will block until a new connection is made
TcpClient client = this._serverListener.AcceptTcpClient();
counter++;
Console.WriteLine(">> Client #{0} joined", counter);

ClientConnection clientConnection = new ClientConnection(this);
clientConnection.StartClient(client, counter.ToString());
this._clientConnections.Add(clientConnection);
}
}
}

The Main() method (not shown) gets the IP address and the port to start the server on, then creates a ChatServer object. That’s all the Main() method needs to do, the ChatServer handles the rest.

The ChatServer has a TcpListner object that it uses to monitor for clients trying to connect to the chat server. The constructor for ChatServer takes care of all the setup. It creates the listner, starts it, creates the listening thread, and starts that. From there on ConnectToClientsThread() runs until the server is shut down, and BroadcastMessage() is called from any ClientConnection whenever it receives a message from it’s client.

The ConnectToClientsThread() thread will block on this._serverListener.AcceptTcpClient() until a new connection is established. After that it creates a ClientConnection, passes the TcpClient to it, gives it a number, tells it to start up, and adds the connection to the server’s list of ClientConnections.

Client Connections
public class ClientConnection
{
private readonly ChatServer _server;
private readonly Object _streamLock = new Object();
private TcpClient _client;
private NetworkStream _clientStream;
private bool _isConnected = true;

public ClientConnection(ChatServer server)
{
this._server = server;
}

private string ClientNum { get; set; }

public void SendMessage(string msg)
{
//encode the message
byte[] sendBytes = Encoding.UTF8.GetBytes(msg);

if (!this._isConnected)
return;

lock (this._streamLock)
{
try
{
this._clientStream.Write(sendBytes, 0, sendBytes.Length);
this._clientStream.Flush();
}
catch
{
//drop for any exception
this._isConnected = false;
Console.WriteLine(">> [{0}] - Exception caught while sending to client, stopping connection",
this.ClientNum);
}
}
}

public void StartClient(TcpClient client, string clientNum)
{
this.ClientNum = clientNum;
this._client = client;
this._clientStream = client.GetStream();

Thread receiveThread = new Thread(this.ReceiveMessagesThread);
receiveThread.Start();
}

private void ReceiveMessagesThread()
{
while (this._isConnected)
{
//sleep for just a little bit to not eat up the CPU
Thread.Sleep(1);

byte[] readBuffer = new byte[this._client.ReceiveBufferSize];
int bytesRead = 0;

lock (this._streamLock)
{
try
{
//if there's data in the stream, get it
if (this._clientStream.CanRead && this._clientStream.DataAvailable)
{
bytesRead = this._clientStream.Read(readBuffer, 0, this._client.ReceiveBufferSize);
this._clientStream.Flush();
}
}
catch
{
this._isConnected = false;
Console.WriteLine(">> [{0}] - Exception caught while reading from client, stopping connection",
this.ClientNum);
}
}

if (bytesRead <= 0)
continue;

try
{
string dataFromClient = Encoding.UTF8.GetString(readBuffer, 0, bytesRead);

//if there was actually data received, tell the server to broadcast it
if (!string.IsNullOrWhiteSpace(dataFromClient))
{
dataFromClient = string.Format("[{0}]: {1}", this.ClientNum, dataFromClient);
this._server.BroadcastMessage(dataFromClient);
}
}
catch
{
Console.WriteLine("Error encoding message");
}
}
}
}

The code for a ClientConnection is a bit more involved, but still not too bad. The constructor just takes a parameter for which ChatServer it belongs to. I probably could have just made ChatServer a static class, but I felt that the way I did it left open the ability to create “rooms” by simply creating a new ChatServer with a different port number (possibly IP Address as well, but port number seems more practical).

Notice the lock statements in the SendMessage() method and the ReceiveMessagesThread(). Because both of those are run on separate threads, it would be possible for SendMessage() to attempt to write to the stream while ReceiveMessagesThread() is simultaneously attempting to read from the stream, this is undoubtedly a bad idea. Having both sections lock on an object will force them to wait until the other is finished with the stream. The statements in the lock sections represent critical sections of the code, and the lock itself is a form of mutex. C# conveniently even has they keyword lock for this purpose. However if we had multiple threads spread across multiple processes and classes then we would likely be better off using an actual Mutex, which the .NET framework has.

Also notice that in the ReceiveMessagesThread() that if an exception is encountered and caught, that the ClientConnection will simply stop by setting _isConnected to false. Next time the ReceiveMessagesThread loops around, the while condition will not be met, and the thread will end. Also, the SendMessage() method will not even try to send the message if _isConnected is false.

The NetworkStream for the client connection reads and writes using an array of bytes. I had the choice here to encode the strings as UTF8 or ASCII. I wanted the ability to send Alt codes such as ☺, •, ♠, Æ, Φ, etc. Not just because they’re neat, but because I could use them for special purposes later. Possibly to signify a system message or as a delimiter between pieces of information. I thought they would be great for that purpose since there aren’t dedicated keys on the keyboard for them and their usage is quite uncommon, especially if using a specific combination of them.

Receiving a message is a relatively short process. First the thread checks to see if the client stream can be read from and if it even has any data in it, if it does it tries to read in data. The TcpClient has a limited buffer size for receiving and sending messages (65536 bytes when I checked). The Read() method of the client stream connection takes in the array of bytes that the message will be placed in, as well as offset and size, so you can grab any contiguous section of bytes out of the stream. In this case I chose to just grab everything every time. A handy feature of the Read() method is that it will return the number of bytes that were actually read from the stream, which you can pass into the encoder so that you don’t end up with a bunch of null characters – '\0' – at the end of your result string.

When the client connection successfully receives a message, it passes that message to the server. The server then calls SendMessage() on each ClientConnection with the message that was passed to it. Notice that in the SendMessage() method that no thread was created to send the message. This means that while the SendMessage() call is waiting for the the listener thread to exit its critical section, that the for loop in BroadcastMessage on the server is also waiting. This is probably not the best way to go about it, but hey this is a proof of concept and is not meant for serious use.

Client Program

There’s really only one class involved in this. Again the Main() method of the client program will prompt the user to enter an IP address and port number, and send that to the ChatClient.

public class ChatClient
{
private readonly TcpClient _clientSocket = new TcpClient();
private readonly Object _streamLock = new Object();
private NetworkStream _serverStream;
private bool _stopThreads;

public void Start(string host, int port)
{
this._clientSocket.Connect(host, port);
this._serverStream = this._clientSocket.GetStream();

Thread listenerThread = new Thread(this.ListenerThread);
listenerThread.Start();

Thread sendThread = new Thread(this.SendMessageThread);
sendThread.Start();
}

private void ListenerThread()
{
while (!this._stopThreads)
{
//sleep for just a little bit to not eat up the CPU
Thread.Sleep(1);

byte[] readBuffer = new byte[this._clientSocket.ReceiveBufferSize];
int bytesRead = 0;

lock (this._streamLock)
{
try
{
if (this._serverStream.CanRead && this._serverStream.DataAvailable)
{
bytesRead = this._serverStream.Read(readBuffer, 0, this._clientSocket.ReceiveBufferSize);
this._serverStream.Flush();
}
}
catch
{
this._stopThreads = true;
}
}

if (bytesRead <= 0)
continue;

try
{
string messageString = Encoding.UTF8.GetString(readBuffer, 0, bytesRead);
if (!string.IsNullOrWhiteSpace(messageString))
Console.WriteLine(messageString);
}
catch
{
Console.WriteLine("Error encoding message");
}
}
}

private void SendMessageThread()
{
while (!this._stopThreads)
{
string msg = Console.ReadLine();

if (string.IsNullOrWhiteSpace(msg))
continue;

byte[] sendBytes = Encoding.UTF8.GetBytes(msg);

lock (this._streamLock)
{
try
{
this._serverStream.Write(sendBytes, 0, sendBytes.Length);
this._serverStream.Flush();
}
catch
{
this._stopThreads = true;
}
}
}
}
}

After a ChatClient is created, its Start() method is called which creates the connections and starts the two threads: the thread that listens for incoming messages, and a thread that gets messages from the user and sends them.

Both threads have a condition to exit when _stopThreads is set to true. They also both use the same lock to access the stream. Notice that the ReceiveMessagesThread() in the ClientConnection class on the server and the ListenerThread() in the ChatClient class on the client project are almost identical, the only thing they really differ on is what they do with the messages they receive and how they handle errors.

The SendMessageThread() in the ChatClient is different from SendMessage in ClientConnection however. The biggest difference is that it is actually a thread in the client program. I felt this to be necessary considering how this project worked. As a console application the client program uses Console.ReadLine() to get a message from the user, this method blocks until the user hits enter or return, at which point the string the user entered is returned from the method call. It would be desirable to still be able to receive messages while the user is typing in their own, hence the two threads.

The send message methods on the client and server differ in purpose as well. The client thread is actually waiting on user interaction for its information. On the server the method serves simply as a relay for the server to take a message from one client and send it to the others, there is no user interaction involved there. I felt that this is such a short process that it didn’t necessitate the use of a thread.

In both projects whenever a thread entered a critical section (a lock statement) I also tried to keep as much code outside of the critical section as possible. This is why the ListenerThread() doesn’t process the incoming array of bytes until after the lock is released, same thing with encoding the message before entering the critical section in the SendMessageThread(). When you have a critical section in your code that forces other threads to wait, you want to have that lock for as short of an amount of time as possible to let other threads have a turn.

Consider what would happen if the Console.ReadLine() call was made inside the lock statement. The ListenerThread() couldn’t actually read any incoming messages because the lock would be tied up waiting for the user to enter a string. In that situation we would be back to where we started in the first place.

Conclusion

This was a neat little program to start learning how to handle threads and network programming. It’s short, usable, and gets across some of the important concepts of both network programming and threading. Of course as a proof of concept program there are things that weren’t done or could/should have been done better, such as the exception handling, but all in all it was a great learning experience and hopefully will be a good foundation for NetChat2.

Like previous projects this one is also hosted on GitHub, you can view the repository here.

Leave a Reply

Your email address will not be published. Required fields are marked *