JavaScript Editor JavaScript Editor     JavaScript Debugger 



Team LiB
Previous Section Next Section

The TalkClient

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:

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.

Click To expand
Figure 4-3: The login form

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 Remotable ClientProcess 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

The MessageReceived event makes use of the following custom EventArgs class, which adds the message-specific information:

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

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.

Click To expand
Figure 4-4: The Talk form

The full form code is shown here:

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.

Click To expand
Figure 4-5: Multiple client interaction

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.

Click To expand
Figure 4-6: The server trace display

Team LiB
Previous Section Next Section


JavaScript Editor Free JavaScript Editor     JavaScript Editor