Introduction
I started writing this application to learn Socket programming, Threads, and UI controls in C#. But in the process, I learned more than that. We would go step by step and will try to cover all important aspects of this application. But before we begin, I'd like to set some expectations about the application. This application demonstrates how multiple clients can connect to a single server and communicate to the server. However, at this point, clients cannot communicate with each other. That would be the next step for this application.
Using the code
What do you need to get started with a multithreaded chat application? Threads and Sockets.
Following is how the application starts and how it works:
- As soon as the application is started, the main thread is spawned by the system.
- The main thread then spawns a thread which keeps listening on a given port.
- As soon as there is any connection request from a client, a connection is established. After the connection gets established, another thread is spawned to open a dialog box for chatting with clients.
- For every connection to a client, a new thread is spawned. Hence, if there are three clients connected to the server, then the total active threads will be five, one main thread, one thread for listening, and one each for chatting with the connected clients.
Now, let's look at what each thread, that corresponds to a client connect, does:
- Calls the asynchronous
BeginReceive
method. A callback method is passed a parameter. This callback method is called when any data is received on that socket. - When data is received on a socket, the data is read and displayed on the rich text box of the chat dialog. However, if a
SocketException
is raised, then the connection is closed, as this means that the client has asked to close the connection. - When the Send button is clicked, data is sent to client.
Huh! Looks pretty simple. Now, let's look at some concepts in C# that are required to understand the implementation.
1. How does asynchronous receive work?
.NET framework's Socket
class provides a BeginReceive
method to receive data asynchronously, i.e., in a non-blocking manner. The BeginReceive
method has the following signature:
public IAsyncResult BeginReceive( byte[] buffer, int offset, int size,
SocketFlags socketFlags, AsyncCallback callback, object state );
The way the BeginReceive
function works is that, you pass the function a buffer and a callback function (delegate) which will be called whenever data arrives. The callback method is called by the system when data is received on the given socket. This method is called using a separate thread (internally spawned by the system). Hence this operation is asynchronous and non-blocking.
Now, where exactly will be the received data? It will be in the buffer that was passed in the BeginReceive
method. But before you read the data, you should know the number of bytes that has been received. This is achieved by calling the EndReceive
method of the Socket
class.
The BeginReceive
call is completed only after the EndReceive
method of the Socket
. The following code will clear what has been explained in the above paragraphs:
BeginReceive call is made:
StateObject state = new StateObject();
state.workSocket = connectedClient.Client;
connectedClient.Client.BeginReceive(state.buffer, 0,
StateObject.BufferSize, 0,new AsyncCallback(OnReceive), state);
Callback function when data is received on the Socket:
public void OnReceive(IAsyncResult ar)
{
String content = String.Empty;
StateObject state = (StateObject)ar.AsyncState;
Socket handler = state.workSocket;
int bytesRead;
if (handler.Connected)
{
try
{
bytesRead = handler.EndReceive(ar);
if (bytesRead > 0)
{
state.sb.Remove(0, state.sb.Length);
state.sb.Append(Encoding.ASCII.GetString(
state.buffer, 0, bytesRead));
content = state.sb.ToString();
SetText(content);
handler.BeginReceive(state.buffer, 0, StateObject.BufferSize,
0,new AsyncCallback(OnReceive), state);
}
}
catch (SocketException socketException)
{
if (socketException.ErrorCode == 10054 ||
((socketException.ErrorCode != 10004) &&
(socketException.ErrorCode != 10053)))
{
String remoteIP =
((IPEndPoint)handler.RemoteEndPoint).Address.ToString();
String remotePort =
((IPEndPoint)handler.RemoteEndPoint).Port.ToString();
this.owner.DisconnectClient(remoteIP, remotePort);
handler.Close();
handler = null;
}
}
catch (Exception exception)
{
MessageBox.Show(exception.Message + "\n" + exception.StackTrace);
}
}
}
2. How can you access a User Interface control (e.g., RichText Box) from a thread which is not an owner of that UI control?
Answer is, use Delegates.
If you look at the application, The rich text box that displays the chat message is created by the thread that creates the chat dialog box. Now, the chat data in the rich text box is updated by a thread that calls the callback function, OnReceive
. This is a system spawned thread!
In order to access it, create a delegate as:
public delegate void SetTextCallback(string s);
Now, create a function to update the rich text box as:
private void SetText(string text)
{
if (this.rtbChat.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else
{
this.rtbChat.SelectionColor = Color.Blue;
this.rtbChat.SelectedText = "\nFriend: "+text;
}
}
History
- Updated on 12/13/2006 - Formatted article, and added a few details.
- Updated on 01/31/2007 - Updated the sample client code and added validation to port the text box in server code.