Search code examples
delphimormot

Use mORMot Framework to send message between server and clients


mORMot Framework (www.synopse.info) have add support to WebSockets, on pack there is also a demo about WebSockets (sample 31). In this sample the client send a message to server and this reply with a new message to client. I'd like use this library to do this:

  1. The client send message to server and send also the IP address (without waiting message from server.);
  2. The server can send message to single client by IP address;

Note: the IP address is used only to identify the client. I can use also a unique name.

Something like a LAN chat between clients and server. I don't understand how edit the sample n. 31 to do it. This sample is based on interface.


Solution

  • No need to store the IP, or whatever low-level implementation parameter (by the way, the IP would not be able to identify a connection in an unique manner: several clients may share the same IP).

    In the mORMot framework, asynchronous callbacks are implemented via interface parameters. On the server side, each instance of this parameter would be in fact a "fake" class instance, linked to the input connection, able to call back the client via its interface methods.

    This is a pretty straightforward way of implementing callbacks - in fact this is a good way of implementing SOLID callbacks on the server side, and the mORMot framework allows to publish this mechanism in a client/server way, by using WebSockets.

    So you first define the callback interface, and the service interface:

      IChatCallback = interface(IInvokable)
        ['{EA7EFE51-3EBA-4047-A356-253374518D1D}']
        procedure BlaBla(const pseudo, msg: string);
      end;
    
      IChatService = interface(IInvokable)
        ['{C92DCBEA-C680-40BD-8D9C-3E6F2ED9C9CF}']
        procedure Join(const pseudo: string; const callback: IChatCallback);
        procedure BlaBla(const pseudo,msg: string);
        procedure CallbackReleased(const callback: IInvokable);
      end;
    

    Then, on the server side, each call to IChatService.Join() would subscribe to an internal list of connections:

      TChatService = class(TInterfacedObject,IChatService)
      protected
        fConnected: array of IChatCallback;
      public
        procedure Join(const pseudo: string; const callback: IChatCallback);
        procedure BlaBla(const pseudo,msg: string);
        procedure CallbackReleased(const callback: IInvokable);
      end;
    
    procedure TChatService.Join(const pseudo: string;
      const callback: IChatCallback);
    begin
      InterfaceArrayAdd(fConnected,callback);
    end;
    

    Then a remote call to the IChatService.BlaBla() method should be broadcasted to all connected clients, just by calling the IChatCallback.BlaBla() method:

    procedure TChatService.BlaBla(const pseudo,msg: string);
    var i: integer;
    begin
      for i := 0 to high(fConnected) do
        fConnected[i].BlaBla(pseudo,msg);
    end;
    

    Note that all the loop calls to IChatCallback.BlaBla() would be made via WebSockets, in an asynchronous and non blocking way, so that even in case of huge number of clients, the IChatService.BlaBla() method won't block. In case of high numbers of messages, the framework is even able to gather push notification messages into a single message, to reduce the resource use.

    The following method will be called by the server, when a client callback instance is released (either explicitly, or if the connection is broken), so could be used to unsubscribe to the notification:

    procedure TChatService.CallbackReleased(const callback: IInvokable);
    begin
      InterfaceArrayDelete(fConnected,callback);
    end;
    

    On the server side, you define the service as such:

      Server.ServiceDefine(TChatService,[IChatService],sicShared).
        SetOptions([],[optExecLockedPerInterface]);
    

    Here, the optExecLockedPerInterface option has been set, so that all method calls would be made thread-safe, so that concurrent access to the internal fConnected[] list would be safe.

    On the client side, you implement the IChatCallback callback interface:

    type
      TChatCallback = class(TInterfacedCallback,IChatCallback)
      protected
        procedure BlaBla(const pseudo, msg: string);
      end;
    
    procedure TChatCallback.BlaBla(const pseudo, msg: string);
    begin
      writeln(#13'@',pseudo,' ',msg);
    end;
    

    Then you subscribe to your remote service as such:

    var Service: IChatService;
        callback: IChatCallback;
    ...
        Client.ServiceDefine([IChatService],sicShared);
        if not Client.Services.Resolve(IChatService,Service) then
          raise EServiceException.Create('Service IChatService unavailable');
    ...
          callback := TChatCallback.Create(Client,IChatCallback);
          Service.Join(pseudo,callback);
    ...
        try
          repeat
            TextColor(ccLightGray);
            readln(msg);
            if msg='' then
              break;
            Service.BlaBla(pseudo,msg);
          until false;
        finally
          callback := nil;
          Service := nil; // release the service local instance BEFORE Client.Free
        end;
    

    If you compare with existing client/server SOA solutions (in Delphi, Java, C# or even in Go or other frameworks), this interface-based callback mechanism sounds pretty unique and easy to work with. Of course, this work from Delphi 6 up to XE7, and also under Linux thanks to FPC or CrossKylix.

    I've uploaded the full sample source code in our repository, both the server side application and the client side.