Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
|
Delphi 8 for .NET and .NET Remoting
This article is the first in a series of articles covering distributed applications written with Delphi 8 for .NET, using the remoting techniques of the .NET Framework, also known as .NET Remoting.
Over time I may also include other techniques, but for now we'll focus only on .NET Remoting.
Remote objects vs. Mobile Objects
A .NET Remoting architecture consists of a server side and a client side, as well as an object that is used "between" the server and the client.
This object can have two different characteristics, resulting in being called a remote object or a mobile object.
A remote object always stays at the server side.
It is created and maintained at the server side.
When the client creates an instance of this remote object, it gets a proxy to the remote object, but is led to believe that this is no different from a local object.
The remote object will actually reside at the server-side.
The client can make methods calls that will be turned into remote method invocations to the remote object at the server side.
A mobile object can be serialised and sent from the server machine to the client machine, meaning that it is then deserialised and run in the local contact of the client.
Method calls will be made on the local copy, and no communication with the server will be needed.
However, this also means that the mobile (and now local) object does not have any special means to use or communicate with server-side resources, like a database at the server side.
This significantly reduces the usability of mobile objects in my view, which is another reason why I will focus on remote objects only at this time.
Remote Objects
As I said in the introduction, today I want to focus on remote objects.
The server side is where the remote object resides, which always remains on the server and is referenced to and used by one or more clients.
The clients create and maintain a reference to the remote object and invoke (remote) methods from this server.
Note that the clients can reside on the same machine, but also on different machines.
At first sight, this is little different from ASP.NET Web Services.
However, .NET Remoting allows us to work with stateful objects more easily than ASP.NET Web Services (those of you who added session support to ASP.NET Web Services know what I mean), and .NET Remoting provides offers us a framework that supports different communication protocols (HTTP and TCP), message formats (binary and SOAP) and security (IIS security and SSL).
Furthermore, you can extend this framework by adding your own communication protocols or message formats! In my view, this makes .NET Remoting a more generic technology for distributed applications than ASP.NET Web Services.
MarshalByRefObject
Remote or mobile objects have to be derived from the MarshalByRefObject class, which means that the objects will be returned by reference.
A remote object is then activated at the server side, while a mobile object is activated at (serialised to) the client side.
When the remote object is activated, the client gets a reference to the remote object, called a proxy (an instance of the TransparentProject class).
The client can then use the proxy to call the remote methods.
When the client calls a remote method, the request is turned into a serialised message by a formatter, in a specified format, like binary or SOAP.
The serialised message is then transported to the remote server object using a transport channel that uses HTTP or TCP for example as communication layer.
At the server side, the remote object receives an incoming message using a transport channel, and has to deserialise the message into a request for a method invocation.
The answer - or result - of the method is formatted into a response method, transported using the transport channel, received by the client again, and deserialised into the answer that we can use.
Both the message formatting layer (supporting binary or SOAP) and the transportation layer (supporting HTTP or TCP) can be extended or replaced by our own formats or techniques, although examples of this will not be shown in this training course.
Shared Assembly
Using .NET Remoting, the client and server must be able to understand each other, talking about the same definition of the remote object.
This can be done in several ways, and we start this time by using a shared assembly - one that has to be deployed on the server machine but also to each client.
The shared assembly contains the definition of a remote object manager interface, which is the shared interface definition between the clients and the server.
As a first simple example, start Delphi 8 and create a new assembly - this done by creating a new package.
So, do File | New Package, and save the package in SharedInterfacePackage.
Now, do File | New - Unit and add a new unit to the package, and save that unit in file SharedInterface.pas.
The interface definition for the remote object has to be placed in this unit, and is as follows:
unit SharedInterface; interface type IRemoteObjectManager = interface procedure ClientStart; procedure ClientEnd; function GetRemoteInfo: String; end; implementation end.The shared assembly only contains the interface definition of the remote object. The interface IRemoteObjectManager consists of three methods: ClientStart, ClientEnd and GetRemoteInfo. The purpose of ClientStart and ClientEnd is that any client that "connects" to the remote object will call ClientStart as first method, and ClientEnd as last method. This will allow the remote object to maintain a list of connected clients. This information will be returned using the GetRemoteInfo function.
Remote Server
Once we have the remote object interface definition, we can implement it inside a remote server project.
Note that there is a little problem with the Delphi 8 for .NET IDE if you start a new project that references an assembly from a previous project that you just created in the IDE.
To avoid these problems, my recommendation is to close the IDE between an assembly project and an assembly-using project.
Restart Delphi 8 for .NET, and create a new project for the remote server.
To keep it simple at this time, create a console project for the remote server, and call it RemoteServer.dpr.
Right-click on the project node and do Add Reference to add the reference to the SharedInterfacePackage.dll.
Since this assembly will not be installed in the Global Assembly Cache, you should use the Browse button to locate it.
Note that after you've compiled the console project, this will automatically copy the SharedInterfacepackage.dll to your project directory.
You should now implement the remote object, by first adding the SharedInterface unit to your uses clause, and then typing the RemoteObjectManager definition as follows:
type RemoteObjectManager = class(MarshalByRefObject, IRemoteObjectManager) end;Now, place your cursor between the class definition and the end (of the class definition), and press Ctrl+Space to get a list of available methods inside the class RemoteObjectManager. This list includes a set of three red methods: still abstract (or "to be implemented"), namely ClientEnd, ClientStart and GetRemoteInfo. Select the three red methods and press enter, so they will be added to your class definition, which should now look as follows:
type RemoteObjectManager = class(MarshalByRefObject, IRemoteObjectManager) public procedure ClientEnd; procedure ClientStart; function GetRemoteInfo: string; end;Continue this code generating exercise by pressing Ctrl+Shift+C which should generate the three skeletons for the methods in the RemoteObjectManager class. In order to implement these three, add two "strict private" fields to the RemoteObjectManager class definition, namely "Counter" and "Alive" to maintain a counter of all clients that ever connected and called ClientStart, and a counter of all clients that are still alive (or at least that haven't explicitly called ClientEnd yet).
{ RemoteObjectManager }
procedure RemoteObjectManager.ClientEnd;
begin
Dec(Alive);
writeln('Client ends - ', Alive, ' still alive.')
end;
procedure RemoteObjectManager.ClientStart;
begin
Inc(Counter);
Inc(Alive);
writeln('Client ', Counter, ' start - ', Alive, ' still alive.')
end;
function RemoteObjectManager.GetRemoteInfo: string;
begin
Result := Counter.ToString + ' connections, with ' +
Alive.ToString + ' still alive.';
end;
System.Runtime.Remoting
So far, nothing special.
Right-click on the project node and add a reference to the System.Runtime.Remoting assembly.
The System.RunTime.Remoting assembly contains a number of useful namespaces, like the System.Runtime.Remoting namespace for the RemotingConfiguration class, the System.Runtime.Remoting.Channels namespace that contains generic channel support, System.Runtime.Remoting.Channels.HTTP for HTTP and System.Runtime.Remoting.Channels.TCP for TCP channels, and finally System.Runtime.Serialization.Formatters for the message formatters.
At this time, we'll use an HTTP channel using the default SOAP message format, so we only need to add the System.Runtime.Remoting, the System.Runtime.Remoting.Channels, and the System.Runtime.Remoting.Channels.HTTP units to the uses clause:
uses SharedInterface, System.Runtime.Remoting, System.Runtime.Remoting.Channels, System.Runtime.Remoting.Channels.HTTP;We can now implement the server as follows:
const
PortNumber = 4242;
ServerResource = 'RemoteObjectManager.soap';
var
Channel: HttpChannel;
begin
writeln('RemoteServer started.');
Channel := HTTPChannel.Create(PortNumber);
ChannelServices.RegisterChannel(Channel);
writeln('Listening for SOAP messages on HTTP port ', PortNumber);
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(RemoteObjectManager),
ServerResource,
WellKnownObjectMode.Singleton); // only one remote object
writeln('Hit <enter> to stop.');
readln
end.
Note that you can only run once instance of the remote server application - the WellKnownObjectMode.Singleton will make sure of that.
Remote Client
Time to start the .NET Remoting client application.
Like the .NET Remoting server, it's easiest to start as a console application, but you can use any type of application as a .NET Remoting client.
Create a new console application, call it RemoteClient.
Right-click on the project node and add the System.Runtime.Remoting and SharedInterfacePackage as references to the project.
Now, add the same units to the uses clause of the project that you used in the RemoteServer.
We need to create a HttpChannel again in order to communicate with the RemoteServer.
Note that this time we do not have to specify a portnumber for the channel, since this is part of the RemoteServer information.
Note that I'm using localhost here to connect to the .NET Remoting server on the local machine called localhost - obviously you need to change that to a remote machine name (or fixed IP address) in order to make it work in a real-world environment.
You can also change it to localhost, in which case you reduce the example from distributed (multiple tiers or applications on different machines) to "merely" multi-tier (different tiers on the same machine).
program RemoteClient; {$APPTYPE CONSOLE} uses SharedInterface, System.Runtime.Remoting, System.Runtime.Remoting.Channels, System.Runtime.Remoting.Channels.HTTP; const RemoteServer = 'http://localhost:4242/'; ServerResource = 'RemoteObjectManager.soap'; var Channel: HttpChannel; ObjManager: IRemoteObjectManager; begin writeln('RemoteClient started.'); Channel := HTTPChannel.Create; // no PortNumber needed ChannelServices.RegisterChannel(Channel); try ObjManager := Activator.GetObject(typeof(IRemoteObjectManager), RemoteServer + ServerResource); writeln('Reference to IRemoteObjectManager received.'); except writeln('Could not get reference to IRemoteObjectManager.') end; if Assigned(ObjManager) then try ObjManager.ClientStart; // Are we alive? (at the server side!) writeln(ObjManager.GetRemoteInfo); except on E: Exception do writeln(E.ClassName, E.Message) end; write('Hit <enter> to stop RemoteClient.'); readln; ObjManager.ClientEnd end.The we can use the GetObject method of the Activator to obtain a reference to the remote object. We need to pass the interface type (IRemoteObjectManager), as well as the resource identifier to connect to the remote server: in our case that's http://localhost:4242/RemoteObjectManager.soap.
Remote Objects in Action
Now, start the RemoteServer application, which will write a few lines and then waits for incoming client connections (until you hit enter to close it).
For every client that you start, a new connection to the server is made, and a reference to the remote object returned to the client.
Since this remote object is a singleton, all clients will share the same remote object, and this will become clear when you watch the output of the server (in the remote server console window), which prints the result of each ClientStart and ClientEnd method.
There will be one remote server object, and it can keep track of all clients that connect to it.
This is the basis of .NET Remoting, and this architecture can be used to build not only multi-tier but also distributed applications with clients on different machines.
Shared Interface plus Shared Object
Let's extend the example now by placing not only the remote object interface definition inside the shared assembly, but also adding a serialisable object definition and implementation to the shared assembly.
This object will be created at the server side, serialised, sent to the client, and deserialised (created) at the client side.
Apart from that, the client can also serialise the object again and passed back to the server (for example with pending updates inside).
The shared assembly contains both the remote object definition and implementation, as well as the shared remote object manager interface.
The latter acts as the shared interface between the client and the server again.
The interface is implemented at the server side, and referenced by the client side.
The actual object that is passed from server to client is also defined in the shared assembly.
Start Delphi 8 and create a new assembly.
The best way to create an assembly that can be used with Delphi 8 for .NET projects is to build a package assembly.
So, do File | New - Package and save it in project SharedDotNetRemotingAssembly.
Now, do File | New - Unit and add a new unit to the package.
Save this unit in SharedRemoteInterface.pas, and get ready to place the remote object as well as the shared interface in this unit.
As example, I want to create a class called RemoteEmployee that has similar fields as the InterBase Employee.gdb EMPLOYEE table: EmpNo, FirstName, LastName, PhoneExt, HireDate (of type System.DateTime, since TDateTime doesn't serialise), DeptNo, JobCode, JobGrade, JobCountry, Salary and FullName.
Actually, FullName is a calculated field, so let's add that as a remote method, just like a method WorkingYears.
In case you're wondering: this is just a preparation to get ready for the connection to a real remote database table next time (yes, that's a teaser, so stay tuned).
The RemoteEmployee class is used by the server as well as the client, and has to be serialised, so we need to declare it with the [Serializable] attribute.
unit SharedRemoteInterface; interface uses System.Reflection; type [Serializable] RemoteEmployee = class public EmpNo: Integer; FirstName: string; LastName: string; PhoneExt: Integer; HireDate: System.DateTime; DeptNo: Integer; JobCode: string; JobGrade: Integer; JobCountry: string; Salary: Double; public constructor Create; function FullName: string; function WorkingYears: integer; end;Apart from the class definition of the RemoteEmployee, we also need to declare an interface, called IRemoteEmployeeManager. This is the interface definition of the manager (the actual remote object that will reside at the server side) that will be called by the client to get an instance of RemoteEmployee.
type IRemoteEmployeeManager = interface function GetRemoteEmployee(const EmpNo: Integer): RemoteEmployee; procedure SetRemoteEmployee(const Emp: RemoteEmployee); function HelloWorld: String; end;The IRemoteEmployeeManager will be implemented by the Remote Server, and called by the clients to obtain instances of employee objects.
implementation constructor RemoteEmployee.Create; begin Inherited Create; Console.WriteLine(Self.ClassName + '.Created') // server side only end; function RemoteEmployee.FullName: String; begin Result := FirstName + ' ' + LastName; Console.WriteLine(Self.ClassName + '.FullName: ' + Result) // wherever the call is made! end; function RemoteEmployee.WorkingYears: integer; var Year, Month: Integer; begin Year := DateTime.Now.Year - HireDate.Year; Month := DateTime.Now.Month - HireDate.Month; if Month = 0 then if (DateTime.Now.Day - HireDate.Day) < 0 then Month := -1; if Month < 0 then Dec(Year); Result := Year end; end.We have to compile the assembly and use it in both the RemoteServer and Client applications. For that purpose, I've created two subdirectories, called Server and Client, in my SharedDotNetRemotingAssembly project directory. You can put a copy of the shared assembly in both directories, and prepare to deploy one of them (for example the client) on a different machine.
Remote Server 2
Let's start with the Remote Server.
Although any application can act as a .NET Remoting server, it's easiest to start with a console application so we can focus on the .NET Remoting details.
So, start Delphi 8 for .NET and do File | New - Other, and create a new Console Application.
Save the project as DotNetRemotingServer.
Right-click on the project node and add a reference to the System.Runtime.Remoting assembly.
Apart from that one, you also need to add a reference to the SharedDotNetRemotingAssembly (use the Browse button for that one, since it's not deployed in the GAC but present in our project directory).
The System.RunTime.Remoting assembly contains a number of useful namespaces, like the System.Runtime.Remoting.Channels namespace that contains generic channel support, System.Runtime.Remoting.Channels.HTTP for HTTP and System.Runtime.Remoting.TCP for TCP channels, and finally System.Runtime.Serialization.Formatters for the message formatters to binary or SOAP.
You can add them all to your uses clause.
Apart from those generic .NET Remoting units, you must also add the SharedRemoteInterface namespace to the uses clause of your DotNetRemotingServer project, as this is needed to implement the IRemoteEmployeeManager interface.
The class that implements this interface has to be derived from MarshalByRefObject.
There's one funny thing that I noticed, and that's the fact that the interface definition of IRemoteEmployee used const values for the EmpNo in the GetRemoteEmployee, and RemoteEmployee in SetRemoteEmployee, but if I simply copy-and-paste these method definitions, then I get a compiler error.
To get around this, you can write the following empty definition:
type RemoteEmployeeManager = class(MarshalByRefObject, IRemoteEmployeeManager) end;Place your cursor just before the end, and press Control+Space. This will give you a list of all available methods in the RemoteEmployeeManager class, including abstract methods (or methods that have to be implemented) which are coloured red. Now, in the code insight window, select all red methods. This isn't too hard, since they will be the first few lines in the list. Once you've selected them all, press enter and they will be inserted in your class definition, as follows:
type RemoteEmployeeManager = class(MarshalByRefObject, IRemoteEmployeeManager) public function GetRemoteEmployee(EmpNo: Integer): RemoteEmployee; function HelloWorld: string; procedure SetRemoteEmployee(Emp: RemoteEmployee); end;Now, press Control+Shift+C to generate the three empty skeleton implementations for the RemoteEmployeeManager, and implement them as follows:
{ RemoteEmployeeManager }
function RemoteEmployeeManager.GetRemoteEmployee(EmpNo: Integer): RemoteEmployee;
begin
Result := RemoteEmployee.Create;
Result.EmpNo := EmpNo;
Result.FirstName := 'Bob';
Result.LastName := 'Swart';
Result.PhoneExt := 42;
Result.HireDate := System.DateTime.Create(1983, 9, 27);
Result.DeptNo := 1;
Result.JobCode := 'real programmer';
Result.JobGrade := 1;
Result.JobCountry := 'The Netherlands';
Result.Salary := 42000
end;
function RemoteEmployeeManager.HelloWorld: string;
begin
writeln('Hello, world!')
end;
procedure RemoteEmployeeManager.SetRemoteEmployee(Emp: RemoteEmployee);
begin
writeln('SetRemoteEmployee not implemented, yet');
writeln('RemoteEmployee:');
writeln('EmpNo: ', Emp.EmpNo);
writeln('FirstName:', Emp.FirstName);
writeln('LastName:', Emp.LastName);
writeln('PhoneExt:', Emp.PhoneExt);
writeln('HireDate:', Emp.HireDate);
writeln('DeptNo:', Emp.DeptNo);
writeln('JobCode:', Emp.JobCode);
writeln('JobGrade:', Emp.JobGrade);
writeln('JobCountry:', Emp.JobCountry);
writeln('Salary:', Emp.Salary:1:0)
end;
apart from the fact that the RemoteEmployeeManager is derived from MarshalByRefObject instead of a normal Object, this doesn't look like anything special.
But now we need to implement the "main" part of the console application, which creates and registers a channel to communicate over.
We will start with the HTTP transport protocol, using the SOAP message format, which means the uses clause should be at least as follows:
uses System.Runtime.Remoting, // for RemotingConfiguration System.Runtime.Remoting.Channels, // for ChannelServices System.Runtime.Remoting.Channels.Http, // for HttpChannel // System.Runtime.Remoting.Channels.Tcp, // System.Runtime.Serialization.Formatters, SharedRemoteInterface;The namespace System.Runtime.Remoting contains the RemotingConfiguration object, the namespace System.Runtime.Remoting.Channels contains the ChannelServices object, and the namespace System.Runtime.Remoting.Channels.Http contains the definition of the HttpChannel class that we must use for this example. We do not need a TCP Channel or message formatter at this time.
const PortNumber = 4242; ServerResource = 'RemoteEmployeeManager.soap'; var Channel: HttpChannel; begin writeln('DotNetRemotingServer started.'); Channel := HTTPChannel.Create(PortNumber); ChannelServices.RegisterChannel(Channel); writeln('Listening for SOAP messages on HTTP port ', PortNumber); RemotingConfiguration.RegisterWellKnownServiceType( typeof(RemoteEmployeeManager), ServerResource, WellKnownObjectMode.Singleton); writeln('Hit <enter> to stop.'); readln end.Save and compile your work. Once you run the DotNetRemotingServer, it will create and register a HTTP channel that will listed to port 4242 for incoming requests. The incoming requests and responses will be sent in the default SOAP format.
Remote Client 2
Time to start the second .NET Remoting client application, again as a console application, this time called DotNetRemotingClient.
Right-click on the project node and add the System.Runtime.Remoting and DotNetSharedAssembly as references to the project.
Now, add the same units to the uses clause of the project that you used in the DotNetRemotingServer.
We need to create a HttpChannel again in order to communicate with the DotNetRemotingServer.
program DotNetRemotingClient; {$APPTYPE CONSOLE} uses System.Runtime.Remoting, // for RemotingConfiguration System.Runtime.Remoting.Channels, // for ChannelServices System.Runtime.Remoting.Channels.Http, // for HttpChannel // System.Runtime.Remoting.Channels.Tcp, // System.Runtime.Serialization.Formatters, SharedRemoteInterface; const RemoteServer = 'http://localhost:4242/'; ServerResource = 'RemoteEmployeeManager.soap'; var Channel: HttpChannel; EmpManager: IRemoteEmployeeManager; Emp: RemoteEmployee; begin writeln('DotNetRemotingClient started.'); Channel := HTTPChannel.Create; // no PortNumber needed ChannelServices.RegisterChannel(Channel); try EmpManager := Activator.GetObject(typeof(IRemoteEmployeeManager), RemoteServer + ServerResource); writeln('Reference to IRemoteEmployeeManager received.'); except writeln('Could not get reference to IRemoteEmployeeManager.') end; if Assigned(EmpManager) then try EmpManager.HelloWorld; // Are we alive? (at the server side!) Emp := EmpManager.GetRemoteEmployee(42); writeln('Fullname of Emp: ', Emp.FullName); Emp.FirstName := 'Robert'; EmpManager.SetRemoteEmployee(Emp); except on E: Exception do writeln(E.ClassName, E.Message) end; write('Hit <enter> to stop DotNetRemotingClient.'); readln end.Note that the remote object is now called EmpManager, and it can be used to create an Employee object at the server side, serialise it, and send it to the client side where it will automatically be deserialised. As a result, the Create constructor code will be executed at the server, but not at the client (deserialisation doesn't call the Create constructor it seems).
.NET Remoting in Action again!
In order to demonstrate the .NET Remoting capabilities in the DotNetRemotingServer and DotNetRemotingClient projects, start the server and then the client.
This will produce the following two console windows.
First, the DotNetRemotingServer, waiting for an incoming request:
As soon as you start the DotNetRemotingClient project, a new console window opens where you'll see that the client receives a reference to IRemoteEmployeeManager and uses that to call GetRemoteEmployee. The RemoteEmployee will be created at the server side (the constructor is only executed at the server), but when the clients calls the method FullName, then this method is executed at the client side again - and not at the server side.
At the end of all this, the DotNetRemotingServer's console window will have the result of the HelloWorld method, as well as the GetRemoteEmployee method that calls the constructor of the RemoteEmployee (only called at the server side), as well as the output of the SetRemoteEmployee method.
Note that the SetRemoteEmployee received the new FirstName, modified by the client.
Formatters and Channels
Apart from the SOAP format and HTTP channel, we can also use the binary message format and TCP as communication channel.
This requires very few changes in your source code.
In order to switch from HTTP to TCP you only need to switch from a HttpChannel to a TcpChannel.
Since they do not have a common ancestor, I've decided to use $IFDEFs to produce code that can use a HTTP or a TCP channel.
const PortNumber = 4242; {$IFDEF HTTP} ServerResource = 'RemoteEmployeeManager.soap'; {$ELSE} // TCP ServerResource = 'RemoteEmployeeManager'; {$ENDIF} var {$IFDEF HTTP} Channel: HttpChannel; {$ELSE} Channel: TcpChannel; {$ENDIF} {$IFDEF BINARY} Provider: BinaryServerFormatterSinkProvider; Hash: HashTable; {$ENDIF}Note that last part of the code snippet above. This is the declaration of the formatter, of type BinaryServerFormatterSinkProvider that will convert the message (by default in SOAP format) to the binary format. The code snippet that will create the correct instance of the channel, and add the formatter to it, is as follows:
begin writeln('DotNetRemotingServer started.'); {$IFDEF BINARY} Provider := BinaryServerFormatterSinkProvider.Create; Provider.TypeFilterLevel := System.Runtime.Serialization.Formatters.TypeFilterLevel.Full; Hash := HashTable.Create; Hash['port'] := System.Convert.ToString(PortNumber); writeln('BinaryServerFormatterSinkProvider created.'); {$ENDIF} {$IFDEF HTTP} {$IFDEF BINARY} Channel := HTTPChannel.Create(Hash, nil, Provider); writeln('Listening for BINARY messages on HTTP port ',PortNumber); {$ELSE} Channel := HTTPChannel.Create(PortNumber); writeln('Listening for SOAP messages on HTTP port ',PortNumber); {$ENDIF} {$ELSE} // TCP {$IFDEF BINARY} Channel := TCPChannel.Create(Hash, nil, Provider); writeln('Listening for BINARY messages on TCP port ',PortNumber); {$ELSE} Channel := TCPChannel.Create(PortNumber); writeln('Listening for SOAP messages on TCP port ',PortNumber); {$ENDIF} ChannelServices.RegisterChannel(Channel); {$ENDIF} RemotingConfiguration.RegisterWellKnownServiceType( typeof(RemoteEmployeeManager), ServerResource, WellKnownObjectMode.Singleton); writeln('Hit <enter> to stop DotNetRemotingServer.'); readln end.Where the DotNetRemotingServer application creates a BinaryServerFormatterSinkProvider to be used as binary formatter, the DotNetRemotingClient application has to create and use a BinaryClientFormatterSinkProvider. Apart from that, the code is very similar, and shown below (including all IFDEFs):
program DotNetRemotingClient; {$APPTYPE CONSOLE} {$DEFINE HTTP} {.$DEFINE BINARY} uses System.Runtime.Remoting, // for RemotingConfiguration System.Runtime.Remoting.Channels, // for ChannelServices System.Runtime.Remoting.Channels.Http, // for HttpChannel System.Runtime.Remoting.Channels.Tcp, System.Runtime.Serialization.Formatters, SharedRemoteInterface; const {$IFDEF HTTP} RemoteServer = 'http://localhost:4242/'; ServerResource = 'RemoteEmployeeManager.soap'; {$ELSE} // TCP RemoteServer = 'tcp://localhost:4242/'; ServerResource = 'RemoteEmployeeManager'; {$ENDIF} var {$IFDEF HTTP} Channel: HttpChannel; {$ELSE} Channel: TcpChannel; {$ENDIF} {$IFDEF BINARY} ClientProvider: BinaryClientFormatterSinkProvider; {$ENDIF} EmpManager: IRemoteEmployeeManager; Emp: RemoteEmployee; begin writeln('DotNetRemotingClient started.'); {$IFDEF HTTP} {$IFDEF BINARY} ClientProvider := BinaryClientFormatterSinkProvider.Create; Channel := HTTPChannel.Create(nil, ClientProvider, nil); writeln('Sending BINARY messages to HTTP server ',RemoteServer); {$ELSE} Channel := HTTPChannel.Create; // no PortNumber needed writeln('Sending SOAP messages to HTTP server ',RemoteServer); {$ENDIF} {$ELSE} // TCP {$IFDEF BINARY} ClientProvider := BinaryClientFormatterSinkProvider.Create; Channel := TCPChannel.Create(nil, ClientProvider, nil); writeln('Sending BINARY messages to TCP server ',RemoteServer); {$ELSE} Channel := TCPChannel.Create; // no PortNumber needed writeln('Sending SOAP messages to TCP server ',RemoteServer); {$ENDIF} {$ENDIF} ChannelServices.RegisterChannel(Channel); writeln('Channel registered.'); try EmpManager := Activator.GetObject(typeof(IRemoteEmployeeManager), RemoteServer + ServerResource); writeln('Reference to IRemoteEmployeeManager received.'); except writeln('Could not get reference to IRemoteEmployeeManager.') end; if Assigned(EmpManager) then try EmpManager.HelloWorld; // Are we alive? (at the server side!) Emp := EmpManager.GetRemoteEmployee(42); writeln('Fullname of Emp: ', Emp.FullName); Emp.FirstName := 'Robert'; EmpManager.SetRemoteEmployee(Emp); except on E: Exception do writeln(E.ClassName, ': ', E.Message) end; write('Hit <enter> to stop DotNetRemotingClient.'); readln end.Obviously, both the DotNetRemotingServer and DotNetRemotingClient must be compiled with the same combination of HTTP and BINARY defined or not defined, to ensure that the two projects can actually communicate with each other over the same transport channel (HTTP or TCP), using the same message format (binary or SOAP).
Summary
In this article, I've tried to explain how we can build simple distributed applications with Delphi 8 for .NET, using .NET Remoting as technique to allow clients to obtain a reference to a remote object.
In a future article, I'll extend the example further to include actual database connectivity, resulting in a multi-tier database application based on .NET Remoting.
Latest News: The CodeGear RAD Studio 2007 (.NET 2.0) version of the enhanced demo is now available for download.