Now that NexusRemoting is generally available as part of NexusDB V3 Public Beta 1 I'll demonstrate how everything comes together in a simple example.
For a fully working NexusRemoting example the following parts are needed:
- An class/interface definition that is shared between client and server.
- An implementation of these classes.
- A server application which hosts these implementation classes and makes them available via one or more NexusDB Transports.
- A client application which connects to this server and makes use of the exposed classes.
For this example I'll be implementing a simple calculator.
Shared Class and Interface Definition
With NexusRemoting this definition comes in form of a simple unit. This unit will be compiled into both the server and the client application.
interface
uses
nxllTypes,
nxivTypes;
nxllTypes is required because it defines TnxGuid. nxivTypes defines InxInvokable and the nxInvokeRegistry.
CLSID_Calc : TnxGuid = '{AA97D66D-F493-4267-95B7-48D166E8B43D}';
NexusRemoting uses GUIDs to identify classes. When the client wants to create a server-side class it will use this GUID in it's CreateInstance call.
To generate a new GUID directly in the Delphi IDE you can press Shift+Ctrl+G. For the assignment to a TnxGuid like shown above it's necessary to remove the [ ] that the IDE automatically put around it.
InxCalc = interface(InxInvokable)
['{5642D468-0B63-4ECF-B932-D33C41932528}']
procedure Clear;
procedure Add(const aValue: Extended);
procedure Subtract(const aValue: Extended);
procedure Multiply(const aValue: Extended);
procedure Divide(const aValue: Extended);
function GetResult: Extended;
property Result: Extended
read GetResult;
end;
This is the interface that the Calc class will be implementing. It derives from InxInvokeable which signals the compiler that it needs to generate RTTI information for this interface.
The required GUID can again be simply created using Shift+Ctrl+G.
It is on purpose designed so that performing calculations and getting the final result will take multiple calls to demonstrate the stateful nature of NexusRemoting.
initialization
nxInvokeRegistry.RegisterInterface(TypeInfo(InxCalc));
end.
By registering the interface with the Invoke Registry NexusRemoting is able to create internal data structures with required information to create proxies and stubs on the fly.
Implementation
The next step is a unit containing the class that implements the actual functionality. This unit will only be compiled into the server application, but the client application will be able to create instances of it and make calls against this instance through NexusRemoting.
interface
uses
nxrdClass,
CalcIntf;
nxrdClass contains TnxClass and TnxClassFactory.
CalcIntf is the unit that contains the interface definition that's shared between client and server.
TnxCalc = class(TnxClass, InxCalc)
protected {private}
clcResult: Extended;
protected
{--- InxCalc ---}
procedure Clear;
procedure Add(const aValue: Extended);
procedure Subtract(const aValue: Extended);
procedure Multiply(const aValue: Extended);
procedure Divide(const aValue: Extended);
function GetResult: Extended;
end;
While it is not strictly necessary for all classes exposed through NexusRemoting to derive from TnxClass, doing so simplifies the code.
In this example our new class only implements a single interface, InxCalc. But there is no restriction on how many interfaces a class can implement or that a specific interface can only be implemented by a single class.
There is also no restriction that all interfaces implemented by a class need to be supported NexusRemoting. Any interface for which no common proxy/stub implementation can be found between client and server is simply not visible to a remote client.
uses
nxrbTypes;
nxrbTypes contains InxClassFactoryControl.
procedure TnxCalc.Add(const aValue: Extended);
begin
clcResult := clcResult + aValue;
end;
procedure TnxCalc.Clear;
begin
clcResult := 0;
end;
procedure TnxCalc.Divide(const aValue: Extended);
begin
clcResult := clcResult / aValue;
end;
function TnxCalc.GetResult: Extended;
begin
Result := clcResult;
end;
procedure TnxCalc.Multiply(const aValue: Extended);
begin
clcResult := clcResult * aValue;
end;
procedure TnxCalc.Subtract(const aValue: Extended);
begin
clcResult := clcResult - aValue;
end;
The actual implementation of the class is very straight forward and is no different from a class that would be used directly in-process.
Control : InxClassFactoryControl;
initialization
TnxClassFactory.RegisterClass(CLSID_Calc, TnxCalc, Control);
end.
The Class Factory is the link between the CreateInstance call with a specific GUID and this actual class. There are different types of class factories which will be explored in more detail in a future post. TnxClassFactory is the simplest one which creates an returns a new instance of the class for every CreateInstance request.
Control is an out parameter which returns a reference to an InxClassFactoryControl. This interface is used to control the lifetime of the class factory registration. By releasing the last reference to it the class factory registration is revoked.
Server Application
For the server application a couple of components are required
ActiveRuntime = True
ActiveDesigntime = True
end
TnxRemotingServer is a very simple implementation of InxClassActivator which passes calls straight through to the Class Factory Registry.
ActiveRuntime = True
ActiveDesigntime = True
CommandHandler = SimpleCommandHandler
PluginEngine = RemotingServer
end
TnxRemotingCommandHandler is a Plugin Command Handler which is a part of the NexusDB Transport Architecture. It takes messages from the Command Handler it's attached to and translates them into method calls against the attached PluginEngine, in this case, the RemotingServer.
end
The job of the Command Handler is to process messages it gets handed by a Transport. Any messages it can't process itself are handed of to the attached Plugin Command Handlers for processing.
TnxSimpleCommandHandler is one of 2 different Command Handlers that ship with NexusDB. As the name implies, it is very simple and does not recognize any messages of it's own. All messages just get forwarded to attached Plugin Command Handlers.
The other Command Handler would be TnxCommandHandler which can be attached to a TnxServerEngine and is used to implement a fully functional NexusDB Database Server.
As this example is only focused on NexusRemoting a TnxSimpleCommandHandler is all that's needed.
CommandHandler = SimpleCommandHandler
Mode = nxtmListen
RespondToBroadcasts = True
ServerNameRuntime = 'NexusRemoteTest'
ServerNameDesigntime = 'NexusRemoteTest'
end
object NamedPipe: TnxNamedPipeTransport
CommandHandler = SimpleCommandHandler
Mode = nxtmListen
RespondToBroadcasts = True
ServerNameRuntime = 'NexusRemoteTest'
ServerNameDesigntime = 'NexusRemoteTest'
end
object SharedMemory: TnxSharedMemoryTransport
CommandHandler = SimpleCommandHandler
Mode = nxtmListen
RespondToBroadcasts = True
ServerNameRuntime = 'NexusRemoteTest'
ServerNameDesigntime = 'NexusRemoteTest'
end
One or more transports in Listen Mode can then be connected to the Command Handler, completing the required change of components:
- Transport (Winsock, NamedPipe, SharedMemory, ...)
- Command Handler (Simple or Full)
- Plugin Command Handler (RemotingCommandHandler)
- Plugin Engine (RemotingServer)
While this might appear rather complex at first glance, the system is very flexible and easy to extend with additional Transports or Plugins and it's only necessary to setup this chain of components once for a Server Application.
All that's now missing for a simple server application are 2 buttons to start and stop the server:
object bnStart: TButton
Left = 8
Top = 8
Width = 75
Height = 25
Caption = 'Start Server'
TabOrder = 0
OnClick = bnStartClick
end
object bnStop: TButton
Left = 89
Top = 8
Width = 75
Height = 25
Caption = 'Stop Server'
TabOrder = 1
OnClick = bnStopClick
end
begin
try
// Winsock.Open;
// NamedPipe.Open;
SharedMemory.Open;
except
SimpleCommandHandler.Close;
raise;
end;
Caption := 'Server started';
end;
procedure TfrmServer.bnStopClick(Sender: TObject);
begin
SimpleCommandHandler.Close;
Caption := 'Server stopped';
end;
And making sure that CalcImpl gets compiled into the application:
uses
Forms,
ServerFrm in 'ServerFrm.pas' {frmServer},
CalcIntf in '..\CalcIntf.pas',
CalcImpl in 'CalcImpl.pas';
Client Application
On the client side 3 components are needed:
- TnxRemotingClient: An implementation of InxClassActivator which sends a message over the Session object it's attached to.
- TnxSimpleSession: The client-side counterpart to TnxSimpleCommandHandler which sends messages to the Command Handler via the the Transport it's attached to.
- A Transport (matching to whatever transport the server is using)
ServerNameDesigntime = 'NexusRemoteTest'
ServerNameRuntime = 'NexusRemoteTest'
end
object SimpleSession: TnxSimpleSession
Transport = SharedMemory
end
object RemotingClient: TnxRemotingClient
Session = SimpleSession
end
With these 3 components in place it's now easy to call CreateInstance on the RemotingClient to create instances of registered Classes in the Server and retrieve an interface reference to it:
var
Calc: InxCalc;
begin
RemotingClient.Open;
if RemotingClient.CreateInstance(CLSID_Calc, nil, InxCalc, Calc) = S_OK then
TfrmCalc.Create(Calc);
end;
The last missing part is the TfrmCalc which presents a user interface to interact with this interface:
TfrmCalc
Caption = 'Calculator'
ClientHeight = 105
ClientWidth = 462
OnClose = FormClose
object edInput: TEdit
Left = 8
Top = 41
Width = 121
Height = 21
TabOrder = 0
end
object bnAdd: TButton
Left = 135
Top = 39
Width = 75
Height = 25
Caption = 'Add'
TabOrder = 1
OnClick = bnAddClick
end
object bnSubtract: TButton
Left = 216
Top = 39
Width = 75
Height = 25
Caption = 'Subtract'
TabOrder = 2
OnClick = bnSubtractClick
end
object bnMultiply: TButton
Left = 297
Top = 39
Width = 75
Height = 25
Caption = 'Multiply'
TabOrder = 3
OnClick = bnMultiplyClick
end
object bnDivide: TButton
Left = 378
Top = 39
Width = 75
Height = 25
Caption = 'Divide'
TabOrder = 4
OnClick = bnDivideClick
end
object bnClear: TButton
Left = 135
Top = 8
Width = 75
Height = 25
Caption = 'Clear'
TabOrder = 5
OnClick = bnClearClick
end
object bnResult: TButton
Left = 135
Top = 70
Width = 75
Height = 25
Caption = 'Result'
TabOrder = 6
OnClick = bnResultClick
end
object edResult: TEdit
Left = 216
Top = 72
Width = 121
Height = 21
ReadOnly = True
TabOrder = 7
end
end
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, CalcIntf, StdCtrls;
type
TfrmCalc = class(TForm)
edInput: TEdit;
bnAdd: TButton;
bnSubtract: TButton;
bnMultiply: TButton;
bnDivide: TButton;
bnClear: TButton;
bnResult: TButton;
edResult: TEdit;
procedure bnClearClick(Sender: TObject);
procedure bnAddClick(Sender: TObject);
procedure bnSubtractClick(Sender: TObject);
procedure bnMultiplyClick(Sender: TObject);
procedure bnDivideClick(Sender: TObject);
procedure bnResultClick(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
fcCalc: InxCalc;
public
constructor Create(aCalc: InxCalc); reintroduce;
end;
implementation
{$R *.dfm}
procedure TfrmCalc.bnAddClick(Sender: TObject);
begin
fcCalc.Add(StrToFloat(edInput.Text));
end;
procedure TfrmCalc.bnClearClick(Sender: TObject);
begin
fcCalc.Clear;
end;
procedure TfrmCalc.bnDivideClick(Sender: TObject);
begin
fcCalc.Divide(StrToFloat(edInput.Text));
end;
procedure TfrmCalc.bnMultiplyClick(Sender: TObject);
begin
fcCalc.Multiply(StrToFloat(edInput.Text));
end;
procedure TfrmCalc.bnResultClick(Sender: TObject);
begin
edResult.Text := FloatToStr(fcCalc.Result);
end;
procedure TfrmCalc.bnSubtractClick(Sender: TObject);
begin
fcCalc.Subtract(StrToFloat(edInput.Text));
end;
constructor TfrmCalc.Create(aCalc: InxCalc);
begin
inherited Create(Application);
fcCalc := aCalc;
Show;
end;
procedure TfrmCalc.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := caFree;
end;
end.
As can be seen, this code is very straight forward and does not in any way have to know that the InxCalc it's operating on is a remoted reference to a Class instance in a different process.
Trying it out
The complete example can be found either in in the folder "NexusDB3\Examples\Remoting\Basic" or downloaded for here. There is a project group which directly opens both the Server and Client project.
As the projects are setup to use the Shared Memory Transport it is important under Vista (or newer) to run them elevated. This is a current limitation of the Shared Memory Transport and does not affect the Winsock or Named Pipe transport.
After compiling both Server and Client, first start the Server.exe and click on "Start Server", then start Client.exe and click on "Open Calc".
It is now possible to enter numbers in the edit on the left and perform one of the 4 operations by clicking one of the buttons on the right of the edit. The button labeled "Result" retrieves the current result and displays it in the edit on the right of it.
As the Calculator Form has not been opened modal, it is possible to click "Open Calc" multiple times, each time a new instance of TnxCalc will be created and a new TfrmCalc will be opened. It is possible to clearly see that the different Calculator Forms are each connected to an independent instance by performing operations in different Forms and seeing that only the result of that specific Form is affected.
More to come...
This post demonstrated the structure of a basic remoting server and client application. In future posts I'll explore the possibilities of two-way communication and the effect of using different types of Class Factories.