Part 1 of the three-part .NET Remoting and Event Handling in VB .NET series introduced a Singleton remote server with published events as a text-chat server. The basic idea is that all clients connect to and register with the single server. When a client sends a message, the server broadcasts the message to all of the connected clients. Part 2 continues the tutorial with an implementation of the client. It completes the remotable, shared client code, as well as the client application. (Part 1 was published in December 2004.)
Implement the Remotable Part of the Client
When a server needs to talk back to a client, a role reversal happens: The server sort of assumes the role of client and the client assumes the role of server. In terms of implementation, this means both client and server must have a remotable piece. Unlike one-way remoting (or Web services), the remote server services the requests the client makes, but it is not connected to the client.
Consider delegates for a moment. A delegate is really just a list of function pointers. A function pointer is just the address of a method. When an event is raised, what really happens is that a method or methods are called. The event spawns an indirect method call. For remote servers to call client methods, the remote server needs to know about a function that lives on a client. In practice, remoting marshals the pointers to client methods through proxy objects just as method calls to remote servers are.
The .NET Framework handles all of the plumbing for you, whether you are implementing a remotable client or server. All you need to do is define your clients so that they inherit from MarshalByRefObject. Listing 1 shows you what the header might look like for a remotable client:
Listing 1: Remotable Clients Must Inherit from MarshalByRefObject Too
Imports System.Runtime.Remoting Imports System.Security.Principal Imports System.Runtime.Remoting.Lifetime Imports Softconcepts.ApplicationBlocks.RadioPattern Public Class Client Inherits MarshalByRefObject //...
Both client and server have to see the definition of this object for both ends of a two-way remoting solution to work. For this reason, the assembly containing the Client class above has to be deployed on both client PCs and the server machine. (There are other ways to share definitions. For example, you can define an interface and deploy it to the server, and then implement the Client class as a realization of the interface—in other words, inherit from the interface.) To satisfy client and server visibility, I placed the Client class in the SharedCode.dll shared assembly and referenced that assembly from both client and server applications.
Configure the client
Like the server, the client needs configuration. Essentially, you need to tell the client how to connect to the server. As previously mentioned, you can configure the clients and servers programmatically or in the App.config file. This example uses the configuration file.
The most important part of the configuration is to specify the <wellknown> tag, which includes the object you want to create, and the URL of the remote server:
Listing 2: The Client’s App.config File
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <channels> <channel ref="http" port="0"> <clientProviders> <formatter ref="binary" /> </clientProviders> <serverProviders> <formatter ref="binary" typeFilterLevel="Full" /> </serverProviders> </channel> </channels> <client> <wellknown type="SharedCode.Chatter, SharedCode" url="http://localhost:6007/Chatter.soap" /> </client> </application> </system.runtime.remoting> <appSettings> <add key="user" value="Your Name Here!" /> <add key="echo" value="true" /> </appSettings> </configuration>
All of the elements are required, but the <wellknown> tag describes where the server is, which port to talk to it on, the name of the assembly, and the namespace that contains the remotable objects. The type attribute contains the namespace followed by the assembly name, SharedCode. The configuration employs .NET and its reflection capabilities to dynamically load the assembly. The URL attribute contains the host, port, and URI (chatter.soap) of the remote server.
The rest of the App.config file contains channel formatter information. Microsoft added quite a bit of these other elements in .NET 1.1. You can use the rest of the code as is in many instances and use the .NET help information to fill in any details.
Implement the send and receive behaviors
The send behavior is a call to the remote server to send out messages to all of the connected clients. The receive behavior is the event handler that receives messages from the server. These methods are straightforward, and while they depend on the .NET remoting plumbing, they are easy to implement:
Listing 3: Sending and Receiving Messages in the Chat Sample
Public Shared Sub Send(ByVal sender As String, _ ByVal message As String) Try Instance.FChatter.Send(sender, message) Catch Console.WriteLine("Not connected") End Try End Sub Public Sub OnMessageEvent(ByVal Sender As Object, _ ByVal e As ChatEventArgs) If (Not IsSelf(e.Sender)) Then Broadcaster.Broadcast(Environment.NewLine) Broadcaster.Broadcast("{0} said: {1}", e.Sender, e.Message) Broadcaster.Broadcast("chat>") End If End Sub
Send attempts to invoke the Chatter.Send method. If it fails, you write the output to the Console. In a real-world application, you may handle the exception some other way.
The OnMessageEvent method matches the signature of the delegate defined by the remote server. When it is called—the server has raised the event—you broadcast the message. You could just handle the message here, but this example uses the Observer pattern to loosen the coupling between the Client class and any presentation layer you might elect to place on top of it. (Part 3 will examine the implementation of the Broadcaster in greater detail.)
Ultimately, just make a mental note that Send pushes the message out to the remote server and OnMessageEvent plays the role of receiver.
Connect to the remote server
Listing 4 shows the complete Client class’s implementation.
Listing 4: The Shared Client Class Implementation
Imports System.Runtime.Remoting Imports System.Security.Principal Imports System.Runtime.Remoting.Lifetime Imports Softconcepts.ApplicationBlocks.RadioPattern Public Class Client Inherits MarshalByRefObject Implements IDisposable Private FChatter As Chatter Private Shared FClient As Client = Nothing Private Sub New() RemotingConfiguration.Configure("client.exe.config") FChatter = New Chatter AddHandler FChatter.MessageEvent, AddressOf OnMessageEvent End Sub Public Sub Dispose() Implements System.IDisposable.Dispose RemoveHandler FChatter.MessageEvent, AddressOf OnMessageEvent End Sub Public Shared Sub Shutdown() Try FClient.Dispose() GC.SuppressFinalize(FClient) Catch End Try End Sub Private Shared ReadOnly Property Instance() As Client Get If (FClient Is Nothing) Then FClient = New Client End If Return FClient End Get End Property Public Shared Sub Send(ByVal sender As String, _ ByVal message As String) Try Instance.FChatter.Send(sender, message) Catch Console.WriteLine("Not conencted") End Try End Sub Public Shared Sub ShowHistory() Try Instance.FChatter.ShowHistory() Catch Console.WriteLine("Not connected") End Try End Sub Public Overrides Function InitializeLifetimeService() As Object Return Nothing End Function Public Sub OnMessageEvent(ByVal Sender As Object, _ ByVal e As ChatEventArgs) If (Not IsSelf(e.Sender)) Then Broadcaster.Broadcast(Environment.NewLine) Broadcaster.Broadcast("{0} said: {1}", e.Sender, e.Message) Broadcaster.Broadcast("chat>") End If End Sub Private Function IsSelf(ByVal sender As String) As Boolean #If DEBUG Then Return False #Else Return sender = WindowsIdentity.GetCurrent().Name #End If End Function End Class