The days of integrated programs all running in a single process on a single machine are, if not dead, at least seriously wounded. Today’s programs consist of complex components running in multiple processes, often across the network. The Web has facilitated distributed applications in a way that was unthinkable even a few years ago, and the trend is toward distribution of responsibility.
A second trend is toward centralizing business logic on large servers. Although these trends appear to be contradictory, in fact they are synergistic: business objects are being centralized while the user interface and even some middleware are being distributed.
The net effect is that objects need to be able to talk with one another at a distance. Objects running on a server handling the web user interface need to be able to interact with business objects living on centralized servers at corporate headquarters.
The process of moving an object across a boundary is called marshal by value . Boundaries exist at various levels of abstraction in your program. The most obvious boundary is between objects running on different machines.
The process of preparing an object to be remoted is called marshaling. On a single machine, objects might need to be marshaled across context, app domain, or process boundaries.
A process is essentially a running application. If an object in your word processor wants to interact with an object in your spreadsheet, they must communicate across process boundaries.
Processes are divided into application domains (often called app domains); these in turn are divided into various contexts . App domains act like lightweight processes, and contexts create boundaries that objects with similar rules can be contained within. At times, objects will be marshaled across both context and app domain boundaries, as well as across process and machine boundaries.
When an object is marshaled by value, it appears to be sent through the wire from one computer to another, much like Captain Kirk being teleported down to the surface of a planet some miles below the orbiting USS Enterprise.
In Star Trek, Kirk was actually sent to the planet, but in the .NET edition, it is all an illusion. If you are standing on the surface of the planet, you might think you are seeing and talking with the real Kirk, but you aren’t talking to Kirk at all: you are talking to a proxy, perhaps a hologram, whose job is to take your message up to the Enterprise where it is relayed to the real Kirk.
Between you and Kirk there are also a number of “sinks.” A sink is an object whose job is to enforce policy. For example, if Kirk tries to tell you something that might influence the development of your civilization, the prime-directive sink might disallow the transmission.
When the real Kirk responds, he passes his response through various sinks until it gets to the proxy and the proxy tells you. It seems to you as though Kirk is really there, but he’s actually sneaking up behind you to thwart your nefarious plans. Alas, it turns out that it was Mr. Sulu who was controlling the hologram the whole time. Better luck next episode.
The actual transmission of your message is done by a channel . The channel’s job is to know how to move the message from the Enterprise to the planet. The channel works with a formatter . The formatter makes sure the message is in the right format. Perhaps you speak only Vulcan, and the poor Captain doesn’t. The formatter can translate your message into Federation Standard, and translate Kirk’s response from Federation Standard back to Vulcan. You appear to be talking with one another, but the formatter (known as the universal translator in the Star Trek universe) is silently facilitating the communication.
This chapter demonstrates how your objects can be marshaled across various boundaries, and how proxies and stubs can create the illusion that your object has been squeezed through the network cable to a machine across the office or around the world. In addition, this chapter explains the role of formatters, channels, and sinks, and how to apply these concepts to your programming.
A process is, essentially, a running application. Each .NET application runs in its own process. If you have Word, Excel, and Visual Studio open, you have three processes running. If you open Outlook, another process starts up. Each process is subdivided into one or more application domains. An app domain acts like a process but uses fewer resources.
App domains can be independently started and halted. They are secure, lightweight, and versatile. An app domain can provide fault tolerance; if you start an object in a second app domain and it crashes, it will bring down the app domain but not your entire program. You can imagine that web servers might use app domains for running users’ code; if the code has a problem, the web server can maintain operations.
An app domain is encapsulated by an instance of the
AppDomain
class, which offers a number of methods
and properties. A few of the most important are listed in
Table 19-1.
Table 19-1. Methods and properties of the AppDomain class
App domains also support a variety of
events—including
AssemblyLoad
,
AssemblyResolve
,
ProcessExit
, and
ResourceResolve--
that are fired as assemblies are found,
loaded, run, and unloaded.
Every process has an initial app domain, and can have additional app domains as you create them. Each app domain exists in exactly one process. Until now, all the programs in this book have been in a single app domain: the default app domain. Each process has its own default app domain. In many, perhaps in most of the programs you write, the default app domain will be all that you’ll need.
However, there are times when a single domain is insufficient. You might create a second app domain if you need to run a library written by another programmer. Perhaps you don’t trust the library, and want to isolate it in its own domain so that if a method in the library crashes the program, only the isolated domain will be affected. If you were the author of Internet Information Server (IIS), Microsoft’s web hosting software), you might spin up a new app domain for each plug-in application or each virtual directory you host. This would provide fault tolerance so that if one web application crashed, it would not bring down the web server.
It is also possible that the other library might require a different security environment; creating a second app domain allows the two security environments to coexist. Each app domain has its own security, and the app domain serves as a security boundary.
App domains aren’t threads and should be distinguished from threads. A Win32 thread exists in one app domain at a time, and a thread can access (and report) which app domain it is executing in. App domains are used to isolate applications; within an app domain there might be multiple threads operating at any given moment (see Chapter 20).
To see how app domains work, let’s set up an
example. Suppose you wish your program to instantiate a
Shape
class, but in a second app domain.
There is no good reason for this Shape
class to be
put in a second app domain, except to illustrate how these techniques
work. It is possible, however, that more complex objects might need a
second app domain to provide a different
security environment. Further, if you are
creating classes that might engage in risky behavior, you might like
the protection of starting them in a second app domain.
Normally, you’d load the Shape
class from a separate assembly, but to keep this example simple,
you’ll just put the definition of the
Shape
class into the same source file as all the
other code in this example (see Chapter 17).
Further, in a production environment, you might run the
Shape
class methods in a separate thread, but for
simplicity, you’ll ignore threading for now.
(Threading is covered in detail in Chapter 20.)
By sidestepping these ancillary issues, you can keep the example
straightforward and focus on the details of creating and using
application domains and marshaling objects across app domain
boundaries.
Create a new app domain by calling the
static method
CreateDomain( )
on the
AppDomain
class:
AppDomain ad2 = AppDomain.CreateDomain("Shape Domain");
This creates a new app domain with the
friendly
name
Shape
Domain
. The friendly name
is a convenience to the programmer; it is a way to interact with the
domain programmatically without knowing the internal representation
of the domain. You can check the friendly name of the domain
you’re working in with the property
System.AppDomain.CurrentDomain.FriendlyName
.
Once you have instantiated an AppDomain
object,
you can create instances of classes, interfaces, and so forth, using
its
CreateInstance( )
method. Here’s the
signature:
public ObjectHandle CreateInstance( string assemblyName, string typeName, bool ignoreCase, BindingFlags bindingAttr, Binder binder, object[] args, CultureInfo culture, object[] activationAttributes, Evidence securityAttributes );
And here’s how to use it:
ObjectHandle oh = ad2.CreateInstance( "ProgCSharp", // the assembly name "ProgCSharp.Shape", // the type name with namespace false, // ignore case System.Reflection.BindingFlags.CreateInstance, // flag null, // binder new object[] {3, 5}, // args null, // culture null, // activation attributes null ); // security attributes
The first parameter (ProgCSharp
) is the name of
the assembly, and the second (ProgCSharp.Shape
) is
the name of the class. The class name must be fully qualified by
namespaces.
A
binder
is an object that enables dynamic binding of an
assembly at runtime. Its job is to
allow you to pass in information about the object you want to create,
to create that object for you, and to bind your reference to that
object. In the vast majority of cases, including this example,
you’ll use the default binder, which is accomplished
by passing in null
.
It is possible, of course, to write your own binder that might, for example, check your ID against special permissions in a database and reroute the binding to a different object, based on your identity or your privileges.
Binding
typically
refers to attaching an object name to an object. Dynamic
binding refers to the ability to make that attachment when
the program is running, as opposed to when it is compiled. In this
example, the Shape
object is bound to the instance
variable at runtime, through the app domain’s
CreateInstance( )
method.
Binding flags help the binder fine-tune its behavior at binding time.
In this example, use the BindingFlags
enumeration
value CreateInstance
. The default binder normally
looks at public classes only for binding, but you can add flags to
have it look at private classes if you have the right permissions.
When you bind an assembly at runtime, don’t specify the assembly to load at compile time; rather, determine which assembly you want programmatically, and bind your variable to that assembly when the program is running.
The constructor you’re calling takes two integers,
which must be put into an object array (new
object[]
{3
,
5}
). You can send null
for the
culture because you’ll use the default
(en
) culture and won’t specify
activation attributes or security attributes.
You get back an object
handle
, which is a type that is used to pass
an object (in a wrapped state) between multiple app domains without
loading the metadata for the wrapped object in each object through
which the ObjectHandle
travels. You can get the
actual object itself by calling
Unwrap( )
on the object handle, and
casting the resulting object to the actual type—in this case,
Shape
.
The
CreateInstance()
method provides an opportunity to
create the object in a new app domain. If you were to create the
object with new
, it would be created in the
current app domain.
You’ve
created a Shape
object in the Shape
domain, but
you’re accessing it through a
Shape
object in the original domain. To access the
shape object in another domain, you must marshal the object across
the domain boundary.
Marshaling is the process of preparing an object to move across a boundary; once again, like Captain Kirk transporting to the planet’s surface. Marshaling is accomplished in two ways: by value or by reference. When an object is marshaled by value, a copy is made. It is as if I called you on the phone and asked you to send me your calculator, and you called up the office supply store and had them send me one that is identical to yours. I can use the copy just as I would the original, but entering numbers on my copy has no effect on your original.
Marshaling by reference is almost like sending me your own calculator. Here’s how it works. You don’t actually give me the original, but instead keep it in your house. You do send me a proxy. The proxy is very smart: when I press a button on my proxy calculator, it sends a signal to your original calculator, and the number appears over there. Pressing buttons on the proxy looks and feels to me just like I touched your original calculator.
The Captain Kirk and calculator analogies are fine as far as analogies go, but what actually happens when you marshal by reference? The CLR provides your calling object with a transparent proxy (TP).
The job of the TP is to take everything known about your method call
(the return value, the parameters, etc.) off of the stack and stuff
it into an object that implements the
IMessage
interface. That
IMessage
is passed to a
RealProxy
object.
RealProxy
is an abstract base class from which all
proxies derive. You can implement your own real proxy, or any of the
other objects in this process except for the transparent proxy. The
default real proxy will hand the IMessage
to a
series of
sink objects.
Any number of sinks
can be used depending on the number of policies you wish to enforce,
but the last sink in a chain will put the IMessage
into a channel.
Channels are split into client-side and server-side channels, and
their job is to move the message across the boundary. Channels are
responsible for understanding the transport protocol. The actual
format of a message as it moves across the boundary is managed by a
formatter. The
.NET Framework provides two formatters: a
SOAP
formatter, which is the default for HTTP channels, and a
Binary
formatter, which is the default for TCP/IP channels. You
are free to create your own formatters and, if you are truly a
glutton for punishment, your own channels.
Once a message is passed across a boundary, it is received by the
server-side channel and formatter, which reconstitute the
IMessage
and pass it to one or more sinks on the
server side. The final sink in a sink chain is the
StackBuilder
, whose job is to take the
IMessage
and turn it back into a stack frame so
that it appears to be a function call to the server.
To
illustrate the distinction between
marshaling by value and marshaling by
reference, in the next example you’ll tell the
Shape
object to marshal by reference but give it a
member variable of type Point
, which
you’ll specify as a marshal by value.
Note that each time you create a class that might be used across a boundary, you must choose how it will be marshaled. Normally, objects can’t be marshaled at all; you must take action to indicate that an object can be marshaled, either by value or by reference.
The easiest way to make an object marshal by value is to mark it with
the Serializable
attribute:
[Serializable] public class Point
When an object is serialized, its internal state is written out to a stream, either for marshaling or for storage. The details of serialization are covered in Chapter 21.
The easiest way to make an object marshal by reference is to derive
its class from
MarshalByRefObject
:
public class Shape : MarshalByRefObject
The Shape
class will have just one member variable,
upperLeft
. This variable will be a
Point
object, which holds the coordinates of the
upper-left corner of the shape.
The constructor for Shape
will initialize its
Point
member:
public Shape(int upperLeftX, int upperLeftY) { Console.WriteLine( "[{0}] Event{1}", System.AppDomain.CurrentDomain.FriendlyName, "Shape constructor"); upperLeft = new Point(upperLeftX, upperLeftY); }
Provide Shape
with a method for displaying its
position:
public void ShowUpperLeft() { Console.WriteLine( "[{0}] Upper left: {1},{2}", System.AppDomain.CurrentDomain.FriendlyName, upperLeft.X, upperLeft.Y); }
Also provide a second method for returning its
upperLeft
member variable:
public Point GetUpperLeft() { return upperLeft; }
The Point
class is very simple as well. It has a
constructor that initializes its two member variables and accessors
to get their value.
Once you create the Shape
, ask it for its
coordinates:
s1.ShowUpperLeft( ); // ask the object to display
Then ask it to return its upperLeft
coordinate as
a Point
object that you’ll
change:
Point localPoint = s1.GetUpperLeft(); localPoint.X = 500; localPoint.Y = 600;
Ask that Point
to print its coordinates, and then
ask the Shape
to print its
coordinates. So, will the change to the local
Point
object be reflected in the
Shape
? That depends on how the
Point
object is marshaled. If it is marshaled by
value, the localPoint
object will be a copy, and
the Shape
object will be unaffected by changing
the localPoint
variables’ values.
If, on the other hand, you change the Point
object
to marshal by reference, you’ll have a proxy to the
actual upperLeft
variable, and changing that
will change the Shape
. Example 19-1 illustrates this point. Make sure you build
Example 19-1 in a project named ProgCSharp. When
Main( )
instantiates the Shape
object, the method is looking for
ProgCSharp.exe.
Example 19-1. Marshaling across app domain boundaries
#region Using directives using System; using System.Collections.Generic; using System.Runtime.Remoting; using System.Reflection; using System.Text; #endregion namespace Marshaling { // for marshal by reference comment out // the attribute and uncomment the base class [Serializable] public classPoint // : MarshalByRefObject { private int x; private int y; public Point (int x, int y) { Console.WriteLine( "[{0}] {1}", System.AppDomain.CurrentDomain.FriendlyName, "Point constructor"); this.x = x; this.y = y; } public int X { get { Console.WriteLine( "[{0}] {1}", System.AppDomain.CurrentDomain.FriendlyName, "Point x.get"); return this.x; } set { Console.WriteLine( "[{0}] {1}", System.AppDomain.CurrentDomain.FriendlyName, "Point x.set"); this.x = value; } } public int Y { get { Console.WriteLine( "[{0}] {1}", System.AppDomain.CurrentDomain.FriendlyName, "Point y.get"); return this.y; } set { Console.WriteLine( "[{0}] {1}", System.AppDomain.CurrentDomain.FriendlyName, "Point y.set"); this.y = value; } } } // the shape class marshals by reference public class Shape : MarshalByRefObject { private Point upperLeft; public Shape(int upperLeftX, int upperLeftY) { Console.WriteLine( "[{0}] {1}", System.AppDomain.CurrentDomain.FriendlyName, "Shape constructor"); upperLeft = new Point(upperLeftX, upperLeftY); } public Point GetUpperLeft( ) { return upperLeft; } public void ShowUpperLeft( ) { Console.WriteLine( "[{0}] Upper left: {1},{2}", System.AppDomain.CurrentDomain.FriendlyName, upperLeft.X, upperLeft.Y); } } public class Tester { public static void Main( ) { Console.WriteLine( "[{0}] {1}", System.AppDomain.CurrentDomain.FriendlyName, "Entered Main"); // create the new app domain AppDomain ad2 = AppDomain.CreateDomain("Shape Domain"); // Assembly a = Assembly.LoadFrom("ProgCSharp.exe"); // Object theShape = a.CreateInstance("Shape"); // instantiate a Shape object ObjectHandle oh = ad2.CreateInstance( "Marshaling", "Marshaling.Shape", false, System.Reflection.BindingFlags.CreateInstance, null, new object[] {3, 5}, null, null, null ); Shape s1 = (Shape) oh.Unwrap( ); s1.ShowUpperLeft( ); // ask the object to display // get a local copy? proxy? Point localPoint = s1.GetUpperLeft( ); // assign new values localPoint.X = 500; localPoint.Y = 600; // display the value of the local Point object Console.WriteLine( "[{0}] localPoint: {1}, {2}", System.AppDomain.CurrentDomain.FriendlyName, localPoint.X, localPoint.Y); s1.ShowUpperLeft( ); // show the value once more } } } Output: [[Marshaling.vshost.exe] Entered Main [Shape Domain] Shape constructor [Shape Domain] Point constructor [Shape Domain] Point x.get [Shape Domain] Point y.get [Shape Domain] Upper left: 3,5 [Marshaling.vshost.exe] Point x.set [Marshaling.vshost.exe] Point y.set [Marshaling.vshost.exe] Point x.get [Marshaling.vshost.exe] Point y.get [Marshaling.vshost.exe] localPoint: 500, 600 [Shape Domain] Point x.get [Shape Domain] Point y.get [Shape Domain] Upper left: 3,5
Read through the code, or better yet, put it in your debugger and
step through it. The output reveals that the Shape
and Point
constructors run in the
Shape
domain, as does the access of the values of
the Point
object in the Shape
.
The property is set in the original app domain, setting the local
copy of the Point
object to 500 and 600. Because
Point
is marshaled by value, however, you are
setting a copy of the Point
object. When you ask the Shape
to display its
upperLeft
member variable, it is unchanged.
To complete the experiment, comment out the attribute at the top of
the Point
declaration and uncomment the base
class:
// [serializable] public class Point : MarshalByRefObject
Now run the program again. The output is quite different:
[Marshaling.vshost.exe] Entered Main [Shape Domain] Shape constructor [Shape Domain] Point constructor [Shape Domain] Point x.get [Shape Domain] Point y.get [Shape Domain] Upper left: 3,5 [Shape Domain] Point x.set [Shape Domain] Point y.set [Shape Domain] Point x.get [Shape Domain] Point y.get [Marshaling.vshost.exe] localPoint: 500, 600 [Shape Domain] Point x.get [Shape Domain] Point y.get [Shape Domain] Upper left: 500,600
This time you get a proxy for the Point
object and
the properties are set through the proxy on the original
Point
member variable. Thus, the changes are
reflected within the Shape
itself.
App domains themselves are subdivided into contexts. Contexts can be thought of as boundaries within which objects share usage rules. These usage rules include synchronization transactions (see Chapter 20), and so forth.
Objects are either context-bound or context-agile. If they are context-bound, they exist in a context, and to interact with them, the message must be marshaled. If they are context-agile, they act within the context of the calling object: their methods execute in the context of the object that invokes the method and so marshaling isn’t required.
Suppose you have an object A that interacts with the database and so is marked to support transactions. This creates a context. All method calls on A occur within the context of the protection afforded by the transaction. Object A can decide to roll back the transaction, and all actions taken since the last commit are undone.
Suppose that you have another object, B, which is context-agile. Now suppose that object A passes a database reference to object B and then calls methods on B. Perhaps A and B are in a callback relationship, in which B will do some work and then call A back with the results. Because B is context-agile, B’s method operates in the context of the calling object; thus it will be afforded the transaction protection of object A. The changes B makes to the database will be undone if A rolls back the transaction because B’s methods execute within the context of the caller. So far, so good.
Should B be context-agile or context-bound? In the case examined so far, B worked fine being agile. Suppose one more class exists: C. C doesn’t have transactions, and it calls a method on B that changes the database. Now A tries to roll back, but unfortunately, the work B did for C was in C’s context and thus was not afforded the support of transactions. Uh-oh: that work can’t be undone.
If B was marked context-bound when A created it, B would have inherited A’s context. In that case, when C invoked a method on B, it would have to be marshaled across the context boundary, but then when B executed the method, it would have been in the context of A’s transaction. Much better.
This would work if B were context-bound but without attributes. B of
course could have its own context attributes, and these might force B
to be in a different context from A. For example, B might have a
transaction attribute marked RequiresNew
. In this
case, when B is created, it gets a new context, and thus
can’t be in A’s context. Thus, when
A rolled back, B’s work could not be undone. You
might mark B with the RequiresNew
enumeration
value because B is an audit function. When A takes an action on the
database, it informs B, which updates an audit trail. You
don’t want B’s work undone when A
undoes its transaction. You want B to be in its own transaction
context, rolling back only its own mistakes, not
A’s.
An object thus has three choices. The first option is to be
context-agile. A context-agile object operates in the context of its
caller. Option two is to be context-bound (accomplished by deriving
from ContextBoundObject
but having no attributes,
and thus operating in the context of the creator). Option three is to
be context-bound with context attributes, and thus operate only in
the context that matches the attributes.
Which you decide upon depends on how your object will be used. If your object is a simple calculator that can’t possibly need synchronization or transactions or any context support, it is more efficient to be context-agile. If your object should use the context of the object that creates it, you should make that object context-bound with no attributes. Finally, if your object has its own context requirements, you should give it the appropriate attributes.
No proxy is needed when accessing context-agile objects within a single app domain. When an object in one context accesses a context-bound object in a second context, it does so through a proxy, and at that time the two context policies are enforced. It is in this sense that a context creates a boundary; the policy is enforced at the boundary between contexts.
For example, when you mark a context-bound object with the
System.EnterpriseServices.Synchronization
attribute, you indicate that
you want the system to manage
synchronization
for that object. All objects outside that context must pass through
the context boundary to touch one of the objects, and at that time
the policy of synchronization will be applied.
Strictly speaking, marking two classes with the
Synchronization
attribute doesn’t
guarantee that they will end up in the same context. Each attribute
gets to vote on whether it is happy with the current context at
activation. If two objects are marked for synchronization, but one is
pooled, they will be forced into different contexts.
Objects are marshaled differently across context boundaries, depending on how they are created:
Typical objects aren’t marshaled at all; within app domains they are context-agile.
Objects marked with the
Serializable
attribute are marshaled by value
across app domains and are context-agile.
Objects that derive from
MarshalByRefObject
are marshaled by reference across app domains and are context-agile.
Objects derived from
ContextBoundObject
are marshaled by reference across app domains as well as by reference
across context boundaries.
In addition to being marshaled across context and app domain boundaries, objects can be marshaled across process boundaries, and even across machine boundaries. When an object is marshaled, either by value or by proxy, across a process or machine boundary, it is said to be remoted.
There are two types of server objects supported for remoting in .NET: well-known and client-activated. The communication with well-known objects is established each time a message is sent by the client. There is no permanent connection with a well- known object, as there is with client-activated objects.
Well-known objects come in two varieties: singleton and single-call . With a well- known singleton object, all messages for the object, from all clients, are dispatched to a single object running on the server. The object is created the first time a client attempts to connect to it, and is there to provide service to any client that can reach it. Well-known objects must have a parameterless constructor.
With a well-known single-call object, each new message from a client is handled by a new object. This is highly advantageous on server farms, where a series of messages from a given client might be handled in turn by different machines depending on load balancing.
Client-activated objects are typically used by programmers who are creating dedicated servers, which provide services to a client they are also writing. In this scenario, the client and the server create a connection, and they maintain that connection until the needs of the client are fulfilled.[1]
The
best way to understand remoting is
to walk through an example. Here, build a simple four-function
Calculator
class, like the one used in an earlier
discussion on web services (see Chapter 15) that
implements the interface shown in Example 19-2.
Example 19-2. The Calculator interface
#region Using directives
using System;
using System.Collections.Generic;
using System.Text;
#endregion
namespace Calculator
{
public interfaceICalc
{
double Add( double x, double y );
double Sub( double x, double y );
double Mult( double x, double y );
double Div( double x, double y );
}
}
Save this in a file named ICalc.cs and compile it into a file named Calculator.dll. To create and compile the source file in Visual Studio, create a new project of type C# Class Library, enter the interface definition in the Edit window, and then select Build on the Visual Studio menu bar. Alternatively, if you have entered the source code using Notepad or another text editor, you can compile the file at the command line by entering:
csc /t:library ICalc.cs
There are tremendous advantages to implementing a server through an interface. If you implement the calculator as a class, the client must link to that class to declare instances on the client. This greatly diminishes the advantages of remoting because changes to the server require the class definition to be updated on the client. In other words, the client and server would be tightly coupled. Interfaces help decouple the two objects; in fact, you can later update that implementation on the server, and as long as the server still fulfills the contract implied by the interface, the client need not change at all.
To build the server used in Example 19-3, create CalculatorServer.cs in a new project of type C# Console Application (be sure to include a reference to Calculator.dll) and then compile it by selecting Build on the Visual Studio menu bar.
The CalculatorServer
class implements
ICalc
. It derives from
MarshalByRefObject
so that it will deliver a proxy of the calculator to the client
application:
class CalculatorServer : MarshalByRefObject, Calculator.ICalc
The implementation consists of little more than a constructor and simple methods to implement the four functions.
In Example 19-3, you’ll put the
logic for the server into the
Main()
method of
CalculatorServer.cs.
Your first task is to create a
channel
.
Use HTTP as the transport mechanism. You can use the
HTTPChannel
type provided by .NET:
HTTPChannel chan = new HTTPChannel(65100);
Notice that you register the channel on TCP/IP port 65100 (see the discussion of port numbers in Chapter 21).
Next, register the channel with the CLR
ChannelServices
using the static
method RegisterChannel
:
ChannelServices.RegisterChannel(chan);
This step informs .NET that you will be providing HTTP services on port 65100, much as IIS does on port 80. Because you’ve registered an HTTP channel and not provided your own formatter, your method calls will use the SOAP formatter by default.
Now you’re ready to ask the
RemotingConfiguration
class to register your
well-known
object. You must pass in the type of the object you want to register,
along with an endpoint. An
endpoint
is a name that RemotingConfiguration
will
associate with your type. It completes the address. If the IP address
identifies the machine and the port identifies the channel, the
endpoint indicates the exact service. To get the type of the object,
you can use typeof
, which returns a
Type
object. Pass in the full name of the object
whose type you want:
Type calcType = typeof( "CalculatorServerNS.CalculatorServer" );
Also, pass in the enumerated type that indicates whether you are
registering a SingleCall
or
Singleton
:
RemotingConfiguration.RegisterWellKnownServiceType ( calcType, "theEndPoint",WellKnownObjectMode.Singleton );
The call to
RegisterWellKnownServiceType
creates the server-side
sink chain. Now
you’re ready to rock and roll. Example 19-3 provides the entire source code for the
server.
Example 19-3. The Calculator server
#region Using directives using System; using System.Collections.Generic; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; using System.Text; #endregion namespace CalculatorServerNS { class CalculatorServer : MarshalByRefObject, Calculator.ICalc { public CalculatorServer( ) {Console.WriteLine( "CalculatorServer constructor" ); } // implement the four functions public double Add( double x, double y ) { Console.WriteLine( "Add {0} + {1}", x, y ); return x + y; } public double Sub( double x, double y ) { Console.WriteLine( "Sub {0} - {1}", x, y ); return x - y; } public double Mult( double x, double y ) { Console.WriteLine( "Mult {0} * {1}", x, y ); return x * y; } public double Div( double x, double y ) { Console.WriteLine( "Div {0} / {1}", x, y ); return x / y; } } public class ServerTest { public static void Main( ) { // create a channel and register it HttpChannel chan = new HttpChannel( 65100 ); ChannelServices.RegisterChannel( chan ); Type calcType = Type.GetType( "CalculatorServerNS.CalculatorServer" ); // register our well-known type and tell the server // to connect the type to the endpoint "theEndPoint" RemotingConfiguration.RegisterWellKnownServiceType ( calcType, "theEndPoint", WellKnownObjectMode.Singleton ); // "They also serve who only stand and wait." (Milton) Console.WriteLine( "Press [enter] to exit..." ); Console.ReadLine( ); } } }
When you run this program, it prints its self-deprecating message:
Press [enter] to exit...
and then waits for a client to ask for service.
While the CLR will preregister the TCP and HTTP channel, you will need to register a channel on the client if you want to receive callbacks or you are using a nonstandard channel. For this example, you can use channel 0:
HTTPChannel chan = new HTTPChannel(0); ChannelServices.RegisterChannel(chan);
The client now need only connect through the remoting services,
passing a Type
object representing the type of the
object it needs (in our case, the
ICalc
interface)
and the Uniform Resource Identifier (URI) of
the service.
Object obj = RemotingServices.Connect (typeof(Programming_CSharp.ICalc), "http://localhost:65100/theEndPoint");
In this case, the server is assumed to be running on your local
machine, so the URI is http://localhost
, followed
by the port for the server (65100
), followed in
turn by the endpoint you declared in the server
(theEndPoint
).
The remoting service should return an object representing the
interface you’ve requested. You can then cast that
object to the interface and begin using it. Because remoting
can’t be guaranteed (the network might be down, the
host machine may not be available, and so forth), you should wrap the
usage in a try
block:
try { Programming_CSharp.ICalc calc = obj as Programming_CSharp.ICalc; double sum = calc.Add(3,4);
You now have a proxy of the calculator operating on the server, but usable on the client, across the process boundary and, if you like, across the machine boundary. Example 19-4 shows the entire client (to compile it, you must include a reference to Calculator.dll as you did with CalcServer.cs).
Example 19-4. The remoting Calculator client
#region Using directives using System; using System.Collections.Generic; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; using System.Text; #endregion namespace CalculatorClient { class CalcClient { public static void Main( ) { int[] myIntArray = new int[3];Console.WriteLine("Watson, come here I need you..."); // create an Http channel and register it // uses port 0 to indicate won't be listening HttpChannel chan = new HttpChannel(0); ChannelServices.RegisterChannel(chan); Object obj = RemotingServices.Connect (typeof(Calculator.ICalc), "http://localhost:65100/theEndPoint"); try { // cast the object to our interface Calculator.ICalc calc = obj as Calculator.ICalc; // use the interface to call methods double sum = calc.Add(3.0,4.0); double difference = calc.Sub(3,4); double product = calc.Mult(3,4); double quotient = calc.Div(3,4); // print the results Console.WriteLine("3+4 = {0}", sum); Console.WriteLine("3-4 = {0}", difference); Console.WriteLine("3*4 = {0}", product); Console.WriteLine("3/4 = {0}", quotient); } catch( System.Exception ex ) { Console.WriteLine("Exception caught: "); Console.WriteLine(ex.Message); } } } } Output on client: Watson, come here I need you... 3+4 = 7 3-4 = -1 3*4 = 12 3/4 = 0.75 Output on server: Calculator constructor Press [enter] to exit... Add 3 + 4 Sub 3 - 4 Mult 3 * 4 Div 3 / 4
The server starts up and waits for the user to press Enter to signal that it can shut down. The client starts and displays a message to the console. The client then calls each of the four operations. You see the server printing its message as each method is called, and then the results are printed on the client.
It is as simple as that; you now have code running on the server and providing services to your client.
To see the difference that
SingleCall
makes versus
Singleton
, change one
line in the server’s
Main( )
method. Here’s the existing code:
RemotingConfiguration.RegisterWellKnownServiceType ( calcType, "theEndPoint", WellKnownObjectMode.Singleton );
Change the object to SingleCall
:
RemotingConfiguration.RegisterWellKnownServiceType ( calcType, "theEndPoint", WellKnownObjectMode.SingleCall);
The output reflects that a new object is created to handle each request:
Calculator constructor Press [enter] to exit... Calculator constructor Add 3 + 4 Calculator constructor Sub 3 - 4 Calculator constructor Mult 3 * 4 Calculator constructor Div 3 / 4
When you called the
RegisterWellKnownServiceType( )
method on the server, what actually
happened? Remember that you obtain a
Type
object for
the Calculator
class:
Type.GetType("CalculatorServerNS.CalculatorServer");
You then called RegisterWellKnownServiceType()
,
passing in that Type
object along with the
endpoint and the
Singleton
enumeration. This signals the CLR to
instantiate your Calculator
and then to associate
it with an endpoint.
To do that work yourself, you would need to modify Example 19-3, changing Main()
to
instantiate a Calculator
and then passing that
Calculator
to the
Marshal()
method of
RemotingServices
with the endpoint to which you
want to associate that instance of Calculator
. The
modified Main( )
is shown in Example 19-5 and, as you can see, its output is identical
to that of Example 19-3.
Example 19-5. Manually instantiating and associating Calculator with an endpoint
public static void Main() { HttpChannel chan = new HttpChannel( 65100 );ChannelServices.RegisterChannel( chan ); CalculatorServerNS.CalculatorServer calculator = new CalculatorServer( ); RemotingServices.Marshal( calculator, "theEndPoint" ); // "They also serve who only stand and wait." (Milton) Console.WriteLine( "Press [enter] to exit..." ); Console.ReadLine( ); }
The net effect is that you have instantiated a
Calculator
object and associated a proxy for
remoting with the endpoint you’ve specified (see the
“Understanding Endpoints,” section
later in this chapter).
You can take that file to your client and reconstitute it on the
client machine. To do so, again create a channel and register it.
This time, however, open a fileStream
on the file
you just copied from the server:
FileStream fileStream = new FileStream ("calculatorSoap.txt", FileMode.Open);
Then instantiate a
SoapFormatter
and call
Deserialize( )
on the
formatter,
passing in the filename and getting back an ICalc
:
SoapFormatter soapFormatter = new SoapFormatter (); try { ICalc calc= (ICalc) soapFormatter.Deserialize (fileStream);
You are now free to invoke methods on the server through that
ICalc
, which acts as a proxy to the
Calculator
object running on the server that you
described in the calculatorSoap.txt file. The
complete replacement for the
client’s
Main( )
method is shown in Example 19-6. You also need to add two
using
statements to this example.
Example 19-6. Replacement of Main() from Example 19-4 (the client)
using System.IO; using System.Runtime.Serialization.Formatters.Soap; // ... public static void Main( ) { int[] myIntArray = new int[3]; Console.WriteLine("Watson, come here I need you..."); // create an Http channel and register it // uses port 0 to indicate you won't be listening HttpChannel chan = new HttpChannel(0); ChannelServices.RegisterChannel(chan); FileStream fileStream = new FileStream ("calculatorSoap.txt", FileMode.Open); SoapFormatter soapFormatter = new SoapFormatter ( ); try { ICalc calc= (ICalc) soapFormatter.Deserialize (fileStream); // use the interface to call methods double sum = calc.Add(3.0,4.0); double difference = calc.Sub(3,4); double product = calc.Mult(3,4); double quotient = calc.Div(3,4); // print the results Console.WriteLine("3+4 = {0}", sum); Console.WriteLine("3-4 = {0}", difference); Console.WriteLine("3*4 = {0}", product); Console.WriteLine("3/4 = {0}", quotient); } catch( System.Exception ex ) { Console.WriteLine("Exception caught: "); Console.WriteLine(ex.Message); } }
When the client starts up, the file is read from the disk and the
proxy is unmarshaled. This is the mirror operation to marshaling and
serializing the object on the server. Once you have unmarshaled the
proxy, you are able to invoke the methods on the
Calculator
object running on the
server.
What is going on when you register the endpoint in Example 19-5 (the server)? Clearly, the server is associating that endpoint with the type. When the client connects, that endpoint is used as an index into a table so that the server can provide a proxy to the correct object (in this case, the calculator).
If you don’t provide an endpoint for the client to
talk to, you can instead write all the information about your
Calculator
object to a file and physically give
that file to your client. For example, you could send it to your
buddy by email, and he could load it on his local computer.
The client can deserialize the object and reconstitute a proxy, which it can then use to access the calculator on your server! (The following example was suggested to me by Mike Woodring, formerly of DevelopMentor, who uses a similar example to drive home the idea that the endpoint is simply a convenience for accessing a marshaled object remotely.)
To see how you can invoke an object without a known endpoint, modify
the Main()
method of Example 19-3 once again. This time, instead
of calling Marshal( )
with an endpoint, just pass in the
object:
ObjRef objRef = RemotingServices.Marshal(calculator)
Marshal()
returns an
ObjRef
object. An
ObjRef
object stores all the information required
to activate and communicate with a remote object. When you do supply
an endpoint, the server creates a table that associates the endpoint
with an objRef
so that the server can create the
proxy when a client asks for it. ObjRef
contains
all the information needed by the client to build a proxy, and
objRef
itself is serializable.
Open a file stream for writing to a new file and create a new SOAP
formatter. You can serialize your ObjRef
to that
file by invoking the Serialize()
method on the
formatter, passing in the file stream and the
ObjRef
you got back from
Marshal
. Presto! You have all the information you
need to create a proxy to your object written out to a disk file. The
complete replacement for Example 19-5s Main( )
is shown in Example 19-7. You’ll
also need to add three using
statements to
CalcServer.cs, as
shown.
Example 19-7. Marshaling an object without a well-known endpoint
using System; using System.IO; using System.Runtime.Serialization.Formatters.Soap; public static void Main( ) { // create a channel and register it HttpChannel chan = new HttpChannel(65100); ChannelServices.RegisterChannel(chan); // make your own instance and call Marshal directly Calculator calculator = new Calculator( ); ObjRef objRef = RemotingServices.Marshal(calculator); FileStream fileStream = new FileStream("calculatorSoap.txt",FileMode.Create); SoapFormatter soapFormatter = new SoapFormatter( ); soapFormatter.Serialize(fileStream,objRef); fileStream.Close( ); // "They also serve who only stand and wait." (Milton) Console.WriteLine( "Exported to CalculatorSoap.txt. Press ENTER to exit..."); Console.ReadLine( ); }
When you run the server, it writes the file calculatorSoap.txt to the filesystem. The server then waits for the client to connect. It might have a long wait. (Though after about 10 minutes, it shuts itself down.)
[1] Client-activated objects can be less robust. If a call fails to a client-activated object, the developer must assume that the object has been lost on the server and must regenerate the object from scratch.
3.136.18.141