Home-brew Your Own Instant Messenger App with Visual Studio .NET

Home-brew Your Own Instant Messenger App with Visual Studio .NET

riting networked applications is one of the most interesting aspects of programming. It is both rewarding and intriguing to see your applications successfully communicating over the network. In this first part of a two-part series on network programming, I am going to build a chat application that works similar to MSN Messenger (or ICQ). Using the chat application, I will illustrate how network programming is done in .NET and the various challenges in building a multi-user chat application.

Figure 1 shows the application that I will build in this article.

In an article that will follow in a few weeks, I will expand on the application built in this article to provide more functionality, such as FTP, private chat, encryption, and more.

Using the TcpClient and TcpListener Classes for Network Communications
Creating a chat application generally involves socket programming, creating a connection between a client and server so that messages can be sent and received by both the client and the server. The System.Net.Sockets namespace provides the functionalities required for Socket programming. I will make use of two classes in the System.Net.Sockets namespace for this article: TcpClient and TcpListener.

Figure 1. A Chat Ahead: The screenshot shows the completed chat application built in this article.

The TcpClient class implements a socket for sending and receiving data using TCP. Because the connection to the remote device is represented as a stream, data can be read and written with .NET Framework stream-handling techniques.

The TcpListener class provides simple methods that listen for and accept incoming connection requests in blocking synchronous mode.

The following code example shows a very simple implementation of a server waiting for an incoming connection:

Imports System.Net.SocketsImports System.TextConst portNo As Integer = 500Dim localAdd As System.Net.IPAddress = _    IPAddress.Parse("")Dim listener As New TcpListener(localAdd, portNo)listener.Start()Dim tcpClient As TcpClient = listener.AcceptTcpClient()Dim ns As NetworkStream = tcpClient.GetStreamDim data(tcpClient.ReceiveBufferSize) As Byte'---read incoming stream; Read() is a blocking call---Dim numBytesRead As Integer = ns.Read(data, 0, _    CInt(tcpClient.ReceiveBufferSize))'---display data received---Console.WriteLine("Received :" & _    Encoding.ASCII.GetString(data, 0, numBytesRead))

To connect to the server and send it a string, the client code would look like this:

Imports System.Net.SocketsImports System.TextConst portNo = 500Dim tcpclient As New TcpClienttcpclient.Connect("", portNo)Dim ns As NetworkStream = tcpclient.GetStreamDim data As Byte() = Encoding.ASCII.GetBytes("Hello")'---send the text---ns.Write(data, 0, data.Length)
Author’s Note: The NetworkStream object works with byte arrays, and hence you need to use the Encoding.ASCII.GetString() and Encoding.ASCII.GetBytes() methods from the System.Text namespace to convert the byte array to string and vice versa.

The example above is relatively simple. But the problem becomes much more pronounced when the server needs to communicate with multiple clients and be able to both send and receive messages from clients, all at the same time. To do so:

  • The server must be able to create connections to multiple clients;
  • The server must be able to asynchronously read data from the client and be able to send messages to the client at any time;
  • The client must be able to asynchronously read data from the server and be able to send messages to the server at any time.

The rest of this article will address these three problems.

Building the ServerThere are two components in our chat application: server and client. I’ll start by building the server. For the server, I will create a console application project using Visual Studio 2005 beta 2 (you can also use Visual Studio .NET 2003). Name the project ‘Server.’

In the default Module1.vb, I’ll first import the System.Net.Sockets namespace; it contains all the relevant classes for my chat application.

Imports System.Net.Sockets

Next, I’ll declare a constant containing the port number to use for this application; I’ve selected port number 500. If you have a firewall installed on the server (or client), be sure to open up port 500 for this application to work.

    Const portNo As Integer = 500

I also need to define the local address to listen to and then create an instance of the TcpListen() class to use for listening for connections from TCP clients:

    Dim localAdd As System.Net.IPAddress = _       System.Net.IPAddress.Parse("")    Dim listener As New TcpListener(localAdd, portNo)

In the Main() function, I use the Start() method from the TcpListener class to start listening for incoming connection requests. The AcceptTcpClient() method is a blocking call and execution will not continue until a connection is established. As my server needs to service multiple clients at the same time, I will create an instance of the ChatClient (which I will define shortly) for each user. The server will loop indefinitely, accepting clients as they connect:

    Sub Main()        listener.Start()        While True            Dim user As New _               ChatClient(listener.AcceptTcpClient)        End While    End Sub

The source for Module1.vb looks like this:

