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 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 is 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 remoting. 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 within which objects with similar rules can be contained.
At times, objects will be marshaled across both context and app
domain boundaries, as well as across process and machine boundaries.
(Processes, app domains, and contexts are all explained in greater
detail later in this chapter.)
When an object is remoted, 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 are not talking to Kirk at all; you are talking to a proxy, or a simulation whose job is to take your message and beam it 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 like Kirk is really there, but he’s actually sitting on the bridge, yelling at Scotty that he needs more power.
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 does not. 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 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
another copy of Word, another process starts up. Each process is
subdivided into one or more application domains
(or app 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 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 that 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 co-exist. Each app domain has its own security, and the app domain serves as a security boundary.
App domains are not threads and should be distinguished from threads. A thread exists in one app domain at a time, and a thread can access (and report) in which app domain it is executing. App domains are used to isolate applications; within an app domain there might be multiple threads operating at any given moment (see Chapter 22).
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 which 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.
You create a new app domain by calling the
static method CreateDomain( )
on the
AppDomain
class:
AppDomain ad2 = AppDomain.CreateDomain("Shape Domain", null, null);
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 we
accomplish by passing in null
.
It is possible, of course, to write your own binder, which 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, you’d use the BindingFlags
enumeration value CreateInstance
. The default
binder normally only looks at public classes 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, you do not specify the assembly to load at compile time, but rather, determine which assembly you want programmatically and bind your variable to that assembly when the program is running.
The constructor we’re calling takes two integers, which must be
put into an object array (new object[] {3, 5}
).
We can send null
for the culture because
we’ll use the default (en
) culture and
won’t specify activation attributes or security attributes.
What you get back is an object
handle
, 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 teleporting
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 hardware 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 do not actually give me the original, you keep that in your house, but you 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 reached through the telephone wire between us and touched your original calculator.
The Captain Kirk and hardware
analogies are fine as far as analogies go, but what actually happens
when you marshal by reference? The
Common Language
Runtime (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 which 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
Simple Object Access Protocol
(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 marshal by
value.
Note that each time you create an object that might be used across a boundary, you must choose how it will be marshaled. Normally, objects cannot 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 will hold 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 will depend 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.
Example 19-1. Marshaling across app domain boundaries
using System;
using System.Runtime.Remoting;
using System.Reflection;
namespace ProgCSharp
{
// for marshal by reference comment out
// the attribute and uncomment the base class
[Serializable]
public class Point // : MarshalByRefObject
{
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;
}
}
private int x;
private int y;
}
// the shape class marshals by reference
public class Shape : MarshalByRefObject
{
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);
}
private Point upperLeft;
}
public class Tester
{
public static void Main( )
{
Console.WriteLine( "[{0}] {1}",
System.AppDomain.CurrentDomain.FriendlyName,
"Entered Main");
// create the new app domain
AppDomain ad2 =
System.AppDomain.CurrentDomain.CreateDomain(
"Shape Domain");
// Assembly a = Assembly.LoadFrom("ProgCSharp.exe");
// Object theShape = a.CreateInstance("Shape");
// instantiate a Shape object
ObjectHandle oh = ad2.CreateInstance(
"ProgCSharp",
"ProgCSharp.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:
[Programming CSharp.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
[Programming CSharp.exe] Point x.set
[Programming CSharp.exe] Point y.set
[Programming CSharp.exe] Point x.get
[Programming CSharp.exe] Point y.get
[Programming CSharp.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, what you are
setting is a copy of the
Point
object, and 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:
[Programming CSharp.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 [Programming CSharp.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.
18.191.233.43