Search code examples
socketsdelphidelphi-10.3-riotlist

How send/receive a List of elements over socket?


I have the following code, where I can draw several rectangles and make a hole to each.

How can I send the RectList object over a socket (TServerSocket) and recover (receive in a TClientSocket) this object directly to a variable of same type (var RectList: TList<TRect>)?

type
  TForm1 = class(TForm)
    ComboBox1: TComboBox;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure FormMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
    procedure FormMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure FormPaint(Sender: TObject);
  private
    { Private declarations }
    Drawing: Boolean;
    RectList: TList<TRect>;
    Rectangle: TRect;
    FormRegion, HoleRegion: HRGN;
    function ClientToWindow(const P: TPoint): TPoint;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

function TForm1.ClientToWindow(const P: TPoint): TPoint;
begin
  Result := ClientToScreen(P);
  Dec(Result.X, Left);
  Dec(Result.Y, Top);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  RectList := TList<TRect>.Create;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  RectList.Free;
end;

procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  Rectangle.Left := X;
  Rectangle.Top := Y;
  Drawing := True;
end;

procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
begin
  if Drawing then
  begin
    Rectangle.Right := X;
    Rectangle.Bottom := Y;
    Invalidate;
  end;
end;

procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  I: Integer;
begin
  Drawing := false;
  Rectangle.Right := X;
  Rectangle.Bottom := Y;
  Invalidate;

  if RectList.Count < StrToInt(ComboBox1.Text) then
  begin
    Rectangle.NormalizeRect;
    if not Rectangle.IsEmpty then
      RectList.Add(Rectangle)
    else
      SetWindowRgn(Handle, 0, True);
  end
  else
  begin
    FormRegion := CreateRectRgn(0, 0, Width, Height);
    for I := 0 to Pred(RectList.Count) do
    begin
      HoleRegion := CreateRectRgn(ClientToWindow(RectList.Items[I].TopLeft).X, ClientToWindow(RectList.Items[I].TopLeft).Y, ClientToWindow(RectList.Items[I].BottomRight).X, ClientToWindow(RectList.Items[I].BottomRight).Y);
      CombineRgn(FormRegion, FormRegion, HoleRegion, RGN_DIFF);
    end;
    SetWindowRgn(Handle, FormRegion, True);
    RectList.Clear;
  end;
end;

procedure TForm1.FormPaint(Sender: TObject);
var
  R: TRect;
begin
  Canvas.Brush.Style := bsClear;
  Canvas.Pen.Style := psSolid;
  Canvas.Pen.Color := clRed;
  Canvas.Rectangle(Rectangle);

  for R in RectList do
    Canvas.Rectangle(R);
end;

end.