Imports System.Net.SocketsModule Module1    Const portNo As Integer = 500    Dim localAdd As System.Net.IPAddress = _       System.Net.IPAddress.Parse("")    Dim listener As New _       System.Net.Sockets.TcpListener(localAdd, portNo)    Sub Main()        listener.Start()        While True            Dim user As New _               ChatClient(listener.AcceptTcpClient)        End While    End SubEnd Module

The next step is to define the ChatClient class. The ChatClient class is used to represent information of each client connecting to the server. Add a new Class to your project in Visual Studio 2005 and name it ChatClient.vb. As usual, the first thing to do is to import the System.Net.Sockets namespace:

Imports System.Net.Sockets

In the ChatClient class, first define the various private members (their uses are described in the comments in the code). You also declare a HashTable object (AllClients) to store a list of all clients connecting to the server. The reason for declaring it as a shared member is to ensure all instances of the ChatClient class are able to obtain a list of all the clients currently connected to the server:

Public Class ChatClient    '---contains a list of all the clients    Public Shared AllClients As New HashTable    '---information about the client    Private _client As TcpClient    Private _clientIP As String    Private _ClientNick As String    '---used for sending/receiving data    Private data() As Byte    '---is the nick name being sent?    Private ReceiveNick As Boolean = True

When a client gets connected to the server, the server will create an instance of the ChatClient class and then pass the TcpClient variable (client) to the constructor of the class. You will also get the IP address of the client and use it as an index to identify the client in the HashTable object. The BeginRead() method will begin an asynchronous read from the NetworkStream object (_client.GetStream) in a separate thread. This allows the server to remain responsive and continue accepting new connections from other clients. When the reading is complete, control will be transferred to the ReceiveMessage() function (which I will define shortly).

    Public Sub New(ByVal client As TcpClient)        _client = client        '---get the client IP address        _clientIP = client.Client.RemoteEndPoint.ToString        '---add the current client to the hash table        AllClients.Add(_clientIP, Me)        '---start reading data from the client in a         '   separate thread        ReDim data(_client.ReceiveBufferSize)        _client.GetStream.BeginRead(data, 0, _           CInt(_client.ReceiveBufferSize), _           AddressOf ReceiveMessage, Nothing)    End Sub

In the ReceiveMessage() function, I first call the EndRead() method to handle the end of an asynchronous read. Here, I check if the number of bytes read is less then 1. If it is, the client has disconnected and you need to remove the client from the HashTable object (using the IP address of the client as an index into the hash table). I also want to broadcast a message to all the clients telling them that this particular client has left the chat. I do this using the Broadcast() function (again, I will define this shortly).

For simplicity, assume that the client will send the nickname of the user the first time it connects to the server. Subsequently, you will just broadcast whatever was sent by the client to everyone. Once this is done, the server will proceed to perform the asynchronous read from the client again.

    Public Sub ReceiveMessage(ByVal ar As IAsyncResult)        '---read from client---        Dim bytesRead As Integer        Try            SyncLock _client.GetStream                bytesRead = _client.GetStream.EndRead(ar)            End SyncLock            '---client has disconnected            If bytesRead " & _                       messageReceived)                End If            End If            '---continue reading from client            SyncLock _client.GetStream                _client.GetStream.BeginRead(data, 0, _                   CInt(_client.ReceiveBufferSize), _                   AddressOf ReceiveMessage, Nothing)            End SyncLock        Catch ex As Exception            AllClients.Remove(_clientIP)            Broadcast(_ClientNick & _               " has left the chat.")        End Try    End Sub

One thing to note in the above code is that you need to use the SyncLock statement to prevent multiple threads from using the NetworkStream object. Without the SyncLock statement your application will be unpredictable, perhaps being subject to frequent crashes. This scenario is likely to occur when your server is connected to multiple clients and all of them are trying to access the NetworkStream object at the same time.

The SendMessage() function allows the server to send a message to the client.

    Public Sub SendMessage(ByVal message As String)        Try            '---send the text            Dim ns As System.Net.Sockets.NetworkStream            SyncLock _client.GetStream                ns = _client.GetStream            End SyncLock            Dim bytesToSend As Byte() = _             System.Text.Encoding.ASCII.GetBytes(message)            ns.Write(bytesToSend, 0, bytesToSend.Length)            ns.Flush()        Catch ex As Exception            Console.WriteLine(ex.ToString)        End Try    End Sub

Finally, the Broadcast() function sends a message to all the clients stored in the AllClients HashTable object.

    Public Sub Broadcast(ByVal message As String)        '---log it locally        Console.WriteLine(message)        Dim c As DictionaryEntry        For Each c In AllClients            '---broadcast message to all users            CType(c.Value, _               ChatClient).SendMessage(message & vbCrLf)        Next    End Sub

