Socket Chat


Description:

This is an implementation of a Chat program using sockets. Along with basic chat capability, it has the ability to send private messages and log the ongoing chat.  There are two applications - ChatServer and ChatClient.

ChatServer manages the chat session. It maintains a list of the active chatters and forwards incoming chat messages. ChatServer is hard coded to listen on port 5555 in case you have to change it. The Chat Server is multithreaded - communication with each client is through a separate thread.  When ChatServer is started, it enters the StartListening() thread to wait for client connections:

private void StartListening()
{
listener =
new TcpListener(listenport);
listener.Start();
while (true)
{
try
{
Socket s = listener.AcceptSocket();
clientsocket = s;
clientservice =
new Thread(new ThreadStart(ServiceClient));
clientservice.Start();
}
catch(Exception e)
{
Console.WriteLine(e.ToString() );
}
}
}

This thread continues forever to listen for clients. When a connection request is received, the server starts a thread called ServiceClient() to service the client.  Note that when a socket is established, a copy of the socket is made. This is necessary for each client to have its own socket. When the thread is started, a Client object is created containing all the relevant info about each client and saved in an ArrayList. The Client class is shown below. Note the Client class contains a reference to it's own ServiceClient() thread and socket. This simplifies the job of the ChatServer and makes the program logic easier because the ChatServer communicates directly with Clients and can kill their threads directly instead of having to use separate lists to keep track of which socket and thread belongs to which Client.

namespace
ChatServer
{
using System.Net.Sockets;
using System.Net;
public class Client
{
private Thread clthread;
private EndPoint endpoint;
private string name;
private Socket sock;
public Client(string _name, EndPoint _endpoint, Thread _thread, Socket _sock)
{
clthread = _thread;
endpoint = _endpoint;
name = _name;
sock = _sock;
}
public override string ToString()
{
return endpoint.ToString()+ " : " + name;
}
public Thread CLThread
{
get{return clthread;}
set{clthread = value;}
}
public EndPoint Host
{
get{return endpoint;}
set{endpoint = value;}
}
public string Name
{
get{return name;}
set{name = value;}
}
public Socket Sock
{
get{return sock;}
set{sock = value;}
}
}
}

The ServiceClient() thread is where all the work is done. ServiceClient() defines the implementation of the server side of the application protocol. The application protocol is the semantics of message exchange between the server and client.The protocol I have used has two traits that are highly desirable when designing a protocol. First, it is stateless, meaning the messages are context-free. The processing of a message does not depend on a previous message or any other context - messages can be received at any time and in any order without confusing the application. Second, the messages are fully self describing, meaning all the data needed to process a message is contained within the message.  The server recognizes four commands, CONN, CHAT, PRIV, and GONE.  CONN establishes a new client, sends a list of current chatters, and notifies other chatters a new person has joined the group. CHAT simply forwards the incoming chat to all recipients. PRIV sends a private message to the person designated.  GONE removes an active chatter account and notifies all other members the chatter has left. GONE will also cause the death of the clients service thread by setting the bool keepalive to false. This is the cleanest way to kill the thread, however the ChatServer can kill the thread itself if it detects the connection is terminated.

The server receives the incoming messages as ASCII strings. The '|' char is used as the separator between parts of the message. A message consists of a command, and one or more other parameters required to process the message.

private void ServiceClient()
{
Socket client = clientsocket;
bool keepalive = true;
while (keepalive)
{
Byte[] buffer =
new Byte[1024];
client.Receive(buffer);
string clientcommand = System.Text.Encoding.ASCII.GetString(buffer);
string[] tokens = clientcommand.Split(new Char[]{'|'});
Console.WriteLine(clientcommand);
if (tokens[0] == "CONN")
{
for(int n=0; n<clients.Count; n++) {
Client cl = (Client)clients[n];
SendToClient(cl, "JOIN|" + tokens[1]);
}
EndPoint ep = client.RemoteEndPoint;
Client c =
new Client(tokens[1], ep, clientservice, client);
clients.Add(c);
string message = "LIST|" + GetChatterList() +"\r\n";
SendToClient(c, message);
lbClients.Items.Add(c);
}
if (tokens[0] == "CHAT")
{
for(int n=0; n<clients.Count; n++)
{
Client cl = (Client)clients[n];
SendToClient(cl, clientcommand);
}
}
if (tokens[0] == "PRIV") {
string destclient = tokens[3];
for(int n=0; n<clients.Count; n++) {
Client cl = (Client)clients[n];
if(cl.Name.CompareTo(tokens[3]) == 0)
SendToClient(cl, clientcommand);
if(cl.Name.CompareTo(tokens[1]) == 0)
SendToClient(cl, clientcommand);
}
}
if (tokens[0] == "GONE")
{
int remove = 0;
bool found = false;
int c = clients.Count;
for(int n=0; n<c; n++)
{
Client cl = (Client)clients[n];
SendToClient(cl, clientcommand);
if(cl.Name.CompareTo(tokens[1]) == 0)
{
remove = n;
found =
true;
lbClients.Items.Remove(cl);
}
}
if(found)
clients.RemoveAt(remove);
client.Close();
keepalive =
false;
}
}
}

