For the purposes of Remoting, you can divide all .NET classes into three types:
Remotable classes. Any class that derives directly or indirectly from MarshalByRefObject automatically gains the ability to be exposed remotely and invoked by .NET peers in other application domains.
Serializable classes. Any class that is marked with the <Serializable> attribute can be copied across application boundaries. Serializable types must be used for the parameters or return values of methods in a remotable class.
Ordinary classes. These classes can't be used to send information across application boundaries, and they can't be invoked remotely. This type of class can still be used in a remotable application, even though it doesn't play a part in Remoting communication.
Figure 3-1 shows both remotable and serializable types in action. Incidentally, it's possible for a class to be both serializable and remotable, but it's not recommended. (In this case, you could interact with a remote instance of the object or send a copy of it across the network.)
Figure 3-1 shows a good conceptual model of what takes place with Remoting, but it omits the work that takes place behind the scenes. For example, serializable types are not moved, but rather copied by converting them into a stream of bytes. Similarly, remotable types aren't accessed directly, but through a proxy mechanism provided by the CLR (see Figure 3-2). This is similar to the way that many high-level distributed technologies work, including web services and COM/DCOM.
With proxy communication, you interact with a remote object by using a local proxy that provides all the same methods. You call a method on the proxy class in exactly the same way that you would call a method on a local class in your application. Behind the scenes, the proxy class opens the required networking channel (with the help of the CLR), calls the corresponding method of the remote object, waits for the response, deserializes any returned information, and then returns it to your code. This entire process is transparent to your .NET code. The proxy object behaves just like the original object would if it were instantiated locally.
In the next two examples, we'll consider serializable and remotable types in more detail, and show you how to make your own.
A serializable type is one that .NET can convert to a stream of bytes and reconstruct later, potentially in another application domain. Serializable classes are a basic feature of .NET programming and are used to persist objects to any type of stream, including a file. (In this case, you use the methods of the BinaryFormatter in the System.Runtime.Serialization.Formatters.Binary namespace or the SoapFormatter in the System.Runtime.Serialization.Formatters.Soap namespace to perform manual serialization.) Serialized classes are also used with Remoting to copy objects from one application domain to another.
All basic .NET types are automatically serializable. That means that you can send integers, floating point numbers, bytes, strings, and date structures to other .NET clients without worry. Some other serializable types include the following:
Arrays and collection classes (such as the ArrayList). However, the content or the array or collection must also be serializable. In other words, an array of serializable objects can be serialized, but an array of non-serializable objects cannot.
The ADO.NET data containers, such as the DataTable, DataRow, and DataSet.
All .NET exceptions. This allows you to fire an exception in a remotable object that an object in another application domain can catch.
All EventArgs classes. This allows you to fire an event from a remotable object and catch it in another application domain.
Many, but not all .NET types are serializable. To determine if a given type is serializable, look it up in the class library reference and check if the type definition is preceded with the <Serializable> attribute.
You can also make your own serializable classes. Here's an example:
<Serializable> _ Public Class Message Public Text As String Public Sender As String End Class
A serializable class must follow several rules:
You must indicate to .NET that the class can be serialized by adding the <Serializable> attribute just before the class declaration.
Every member variable and property must also be serializable. The previous example works because the Message class encapsulates two strings, and strings are serializable.
If you derive from a parent class, this class must also be serializable.
Both the client and recipient must understand the object. If you try to transmit an unrecognized object, the recipient will simply end up with a stream of uninterpretable bytes. Similar problems can occur if you change the version of the object on one end.
Remember, when you send a serializable object you're in fact copying it. Thus, if you send a Message object to another peer, there will be two copies of the message: one in the application domain of the sender (which will probably be released because it's no longer important) and one in the application domain of the recipient.
When a class is serialized, every object it references is also serialized. This can lead to transmitting more information than you realize. For example, consider this revised version of the Message class that stores a reference to a previous message:
<Serializable> _ Public Class Message Public Text As String Public Sender As String Public PreviousMessage As Message End Class
When serializing this object, the Message referred to by the PreviousMessage member variable is also serialized and transmitted. If this message refers to a third message, it will also be serialized, and so on. This is a dangerous situation for data integrity because it can lead to duplicate copies of the same object in the remote application domain.
Finally, if there's any information you don't want to serialize, add the <NonSerialized> attribute just before it. The variable will be reinitialized to an empty value when the object is copied, as follows:
<Serializable> _ Public Class Message Public Text As String Public Sender As String <NonSerialized> Public PreviousMessage As Message End Class
This technique is useful if you need to omit information for security reasons (for example, a password), or leave out a reference that may not be valid in another application domain (for example, file handles).
A remotable type is one that can be accessed from another application domain. Following is an example of a simple remotable object. It's identical to any other .NET class, except for the fact that it derives from the System.MarshalByRefObject class.
Public Class RemoteObject Inherits MarshalByRefObject Public Sub ReceiveMessage(ByVal message As Message) Console.WriteLine("Received message: " & message.Text) End Sub End Class
Every public property, method, and member variable in a remotable class is automatically accessible to any other application. In the previous example, this means that any .NET application can call RemoteObject.ReceiveMessage(), as long as it knows the URL where it can find the object. All the ByVal parameters used by ReceiveMessage() must be serializable. ByRef parameters, on the other hand, must be remotable. (In this case, the ByRef parameter would pass a proxy reference to the original object in the sender's application domain.)
In this example, RemoteObject represents the complete, viable code for a remote object that writes a message to a console window. With the aid of configuration files, we'll develop this into a working example.
Tip |
Like any public method, the ReceiveMessage() method could also be called by another class in the same application. However, to prevent confusion, it's best to only include methods that are designed exclusively for remote communication in a MarshalByRefObject. |
MarshalByRefObject instances have the ability to be invoked remotely. However, simply creating a MarshalByRefObject doesn't make it available to other applications. Instead, you need a server application (also called a component host) that listens for requests, and provides the remotable objects as needed. The component host also determines the URL the client must use to locate or create the remote object, and configures how the remote object is activated and how long it should live. This information is generally set in the component host's configuration file. Any executable .NET application can function as a component host, including a Windows application, console application, or Windows service.
A component host requires very little code because the Remoting infrastructure handles most of the work. For example, if you place your configuration information into a single file, you can configure and initialize your component host with a single line of code, as follows:
RemotingConfiguration.Configure(ConfigFileName)
In this case, ConfigFileName is a string that identifies a configuration file that defines the application name, the protocol used to send messages, and the remote objects that should be made available. We'll consider these settings in the next section.
Once you have called the RemotingConfiguration.Configure() method, the CLR will maintain a pool of threads to listen for incoming requests, as long as the component host application is running. If it receives a request that requires the creation of a new remotable object, this object will be created in the component host's application domain. However, these tasks take place on separate threads. The component host can remain blissfully unaware of them and continue with other tasks, or—more commonly—remain idle (see Figure 3-3).
This model is all well and good for a distributed enterprise application, but it's less useful in a peer-to-peer scenario. In an enterprise application, a component host exposes useful server-side functionality to a client. Typically, each client will create a separate object, work with it, and then release it. By using Remoting, the object is allowed to execute on the server, where it can reap a number of benefits including database connection pooling and the use of higher-powered server hardware.
In a peer-to-peer application, however, the component host and the remote component are tied together as one application that supports remote communication. This means that every peer in a peer-to-peer application consists of a remotable interface that's exposed to the world and a component host that contains the rest of the application. Figure 3-4 diagrams this approach.
These two models are dramatically different. Enterprise systems use a stateless approach. Communication is usually initiated by the client and all functionality is held at the server (much like the client-server model). Peer-to-peer applications use a stateful model in which independent peers converse through Remoting front-ends.
This difference between enterprise development and peer-to-peer applications becomes evident when you need to choose an activation type for a remote object. Remotable types can be configured with one of three activation types, depending on the configuration file settings:
SingleCall. This defines a stateless object that is automatically created at the start of every method invocation and destroyed at the end. This is similar to how web services work.
Client-activated. This defines a stateful object that is created by the client and lives until its set lifetime expires, as defined by client usage and configuration settings. Client-activated objects are the most similar to local.NET objects.
Singleton. This defines a stateful object that is accessible to the remote client, but has a lifetime controlled by the server.
Generally, SingleCall objects are perfect for enterprise applications that simply want to expose server resources. They can't be used in a peer-to-peer application as the basis for bidirectional communication between long-running applications. In a peer-to-peer application, you need to use the Singleton type, which associates an endpoint with a single object instance. No matter how many clients connect, there's only ever one remote object created. Or, to put it another way, a Singleton object points to a place where a specific object exists. SingleCall and client-activated addresses point to a place where a client can create its own instance of a remotable object. In this book, we'll focus on the Singleton activation type.
There is one other twist to developing with Remoting. In order for another application to call a method on a remote object, it needs to know some basic information about the object. This information takes the form of .NET metadata. Without it, the CLR can't verify your remote network calls (checking, for example, that you have supplied the correct number of parameters and the correct data types). Thus, in order to successfully use Remoting to communicate, you need to distribute the assembly for the remote object to the client and add a reference to it.
There are some ways of minimizing this inconvenience, either by pre-generating a proxy class or by using interfaces. We'll use the latter method in the next chapter when we develop a real Remoting example.
Note |
It may seem counterintuitive that you need to distribute the assembly for remote objects to all clients. This is one of the quirks of using an object-based model for remote communication (such as Remoting or web services). This problem won't appear when we use a lower-level networking approach in the third part of this book. |
The configuration files use an XML format to define the channels and ports that should be used for communication, the type of formatting for messages, and the objects that should be exposed. In addition, they can specify additional information such as a lifetime policy for remotable objects. Here's the basic framework for a configuration file with Remoting:
<configuration> <system.runtime.remoting> <application> <service> <!-- Information about the supported (remotable) objects. --> </service> <channels> <!-- Information about the channels used for communication. --> </channels> <!-- Optional information about the lifetime policy (tag below). --> <lifetime /> </application> </system.runtime.remoting> </configuration>
You can create this configuration file outside of Visual Studio .NET, provided you place it in the bin directory where the compiled application will be executed. A simpler approach is to add your configuration file to the Visual Studio .NET project. Simply right-click on the project in the Solution Explorer and select Add → New Item. Then, choose Application Configuration File under the Utility node (see Figure 3-5).
The application configuration file is automatically given the name app.config. When Visual Studio .NET compiles your project, it will copy the app.config file to the appropriate directory and give it the full name (the name of the application executable, plus the .config extension). To see this automatically generated configuration file for yourself, select Project Show All Files from the menu. Once you compile your project, you'll see the appropriate file appear in the bin directory (see Figure 3-6).
Configuration files are required for every application that needs to communicate using Remoting. This includes a component host and any client that wants to interact with a remote object. The next section shows specific configuration file examples.