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