Solution

  • I made some code to show you how to do it.

    In your code, I added a TClientSocketon the form and assigned a few events. Also added a TButton to send the RectList to the other side (server side) thru the TClientSocket.

    I designed a new simple server application having a TServerSocket set to listen for client connection and accepting commands from the client. I implemented two commands: rectangle and clear. Obviously clear command is implemented to clear the display on the rectangle list. The rectangle command is used to sent a rectangle (Left, top, right and bottom as coma delimited integers).

    Since client and server must understand each other, I designed a very simple communication protocol. Data is exchanged between client and server using ASCII lines. A line is any character collection terminated by a CRLF pair. TCP port 2500 (Almost any other would do) is used.

    For example, the command

    rectangle 10,20,30,40

    will sent a rectangle from client to server (The line above is terminated by CRLF).

    If the server receive a valid command, it act on it and then send

    OK

    The line above is terminated by CRLF. In case of an error, an error message is sent back to the client.

    When a client establish a connection, the first thing the server does is to send a welcome banner. That is a line terminated by CRLF.

    The client wait to receive the banner before sending any command. Then it send the clear command, wait for OK, then send a rectangle command with first item in RectList and wait for OK, then loop sending all rectangle commands and waiting for OK acknowledge until all RectList has been sent. The the client close the connection.

    I'm not completely correct when I say wait for. Actually the socket is event driven. That means everything is done thry events. For example, when a line comes in - sent by the other side - the socket triggers an OnRead event. In the corresponding event handler, you receive the line that is already received.

    I used this line oriented protocol because it is really simple, easy to debug and cross platform. Actually, if looks much like the SMTP protocol which is used to send an email! Sending binary data is surely faster but has a lot of difficulties. Binary data format is compiler and platform specific. This result in difficulties. Binary data is diffcult to read for a human and so it is difficult to debug.

    Below you'll find your enhanced source code and DFM (This is the client), then the server source code and DFM.

    Client source code:

    unit SktSocketClientDemoMain;
    
    interface
    
    uses
        Winapi.Windows, Winapi.Messages,
        System.SysUtils, System.Variants, System.Classes,
        System.Generics.Collections,
        Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
        System.Win.ScktComp;
    
    type
        TSktSocketClientMainForm = class(TForm)
            ComboBox1 : TComboBox;
            SocketSendButton : TButton;
            ClientSocket1 : TClientSocket;
            Memo1 : TMemo;
            procedure ClientSocket1Connect(
                Sender : TObject;
                Socket : TCustomWinSocket);
            procedure ClientSocket1Connecting(
                Sender : TObject;
                Socket : TCustomWinSocket);
            procedure ClientSocket1Read(
                Sender : TObject;
                Socket : TCustomWinSocket);
            procedure FormCreate(Sender : TObject);
            procedure FormDestroy(Sender : TObject);
            procedure FormMouseDown(
                Sender : TObject;
                Button : TMouseButton;
                Shift  : TShiftState;
                X, Y   : Integer);
            procedure FormMouseMove(
                Sender : TObject;
                Shift  : TShiftState;
                X, Y   : Integer);
            procedure FormMouseUp(
                Sender : TObject;
                Button : TMouseButton;
                Shift  : TShiftState;
                X, Y   : Integer);
            procedure FormPaint(Sender : TObject);
            procedure SocketSendButtonClick(Sender : TObject);
        private
            Drawing                : Boolean;
            RectList               : TList<TRect>;
            Rectangle              : TRect;
            FormRegion, HoleRegion : HRGN;
            FBanner                : string;
            FSendIndex             : Integer;
            function ClientToWindow(const P : TPoint) : TPoint;
        end;
    
    var
        SktSocketClientMainForm : TSktSocketClientMainForm;
    
    implementation
    
    {$R *.dfm}
    
    
    function TSktSocketClientMainForm.ClientToWindow(const P : TPoint) : TPoint;
    begin
        Result := ClientToScreen(P);
        Dec(Result.X, Left);
        Dec(Result.Y, Top);
    end;
    
    procedure TSktSocketClientMainForm.FormCreate(Sender : TObject);
    begin
        RectList := TList<TRect>.Create;
    end;
    
    procedure TSktSocketClientMainForm.FormDestroy(Sender : TObject);
    begin
        RectList.Free;
    end;
    
    procedure TSktSocketClientMainForm.FormMouseDown(
        Sender : TObject;
        Button : TMouseButton;
        Shift  : TShiftState;
        X, Y   : Integer);
    begin
        Rectangle.Left := X;
        Rectangle.Top  := Y;
        Drawing        := True;
    end;
    
    procedure TSktSocketClientMainForm.FormMouseMove(
        Sender : TObject;
        Shift  : TShiftState;
        X, Y   : Integer);
    begin
        if Drawing then begin
            Rectangle.Right  := X;
            Rectangle.Bottom := Y;
            Invalidate;
        end;
    end;
    
    procedure TSktSocketClientMainForm.FormMouseUp(
        Sender : TObject;
        Button : TMouseButton;
        Shift  : TShiftState;
        X, Y   : Integer);
    var
        I : Integer;
    begin
        Drawing          := false;
        Rectangle.Right  := X;
        Rectangle.Bottom := Y;
        Invalidate;
    
        if RectList.Count < StrToInt(ComboBox1.Text) then begin
            Rectangle.NormalizeRect;
            if not Rectangle.IsEmpty then
                RectList.Add(Rectangle)
            else
                SetWindowRgn(Handle, 0, True);
        end
        else begin
            FormRegion := CreateRectRgn(0, 0, Width, Height);
            for I      := 0 to Pred(RectList.Count) do
            begin
                HoleRegion :=
                    CreateRectRgn(ClientToWindow(RectList.Items[I].TopLeft).X,
                    ClientToWindow(RectList.Items[I].TopLeft).Y,
                    ClientToWindow(RectList.Items[I].BottomRight).X,
                    ClientToWindow(RectList.Items[I].BottomRight).Y);
                CombineRgn(FormRegion, FormRegion, HoleRegion, RGN_DIFF);
            end;
            SetWindowRgn(Handle, FormRegion, True);
            RectList.Clear;
        end;
    end;
    
    procedure TSktSocketClientMainForm.FormPaint(Sender : TObject);
    var
        R : TRect;
    begin
        Canvas.Brush.Style := bsClear;
        Canvas.Pen.Style   := psSolid;
        Canvas.Pen.Color   := clRed;
        Canvas.Rectangle(Rectangle);
    
        for R in RectList do
            Canvas.Rectangle(R);
    end;
    
    procedure TSktSocketClientMainForm.SocketSendButtonClick(Sender : TObject);
    begin
        FBanner               := '';
        FSendIndex            := 0;
        ClientSocket1.Port    := 2500; // Must be the same as server side
        ClientSocket1.Address := '127.0.0.1';
        ClientSocket1.Active  := True;
    end;
    
    procedure TSktSocketClientMainForm.ClientSocket1Connect(
        Sender : TObject;
        Socket :
        TCustomWinSocket);
    begin
        Memo1.Lines.Add('Connected');
    end;
    
    procedure TSktSocketClientMainForm.ClientSocket1Connecting(
        Sender : TObject;
        Socket : TCustomWinSocket);
    begin
        Memo1.Lines.Add('Connecting...');
    end;
    
    procedure TSktSocketClientMainForm.ClientSocket1Read(
        Sender : TObject;
        Socket : TCustomWinSocket);
    var
        Line    : string;
        CmdLine : string;
        R       : TRect;
    begin
        Line := Trim(string(Socket.ReceiveText));
        Memo1.Lines.Add('Rcvd: "' + Line + '"');
        if FBanner = '' then begin
            FBanner := Line;
            Socket.SendText('Clear' + #13#10);
            Exit;
        end;
        if Line <> 'OK' then begin
            Memo1.Lines.Add('Expected "OK", received "' + Line + '"');
            Socket.Close;
            Exit;
        end;
        if FSendIndex >= RectList.Count then begin
            // We have sent everything in RectList
            Memo1.Lines.Add('Send completed OK');
            Socket.Close;
            Exit;
        end;
        // Send next item in RectList
        R       := RectList[FSendIndex];
        CmdLine := Format('Rectangle %d,%d,%d,%d' + #13#10,
            [R.Left, R.Top, R.Right, R.Bottom]);
        Inc(FSendIndex);
        Socket.SendText(AnsiString(CmdLine));
    end;
    
    end.
    

    Client DFM:

    object SktSocketClientMainForm: TSktSocketClientMainForm
      Left = 0
      Top = 0
      Caption = 'SktSocketClientMainForm'
      ClientHeight = 299
      ClientWidth = 635
      Color = clBtnFace
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWindowText
      Font.Height = -11
      Font.Name = 'Tahoma'
      Font.Style = []
      OldCreateOrder = False
      OnCreate = FormCreate
      OnDestroy = FormDestroy
      OnMouseDown = FormMouseDown
      OnMouseMove = FormMouseMove
      OnMouseUp = FormMouseUp
      OnPaint = FormPaint
      DesignSize = (
        635
        299)
      PixelsPerInch = 96
      TextHeight = 13
      object ComboBox1: TComboBox
        Left = 24
        Top = 12
        Width = 145
        Height = 21
        Style = csDropDownList
        ItemIndex = 4
        TabOrder = 0
        Text = '5'
        Items.Strings = (
          '1'
          '2'
          '3'
          '4'
          '5'
          '6'
          '7'
          '8'
          '9')
      end
      object SocketSendButton: TButton
        Left = 188
        Top = 8
        Width = 75
        Height = 25
        Caption = 'Send'
        TabOrder = 1
        OnClick = SocketSendButtonClick
      end
      object Memo1: TMemo
        Left = 8
        Top = 192
        Width = 621
        Height = 101
        Anchors = [akLeft, akTop, akRight, akBottom]
        Lines.Strings = (
          'Memo1')
        TabOrder = 2
      end
      object ClientSocket1: TClientSocket
        Active = False
        ClientType = ctNonBlocking
        Port = 0
        OnConnecting = ClientSocket1Connecting
        OnConnect = ClientSocket1Connect
        OnRead = ClientSocket1Read
        Left = 44
        Top = 148
      end
    end
    

    Server source code:

    unit SktSocketServerDemoMain;
    
    interface
    
    uses
      Winapi.Windows, Winapi.Messages,
      System.SysUtils, System.Variants, System.Classes,
      System.Generics.Collections,
      Vcl.Graphics,
      Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, System.Win.ScktComp,
      Vcl.ExtCtrls;
    
    type
      TCmdProc = procedure (Socket       : TCustomWinSocket;
                            const Params : String) of object;
      TCmdItem = record
          Cmd  : String;
          Proc : TCmdProc;
          constructor Create(const ACmd : String; AProc : TCmdProc);
      end;
    
        TServerMainForm = class(TForm)
            ServerSocket1 : TServerSocket;
            Memo1 : TMemo;
            ServerStartButton : TButton;
            PaintBox1 : TPaintBox;
            ServerStopButton : TButton;
            procedure PaintBox1Paint(Sender : TObject);
            procedure ServerSocket1ClientConnect(
                Sender : TObject;
                Socket : TCustomWinSocket);
            procedure ServerSocket1ClientDisconnect(
                Sender : TObject;
                Socket :
                TCustomWinSocket);
            procedure ServerSocket1ClientRead(
                Sender : TObject;
                Socket : TCustomWinSocket);
            procedure ServerSocket1Listen(
                Sender : TObject;
                Socket : TCustomWinSocket);
            procedure ServerStartButtonClick(Sender : TObject);
            procedure ServerStopButtonClick(Sender : TObject);
        private
            RectList : TList<TRect>;
            CmdList  : TList<TCmdItem>;
            procedure ProcessCmd(
                Socket        : TCustomWinSocket;
                const CmdLine : string);
            procedure CmdNoop(
                Socket       : TCustomWinSocket;
                const Params : string);
            procedure CmdClear(
                Socket       : TCustomWinSocket;
                const Params : string);
            procedure CmdRectangle(
                Socket       : TCustomWinSocket;
                const Params : string);
        public
            constructor Create(AOwner : TComponent); override;
            destructor Destroy; override;
        end;
    
    var
        ServerMainForm: TServerMainForm;
    
    implementation
    
    {$R *.dfm}
    
    function SkipOverWhiteSpaces(const CmdLine : String; Index : Integer) : Integer;
    var
        I : Integer;
    begin
        I := Index;
        while (I <= Length(CmdLine)) and
              CharInSet(CmdLine[I], [' ', #13, #10, #9]) do
            Inc(I);
        Result := I;
    end;
    
    function SkipToNextWhiteSpace(const CmdLine : String; Index : Integer) : Integer;
    var
        I : Integer;
    begin
        I := Index;
        while (I <= Length(CmdLine)) and
              (not CharInSet(CmdLine[I], [' ', #13, #10, #9])) do
            Inc(I);
        Result := I;
    end;
    
    function SkipToNextDelimiter(
        const CmdLine : String;
        Index         : Integer;
        Delimiters    : array of const) : Integer;
    var
        I    : Integer;
        nArg : Integer;
        V    : TVarRec;
    begin
        I := Index;
        while I <= Length(CmdLine) do begin
            nArg := 0;
            while nArg <= High(Delimiters) do begin
                V       := Delimiters[nArg];
                case (V.VType and varTypeMask) of
                vtWideChar:
                    begin
                        if CmdLine[I] = V.VWideChar then begin
                            Result := I;
                            Exit;
                        end;
                    end;
                end;
                Inc(nArg);
            end;
            Inc(I);
        end;
        Result := I;
    end;
    
    function GetInteger(
        const CmdLine : String;
        Index         : Integer;
        out Value     : Integer) : Integer;
    var
        I : Integer;
    begin
        Value := 0;
        I := SkipOverWhiteSpaces(CmdLine, Index);
        while (I <= Length(CmdLine)) and
              CharInSet(CmdLine[I], ['0'..'9']) do begin
            Value := Value * 10 + Ord(CmdLine[I]) - Ord('0');
            Inc(I);
        end;
        Result := I;
    end;
    
    procedure TServerMainForm.CmdClear(Socket: TCustomWinSocket; const Params: String);
    begin
        RectList.Clear;
        PaintBox1.Invalidate;
        Socket.SendText('OK' + #13#10);
    end;
    
    procedure TServerMainForm.CmdNoop(Socket: TCustomWinSocket; const Params: String);
    begin
        Socket.SendText('OK' + #13#10);
    end;
    
    procedure TServerMainForm.CmdRectangle(Socket: TCustomWinSocket; const Params: String);
    var
       Param : array [0..3] of Integer;
       I, J, K : Integer;
    begin
        // Clear all parameters
        for K := Low(Param) to High(Param) do
            Param[K] := 0;
    
        // Parse all parameters
        J := 1;
        K := Low(Param);
        while K <= High(Param) do begin
            I := GetInteger(Params, J, Param[K]);
            J := SkipOverWhiteSpaces(Params, I);
            if J > Length(Params) then
                break;
            if K = High(Param) then       // Check if we got all
                break;
            if Params[J] <> ',' then      // Check for coma delimiter
                break;
            Inc(J);                       // Skip over coma
            Inc(K);
        end;
        if K <> High(Param) then begin
            Socket.SendText('Rectangle requires 4 parameters.'#13#10);
            Exit;
        end;
    
        RectList.Add(TRect.Create(Param[0], Param[1], Param[2], Param[3]));
        PaintBox1.Invalidate;
        Socket.SendText('OK'#13#10);
    end;
    
    constructor TServerMainForm.Create(AOwner: TComponent);
    begin
        inherited Create(AOwner);
        RectList := TList<TRect>.Create;
    
        RectList.Add(TRect.Create(10, 10, 50, 50));
        RectList.Add(TRect.Create(20, 30, 80, 100));
    
        CmdList  := TList<TCmdItem>.Create;
        CmdList.Add(TCmdItem.Create('',          CmdNoop));
        CmdList.Add(TCmdItem.Create('Clear',     CmdClear));
        CmdList.Add(TCmdItem.Create('Rectangle', CmdRectangle));
    end;
    
    destructor TServerMainForm.Destroy;
    begin
        FreeAndNil(CmdList);
        FreeAndNil(RectList);
        inherited Destroy;
    end;
    
    procedure TServerMainForm.PaintBox1Paint(Sender: TObject);
    var
        R: TRect;
        ACanvas : TCanvas;
    begin
        ACanvas := (Sender as TPaintBox).Canvas;
        ACanvas.Brush.Style := bsClear;
        ACanvas.Pen.Style   := psSolid;
        ACanvas.Pen.Color   := clRed;
    
        for R in RectList do
            ACanvas.Rectangle(R);
    end;
    
    procedure TServerMainForm.ServerSocket1ClientConnect(
        Sender: TObject;
        Socket: TCustomWinSocket);
    begin
        Memo1.Lines.Add('Client connected');
        Socket.SendText('Welcome to myServer' + #13#10);
    end;
    
    procedure TServerMainForm.ServerSocket1ClientRead(Sender: TObject; Socket:
        TCustomWinSocket);
    var
        CmdLine : String;
    begin
        CmdLine := String(Socket.ReceiveText);
        Memo1.Lines.Add('Rcvd: "' + CmdLine + '"');
        ProcessCmd(Socket, CmdLine);
    end;
    
    procedure TServerMainForm.ProcessCmd(
        Socket        : TCustomWinSocket;
        const CmdLine : String);
    var
        Cmd    : String;
        Params : String;
        I, J   : Integer;
    begin
        I := SkipOverWhiteSpaces(CmdLine, 1);
        J := SkipToNextWhiteSpace(CmdLine, I);
        // Split command and parameters
        Cmd    := UpperCase(Copy(CmdLine, I, J - I));
        Params := Copy(CmdLine, J, MAXINT);
        Memo1.Lines.Add(Format('Cmd="%s"  Params="%s"', [Cmd, Params]));
        for I := 0 to CmdList.Count - 1 do begin
            if CmdList[I].Cmd = Cmd then begin
                CmdList[I].Proc(Socket, Params);
                Exit;
            end;
        end;
        Socket.SendText('Unknown command' + #13#10);
    end;
    
    procedure TServerMainForm.ServerSocket1ClientDisconnect(Sender: TObject; Socket:
        TCustomWinSocket);
    begin
        Memo1.Lines.Add('Client disconnected');
    end;
    
    procedure TServerMainForm.ServerSocket1Listen(Sender: TObject; Socket: TCustomWinSocket);
    begin
        Memo1.Lines.Add('Waiting for client connection');
    end;
    
    procedure TServerMainForm.ServerStartButtonClick(Sender: TObject);
    begin
        ServerSocket1.Port := 2500;   // Almost any (free) port is OK
        ServerSocket1.Open;           // Start listening for clients
    end;
    
    procedure TServerMainForm.ServerStopButtonClick(Sender: TObject);
    begin
        ServerSocket1.Close;
        Memo1.Lines.Add('Server stopped');
    end;
    
    { TCmdItem }
    
    constructor TCmdItem.Create(const ACmd: String; AProc: TCmdProc);
    begin
        Cmd  := UpperCase(ACmd);
        Proc := AProc;
    end;
    
    end.
    

    Server DFM:

    object ServerMainForm: TServerMainForm
      Left = 0
      Top = 0
      Caption = 'ServerMainForm'
      ClientHeight = 498
      ClientWidth = 635
      Color = clBtnFace
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWindowText
      Font.Height = -11
      Font.Name = 'Tahoma'
      Font.Style = []
      OldCreateOrder = False
      DesignSize = (
        635
        498)
      PixelsPerInch = 96
      TextHeight = 13
      object PaintBox1: TPaintBox
        Left = 8
        Top = 48
        Width = 617
        Height = 273
        Anchors = [akLeft, akTop, akRight, akBottom]
        OnPaint = PaintBox1Paint
      end
      object Memo1: TMemo
        Left = 8
        Top = 329
        Width = 617
        Height = 161
        Anchors = [akLeft, akTop, akRight, akBottom]
        Lines.Strings = (
          'Memo1')
        TabOrder = 0
      end
      object ServerStartButton: TButton
        Left = 12
        Top = 8
        Width = 75
        Height = 25
        Caption = 'Server Start'
        TabOrder = 1
        OnClick = ServerStartButtonClick
      end
      object ServerStopButton: TButton
        Left = 93
        Top = 8
        Width = 75
        Height = 25
        Caption = 'Server Stop'
        TabOrder = 2
        OnClick = ServerStopButtonClick
      end
      object ServerSocket1: TServerSocket
        Active = False
        Port = 0
        ServerType = stNonBlocking
        OnListen = ServerSocket1Listen
        OnClientConnect = ServerSocket1ClientConnect
        OnClientDisconnect = ServerSocket1ClientDisconnect
        OnClientRead = ServerSocket1ClientRead
        Left = 64
        Top = 196
      end
    end