Overview
If you have ever worked with DCOM or CORBA, then you have an
understanding of how to work with Remote Objects and how to deal with application
communications across boundaries. .NET Remoting is Microsoft’s new
infrastructure that provides a rich set of classes that allow developers to ignore most
of the complexities of deploying and managing remote objects. In .NET Remoting, calling
methods on remote objects is nearly identical to calling local methods.
Remoting is a framework built into the common language runtime (CLR) that can be used to build sophisticated distributed
applications and network services. When a client creates an instance of a remote
object, it receives a Proxy to the class instance on the
server. All methods called on the Proxy will automatically be
forwarded to the remote class and any results will be returned to the client. From the
client's perspective, this process is no different than making a local call.
To use .NET remoting to build an application in which two components communicate
directly across an application domain boundary, you need to build only the following:
- A remotable object.
- A host application domain to listen for requests for that object.
- A client application domain that makes requests for that object.
Even in a complex, multiclient/multiserver application, .NET remoting can be thought
of in this way. The host and the client application must also be configured with the
remoting infrastructure and you must understand the lifetime and activation issues that
the remoting infrastructure introduces.
- Proxy objects. When a client creates an instance of a remote object,
it receives a proxy to the class instance on the server. All methods called on
the proxy will automatically be forwarded to the remote class and any results
will be returned to the client. From the client's perspective, this process is no
different than making a local call. Any exceptions thrown by the remote object
will automatically be returned to the client. This enables the client to use
normal try and catch blocks around sections of the code to trap and deal with
exceptions.
|
- Object passing. All objects created remotely are returned by reference
and have to derive from MarshallByRefObject. Objects passed as parameters
to a remote method call can be forwarded by value or by reference. The default
behavior is pass by value provided the object in question is marked by the custom
attribute [serializable]. Additionally, the object could implement the
ISerializable interface, which provides flexibility in how the object
should be serialized and deserialized. Objects that are not marshal by reference
or marshal by value are not remotable.
|
- Activation models. Remote objects can easily be created from a client
by calling new. The framework contains enough "intelligence" to realize
you are dealing with a remote object and will ensure an instance of the object
gets created in the relevant remote application. Creating instances of remote
objects is not limited to default constructors; you can even do this using a
constructor that requires one or more parameters. The Activator class
contains two methods, CreateInstance and GetObject, that can also
be used to create an instance of remote objects. The former can be used in place
of new to create an object instance while the latter is normally used to connect
to an object at a specified URL.
|
- Stateless and Stateful objects. The framework makes a provision for
creating remote objects as stateless. When an object is configured as
SingleCall, it will be created when a method is called on that object. The
object processes the call, returns an optional result, and is then collected by
the garbage collector. This way the client is always connected to a fresh object
with each call. Configuring an object as a Singleton ensures that all
clients will be connected to the same object whenever a call is made to that
object. ClientActivated objects allow the client to pass parameters to the
constructor of a remote object when it gets created. Each activation request for
a client activated object (Activator.CreateInstance or new in combination
with entries in the configuration file) on the client results in a new object on
the server.
|
- Channels and Serialization. When a client calls a method on a remote
object, the remoting framework automatically serializes any data associated with
the request and transports the data to the remote object using a channel. Some of
the more popular channels supported are HTTP, TCP, and SMTP. In the case of HTTP,
the framework uses the SOAP protocol to transport data in XML format from
the client to the server and back. The default serialization formatter for HTTP
is a SOAP formatter. Since programmers can create custom formatters for use with
any channel, the remoting framework can be configured to work with any external
.NET Framework on other platforms. The TCP channel uses plain sockets and Binary
Serialization by default and can be used to communicate with any object on a
remote server.
|
Example
The following example, we will show how to transfer
DataSets between different computers. DataSets were built to work with .NET remoting capabilities. Not only
are DataSets designed to be marshaled accross boundaries, but they also have capabilities
specifically designed to make multi-tier development more efficient.
Creating the Server
This class needs to be in a DLL that
can be called by the server and can also be referenced by the client (so that the client
knows what methods are available to call). We create a class library called
«MyServer», using Visual Studio. We then create a new class called
«RemoteServer». It is important that the «RemoteServer» class is
inherited from MarshalByRefObject which tells .NET how this class should be
transfered over the network. An object can be passed from one place to another in two
ways. When you pass something like a number or a string, you usually only want to pass
the value, this is called marshaling by value. The other way of passing an
object is by reference. Instead of just copying the data within an object, a
reference to the object is passed. The receiver of the object can then call methods on
the object directly. This is how most objects in C# are passed - if you pass a DataTable
to a method, you are passing a reference to that DataTable, not making a copy of it.
Behind the scenes, .NET automatically builds the proxy object to represent the
object on the other machine you are calling.
using System;
using System.Data;
using System.Data.SqlClient;
namespace Akadia.MyServer
{
// This class represents the Server Object,
which can be
// accessed remotly using the Proxy Object.
public class RemoteServer : MarshalByRefObject
{
// Method to be called
by client that returns a DataSet
// across the wire.
public DataSet GetDataSet()
{
DataSet ds = null;
string strConnect =
"server=XEON;database=Northwind;
user id=sa;password=manager";
SqlConnection conn =
new SqlConnection(strConnect);
try
{
conn.Open();
string strSQL = "SELECT CustomerID,ContactName FROM Customers";
SqlDataAdapter sda = new SqlDataAdapter(strSQL,conn);
ds = new DataSet();
sda.Fill(ds,"Customers");
}
finally
{
conn.Close();
}
return ds;
}
}
}
Hosting the Server Object
We now have an object designed to be called from
remote, it is however not enough. We cannot just compile the code and wait for .NET to
find it. For our purposes, though, we will build a small server that sits on the computer
Focus and waits for requests. Our server has two parts: a small console
application and a configuration file. Lets look at the application first. We
create a new console application called «TestApp». Once the application is
created, we add a reference to the «MyServer» DLL, so that the server can
create the appropriate object - the «RemoteServer».
|
|
Add a Reference to the «MyServer» DLL from
the Test Application «TestApp» |
using System;
using System.Runtime.Remoting;
using System.IO;
using System.Reflection;
namespace Akadia.TestApp
{
// Set up a fully functional server listening
for requests.
// It is important that the TestApp has a reference to the
// assembly containing the server code (MyServer). The Server
// can be tested with thw following URL from any Web-Browser
//
http://<ServerHost>:8086/MyServer/RemoteServer.soap?WSDL
class TestApp
{
static void Main(string[] args)
{
// Get the Application Directory
string strAppDir =
Path.GetDirectoryName(
Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName);
// Use the Configuration File to configure and start the Server
string strConfigFile =
strAppDir + "\\TestApp.exe.Config";
RemotingConfiguration.Configure(strConfigFile);
// Keep the Server alive to respond to
requests
Console.WriteLine("Server running and waiting for requests ...");
Console.WriteLine("Press [ENTER] to shut down Server");
Console.ReadLine();
}
}
}
The Configuration File
The Configuration File contains the
heart of what the server is supposed to do. The configuration file's name follows the
remoting convention - the executable name followed by .Config -
TextApp.exe.Config.
<configuration>
<system.runtime.remoting>
<application
name="MyServer">
<service>
<wellknown mode="Singleton"
type="Akadia.MyServer.RemoteServer, MyServer"
objectUri="RemoteServer.soap"/>
</service>
<channels>
<channel ref="http" port="8086"/>
</channels>
</application>
</system.runtime.remoting>
</configuration>
|
(1)
(2)
(3)
|
(1) -
The Name of the application that will be exposed.
(2) - The
first attribute mode indicates how the object
should be accessed, the activation model
There are 2 activation models for
Remote Objects:
The Server Side Activation
(you can also
call this method “Well Known” activation) is when the Remote object is created and executed
totally on the server side while the client creates a proxy to trick it into thinking
that the Object is available on the client side. Even in this activation model we
have 2 separate ways to deal with the state fullness of the Object:
|
The SingleCall flag notifies the
server that each remote method call into the server will create a new instance of
the object on the server which means there will be no state kept for that object
on the server between method calls. |
|
The Singleton flag notifies the
server that remote method calls into the server do not destroy the instance of
the remote object on the server after the method returns which means subsequent
calls from the client can take advantage of the state of the object made from
previous calls. |
The second activation model is the
Client Side Activation, and that is when the client is allowed to pass parameters to
the constructor of a remote object when it gets created. The trick here is that the
Object in question has to have the [Serializable] attribute set.
As you can see in the example, the
object is a singleton - we want to have only one server running.
The next attribute, type, identifies the type of object to create. The first part
of the argument (before the comma) is the classname (RemoteServer), along with its namespace (Akadia.MyServer). The second part of the argument (after the
comma) is the name of the assembly that contains the class. In this example, the
assembly and the DLL are one and the same, so the assembly has the same name as the DLL
(MyServer).
The objectURI specifies how the object will be referenced, this
value will become part of the URL, that will be used to find and communicate with the
server (http://focus:8086/MyServer/RemoteServer.soap)
(3) - This
information says that the server will be available via HTTP, using port 8086.
Testing the Server
Although we do not yet have a client,
we now have everything we need to run the server. Of course, we should test to make sure
the server is working correctly. Fortunately, we habe a generic client we can use that
already knows how to talk HTTP, the web browser.
The remoting services automatically
create a special call for describing the interface of a remote application. The format
for the description is called WSDL (Web Service Description Language), which is
XML based. You can access it directly from a web browser - of course you must
start the server application first.
Start the Server:
Test the Server from any web
browser:
Although you can go through the output
to determine information about the server, the important thing is taht, by getting any
response back, we know the server is running, click here for the full
output.
Creating the client
Now that we have proven that the server
is working, the last step is to build a client application to talk to the server. We do
this with yet another console application. It needs to have a reference to the MyServer
DLL so that it knows what methods are available.
|
|
Add a Reference to the «MyServer» and
System.Runtime.Remoting DLL from the Client Application
«MyClient» |
using System;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using System.Data;
using Akadia.MyServer;
namespace Akadia.MyClient {
class ShowDataSet
{
// Remoting client
// You must add a reference to MyServer, and
to System.Runtime.Remoting
[STAThread]
static void Main(string[] args)
{
// Register a channel
HttpChannel ch =
new HttpChannel(8088);
ChannelServices.RegisterChannel(ch);
try
{
// Get a proxy to the server object
RemoteServer dss =
(RemoteServer)Activator.GetObject(typeof(RemoteServer),
"http://focus:8086/MyServer/RemoteServer.soap");
// Call the method on the proxy
DataSet ds = dss.GetDataSet();
// Write out the results
PrintDataTable(ds.Tables[0]);
}
catch(Exception
ex)
{
Console.WriteLine(ex.ToString());
}
Console.WriteLine("\r\nPress enter to exit");
Console.ReadLine();
}
// Writes out the passed
DataTable
public static void PrintDataTable(DataTable
dt)
{
foreach(DataRow dr in
dt.Rows)
{
// Step through each column in the row and write it out
foreach(DataColumn dc in dt.Columns)
{
if (dc.Ordinal > 0)
Console.Write(", ");
if (dr.RowState == DataRowState.Deleted)
{
// For Deleted rows, need to access the original value
Console.Write(dr[dc,DataRowVersion.Original].ToString().Trim());
}
else
{
// Otherwise, just write out the regular version
Console.Write(dr[dc].ToString().Trim());
}
}
Console.WriteLine("");
}
}
}
}
using System;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using System.Data;
using Akadia.MyServer;
There are a bunch of namespaces here. Channels
contains classes for setting up a channel - a channel
is used to transport messages. The Http namespace has
the specific channel capabilities for using HTTP, as you would expect.
HttpChannel ch = new HttpChannel(8088);
ChannelServices.RegisterChannel(ch);
This code creates a new HTTP channel and registers
it for use. The argument to the HttpChannel's constructor is the port on which
communication should occur. You may have noticed that this port is not the same as the
one we used for the server. That difference is deliberate - messages go out on one port
and return on another. You cannot use the same port for both sides on the same machine,
although you can use the same port on multiple machines - therefor we used different
ports.
RemoteServer dss =
(RemoteServer)Activator.GetObject(typeof(RemoteServer),
"http://focus:8086/MyServer/RemoteServer.soap");
This is the real magic line. The call to the
Activator GetObject() method creates a proxy for a
currently running remote object, server-activated well-known object, or XML Web
service. It really just setup to call the object remotely. The information we are
providing includes the type of the object we want and the location of the real object.
The Activator does the rest for us. We must typecast the result to be a RemoteServer,
though, because GetObject() returns a regular
object.
One interesting point here is - even though this
code has created the proxy for us, it has in no way connected to the server or
confirmed that the real object exists. Doing so woulkd be inefficient, because it would
require a round-trip. The connection won't be tested until we call a method on the
object.
DataSet ds = dss.GetDataSet();
PrintDataTable(ds.Tables[0]);
Even though we know the reference returned to us is not the
RemoteServer, but is, in fact, a proxy, we can treat it as though it really were the
object. The code simply calls the methods on the object as though it were local
!
In this case case, the server is asked to generate a DataSet and
return it. The client, with no code of its own create a DataSet, nonetheless is handed
one. This code would work if there were no way for the client to talk to the database,
because the client never talks to the database. The PrintDataTable() method
simply prints out the DataSet content.
Testing the Client
After all, we have to install the client code on any
machine. First you have to install the Microsoft .NET Framework Version 1.1
Redistributable Package. The package includes everything you need to run applications
developed using the .NET Framework. You can download it from
Microsofts Download Site.
Conclusion
After all that, the output from the client is not
very exiting - it simply shows the output from the table Customers on one of our
SQL-Servers. However, the example doesn't matter as much as the power behind the
concepts. With very little code is was possible to create a distributed
application.
|