The client portion of Talk .NET is called TalkClient. It's designed as a Windows application (much like Microsoft's Windows Messenger). It has exactly two responsibilities: to allow the user to send a message to any other online user and to display a log of sent and received messages.
When the TalkClient application first loads, it executes a startup procedure, which presents a login form and requests the name of the user that it should register. If one isn't provided, the application terminates. Otherwise, it continues by taking two steps:
It creates and shows the main chat form, named Talk, around which most of the application revolves.
The startup code is shown here:
Public Class Startup Public Shared Sub Main() ' Create the login window (which retrieves the user identifier). Dim frmLogin As New Login() ' Only continue if the user successfully exits by clicking OK ' (not the Cancel or Exit button). If frmLogin.ShowDialog() = DialogResult.OK Then ' Create the new remotable client object. Dim Client As New ClientProcess(frmLogin.UserName) ' Create the client form. Dim frm As New Talk() frm.TalkClient = Client ' Show the form. frm.ShowDialog() End If End Sub End Class
On startup, the ClientProcess object registers the user with the coordination server. Because ClientProcess is a remotable type, it will remain accessible to the server for callbacks throughout the lifetime of the application. These call-backs will, in turn, be raised to the user interface through local events. We'll dive into this code shortly.
The login form (shown in Figure 4-3) is quite straightforward. It exposes a public UserName property, which allows the Startup routine to retrieve the user name without violating encapsulation. This property could also be used to pre-fill the txtUser textbox by retrieving the previously used name, which could be stored in a configuration file or the Windows registry on the current computer.
Public Class Login Inherits System.Windows.Forms.Form ' (Designer code omitted.) Private Sub cmdExit_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdExit.Click Me.Close() End Sub Public Property UserName() Get Return txtUser.Text End Get Set(ByVal Value) txtUser.Text = UserName End Set End Property End Class
The ClientProcess class does double duty. It allows the TalkClient to interact with the TalkServer to register and unregister the user or send a message destined for another user. The ClientProcess also receives callbacks from the TalkServer and forwards these to the TalkClient through an event. In the Talk .NET system, the only time the TalkServer will call the ClientProcess is to deliver a message sent from another user. At this point, the ClientProcess will forward the message along to the user interface by raising an event. Because the server needs to be able to call ClientProcess.ReceiveMessage() across the network, the ClientProcess class must inherit from MarshalByRefObject. ClientProcess also implements ITalkClient.
Here's the basic outline for the ClientProcess class. Note that the user name is stored as a member variable named _Alias, and exposed through the public property Alias. Because alias is a reserved keyword in VB .NET, you will have to put this word in square brackets in the code.
Imports System.Runtime.Remoting Imports TalkComponent Public Class ClientProcess Inherits MarshalByRefObject Implements ITalkClient ' This event occurs when a message is received. ' It's used to transfer the message from the remotable ' ClientProcess object to the Talk form. Event MessageReceived(ByVal sender As Object, _ ByVal e As MessageReceivedEventArgs) ' The reference to the server object. ' (Technically, this really holds a proxy class.) Private Server As ITalkServer ' The user ID for this instance. Private _Alias As String Public Property [Alias]() As String Get Return _Alias End Get Set(ByVal Value As String) _Alias = Value End Set End Property Public Sub New(ByVal [alias] As String) _Alias = [alias] End Sub ' This override ensures that if the object is idle for an extended ' period, waiting for messages, it won't lose its lease and ' be garbage collected. Public Overrides Function InitializeLifetimeService() As Object Return Nothing End Function Public Sub Login() ' (Code omitted.) End Sub Public Sub LogOut() ' (Code omitted.) End Sub Public Sub SendMessage(ByVal recipientAlias As String, _ ByVal message As String) ' (Code omitted.) End Sub Private Sub ReceiveMessage(ByVal message As String, _ ByVal senderAlias As String) Implements ITalkClient.ReceiveMessage ' (Code omitted.) End Sub Public Function GetUsers() As ICollection ' (Code omitted.) End Function End Class
The InitializeLifetimeService() method must be overridden to preserve the life of all ClientProcess objects. Even though the startup routine holds a reference to a ClientProcess object, the ClientProcess object will still disappear from the network after its lifetime lease expires, unless you explicitly configure an infinite lifetime. Alternatively, you can use configuration file settings instead of overriding the InitializeLifetimeService() method, as described in the previous chapter.
One other interesting detail is found in the ReceiveMessage() method. This method is accessible remotely to the server because it implements ITalkClient.ReceiveMessage. However, this method is also marked with the Private keyword, which means that other classes in the TalkClient application won't accidentally attempt to use it.
The Login() method configures the client channel, creates a proxy to the server object, and then calls the ServerProcess.AddUser() method to register the client. The Logout() method simply unregisters the user, but it doesn't tear down the Remoting channels—that will be performed automatically when the application exits. Finally, the GetUsers() method retrieves the user names of all the users currently registered with the coordination server.
Public Sub Login() ' Configure the client channel for sending messages and receiving ' the server callback. RemotingConfiguration.Configure("TalkClient.exe.config") ' You could accomplish the same thing in code by uncommenting ' the following two lines: ' Dim Channel As New System.Runtime.Remoting.Channels.Tcp.TcpChannel(0) and ' ChannelServices.RegisterChannel(Channel). ' Create the proxy that references the server object. Server = CType(Activator.GetObject(GetType(ITalkServer), _ "tcp://localhost:8000/TalkNET/TalkServer"), ITalkServer) ' Register the current user with the server. ' If the server isn't running, or the URL or class information is ' incorrect, an error will most likely occur here. Server.AddUser(_Alias, Me) End Sub Public Sub LogOut() Server.RemoveUser(_Alias) End Sub Public Function GetUsers() As ICollection Return Server.GetUsers() End Function
Following is the client configuration, which only specified channel information. The client port isn't specified and will be chosen dynamically from the available ports at runtime. As with the server configuration file, you must enable full serialization if you are running the Talk .NET system with .NET 1.1. Otherwise, the TalkClient will not be allowed to transmit the ITalkClient reference over the network to the server.
<configuration> <system.runtime.remoting> <application> <channels> <channel port="0" ref="tcp" > <!-- If you are using .NET 1.1, uncomment the lines below. --> <!-- <serverProviders> <formatter ref="binary" typeFilterLevel="Full" /> </serverProviders> --> </channel> </channels> </application> </system.runtime.remoting> </configuration>
You'll notice that the Login() method mingles some dynamic Remoting code (used to create the TalkServer instance) along with a configuration file (used to create the client channel). Unfortunately, it isn't possible to rely exclusively on a configuration file when you use interface-based programming with Remoting. The problem is that the client doesn't have any information about the server, only an interface it supports. The client thus cannot register the appropriate object type and create it directly because there's no way to instantiate an interface. The previous solution, which uses the Activator.GetObject() method, forces you to include several distribution details in your code. This means that if the object is moved to another computer or exposed through another port, you'll need to recompile the code.
You can resolve this problem in several ways. One option is simply to add a custom configuration setting with the full object URI. This will be an application setting, not a Remoting setting, so it will need to be entered in the <appSettings> section of the client configuration file, as shown here:
<configuration> <appSettings> <add key="TalkServerURL" value="tcp://localhost:8000/TalkNET/TalkServer" /> </appSettings>< <system.runtime.remoting> <application> <channels> <channel port="0" ref="tcp" > <!-- If you are using .NET 1.1, uncomment the lines below. --> <!-- <serverProviders> <formatter ref="binary" typeFilterLevel="Full" /> </serverProviders> --> </channel> </channels> </application> </system.runtime.remoting> </configuration>
You can then retrieve this setting using the ConfigurationSettings.AppSettings collection:
Server = CType(Activator.GetObject(GetType(ITalkServer), _ ConfigurationSettings.AppSettings("TalkServer")), ITalkServer)
Note that in this example, we use the loopback alias localhost, indicating that the server is running on the same computer. You should replace this value with the name of the computer (if it's on your local network), the domain name, or the IP address where the server component is running.
The last ingredient is the ClientProcess methods for sending and receiving messages. The following code shows the SendMessage() and ReceiveMessage() methods. The SendMessage() simply executes the call on the server and the ReceiveMessage() raises a local event for the client, which will be handled by the Talk form.
Public Sub SendMessage(ByVal recipientAlias As String, ByVal message As String) Server.SendMessage(_Alias, recipientAlias, message) End Sub Private Sub ReceiveMessage(ByVal message As String, _ ByVal senderAlias As String) Implements ITalkClient.ReceiveMessage RaiseEvent MessageReceived(Me, New MessageReceivedEventArgs(message, _ senderAlias)) End Sub
Public Class MessageReceivedEventArgs Inherits EventArgs Public Message As String Public SenderAlias As String Public Sub New(ByVal message As String, ByVal senderAlias As String) Me.Message = message Me.SenderAlias = senderAlias End Sub End Class
The Talk form is the front-end that the user interacts with. It has four key tasks:
Log the user in when the form loads and log the user out when the form closes.
Periodically refresh the list of active users by calling ClientProcess.GetUsers(). This is performed using a timer.
Invoke ClientProcess.SendMessage() when the user sends a message.
Handle the MessageReceived event and display the corresponding information on the form.
The form is shown in Figure 4-4. Messages are recorded in a RichTextBox, which allows the application of formatting, if desired. The list of clients is maintained in a ListBox.
Public Class Talk Inherits System.Windows.Forms.Form ' (Designer code omitted.) ' The remotable intermediary for all client-to-server communication. Public WithEvents TalkClient As ClientProcess Private Sub Talk_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Me.Text &= " - " & TalkClient.Alias ' Attempt to register with the server. TalkClient.Login() ' Ordinarily, a user list is periodically fetched from the ' server. In this case, the code enables the timer and calls it ' once (immediately) to initially populate the list box. tmrRefreshUsers_Tick(Me, EventArgs.Empty) tmrRefreshUsers.Enabled = True lstUsers.SelectedIndex = 0 End Sub Private Sub TalkClient_MessageReceived(ByVal sender As Object, _ ByVal e As MessageReceivedEventArgs) Handles TalkClient.MessageReceived txtReceived.Text &= "Message From: " & e.SenderAlias txtReceived.Text &= " delivered at " & DateTime.Now.ToShortTimeString() txtReceived.Text &= Environment.NewLine & e.Message txtReceived.Text &= Environment.NewLine & Environment.NewLine End Sub Private Sub cmdSend_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdSend.Click ' Display a record of the message you're sending. txtReceived.Text &= "Sent Message To: " & lstUsers.Text txtReceived.Text &= Environment.NewLine & txtMessage.Text txtReceived.Text &= Environment.NewLine & Environment.NewLine ' Send the message through the ClientProcess object. Try TalkClient.SendMessage(lstUsers.Text, txtMessage.Text) txtMessage.Text = "" Catch Err As Exception MessageBox.Show(Err.Message, "Send Failed", _ MessageBoxButtons.OK, MessageBoxIcon.Exclamation) End Try End Sub ' Checks every 30 seconds. Private Sub tmrRefreshUsers_Tick(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles tmrRefreshUsers.Tick ' Prepare list of logged-in users. ' The code must copy the ICollection entries into ' an ordinary array before they can be added. Dim UserArray() As String Dim UserCollection As ICollection = TalkClient.GetUsers ReDim UserArray(UserCollection.Count - 1) UserCollection.CopyTo(UserArray, 0) ' Replace the list entries. At the same time, ' the code will track the previous selection and try ' to restore it, so the update won't be noticeable. Dim CurrentSelection As String = lstUsers.Text lstUsers.Items.Clear() lstUsers.Items.AddRange(UserArray) lstUsers.Text = CurrentSelection End Sub Private Sub Talk_Closed(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles MyBase.Closed TalkClient.LogOut() End Sub End Class
The timer fires and refreshes the list of user names seamlessly every 30 seconds. In a large system, you would lower this value to ease the burden on the coordinator. For a very large system with low user turnover, it might be more efficient to have the server broadcast user-added and user-removed messages. To support this infrastructure, you would add methods such as ITalkClient.NotifyUserAdded() and ITalkClient.NotifyUserRemoved(). Or you might just use a method such as ITalkClient.NotifyListChanged(), which tells the client that it must contact the server at some point to update its information.
The ideal approach isn't always easy to identify. The goal is to minimize the network chatter as much as possible. In a system with 100 users who query the server every 60 seconds, approximately 100 request messages and 100 response messages will be sent every minute. If the same system adopts user-added and user-removed broadcasting instead, and approximately 5 users join or leave the system in a minute, the server will likely need to send 5 messages to each of 100 users, for a much larger total of 500 messages per minute. The messages themselves would be smaller (because they would not contain the full user list), but the network overhead would probably be great enough that this option would work less efficiently.
In a large system, you might use "buddy lists" so that clients only receive a user list with a subset of the total number of users. In this case, the server broadcast approach would be more efficient because a network exchange would only be required for those users who are on the same list as the entering or departing peer. This reduces the total number of calls dramatically. Overall, this is probably the most sustainable option if you want to continue to develop the Talk .NET application to serve a larger audience.
Because the client chooses a channel dynamically, it's possible to run several instances of the TalkClient on the same computer. After starting the new instances, the user list of the original clients will quickly be refreshed to represent the full user list. You can then send messages back and forth, as shown in Figure 4-5. Clients can also send messages to themselves.
In each case, the coordination server brokers the communication. The trace output for a sample interaction on the server computer is shown in Figure 4-6.