Extend Your Instant Messenger Application with FTP Support and Private Chat

n part 1of this article, you saw how to build your own instant messenger application that allows many users to chat simultaneously. While the application is interesting, it is not very flexible as you cannot choose the user(s) you want to chat with; all messages are broadcasted to everyone in the chat.

Building on the earlier article, I’ll show you how to enhance the application to allow private chats between selected users. You will also build FTP support into the application so that you can transfer files between users.

Defining Your Own Protocol
In order to enhance the chat application, you have to define your own protocol for the various functions. For example, when you want to chat with someone, you need to indicate the user name to the server so that only messages destined for this user are sent to him. Similarly, when you need to perform a file transfer, there must be several handshaking processes to ensure that the recipient explicitly accepts the file transfer before it is sent.

This section describes the communication between the users and the server.

Logging in
User1 signs in to the server:

  • User1 sends [Join][User1] to the server indicating its presence
  • Server broadcast [Join][User1] to all the users currently connected

Requesting users’ names
When User1 logs in to the server, he needs to know who is currently online:

  • User1 sends [Usrs] to the server, asking for a list of users currently online
  • Server sends back to User1 [Usrs][User1,User2,UserN,] containing a list of all user names

Chatting
User1 wants to chat with User2 and User3:

  • User1 sends [Talk][User2,User3,]User1>Hello! to server
  • Server sends [Talk][User2,User3,]User1>Hello! to both User2 and User3

File transfer
User1 (IP address 1.2.3.4) wants to send a file named File1.txt to User2 (IP address 3.4.5.6):

  • User1 sends [File][User1,User2,][File1.txt] to server
  • Server sends [File][User1][File1.txt] to User2 to confirm if he wants to receive the file
  • If User2 responds with Yes, server sends [Send_File][User1, User2] to server indicating that it wishes to receive the file
  • User2 starts to listen at port number 501 for incoming data
  • Server looks up the IP address of User2 and sends [Send_File][3.4.5.6] to User1
  • User1 starts the FTP service by using the IP address (3.4.5.6), port number 501.
Author’s Note: Note that for file transfer, the actual transferring of files takes place between the clients; the server is not involved.

Leaving a chat
User1 signs out of the chat:

  • User1 sends [Left][User1] to the server
  • Server will broadcast [Left][User1] to all users

Feature Walkthrough
Before you learn how to write the chat application in this article, I’ll do a walkthrough of its features.

When you log in to the server, a list of online users will be shown on the ListBox (see the left of Figure 1).


Figure 1. Logging In: The server will display a list of logged in users in the left frame.
?
Figure 2. Chatting with One User: The image shows the beginning of a chat between two users.

To chat with a user, simply select the user you want to chat with, type your message, and click the Send button to send the message (see Figure 2).

To chat with multiple users, control-click on the users? names in the ListBox control (see Figure 3).


Figure 3. Chatting with Multiple Users: The image shows a third user brought into the chat.
?
Figure 4. Sending a File: Users browse for the file they wish to send to another user.

To send a file to another user, select the recipient?s name and click on the Send File button. Then select the file you want to send and click Open (see Figure 4).

The recipient will receive a prompt requesting to download the file. If he clicks Yes, the file is downloaded (see Figure 5).


Figure 5. Prompting for Download: A dialog gives the file recipient the opportunity to accept or deny the file download.
?
Figure 6. Making Progress: File recipients can see how much of the file has downloaded until the download is complete.

As the file is downloaded, the status bar will display the number of bytes received (see Figure 6).

Building the Server
There are two components in this chat application: server and client. For the server, I will create a console application project using Visual Studio 2005 beta 2. Name the project “Server.”

Editor’s Note: While we originally reported this project as suitable for Visual Studio 2003, we have learned that one line of code causes a problem in that version. We regret the error.

In the default Module1.vb, populate it with the following:

