In this two part tutorial you will learn how to create a chat client that connects to a chat server and exchanges messages with all the other connected clients. This second part covers the development of the chat server.
» C# Chat: Part 1 - Building the Chat Client
» C# Chat: Part 2 - Building the Chat Server (currently reading)
This is the second part of a tutorial that teaches you how to create a C# chat application. In Part 1 – Building the Chat Client we’ve looked at how to create the chat application that connects to a chat server, and here we now look at how to build the chat server. The Chat Server
The Chat Server application is a tad more complex than the Chat Client application because it needs to hold information on all the connected clients, await for messages from each and send incoming messages to all. I commented all the code so hopefully you won’t have any problems figuring it out without much commentary on the side from me.
Fire up a new instance of Visual Studio 2005 and in a new Windows Application project draw the following form:
The only controls we are interested in are the two TextBoxes (txtIp and txtLog) and the btnListen button. The IP address inside txtIp is the one where you want your chat server to be listening for incoming connections. You should use your local IP address, which if you’re not in a network it could be 192.168.0.1, but it’s best that you check using the ipconfig /all command in the MS-DOS Command Prompt window. The multi-line TextBox will hold information about connected clients and the messages that they are exchanging.
In the code view, start with the following using statements:
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.IO;
Move inside the class and declare a delagate which we will need in order to update the txtLog TextBox from another thread :
private delegate void UpdateStatusCallback(string strMessage);
Double-click the Start Listening button and you should arrive to it Click event. Have the event handler look like this:
private void btnListen_Click(object sender, EventArgs e)
{
// Parse the server's IP address out of the TextBox
IPAddress ipAddr = IPAddress.Parse(txtIp.Text);
// Create a new instance of the ChatServer object
ChatServer mainServer = new ChatServer(ipAddr);
// Hook the StatusChanged event handler to mainServer_StatusChanged
ChatServer.StatusChanged += new StatusChangedEventHandler(mainServer_StatusChanged);
// Start listening for connections
mainServer.StartListening();
// Show that we started to listen for connections
txtLog.AppendText("Monitoring for connections...\r\n");
}
A couple of objects are being instantiated, including a ChatServer object. We will write the ChatServer class very soon, and you will see that it handles all the incoming connections. In turn, it will make use of another class that we will write, called Connection.
The next thing we do is to set up an event handler for the StatusChanged event, which is a custom event that we’re going to write very soon. It will inform us when a client has connected, a new message has been received, a client has disconnected, etc.
Finally the StartListening() method tells the ChatServer object to start listening for incoming connections.
Believe it or not, there are only a few more lines of code to go in this class. One of them is the event handler that we hooked earlier, and the other is the UpdateStatus() method that gets called when an update needs to be made to the form. It’s needed because we use Invoke() and the delegate we created earlier to make a cross-thread call (since the ChatServer will be working in a different thread):
public void mainServer_StatusChanged(object sender, StatusChangedEventArgs e)
{
// Call the method that updates the form
this.Invoke(new UpdateStatusCallback(this.UpdateStatus), new object[] { e.EventMessage });
}
private void UpdateStatus(string strMessage)
{
// Updates the log with the message
txtLog.AppendText(strMessage + "\r\n");
}
And we’re done. Done with the Form1 class, of course. You should now create another file (I called mine ChatServer.cs) and inside it make sure you have all these using statements:
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading;
using System.Collections;
We’ll need to use an event that notifies the form class when a new client has connected, disconnected, sent a message, etc. In order to create our own custom event, we need to first define its arguments. More exactly we want a string argument that tells us what type of event has occured (user has connected, user has disconnected, user x says y, etc..) If you’re not familiar with events and delegates you might want to read the Delegates and Events in C# tutorial from a week ago. Here is the class that gets and sets the event arguments:
// Holds the arguments for the StatusChanged event
public class StatusChangedEventArgs : EventArgs
{
// The argument we're interested in is a message describing the event
private string EventMsg;
// Property for retrieving and setting the event message
public string EventMessage
{
get
{
return EventMsg;
}
set
{
EventMsg = value;
}
}
// Constructor for setting the event message
public StatusChangedEventArgs(string strEventMsg)
{
EventMsg = strEventMsg;
}
}
Moving on, outside this class we declare the actual delegate for the event handler:
// This delegate is needed to specify the parameters we're passing with our event
public delegate void StatusChangedEventHandler(object sender, StatusChangedEventArgs e);
And now before we actually get to fire this event, we define the ChatServer class, in all its glory. Remember this is the class that we used back in Form1.cs:
class ChatServer
{
// This hash table stores users and connections (browsable by user)
public static Hashtable htUsers = new Hashtable(30); // 30 users at one time limit
// This hash table stores connections and users (browsable by connection)
public static Hashtable htConnections = new Hashtable(30); // 30 users at one time limit
// Will store the IP address passed to it
private IPAddress ipAddress;
private TcpClient tcpClient;
// The event and its argument will notify the form when a user has connected, disconnected, send message, etc.
public static event StatusChangedEventHandler StatusChanged;
private static StatusChangedEventArgs e;
// The constructor sets the IP address to the one retrieved by the instantiating object
public ChatServer(IPAddress address)
{
ipAddress = address;
}
// The thread that will hold the connection listener
private Thread thrListener;
// The TCP object that listens for connections
private TcpListener tlsClient;
// Will tell the while loop to keep monitoring for connections
bool ServRunning = false;
// Add the user to the hash tables
public static void AddUser(TcpClient tcpUser, string strUsername)
{
// First add the username and associated connection to both hash tables
ChatServer.htUsers.Add(strUsername, tcpUser);
ChatServer.htConnections.Add(tcpUser, strUsername);
// Tell of the new connection to all other users and to the server form
SendAdminMessage(htConnections[tcpUser] + " has joined us");
}
// Remove the user from the hash tables
public static void RemoveUser(TcpClient tcpUser)
{
// If the user is there
if (htConnections[tcpUser] != null)
{
// First show the information and tell the other users about the disconnection
SendAdminMessage(htConnections[tcpUser] + " has left us");
// Remove the user from the hash table
ChatServer.htUsers.Remove(ChatServer.htConnections[tcpUser]);
ChatServer.htConnections.Remove(tcpUser);
}
}
// This is called when we want to raise the StatusChanged event
public static void OnStatusChanged(StatusChangedEventArgs e)
{
StatusChangedEventHandler statusHandler = StatusChanged;
if (statusHandler != null)
{
// Invoke the delegate
statusHandler(null, e);
}
}
// Send administrative messages
public static void SendAdminMessage(string Message)
{
StreamWriter swSenderSender;
// First of all, show in our application who says what
e = new StatusChangedEventArgs("Administrator: " + Message);
OnStatusChanged(e);
// Create an array of TCP clients, the size of the number of users we have
TcpClient[] tcpClients = new TcpClient[ChatServer.htUsers.Count];
// Copy the TcpClient objects into the array
ChatServer.htUsers.Values.CopyTo(tcpClients, 0);
// Loop through the list of TCP clients
for (int i = 0; i < tcpClients.Length; i++)
{
// Try sending a message to each
try
{
// If the message is blank or the connection is null, break out
if (Message.Trim() == "" || tcpClients[i] == null)
{
continue;
}
// Send the message to the current user in the loop
swSenderSender = new StreamWriter(tcpClients[i].GetStream());
swSenderSender.WriteLine("Administrator: " + Message);
swSenderSender.Flush();
swSenderSender = null;
}
catch // If there was a problem, the user is not there anymore, remove him
{
RemoveUser(tcpClients[i]);
}
}
}
// Send messages from one user to all the others
public static void SendMessage(string From, string Message)
{
StreamWriter swSenderSender;
// First of all, show in our application who says what
e = new StatusChangedEventArgs(From + " says: " + Message);
OnStatusChanged(e);
// Create an array of TCP clients, the size of the number of users we have
TcpClient[] tcpClients = new TcpClient[ChatServer.htUsers.Count];
// Copy the TcpClient objects into the array
ChatServer.htUsers.Values.CopyTo(tcpClients, 0);
// Loop through the list of TCP clients
for (int i = 0; i < tcpClients.Length; i++)
{
// Try sending a message to each
try
{
// If the message is blank or the connection is null, break out
if (Message.Trim() == "" || tcpClients[i] == null)
{
continue;
}
// Send the message to the current user in the loop
swSenderSender = new StreamWriter(tcpClients[i].GetStream());
swSenderSender.WriteLine(From + " says: " + Message);
swSenderSender.Flush();
swSenderSender = null;
}
catch // If there was a problem, the user is not there anymore, remove him
{
RemoveUser(tcpClients[i]);
}
}
}
public void StartListening()
{
// Get the IP of the first network device, however this can prove unreliable on certain configurations
IPAddress ipaLocal = ipAddress;
// Create the TCP listener object using the IP of the server and the specified port
tlsClient = new TcpListener(1986);
// Start the TCP listener and listen for connections
tlsClient.Start();
// The while loop will check for true in this before checking for connections
ServRunning = true;
// Start the new tread that hosts the listener
thrListener = new Thread(KeepListening);
thrListener.Start();
}
private void KeepListening()
{
// While the server is running
while (ServRunning == true)
{
// Accept a pending connection
tcpClient = tlsClient.AcceptTcpClient();
// Create a new instance of Connection
Connection newConnection = new Connection(tcpClient);
}
}
}
Overwhelmed? It’s really not that complicated if you take it line by line and read the comments. It starts by defining two hash tables. These two hash tables will hold the username and the TCP connection associated with it. We need two of them because at one point we’ll want to retrieve the TCP connection by giving the username, and at some other point we’ll want to retrieve the username by giving the TCP connection. The 30 defines how many users the chat server can hold at one given point, but you can easily go into hundreds if needed, without worrying about a performance decrease.
The AddUser() method is obvious – it adds a new user to the hash tables, and thus to our list of connected chat clients. The RemoveUser() method does the opposite. The OnStatusChanged will fire the StatusChanged event, which is right now handled inside Form1.cs. Thus, it’s our way of updating the form with the latest message from inside this ChatServer object.
The SendAdminMessage sends an administrative message to all connected clients. You can see how it loops through the hash table and attempts to send them the message. If the message didn’t get through, they probably disconnected and we then remove them. This is very similar to what the SendMessage() method does, only that this time it sends a message from a specific chat client to all the others.
The StartListening() method is the one we called inside Form1, and it’s the fire starter, the instigator. It defines the first needed objects and starts a new thread that keeps listening for connections, and that is the KeepListening() method. And that’s where our story continues, because if you look inside the KeepListening() method you will see we create a new object of type Connection. That’s because each user connected to our server will have its own instance of Connection. If there are 10 users currently connected, there will be 10 instances of the Connection object. So let’s look at the final class of our chat server:
// This class handels connections; there will be as many instances of it as there will be connected users
class Connection
{
TcpClient tcpClient;
// The thread that will send information to the client
private Thread thrSender;
private StreamReader srReceiver;
private StreamWriter swSender;
private string currUser;
private string strResponse;
// The constructor of the class takes in a TCP connection
public Connection(TcpClient tcpCon)
{
tcpClient = tcpCon;
// The thread that accepts the client and awaits messages
thrSender = new Thread(AcceptClient);
// The thread calls the AcceptClient() method
thrSender.Start();
}
private void CloseConnection()
{
// Close the currently open objects
tcpClient.Close();
srReceiver.Close();
swSender.Close();
}
// Occures when a new client is accepted
private void AcceptClient()
{
srReceiver = new System.IO.StreamReader(tcpClient.GetStream());
swSender = new System.IO.StreamWriter(tcpClient.GetStream());
// Read the account information from the client
currUser = srReceiver.ReadLine();
// We got a response from the client
if (currUser != "")
{
// Store the user name in the hash table
if (ChatServer.htUsers.Contains(currUser) == true)
{
// 0 means not connected
swSender.WriteLine("0|This username already exists.");
swSender.Flush();
CloseConnection();
return;
}
else if (currUser == "Administrator")
{
// 0 means not connected
swSender.WriteLine("0|This username is reserved.");
swSender.Flush();
CloseConnection();
return;
}
else
{
// 1 means connected successfully
swSender.WriteLine("1");
swSender.Flush();
// Add the user to the hash tables and start listening for messages from him
ChatServer.AddUser(tcpClient, currUser);
}
}
else
{
CloseConnection();
return;
}
try
{
// Keep waiting for a message from the user
while ((strResponse = srReceiver.ReadLine()) != "")
{
// If it's invalid, remove the user
if (strResponse == null)
{
ChatServer.RemoveUser(tcpClient);
}
else
{
// Otherwise send the message to all the other users
ChatServer.SendMessage(currUser, strResponse);
}
}
}
catch
{
// If anything went wrong with this user, disconnect him
ChatServer.RemoveUser(tcpClient);
}
}
}
It doesn’t look too complicated, does it? There’s the constructor that initializes the TcpClient object, then there’s CloseConnection() which is called when we want to get rid of a currently connected client, and finally there’s AcceptClient() which checks for the username validity and if all is fine, it adds the user to the hash tables. If anything goes bad, it removes the user.
I’ll leave you chew on the code for a while now. It’s pretty much as easy as it can get for a C# based chat/server application, but that also means it can use more error handling. Also if you have any suggestions to improve the code or to fix a bug, feel free to post a comment below.