Building the Client
Now that the server is built, it is time to build the client. Using Visual Studio 2005, I create a new Windows application (name it WinClient) and populate the default form with the controls shown in Table 1 (see the resulting form in Figure 2).

Table 1. Client Controls

Control(Name) Property Value
Label Text Nick
TextBox Name txtNick
TextBox Name txtMessageHistory
? MultiLine True
? ReadOnly True
TextBox Name txtMessage
Button Name btnSignIn
? Text Sign In
Button Name btnSend
? Text Send

The client application logic is very similar to the server, albeit more straightforward. Double-click on the form to switch to the edit window, and import the following namespace:

Imports System.Net.Sockets

Define the following constant and variables within the form:

    Const portNo As Integer = 500    Dim client As TcpClient    Dim data() As Byte
Figure 2. Adding Controls: Populate the Windows Form with the various controls from Table 1.

When the user signs in, the client first connects to the server and sends the nickname of the user using the SendMessage() subroutine (defined shortly). Then it should begin reading data from the server asynchronously and change the name of the Sign In button to “Sign Out.” When the user signs out from the chat application, you invoke the Disconnect() subroutine (defined shortly).

    Private Sub btnSignIn_Click( _       ByVal sender As System.Object, _       ByVal e As System.EventArgs) _       Handles btnSignIn.Click        If btnSignIn.Text = "Sign In" Then            Try                '---connect to server                client = New TcpClient                client.Connect("", portNo)                ReDim data(client.ReceiveBufferSize)                SendMessage(txtNick.Text)                '---read from server                client.GetStream.BeginRead( _                   data, 0, _                   CInt(client.ReceiveBufferSize), _                   AddressOf ReceiveMessage, Nothing)                btnSignIn.Text = "Sign Out"                btnSend.Enabled = True            Catch ex As Exception                MsgBox(ex.ToString)            End Try        Else            '---disconnect from server            Disconnect()            btnSignIn.Text = "Sign In"            btnSend.Enabled = False        End If    End Sub

When the user clicks on the Send button, the application sends a message to the server:

    Private Sub btnSend_Click( _       ByVal sender As System.Object, _       ByVal e As System.EventArgs) _       Handles btnSend.Click        SendMessage(txtMessage.Text)        txtMessage.Clear()    End Sub

The SendMessage() subroutine, used in the code above, allows the client to send a message to the server:

    Public Sub SendMessage(ByVal message As String)        Try            '---send a message to the server            Dim ns As NetworkStream = client.GetStream            Dim data As Byte() = _             System.Text.Encoding.ASCII.GetBytes(message)            '---send the text---            ns.Write(data, 0, data.Length)            ns.Flush()        Catch ex As Exception            MsgBox(ex.ToString)        End Try    End Sub

The ReceiveMessage() subroutine asynchronously reads data sent from the server in a separate thread. When the data is received, it will display the data in the txtMessageHistory control. As Windows controls are not thread-safe, you need to use a delegate, delUpdateHistory(), to update the controls:

    Public Sub ReceiveMessage(ByVal ar As IAsyncResult)        Try            Dim bytesRead As Integer            bytesRead = client.GetStream.EndRead(ar)            If bytesRead 

The delUpdateHistory() delegate is used to invoke the UpdateHistory() function in the main thread:

    '---delegate and subroutine to update the     '   TextBox control    Public Delegate Sub delUpdateHistory( _       ByVal str As String)    Public Sub UpdateHistory(ByVal str As String)        txtMessageHistory.AppendText(str)    End Sub    Private Sub Form1_FormClosing( _       ByVal sender As Object, _       ByVal e As _          System.Windows.Forms.FormClosingEventArgs) _       Handles Me.FormClosing        Disconnect()    End Sub

Finally, the Disconnect() subroutine disconnects the client from the server:

    Public Sub Disconnect()        '---Disconnect from server        Try            client.GetStream.Close()            client.Close()        Catch ex As Exception            MsgBox(ex.ToString)        End Try    End Sub

Testing the Application
To test the applications, first run the server by pressing F5 in Visual Studio 2005. You want to launch multiple copies of the client to test the multi-user capabilities of the server. To do this, compile the client-side code files provided with this article into an .exe file. Run multiple copies of Winclient.exe and sign in and chat at the same time.

In this article, I've shown how the TcpClient class allows you to perform asynchronous communication between two computers. While the chat application developed in this article is simple, it is a foundation on which to build more complicated chat applications. In my next article I'll do exactly that, adding FTP, private chats, and more to this simple chat application.


Share the Post: