Now that we've defined the basic building blocks for the Talk .NET system, it's time to move ahead and build the server. The TalkServer application has the task of tracking clients and routing messages from one user to another. The core of the application is implemented in the remotable ServerProcess class, which is provided to clients as a Singleton object. A separate module, called Startup, is used to start the TalkServer application. It initializes the Remoting configuration settings, creates and initializes an instance of the FormTraceListener, and displays the trace form modally. When the trace form is closed, the application ends, and the ServerProcess object is destroyed.
The startup code is shown here:
Imports System.Runtime.Remoting Public Module Startup Public Sub Main() ' Create the server-side form (which displays diagnostic information). ' This form is implemented as a diagnostic logger. Dim frmLog As New TraceComponent.FormTraceListener() Trace.Listeners.Add(frmLog) ' Configure the connection and register the well-known object ' (ServerProcess), which will accept client requests. RemotingConfiguration.Configure("TalkServer.exe.config") ' From this point on, messages can be received by the ServerProcess ' object. The object will be created for the first request, ' although you could create it explicitly if desired. ' Show the trace listener form. By using ShowDialog(), we set up a ' message loop on this thread. The application will automatically end ' when the form is closed. Dim frm As Form = frmLog.TraceForm frm.Text = "Talk .NET Server (Trace Display)" frm.ShowDialog() End Sub End Module
When you start the server, the ServerProcess Singleton object isn't created. Instead, it's created the first time a client invokes one of its methods. This will typically mean that the first application request will experience a slight delay, while the Singleton object is created.
The server configuration file is shown here. It includes three lines that are required if you want to run the Talk .NET applications under .NET 1.1 (the version of .NET included with Visual Studio .NET 2003). These lines enable full serialization, which allows the TalkServer to use the ITalkClient reference. If you are using .NET 1.0, these lines must remain commented out, because they will not be recognized. .NET 1.0 uses a slightly looser security model and allows full serialization support by default.
<configuration> <system.runtime.remoting> <application name="TalkNET"> <service> <wellknown mode="Singleton" type="TalkServer.ServerProcess, TalkServer" objectUri="TalkServer" /> </service> <channels> <channel port="8000" 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>
Most of the code for the ServerProcess class is contained in the methods implemented from the ITalkServer interface. The basic outline is shown here:
Public Class ServerProcess Inherits MarshalByRefObject Implements ITalkServer ' Tracks all the user aliases, and the "network pointer" needed ' to communicate with them. Private ActiveUsers As New Hashtable() Public Sub AddUser(ByVal [alias] As String, ByVal client As ITalkClient) _ Implements TalkComponent.ITalkServer.AddUser ' (Code omitted.) End Sub Public Sub RemoveUser(ByVal [alias] As String) _ Implements TalkComponent.ITalkServer.RemoveUser ' (Code omitted.) End Sub Public Function GetUsers() As System.Collections.ICollection _ Implements TalkComponent.ITalkServer.GetUsers ' (Code omitted.) End Function <System.Runtime.Remoting.Messaging.OneWay()> _ Public Sub SendMessage(ByVal senderAlias As String, _ ByVal recipientAlias As String, ByVal message As String) _ Implements TalkComponent.ITalkServer.SendMessage ' (Code omitted.) End Sub End Class
You'll see each method in more detail in the next few sections.
The Hashtable is a key/value collection (unlike some collections, which do not require keys). This allows you to associate two pieces of information: the user name and a network reference to the client.
The Hashtable is optimized for quick key-based lookup. This is ideal, because users send messages based on the user's name. The server can speedily retrieve the client's location information.
The Hashtable allows easy synchronization for thread-safe programming. We'll look at these features in the next chapter.
The collection stores ITalkClient references, indexed by user name. Technically, the ITalkClient reference really represents an instance of the System.Runtime.Remoting.ObjRef class. This class is a kind of network pointer—it contains all the information needed to generate a proxy object to communicate with the client, including the client channel, the object type, and the computer name. This ObjRef can be passed around the network, thus allowing any other user to locate and communicate with the client.
Following are the three collection-related methods that manage user registration. They're provided by the server.
Public Sub AddUser(ByVal [alias] As String, ByVal client As ITalkClient) _ Implements TalkComponent.ITalkServer.AddUser Trace.Write("Added user '" & [alias] & "'") ActiveUsers([alias]) = client End Sub Public Sub RemoveUser(ByVal [alias] As String) _ Implements TalkComponent.ITalkServer.RemoveUser Trace.Write("Removed user '" & [alias] & "'") ActiveUsers.Remove([alias]) End Sub Public Function GetUsers() As System.Collections.ICollection _ Implements TalkComponent.ITalkServer.GetUsers Return ActiveUsers.Keys End Function
The AddUser() method doesn't check for duplicates. If the specified user name doesn't exist, a new entry is created. Otherwise, any entry with the same key is overwritten. The next chapter introduces some other ways to handle this behavior, but in a production application, you would probably want to authenticate users against a database with password information. This allows you to ensure that each user has a unique user name. If a user were to log in twice in a row, only the most recent connection information would be retained.
Note that only one part of the collection is returned to the client through the GetUsers() method: the user names. This prevents a malicious client from using the connection information to launch attacks against the peers on the system. Of course, this approach isn't possible in a decentralized peer-to-peer situation (wherein peers need to interact directly), but in this case, it's a realistic level of protection to add.
The process of sending a message requires slightly more work. The server performs most of the heavy lifting in the SendMessage() method, which looks up the appropriate client and invokes its ReceiveMessage() method to deliver the message. If the recipient cannot be found (probably because the client has recently disconnected from the network), an error message is sent to the message sender by invoking its ReceiveMessage() method. If neither client can be found, the problem is harmlessly ignored.
Public Sub SendMessage(ByVal senderAlias As String, _ ByVal recipientAlias As String, ByVal message As String) _ Implements TalkComponent.ITalkServer.SendMessage ' Deliver the message. Dim Recipient As ITalkClient If ActiveUsers.ContainsKey(recipientAlias) Then Trace.Write("Recipient '" & recipientAlias & "' found") Recipient = CType(ActiveUsers(recipientAlias), ITalkClient) Else ' User wasn't found. Try to find the sender. If ActiveUsers.ContainsKey(senderAlias) Then Trace.Write("Recipient '" & recipientAlias & "' not found") Recipient = CType(ActiveUsers(senderAlias), ITalkClient) message = "'" & message & "' could not be delivered." senderAlias = "Talk .NET" Else Trace.Write("Recipient '" & recipientAlias & "' and sender '" & _ senderAlias & "' not found") ' Both sender and recipient weren't found. ' Ignore this message. End If End If Trace.Write("Delivering message to '" & recipientAlias & "' from '" & _ senderAlias & "'") If Not Recipient Is Nothing Then Dim callback As New ReceiveMessageCallback( _ AddressOf Recipient.ReceiveMessage) callback.BeginInvoke(message, senderAlias, Nothing, Nothing) End If End Sub
You'll see that the server doesn't directly call the ClientProcess.ReceiveMessage() method because this would stall the thread and prevent it from continuing other tasks. Instead, it makes the call on a new thread by using the BeginInvoke() method provided by all delegates. It's possible to use a server-side callback to determine when this call completes, but in this case, it's not necessary.
This completes the basic framework for the TalkServer application. The next step is to build a client that can work with the server to send instant messages around the network.