Introduction
In this article, I will discuss a chat application using asynchronous TCP sockets in C#. In the next part of this article, I will present an asynchronous UDP socket based chat application.
TCP Asynchronous Sockets
TCP asynchronous sockets have a Begin and End appended to the standard socket functions, like BeginConnect
, BeginAccept
, BeginSend
, and BeginReceive
. Let's take a look at one of them:
IAsyncResult BeginAccept(AsyncCallback callback, object state);
The AsyncCallback
function is called when the function completes. Just as events can trigger delegates, .NET also provides a way for methods to trigger delegates. The .NET AsyncCallback
class allows methods to start an asynchronous function and supply a delegate method to call when the asynchronous function completes.
The state
object is used to pass information between the BeginAccept
and the corresponding AsyncCallback
function.
The following shows an implementation of it:
Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
ProtocolType.Tcp);
IPEndPoint iep = new IPEndPoint(IPAddress.Any, 9050);
sock.Bind (iep);
sock.Listen (5);
sock.BeginAccept (new AsyncCallback(CallAccept), sock);
private void CallAccept(IAsyncResult iar)
{
Socket server = (Socket)iar.AsyncState;
Socket client = server.EndAccept(iar);
}
The original sock
object is retrieved in CallAccept
by using the AsyncState
property of IAsyncResult
. Once we have the original socket (on which the operation completed), we can get the client socket and perform operations on it.
All other asynchronous socket functions work like this, you should look on MSDN for further details. There are a few nice articles explaining the details on the net, so kindly search for them as well.
Getting Started
For exchange of messages between the client and the server, both of them use the following trivial commands:
enum Command
{
Login,
Logout,
Message,
List
}
The data structure used to exchange data between the client and the server is shown below. Sockets transmit and receive data as an array of bytes; the overloaded constructor and the ToByte
member function performs this conversion.
class Data
{
public Data()
{
this.cmdCommand = Command.Null;
this.strMessage = null;
this.strName = null;
}
public Data(byte[] data)
{
this.cmdCommand = (Command)BitConverter.ToInt32(data, 0);
int nameLen = BitConverter.ToInt32(data, 4);
int msgLen = BitConverter.ToInt32(data, 8);
if (nameLen > 0)
this.strName = Encoding.UTF8.GetString(data, 12, nameLen);
else
this.strName = null;
if (msgLen > 0)
this.strMessage = Encoding.UTF8.GetString(data,
12 + nameLen, msgLen);
else
this.strMessage = null;
}
public byte[] ToByte()
{
List<byte> result = new List<byte>();
result.AddRange(BitConverter.GetBytes((int)cmdCommand));
if (strName != null)
result.AddRange(BitConverter.GetBytes(strName.Length));
else
result.AddRange(BitConverter.GetBytes(0));
if (strMessage != null)
result.AddRange(BitConverter.GetBytes(strMessage.Length));
else
result.AddRange(BitConverter.GetBytes(0));
if (strName != null)
result.AddRange(Encoding.UTF8.GetBytes(strName));
if (strMessage != null)
result.AddRange(Encoding.UTF8.GetBytes(strMessage));
return result.ToArray();
}
public string strName;
public string strMessage;
public Command cmdCommand;
}
TCP Server
The server application listens on a particular port and waits for the clients. The clients connect to the server and join the room. The client then sends a message to the server, the server then sends this to all the users in the room.
The server application has the following data members:
struct ClientInfo
{
public Socket socket;
public string strName;
}
ArrayList clientList;
Socket serverSocket;
byte[] byteData = new byte[1024];
Here is how it starts listening for the clients and accepts the incoming requests:
private void Form1_Load(object sender, EventArgs e)
{
try
{
serverSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Any, 1000);
serverSocket.Bind(ipEndPoint);
serverSocket.Listen(4);
serverSocket.BeginAccept(new AsyncCallback(OnAccept), null);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "SGSserverTCP",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
With IPAddress.Any
, we specify that the server should accept client requests coming on any interface. To use any particular interface, we can use IPAddress.Parse (“192.168.1.1”)
instead of IPAddress.Any
. The Bind
function then bounds the serverSocket
to this IP address. The Listen
function makes the server wait for the clients, the value passed to Listen
specifies how many incoming client requests will be queued by the operating system. If there are pending client requests and another comes in, then it will be rejected.
Below is the corresponding OnAccept
function:
private void OnAccept(IAsyncResult ar)
{
try
{
Socket clientSocket = serverSocket.EndAccept(ar);
serverSocket.BeginAccept(new AsyncCallback(OnAccept), null);
clientSocket.BeginReceive(byteData, 0,
byteData.Length, SocketFlags.None,
new AsyncCallback(OnReceive), clientSocket);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "SGSserverTCP",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
EndAccept
returns the clientSocket
object of the connecting client. We then again start listening for more incoming client requests by making a call to serverSocket.BeginAccept
; this is essential as the server won’t be able to process any other incoming client request without it.
With BeginReceive
we start receiving the data that will be sent by the client. Note that we pass clientSocket
as the last parameter of BeginReceive
; the AsyncCallback OnReceive
gets this object via the AsyncState
property of IAsyncResult
, and it then processes the client requests (login, logout, and send messages to the users). Please see the code attached to understand the implementation of OnReceive
.
TCP Client
Some of the data members used by the client are:
public Socket clientSocket;
public string strName;
private byte[] byteData = new byte[1024];
The client firstly connects to the server:
try
{
clientSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
IPAddress ipAddress = IPAddress.Parse(txtServerIP.Text);
IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, 1000);
clientSocket.BeginConnect(ipEndPoint,
new AsyncCallback(OnConnect), null);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "SGSclient",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
Once connected, a login message is sent to the server. And after that, we send a list message to get the names of clients in the chat room.
private void btnSend_Click(object sender, EventArgs e)
{
try
{
Data msgToSend = new Data();
msgToSend.strName = strName;
msgToSend.strMessage = txtMessage.Text;
msgToSend.cmdCommand = Command.Message;
byteData = msgToSend.ToByte();
clientSocket.BeginSend (byteData, 0, byteData.Length,
SocketFlags.None,
new AsyncCallback(OnSend), null);
txtMessage.Text = null;
}
catch (Exception)
{
MessageBox.Show("Unable to send message to the server.",
"SGSclientTCP: " + strName,
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
The message typed by the user is send as a command message to the server which then sends it to all other users in the chat room.
Upon receiving a message, the client processes it accordingly (depending on whether it’s a login, logout, command, or a list message). The code for this is fairly straightforward, kindly see the attached project.