Remoting
Remoting technology uses all of the key concepts in the .NET Application Model. While a complete discussion of remoting is beyond the scope of this book, a brief introduction provides a powerful example of how metadata and marshal by reference (MBR) work. Remoting also provides a mechanism that supports executable servers.
Unlike remoting in Microsoft's COM technology, there is a minimal amount of infrastructure programming required. What infrastructure program is required allows programmers either a degree of flexibility or the ability to customize remoting for their particular applications.
The .NET Framework provides two ways to provide connections between two applications on different computers. Web Services, discussed in Chapter 11, enable computers that do not host the CLR to communicate with computers that do. The remoting technology discussed here builds distributed applications between computers that host the CLR.
Remoting Overview
The key parts of Remoting are
Interception, which allows for message generation for communication over the channels.
Formatters to put the messages into a byte stream that is sent over the channel. These are the same formatters that were discussed in the section on serialization.
Communication channels for transport of messages.
Interception
Proxies and stubs (referred to in .NET as dispatchers) transform the function calls on the client or server side into messages that are sent over the network. This is called interception, because the proxies and dispatchers intercept a method call to send it to its remote destination. Unlike COM, metadata provides the information so the CLR can generate the proxies and stubs for you.
A proxy takes the function call off the stackframe of the caller and transforms it into a message. The message is then sent to its destination. A dispatcher takes the message and transforms it into a stackframe so that a call can be made to the object.
For example, assume the UnregisterCustomer method from the Customer assembly runs in one application domain and is called from another. It makes no difference if the application domains are in the same process or on the same machine.
The proxy would take the integer id argument on the stackframe of the client making the call and put it in a message that encoded the call and its argument. On the server side, the dispatcher would take that message and create a function call on the server's stack for the call UnregisterCustomer(int id) and make that call into the object. The client and server code do not need to know that they are being remoted.
Channels and Formatters
The formatter converts the message into a byte stream. The .NET Framework comes with two formatters, binary and SOAP (text-based XML, discussed in the Chapter 11). The byte stream is then sent over a communication channel.
The .NET Framework comes with two channels, although you can write your own. The HTTP channel uses the HTTP protocol and is good for communicating over the Internet or through firewalls. The TCP channel uses the TCP (sockets) protocol and is designed for high-speed communication. You have four permutations of formatters and transport: binary over TCP, binary over HTTP, SOAP over HTTP, and SOAP over TCP.
Remote Objects
Clients obtain a proxy by activating a remote object. Remote objects must derive from MarshalByRefObject, because you work with a proxy to the object reference, not with the object reference itself. This is the same concept discussed in the section on contexts, where marshal by reference is also used to access context bound objects.
Local objects passed as method parameters from one application domain to another can be passed by value (copied) or by reference.
To be passed by value, they must be serializable. The object is serialized, sent across the transport layer, and re-created on the other side. We have already seen an example of this in the AppDomain example.
To be passed by reference, the class must derive from MarshalByRefObject. The Remoting example illustrates pass by reference.
Remote objects can be either server or client activated. Server-activated objects are not created until the first method call on the object. Server-activated objects come in two flavors, SingleCall and Singleton. SingleCall objects are stateless. Each method causes a new object to be created. Singleton objects can be used by multiple client activation requests. Singleton objects can maintain state. SingleCall objects will scale better than Singleton objects because they do not retain state and can be load balanced.
Client-activated objects are activated when the client requests them. While they can last for multiple calls and hold state, they cannot store information from different client activations. This is similar to calling CoCreateInstanceEx in DCOM.
Activation
Objects are activated on the client side in one of three ways by using the Activator class.
Activator::GetObject is used to get a reference to a server-activated object.
Activator::CreateInstance is used to create a client-activated object. You can pass parameters to the object's constructor using one of the overloaded CreateInstance methods that takes an array of objects to be passed to the constructor.
The C++ new syntax can be used to create a server or a client activated object. A configuration file is used to describe how new should be used.
Sample Remotable Object
For our remoting example, we remote our Customers object from the Customer assembly. In the Remoting example directory there are two solutions. One represents the client program, the other the server program. Build the server solution first, which will also build the dependent Customer.dll. Copy this DLL into the Debug directory of both Server and Client. Start the server program first. Notice it waits for a client request. You can then run the client program that will run against objects that live inside of the server. We will discuss the details of the client and server code and output in the next few sections.
Notice that we only had to make two simple changes to our object. The Customers class in the server project had to be made remotable by inheriting from MarshalByRefObject.
public __gc class Customers : public MarshalByRefObject, public ICustomer
The CustomerListItem that was going to be transferred by value had to be made serializable.
[Serializable] public __value struct CustomerListItem { public: int CustomerId; String *FirstName; String *LastName; String *EmailAddress; };
Sample Remoting Program
In the Remoting example the client accesses a server-activated object. The server is the TcpServerChannel class that uses a binary format with the TCP protocol. The channel will use port 8085. The server registers the type being remoted, the endpoint name to refer to this object, and the type of activation. The server then waits for client requests.
TcpServerChannel *chan = new TcpServerChannel(8085); ChannelServices::RegisterChannel(chan); RemotingConfiguration::RegisterWellKnownServiceType( __typeof(Customers), "AcmeCustomer", WellKnownObjectMode::Singleton); ...
The server has to be started before the client program can access the object.
The client sets up a TcpClientChannel object and then connects to the object. It specifies the type of the object it wants and the endpoint where the server is listening for object requests. If you want to run the client and server on separate machines, substitute the server machine name for localhost in the endpoint. Unlike COM location transparency, the client has to specify a specific endpoint; there is no redirection through an opaque registry entry.
TcpClientChannel *chan = new TcpClientChannel; ChannelServices::RegisterChannel(chan); Customers *obj = dynamic_cast<Customers *>( Activator::GetObject( __typeof(Customers), "tcp://localhost:8085/AcmeCustomer")); if (obj == 0) Console::WriteLine("Could not locate server"); else { ...
The client then uses the proxy to make calls on the object as if it were a local instance.
bool bRet = RemotingServices::IsTransparentProxy(obj); ... ArrayList *ar; ar = obj->GetCustomer(-1); ShowCustomerArray(ar);
To run the program, start the server program in one console window, and then run the client program from another console window.
The output depends on what kind of server-activated object is being activated. If the server activation type is Singleton, which supports the maintaining state, you get the behavior you would expect from the non-remoted case. A new customer is added, and you find that new customer in the list when you ask for all the existing customers. As you would expect, the initial activate call results in the Customers constructor being called once for each server invocation no matter how many times the client program is run.
Object reference a proxy?: True Client: AppDomain Client.exe Thread 19 Context 0 1 Rocket Squirrel rocky@frosbitefalls.com 2 Bullwinkle Moose moose@wossamotta.edu 1 Rocket Squirrel rocky@frosbitefalls.com 2 Bullwinkle Moose moose@wossamotta.edu 3 Boris Badenough boris@no-goodnicks.com
If the activation type is SingleCall, which creates a new object instance for every method call, the results are quite different. Four different objects are created. The first object is created by the initial activate request. The second is created by the initial call to GetCustomer. The third object is created by the RegisterCustomer call. The fourth object is created by the second call to GetCustomer. The last object created never sees the new customer because no state is saved. Note that the static nextCustId member of the Customer class is treated as static with respect to the new object instances of the Customer class, just as you would expect. Same client code, different results! Since the object is already activated, if you run the client program a second time for the same server invocation, the Customers constructor will be called only three times.
Object reference a proxy?: True Client: AppDomain Client.exe Thread 19 Context 0 3 Rocket Squirrel rocky@frosbitefalls.com 4 Bullwinkle Moose moose@wossamotta.edu 8 Rocket Squirrel rocky@frosbitefalls.com 9 Bullwinkle Moose moose@wossamotta.edu
Since the client uses a proxy, the object executes inside the server's application domain, but on a different thread than the main server thread. The object's constructor is not called until the first method call on the object. Notice how in both cases we have remoted an ArrayList of types without any special work aside from making the type serializable. The presence of metadata makes the programmer's work much easier.
Metadata and Remoting
In order for the client to request an object of a specific type, metadata about the type has to be available to the client. For the remoting example shown in this chapter, a reference is simply made to the actual assembly where the object is stored. However, for many applications, you do not want to give the client access to your actual assembly, since it can be decompiled into source code via reflection (i.e., reverse engineering). For the metadata that the client needs, a reference need only be made to an object that contains interface information, but no actual implementation details.
One way to do this is to build a version of the object that has methods with no implementation. This interface class can then be built into an assembly that can be given to the client. You can throw the System::NotSupportedException in the methods if you wish to make sure that they are never used by mistake for the real object.
For Web Services, you use the SOAPSUDS tool to extract the metadata from the service, and then generate an assembly that has the required metadata. You can then build a proxy DLL and have the client program refer to it. This is conceptually equivalent to the first approach. The server, of course, has to reference the real object's assembly.
Unlike the COM model, there is no reference counting, interface negotiation, building and registering separate proxies and stubs, worrying about global identifiers, or use of the registry. Because of metadata, all you have to do is inherit from MarshalByRefObject to make an object remotable.
Remoting Configuration Files
You use configuration files to define where the object is activated. The client can then use the new operator to create the object. The big advantage in doing this is that as the object location changes (such as a URL or TCP channel), or the formatter you want to use changes, the client does not have to be rebuilt.
Multiple classes can be configured on the client. Configuration files are loaded into the client using the RemotingConfiguration::Configure method.