In the One-Way Remoting example, the client always contacts the server. The server can respond to the client through the method return value, but once the method call is finished, the client closes the connection and the server can no longer contact the client. This is not appropriate for a peer-to-peer system, which requires bidirectional communication.
In order to support bidirectional communication, the client must meet three criteria:
It must provide a remotable type (a class that derives from MarshalByRefObject) that the server can call.
It must open a bidirectional Remoting channel, which it will use to listen for calls initiated from the server.
There must be some way to transfer the received information from the remotable client type to the main client application. This can be accomplished in a loosely coupled way by using a local event.
Once these criteria are met, there are several choices for the actual method of communication:
The server can fire an event, which will be delivered by Remoting to the client's remotable object.
The client can create a delegate that points to one of the methods in its remotable object, and then submit this delegate to the server. The server can then trigger the method by invoking the delegate.
The client can create a reference to its local remotable object and pass this reference to the server. The server can then call a method on the local object directly. Alternatively, you could pass the reference as an interface implemented by the remotable object. In either case, the server must know enough about the remotable client object or its interface to be able to call one of its methods.
The first option—using events—requires the least amount of work. Multiple clients can attach event handlers to the same event, and the server doesn't need to worry about who is being contacted when it fires the event. The only consideration is making sure that the EventArgs object is serializable, so that it can leap across application domain boundaries. However, the event-based approach is less practical because it doesn't allow the flexibility for the server to call a specific client. It can also lead to problems if clients disconnect from the network without unregistering their event handlers properly.
The delegate or interface approaches are more flexible. In both cases, the server is in charge of tracking clients (typically by using some sort of collection object), and removing them from the collection when they can no longer be contacted. The instant-messaging example in the next chapter uses an interface-based approach.
The following example uses a similar, yet slightly different approach: a delegate that both the server and client recognize. This project can be found in the TwoWayRemoting directory with the samples for this chapter. This example uses a Windows client. The server (component host) is unchanged.
The first step is to modify the server-side remotable object so that it will attempt to contact the client after a short delay through a callback. It works like this:
The client calls a method in the remote object.
The method sets up a timer and returns.
When the timer ticks, a new message is sent to the client. This requires opening a new connection because the original connection has been closed. This time, the server is acting as a client because it's opening the connection.
Here's the code for our simple example:
Public Delegate Sub ConfirmationCallback(ByVal message As String) Public Class RemoteObject Inherits MarshalByRefObject Private WithEvents tmrCallback As New System.Timers.Timer() Private Callback As ConfirmationCallback Private Message As String Public Sub ReceiveMessage(ByVal message As String, _ ByVal callback As ConfirmationCallback) Me.Callback = callback Me.Message = "Received message: " & message tmrCallback.Interval = 5000 tmrCallback.Start() End Sub Private Sub tmrCallback_Elapsed(ByVal sender As System.Object, _ ByVal e As System.Timers.ElapsedEventArgs) _ Handles tmrCallback.Elapsed tmrCallback.Stop() Callback.Invoke(Message) End Sub End Class
Note |
This simple design isn't suitable for a system that experiences multiple calls in close succession because the ConfirmationCallback and Message values will be overwritten with each new call. Don't worry too much about this limitation now—the next two chapters will explore these limitations in detail and resolve them. |
The RemoteLibrary project also contains the remotable portion of the client, which is a dedicated listener object. This object is created in the client's application domain for the sole purpose of receiving the callback. It raises a local event so the client application can become notified of the callback. This is a common pattern in peer-to-peer systems with Remoting, and you'll see it again in the next chapter.
Public Class Listener Inherits MarshalByRefObject Public Event CallbackReceived(ByVal sender As Object, _ ByVal e As MessageEventArgs) Public Sub ConfirmationCallback(ByVal message As String) RaiseEvent CallbackReceived(Me, New MessageEventArgs(message)) End Sub ' Ensures that this object will not be prematurely released. Public Overrides Function InitializeLifetimeService() As Object Return Nothing End Function End Class Public Class MessageEventArgs Inherits EventArgs Public Message As String Public Sub New(ByVal message As String) Me.Message = message End Sub End Class
The configuration files require only a single change from the previous example. In the simple One-Way Remoting example, the client declared a client-only channel (TCP client), while the server declared a server-only channel (TCP server). To remedy this design, you must configure a bidirectional channel that can create new outgoing connections and receive incoming connections.
The changed line looks like this in the server:
<channel ref="tcp" port="8000" />
The client configuration file requires a similar change. It doesn't define a port number because the .NET Framework will dynamically choose the first available dynamic port.
<channel ref="tcp"/>
The client is modeled after the One-Way Remoting example. It allows any message to be dispatched to the client. The message is then returned through a callback and handled in a local event, which displays the message box shown in Figure 3-10.
The client code is encapsulated in a single form, as follows:
Imports System.Runtime.Remoting Public Class Client Inherits System.Windows.Forms.Form ' Create the local remotable object that can receive the callback. Private ListenerObject As New RemoteLibrary.Listener() ' Create the remote object. Private TestObject As New RemoteLibrary.RemoteObject() Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load RemotingConfiguration.Configure("Client.exe.config") End Sub Private Sub cmdSend_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdSend.Click ' Create the delegate that points to the client object. Dim Callback As New RemoteLibrary.ConfirmationCallback( _ AddressOf ListenerObject.ConfirmationCallback) ' Connect the event handler to the local listener class. AddHandler ListenerObject.CallbackReceived, _ AddressOf ListenerObject_CallbackReceived ' Send the message to the remote object. TestObject.ReceiveMessage(txtMessage.Text, Callback) End Sub Private Sub ListenerObject_CallbackReceived(ByVal sender As Object, _ ByVal e As RemoteLibrary.MessageEventArgs) MessageBox.Show(e.Message) End Sub End Class
Note |
You might assume that server callbacks and events work using the channel established by the client. However, due to the way that Remoting works, this isn't possible. Instead, the server opens a new channel to deliver its message, which has significant implications if the client is behind a firewall or network address translation (NAT) device. Ingo Rammer has created a proof-of-concept bidirectional TCP channel that solves this issue and allows the server to use the client-created channel (it's available at http://www.dotnetremoting.cc/projects/modules/BidirectionalTcpChannel.asp). Unfortunately, this sample isn't yet ready for a production environment. Your best bet may be to wait for future .NET platform releases, since Microsoft Remoting architects are actively considering this issue. |