Imports System.Net.SocketsModule Module1    Const portNo As Integer = 500    Dim localAdd As System.Net.IPAddress = _    System.Net.IPAddress.Parse("10.0.1.4")    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 from each client connecting to the server. Add a new Class to your project in Visual Studio 2005 and name it ChatClient.vb.

First, import the following 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:

'---class to contain information of each clientPublic 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

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).

'---when a client is connectedPublic 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 - 1)    _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).

In this function, you check the various message formats sent from the client and take the appropriate action. For example, if the client initiates a FTP request, you need to repackage the message (as described in the earlier section “Protocol Description”) and send it to the recipient. Listing 1 shows the full code for the ReceiveMessage() function.

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

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

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

'---broadcast message to selected usersPublic Sub Broadcast(ByVal message As String, _   ByVal users() As String)            If users Is Nothing Then        '---broadcasting to everyone        Dim c As DictionaryEntry        For Each c In AllClients            '---broadcast message to all users            CType(c.Value, _               ChatClient).SendMessage(message & vbCrLf)        Next    Else        '---broadcasting to selected ones        Dim c As DictionaryEntry        For Each c In AllClients            Dim user As String            For Each user In users                If CType(c.Value, ChatClient). _                _ClientNick = user Then                    '---send message to user                    CType(c.Value, ChatClient). _                    SendMessage(message & vbCrLf)                    '---log it locally                    Console.WriteLine("sending -----> " _                    & message)                    Exit For                End If            Next        Next    End IfEnd 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 Figure 7 and note the following:

  • Set the Multiline property of txtMessageHistory to True and the ReadOnly property to True.
  • Set the SelectionMode property of lstUsers to MultiExtended.
Figure 7. Adding Controls: Populate the Windows Form with the various controls shown in the screen shot.

Double-click on the form to switch to the code behind. Import the following namespaces:

Imports System.Net.SocketsImports System.IO

Within the Form1 class, define the following variables and constants:

    '---get own IP address    Dim ips As Net.IPHostEntry = _       Net.Dns.Resolve(Net.Dns.GetHostName())    '---port nos and server IP address    Const PORTNO As Integer = 500    Const FTPPORTNO As Integer = 501    Const SERVERIP As String = "10.0.1.4"    Dim client As TcpClient    '--used for sending and receiving data    Dim data() As Byte    '---for FTP use     Dim fs As System.IO.FileStream    Dim filename As String    Dim fullfilename As String

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 begins reading data from the server asynchronously and changes the name of the Sign In button to “Sign Out.” It will also ask for the list of names of users currently logged in.

When the user signs out of the chat application, you invoke the Disconnect() subroutine (defined shortly).

'--Sign in to server---Private Sub btnSignIn_Click( _   ByVal sender As System.Object, _   ByVal e As System.EventArgs) _   Handles btnSignIn.Click    If btnSignIn.Text = "Sign In" Then        '---Sign in to the server        Try            client = New TcpClient            ' client.NoDelay = True            '---connect to the server            client.Connect(SERVERIP, PORTNO)            ReDim data(client.ReceiveBufferSize - 1)            '---inform the server of your nick name---            ' e.g. [Join][User1]            SendMessage("[Join][" & txtNick.Text & "]")            '---begin reading data asynchronously from             'the server            client.GetStream.BeginRead( _               data, 0, CInt(client.ReceiveBufferSize), _               AddressOf ReceiveMessage, Nothing)            '---change the button and textbox            btnSignIn.Text = "Sign Out"            btnSend.Enabled = True            txtNick.Enabled = False            '---get all users connected            ' e.g. [Usrs]            System.Threading.Thread.Sleep(500)            SendMessage("[Usrs]")        Catch ex As Exception            MsgBox(ex.ToString)        End Try    Else        '---Sign off from the server        Disconnect()        lstUsers.Items.Clear()        '---change the button and textbox        btnSignIn.Text = "Sign In"        btnSend.Enabled = False        txtNick.Enabled = True    End IfEnd Sub