The ChatServer is shown below. Visually, there isn't much to it, it just displays the active chatter's host ip and name



The ChatClient allows users to log on to the chat and send and receive messages.  If the ChatClient is started without any command line parameters, it assumes the ChatServer is on the localhost. If it is not, you must supply an ip address for the ChatServer on the command line. After starting up, put your chat name in the chat entry box and click Connect. The ChatServer will respond with a list of current chatters which ChatClient puts in a ListBox.  After that you can send messages by typing them on the send line and clicking Send. If you wish to send a private message, click Private and select a name from the ListBox. Only one name can be selected at a time. Then send the message. The receiver will get the message with a "Private from" string prepended to it.

When the client attempts to connect, a connection is established and registers with the ChatServer.  EstablishConnection() uses a TcpClient to connect to the ChatServer. A NetworkStream is established that will be used to send messages.  Again the ChatServer port is hard coded to 5555.

private void EstablishConnection()
{
statusBar1.Text = "Connecting to Server";
try
{
clientsocket =
new TcpClient(serveraddress,serverport);
ns = clientsocket.GetStream();
sr =
new StreamReader(ns);
connected =
true;
}
catch (Exception e)
{
MessageBox.Show("Could not connect to Server","Error",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
statusBar1.Text = "Disconnected";
}
}

The RegisterWithServer() method constructs and sends a CONN command to the ChatServer.  The CONN command contains the chatters name as well. The command is constructed using the '|' char as a separator. The ChatClient then receives the list of chatters and adds them to the ListBox.

private void RegisterWithServer()
{
try
{
string command = "CONN|" + ChatOut.Text;
Byte[] outbytes = System.Text.Encoding.ASCII.GetBytes(command.ToCharArray());
ns.Write(outbytes,0,outbytes.Length);
string serverresponse = sr.ReadLine();
serverresponse.Trim();
string[] tokens = serverresponse.Split(new Char[]{'|'});
if(tokens[0] == "LIST")
{
statusBar1.Text = "Connected";
btnDisconnect.Enabled =
true;
}
for(int n=1; n<tokens.Length-1; n++)
lbChatters.Items.Add(tokens[n].Trim(
new char[]{'\r','\n'}));
this.Text = clientname + ": Connected to Chat Server";
}
catch (Exception e)
{
MessageBox.Show("Error Registering","Error",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
}

After the ChatClient has connected, it spawns a ReceiveChat() thread to handle all the ins and outs.  ReceiveChat() implements the client side of the application protocol.

private void ReceiveChat()
{
bool keepalive = true;
while (keepalive)
{
try
{
Byte[] buffer =
new Byte[2048];
ns.Read(buffer,0,buffer.Length);
string chatter = System.Text.Encoding.ASCII.GetString(buffer);
string[] tokens = chatter.Split(new Char[]{'|'});
if (tokens[0] == "CHAT")
{
rtbChatIn.AppendText(tokens[1]);
if(logging)
logwriter.WriteLine(tokens[1]);
}
if (tokens[0] == "PRIV") {
rtbChatIn.AppendText("Private from ");
rtbChatIn.AppendText(tokens[1].Trim() );
rtbChatIn.AppendText(tokens[2] + "\r\n");
if(logging){
logwriter.Write("Private from ");
logwriter.Write(tokens[1].Trim() );
logwriter.WriteLine(tokens[2] + "\r\n");
}
}
if (tokens[0] == "JOIN")
{
rtbChatIn.AppendText(tokens[1].Trim() );
rtbChatIn.AppendText(" has joined the Chat\r\n");
if(logging)
logwriter.WriteLine(tokens[1]+" has joined the Chat");
string newguy = tokens[1].Trim(new char[]{'\r','\n'});
lbChatters.Items.Add(newguy);
}
if (tokens[0] == "GONE")
{
rtbChatIn.AppendText(tokens[1].Trim() );
rtbChatIn.AppendText(" has left the Chat\r\n");
if(logging)
logwriter.WriteLine(tokens[1]+" has left the Chat");
lbChatters.Items.Remove(tokens[1].Trim(
new char[]{'\r','\n'}));
}
if (tokens[0] == "QUIT")
{
ns.Close();
clientsocket.Close();
keepalive =
false;
statusBar1.Text = "Server has stopped";
connected=
false;
btnSend.Enabled =
false;
btnDisconnect.Enabled =
false;
}
}
catch(Exception e){}
}
}

Clicking the Start Logging button will begin writing all the chat and messages to a text file. The file name is built from the current date and time. A sub directory called "logs" will be created to store the log files.

The ChatClient is shown below.



ISSUES:  No guarantees about this code being bullet proof, its never really been tested with lots of users. Access to the Client list should be synchronized. Don't try to use the text log feature with multiple clients on the same machine or it could break if they are assigned the same file name. Sometimes the clients list of active chatters does not update properly when someone quits. I haven't figured out why yet. If you disconnect and log back in it will fix it.  Some characters are sometimes missing from the log files. 

Requirement:

Requires Beta 2 .NET SDK


Similar Articles