Search code examples
socketsdelphitcpindydelphi-10.3-rio

Can a Indy TCP Socket be shared between two threads?


I want a connection where both terminals wait reading for commands, and from time to time, one send some data to the other. Of course, I know how to implement my protocol, but if I do a Socket.ReadUInt32 it will block my execution path and I can't send commands when needed. So I thought, what if I send commands from another thread, using the same socket ? I write a minimal example... and it seems it is working. But I don't know for sure if this is correct way to do it, or it was jus luck this time... Is this technique threadsafe ? The extra thread will use the socket just for sending a UInt32 value, to wake the other side, and then, the connection will take place in the main threads.

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, IdTCPConnection,
  IdTCPClient, IdBaseComponent, IdComponent, IdCustomTCPServer, IdTCPServer,
  IdContext, IdThreadComponent;

const
  WM_LOGEVENT = WM_USER + 1;

type
  TForm1 = class(TForm)
    Server: TIdTCPServer;
    Client: TIdTCPClient;
    BStartServer: TButton;
    BConnectClient: TButton;
    ClientWriteThread: TIdThreadComponent;
    Memo: TMemo;
    BSendCommand: TButton;
    ClientReadThread: TIdThreadComponent;
    procedure BStartServerClick(Sender: TObject);
    procedure ServerExecute(AContext: TIdContext);
    procedure BConnectClientClick(Sender: TObject);
    procedure ClientWriteThreadRun(Sender: TIdThreadComponent);
    procedure BSendCommandClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure ClientReadThreadRun(Sender: TIdThreadComponent);
  private
    procedure LogEvent(var Msg: TMessage); message WM_LOGEVENT;
    procedure SendEvent(const Msg: String);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.LogEvent(var Msg: TMessage);
var PStr: PString;
begin
 PStr:= PString(Msg.WParam);
 Memo.Lines.Add(Copy(PStr^, 1, Length(PStr^)));
end;

procedure TForm1.SendEvent(const Msg: String);
begin
 SendMessage(Handle, WM_LOGEVENT, WPARAM(@Msg), 0);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
 Server.Bindings.Clear;
 Server.Bindings.Add.SetBinding('192.168.0.3', 60200);
 Client.Host:= '192.168.0.3';
 Client.Port:= 60200;
end;

procedure TForm1.BStartServerClick(Sender: TObject);
begin
 Server.Active:= True;
 Caption:= IntToStr(Byte(Server.Active));
end;

procedure TForm1.BConnectClientClick(Sender: TObject);
begin
 Client.Connect;
 ClientReadThread.Start;
end;

procedure TForm1.BSendCommandClick(Sender: TObject);
begin
 ClientWriteThread.Start;
end;

procedure TForm1.ClientWriteThreadRun(Sender: TIdThreadComponent);
var Data: Cardinal;
begin
 Data:= 1234;
 SendEvent('Client send: '+ IntToStr(Data));
 Client.Socket.Write(Data);
 Sender.Terminate;
end;

procedure TForm1.ClientReadThreadRun(Sender: TIdThreadComponent);
var Data: Cardinal;
begin
 SendEvent('Client listening...');
 Data:= Client.Socket.ReadUInt32;
 SendEvent('Client received: '+ IntToStr(Data));

 Client.Disconnect;
 SendEvent('Client stopped.');
 Sender.Terminate;
end;

procedure TForm1.ServerExecute(AContext: TIdContext);
var Data: Cardinal;
    Srv: String;
begin
 Srv:= 'Server '+IntToStr(Random(100));
 SendEvent(Srv+' listening...');
 Data:= AContext.Connection.Socket.ReadUInt32;
 SendEvent(Srv+' received: '+IntToStr(Data));

 Data:= 9999;
 SendEvent(Srv+' Send: '+IntToStr(Data));
 AContext.Connection.Socket.Write(Data);

 AContext.Connection.Disconnect;
 SendEvent(Srv+' stopped.');
end;

end.

Solution

  • Yes, it is safe to send on a connection in one thread while reading from the same connection in another thread. The socket has separate internal buffers for inbound and outbound data.

    Just make sure that you don't try to send from both threads at the same time, or read from both threads at the same time, without synchronizing access to the connection.