9. Remote Callbacks
Callbacks are just
as useful in distributed applications as they are in local applications.
A client can pass in as a method parameter a reference to a client-side
marshal-by-reference object to be used by a remote object. A client can
also provide a remote server with a delegate targeting a client-side
method, so that the remote server can raise an event or simply call the
client-side method. The difference in the case of remote callbacks
is that the roles are reversed: the remote server object becomes the
client, and the client (or client-side object) becomes the server. In
fact, as far as the server is concerned, the client (or the target
object) is essentially a client-activated object, because the server has
no URI associated with the target object and cannot treat it as a
well-known object.
To receive a remote
callback, the client needs to register a port and a channel and have
.NET listen on that port for remote callbacks.
The problem is, how does the remote server know about that port? And
for that matter, how does the remote server object know what URL to use
to connect to the client? The answer is built into the remoting
architecture. As mentioned already, whenever a reference to an object is
marshaled across an app domain boundary, the object reference contains
information about the location of the remote object and the channels the
host has registered. The object reference is part of the proxy. When
the server calls the proxy, the proxy knows where to marshal the call
and which channel and port to use. In essence, this is also how
delegates work across remoting. The client can create a delegate that
targets a method on an object on the client's side and then add that
delegate to a public delegate or an event maintained by the server
object.
9.1. Registering callback channels
The client must register
the channels on which it would like to receive remote callbacks. The
client provides a port number to the channel constructor, just like the
host application does:
//Registering a channel with a specific port number on the client side,
//to enable callbacks:
IChannel channel = new TcpChannel(9005);
ChannelServices.RegisterChannel(channel);
When the client invokes a
call on the server, the client needs to know in advance which ports the
host is listening on. This isn't the case when the server makes a
callback to the client, because the proxy on the server side already
knows the client-side port number. Consequently, the client doesn't
really have to use a pre-designated port for the callback; any available
port will do. To instruct .NET to select an available port
automatically and listen on that port for callbacks, the client simply
needs to register the channels with port 0:
//Instructing .NET to select any available port on the client side
IChannel channel = new TcpChannel(0);
ChannelServices.RegisterChannel(channel);
Or, if you're using a configuration file:
<channels>
<channel ref="tcp" port="0"/>
</channels>
The client can also register an IPC channel for the callbacks:
//Registering an IPC callback channel
IChannel ipcChannel = new IpcChannel("MyCallback");
ChannelServices.RegisterChannel(ipcChannel);
An interesting
scenario is when the client registers multiple channels for callbacks.
The client can assign a priority to each channel, using a named property
called priority as part of a collection
of named properties provided to the channel constructor (similar to
explicitly specifying a formatter). The client can also assign
priorities to channels in the configuration file:
<application>
<channels>
<channel ref="tcp" port="0" priority="1"/>
<channel ref="http" port="0" priority="2"/>
</channels>
</application>
The channels' priority
information is captured by the object reference. The remote server
tries to use the channels according to their priority levels. If the
client registers multiple channels but doesn't assign priorities, the
host selects a channel for the call.
9.2. Remote callbacks and type filtering
Every remoting channel has a
filter associated with it. The filter controls the kinds of types the
channel is willing to serialize across. In a distributed application the
server is inherently more susceptible to attacks than the client, and
it is particularly susceptible to being handed a harmful callback object
by a malicious client. To protect against such attacks, the default
level of the filter is set to Low. Low-level
type filtering allows only a limited set of types to be passed in as
parameters to remote methods as callback objects. Types allowed under
low type filtering include remoting infrastructure types and simple
compositions of reference types out of primitive types. Full
type filtering allows all marshalable types to be passed in as callback
objects. Setting the filter level to Full does not eliminate the
security threat; it just makes it your explicit decision to take the
risk, rather than Microsoft's decision.
To enable callbacks, the
host must set its type-filtering level to Full. In most practical
scenarios the client must elevate its type filtering to Full as well,
unless only very simple callback objects are involved. Changing the type
filtering is done on a per-formatter-per-channel basis. You can set the
type filtering to Full both programmatically and administratively. To
set it programmatically, supply a set of properties to the channel, and
set the TypeFilterLevel property of the channel formatter to Full. TypeFilterLevel is of the enum type TypeFilterLevel, defined as:
public enum TypeFilterLevel
{
Full,
Low
}
For example, here is how
you programmatically set the type filtering of the binary formatter
using a TCP channel on the host side:
BinaryServerFormatterSinkProvider formatter;
formatter = new BinaryServerFormatterSinkProvider( );
formatter.TypeFilterLevel = TypeFilterLevel.Full;
IDictionary channelProperties = new Hashtable( );
channelProperties["name"] = "FullHostTCPChannel";
channelProperties["port"] = 8005;
IChannel channel = new TcpChannel(channelProperties,null,formatter);
ChannelServices.RegisterChannel(channel);
When using an IPC callback channel, instead of port number you will need to provide the pipe's name in the portName property:
channelProperties["portName"] = "MyClientCallback";
To set the filter level administratively, use the serverProviders
tag under each channel, and set the filter level for each formatter.
For example, to set type filtering to Full for a TCP channel on the host
side, provide this configuration file:
<channels>
<channel ref="tcp" port="8005">
<serverProviders>
<formatter ref="soap" typeFilterLevel="Full"/>
<formatter ref="binary" typeFilterLevel="Full"/>
</serverProviders>
</channel>
</channels>
Here is the matching client-side configuration file:
<channels>
<channel ref="tcp" port="0">
<serverProviders>
<formatter ref="soap" typeFilterLevel="Full"/>
<formatter ref="binary" typeFilterLevel="Full"/>
</serverProviders>
</channel>
</channels>
Note the use of port 0 on
the client side, which tells .NET to automatically select any available
port for the incoming callback.
9.3. Remote callbacks and metadata
Another side effect
of reversing the roles of the client and server when dealing with remote
callbacks has to do with the client's metadata. At runtime, the host
must be able to build a proxy to the client-side object, and therefore
the host needs to have access to the object's metadata. As a result, you
typically need to package the client-side callback objects (or event
subscribers) in class libraries and have the host reference those
assemblies.
9.4. Remote callbacks and error handling
On top of the usual
things that can go wrong when invoking a callback, with remote callbacks
there is also the potential for network problems and other wire-related
issues. This is a particular concern in the case of remote event
publishers, since they have to try to reach every subscriber and may
have to wait a considerable amount of time for each because of network
latency. However, because a publisher/subscriber relationship is by its
very nature a looser relationship than that of a client and server,
often the publisher doesn't need to concern itself with whether the
subscriber managed to process the event successfully, or even if the
event was delivered at all. If that is the case with your application,
it's better if you don't publish events simply by calling the delegate.
There is something you can do on the subscriber's side to make the life of the remote publisher easier. The OneWay attribute, defined in the System.Runtime.Remoting.Messaging,
makes any remote method call a fire-and-forget asynchronous call. If
you designate a subscriber's event-handling method as a one-way method,
the remoting infrastructure only dispatches the callback and doesn't
wait for a reply or for completion. As a result, even if you publish an
event by calling a delegate directly, the event publishing will be
asynchronous and concurrent: it's asynchronous because the publisher
doesn't wait for the subscribers to process the event, and it's
concurrent because every remote subscriber is served on an impendent
worker thread (remote calls use threads from the thread pool). In
addition, any errors on the subscriber's side don't propagate to the
publisher's side, so the publisher doesn't need to program to catch and
handle exceptions raised by the event subscribers.
Because static
members and methods aren't remotable (no object reference is possible),
you can't subscribe to a remote static event, and you can't provide a
static method as a target for a remote publisher. You can only pass a
remote object a delegate targeting an instance method on the client
side. If you pass a remote publisher a delegate targeting a static
method, the event is delivered to a static method on the remote host
side. |
|
9.5. Remote callback example
Example 4
shows a publisher firing events on a remote subscriber. These projects are a variation of the projects presented in Example 3. The host is identical to the one in Example 3; the only changes are in the host configuration file. The host exposes the type RemoteServer.MyPublisher
as a client-activated object and as a server-activated object. The host
also elevates type filtering to Full on its channels. The ServerAssembly
class library contains both the subscriber and the publisher classes.
Recall that this is required so that both the client and the host can
gain access to these types' metadata. Note that both the publisher and
the subscriber are derived from MarshalByRefObject. The publisher uses GenericEventHandler by aliasing it to NumberChangedEventHandler:
using NumberChangedEventHandler = GenericEventHandler<int>;
The publisher publishes to the subscribers in the FireEvent( ) method using EventsHelper. The subscriber is the MySubscriber class. The subscriber's event-handling method is OnNumberChanged( ), which pops up a message box with the value of the event's argument:
[OneWay]
public void OnNumberChanged(int number)
{
MessageBox.Show("New Value: " + number);
}
The interesting part of OnNumberChanged( ) is that it's decorated with the OneWay attribute. As a result, the publisher's FireEvent( )
method actually fires the event asynchronously and concurrently to the
various subscribers. The client configuration file is the same as in Example 3,
except this time the client registers the port 0 with the channel,
which allows the client to receive remote callbacks. Interestingly
enough, the subscriber is simple enough to pass on the client channel
even with client-side type filtering set to Low. The client
configuration file registers the publisher as a remote object. The
client creates a local instance of the subscriber and a remote instance
of the publisher, and saves them as class members. The client is a
Windows Forms dialog. The dialog allows the user to subscribe to or
unsubscribe from the publisher, and to fire the event. The dialog
reflects the event's argument in a text box. Subscribing to and
unsubscribing from the event are done using the conventional += and -= operators, as in the local case.
Example 4. Remote events
///////////// RemoteServerHost.exe.config : the host configuration file ////////
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.runtime.remoting>
<application>
<service>
<activated type="RemoteServer.MyPublisher,ServerAssembly"/>
<wellknown type="RemoteServer.MyPublisher,ServerAssembly"
mode="SingleCall" objectUri="MyRemotePublisher"/>
</service>
<channels>
<channel ref="tcp" port="8005">
<serverProviders>
<formatter ref="soap" typeFilterLevel="Full"/>
<formatter ref="binary" typeFilterLevel="Full"/>
</serverProviders>
</channel>
<channel ref="http" port="8006">
<serverProviders>
<formatter ref="soap" typeFilterLevel="Full"/>
<formatter ref="binary" typeFilterLevel="Full"/>
</serverProviders>
</channel>
</channels>
</application>
</system.runtime.remoting>
</configuration>
///////////////////// ServerAssembly class library ////////////////////////////
using NumberChangedEventHandler = GenericEventHandler<int>;
namespace RemoteServer
{
public class MyPublisher : MarshalByRefObject
{
public event NumberChangedEventHandler NumberChanged;
public void FireEvent(int number)
{
EventsHelper(NumberChanged,number);
}
}
public class MySubscriber : MarshalByRefObject
{
[OneWay]
public void OnNumberChanged(int number)
{
MessageBox.Show("New Value: " + number);
}
}
}
//////////////// Client.exe.config: the client configuration file //////////////
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.runtime.remoting>
<application>
<client url="tcp://localhost:8005">
<activated type="RemoteServer.MyPublisher,ServerAssembly"/>
</client>
<channels>
<channel ref="tcp" port="0"/>
</channels>
</application>
</system.runtime.remoting>
</configuration>
/////////////////////////// Client EXE assembly ////////////////////////////////
using RemoteServer;
partial class SubscriberForm : Form
{
Button m_FireButton;
Button m_SubscribeButton;
Button m_UnsubscribeButton;
TextBox m_NumberValue;
MyPublisher m_Publisher;
MySubscriber m_Subscriber;
public SubscriberForm( )
{
InitializeComponent( );
m_Publisher = new MyPublisher( );
m_Subscriber = new MySubscriber( );
}
void InitializeComponent( )
{...}
static void Main( )
{
RemotingConfigurationEx.Configure( );
Application.Run(new SubscriberForm( ));
}
void OnFire(object sender,EventArgs e)
{
int number = Convert.ToInt32(m_NumberValue.Text);
m_Publisher.FireEvent(number);
}
void OnUnsubscribe(object sender,EventArgs e)
{
m_Publisher.NumberChanged -= m_Subscriber.OnNumberChanged;
}
void OnSubscribe(object sender,EventArgs e)
{
m_Publisher.NumberChanged += m_Subscriber.OnNumberChanged;
}
}
|