The Send button sends a message to the server. Note that you need to select a user in the ListBox before you can send a message.

'---Send ButtonPrivate Sub btnSend_Click( _   ByVal sender As System.Object, _   ByVal e As System.EventArgs) _   Handles btnSend.Click    ' e.g. [Talk][User2,User3,etc]User1>Hello world!    '---select users to chat    If lstUsers.SelectedItems.Count < 1 Then        MsgBox("You must select who to chat with.")        Exit Sub    End If    '---formulate the message    Dim Message As String = "[Talk]["    '---check who to chat with    Dim user As Object    For Each user In lstUsers.SelectedItems        Message += user & ","    Next    Message += "]" & txtNick.Text & ">" & txtMessage.Text    '---update the message history    txtMessageHistory.Text += txtNick.Text & _    ">" & txtMessage.Text & vbCrLf    '---send message    SendMessage(Message)    txtMessage.Clear()End Sub

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

'---Sends the message to the serverPublic Sub SendMessage(ByVal message As String)    Try        '---send the text        Dim ns As System.Net.Sockets.NetworkStream        SyncLock client.GetStream            ns = client.GetStream            Dim bytesToSend As Byte() = _            System.Text.Encoding. _            ASCII.GetBytes(message)            '---sends the text---            ns.Write(bytesToSend, 0, bytesToSend.Length)            ns.Flush()        End SyncLock    Catch ex As Exception        MsgBox(ex.ToString)    End TryEnd 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.

'---Receives a message from the serverPublic Sub ReceiveMessage(ByVal ar As IAsyncResult)    Try        Dim bytesRead As Integer        bytesRead = client.GetStream.EndRead(ar)        If bytesRead < 1 Then            Exit Sub        Else            Dim messageReceived As String = _            System.Text.Encoding.ASCII.GetString( _            data, 0, bytesRead)            '---update the message history            Dim para() As Object = {messageReceived}            Me.Invoke(New delUpdateHistory(AddressOf _            Me.UpdateHistory), para)        End If       '---continue reading for more data       client.GetStream.BeginRead(data, 0, _       CInt(client.ReceiveBufferSize), _       AddressOf ReceiveMessage, Nothing)    Catch ex As Exception        ' MsgBox(ex.ToString)    End TryEnd Sub

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

'---delegate to update the textboxes in the main threadPublic Delegate Sub delUpdateHistory(ByVal str As String)

In the UpdateHistory() subroutine, you examine the message format and perform the appropriate action. For example, if the user has left a chat (through the [Left] message), you must remove the user name from your ListBox. Listing 2 shows the code for the UpdateHistory() subroutine.

When the Send File button is clicked, check to see that a recipient user is selected and then prompt the user to select a file to send (see Listing 3).

The FTP_Send subroutine (see Listing 4) sends a file to the recipient through the TCP port 501. It sends files in blocks of 8192 bytes (the maximum buffer size).

The FTP_Receive subroutine (see Listing 5) receives an incoming file through TCP port 501. It writes the file to the c: emp directory.

When the form is closed (by clicking on the "X" button on the window), disconnect the client from the server. This code handles the form close action:

Private Sub Form1_FormClosing( _  ByVal sender As Object, _  ByVal e As System.Windows.Forms.FormClosingEventArgs) _  Handles Me.FormClosing    Disconnect()End Sub

The Disconnect() subroutine handles the actual disconnection of the client from the server:

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

Testing the Application
In the interest of simplicity I have assumed that data sent over the TCP stream are sent and received in the same block. However, this is not always true. Data sent over the TCP stream are not guaranteed to arrive at once; you may receive a portion of the message in the current read cycle and receive the rest in the next read cycle, or several messages may be read at the same time.

In this case, you need to modify your application so that you are able to differentiate the different messages sent by the user.

To test the application, 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 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 have shown how you can define your own protocols to build a robust chat application. It's a fun application that will also help you learn to do some sophisticated tasks using network programming in .NET.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: