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.

unit CalcIntf;

interface

uses
  nxllTypes,
  nxivTypes;

nxllTypes is required because it defines TnxGuid. nxivTypes defines InxInvokable and the nxInvokeRegistry.

const
  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.

type
  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.

implementation

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.

unit CalcImpl;

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.

type
  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.

implementation

uses
  nxrbTypes;

nxrbTypes contains InxClassFactoryControl.

{ TnxCalc }

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.

var
  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

  object RemotingServer: TnxRemotingServer
    ActiveRuntime = True
    ActiveDesigntime = True
  end

TnxRemotingServer is a very simple implementation of InxClassActivator which passes calls straight through to the Class Factory Registry.

  object RemotingCommandHandler: TnxRemotingCommandHandler
    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.

    object SimpleCommandHandler: TnxSimpleCommandHandler
  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.

  object Winsock: TnxWinsockTransport
    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
procedure TfrmServer.bnStartClick(Sender: TObject);
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:

program Server;

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)
  object SharedMemory: TnxSharedMemoryTransport
    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:

procedure TForm1.bnCalcClick(Sender: TObject);
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:

TfrmCalcTfrmCalc

object frmCalc: 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
  unit CalcFrm;

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.

Home | Community | Blogs | Thorsten's Blog