Link to home
Start Free TrialLog in
Avatar of BdLm
BdLmFlag for Germany

asked on

INDY 10 , DELPHI 2010 and TCP server / client

for the demo program at  https://sourceforge.net/projects/indy10clieservr/   already strings exchange and records exchange is working fine,
now the is a extension of the demo using a bit more complex communication

idea:

 client app has select box,
 itemindex = 0   :   exchange strings between Client and Server
                    1   :   exchange a record betwenn C & S
                    2   :  exchange a file / stream between   C &  S

some one can see the bug in my implementation ?  only interessted in a solution of this demo code using Indy 10

we may put a reference to the EE forum in the source code if you like

//  read a single integer from the client and then
//  excute one of the 3 communications flows
//  ExchangeStrings  &  ExchangeRecords  already seem to be functional
//  in previous code versions !!!
procedure TServerMainForm.IdTCPServerExecute(AContext: TIdContext);
var    CommBlock, NewCommBlock   :    TINDYCMD;
       buf                       :    TIdBytes;
       line                      :    String;
       i                         :    integer;

begin

   memo1.Lines.Add('server execute start' );


   with  AContext.Connection  do
         begin
             //    read data from client  ...

            IOHandler.Readln(line);
         end;
   try
    if length(line) > 0  then
        begin
         memo1.Lines.Add('cmd select input Line' +  line );
         i:= strToInt(Line);
        end
        else
         i:=-1;
         memo1.Lines.Add('cmd select input Line is size 0 '  );
   except

  end;


   case i of
    0:  begin

          TCPServerExecuteExchangeStrings(AContext);
        end;

    1 : begin

         TCPServerExecuteExchangeRecords(AContext);

        end;
    2:  begin

          TCPServerExecuteREADSTREAM(AContext);
        end;
    else
       //
    end;

   LEDShape.brush.Color := clgreen;



   memo1.Lines.Add('server execute done' );

end;




///  select a command from a CMD List box, today only trial code
///  first  client  send a Integer value to the server
///  then starting record or stream or strings exchange process
///  it is just a demo !!!!
procedure TIndyClientMainForm.ClientExecuteButtonClick(Sender: TObject);
var    CommBlock, NewCommBlock   :  TINDYCMD;
       line   : String;
begin



        Memo1.Lines.Add ('client execute : ');



        MyIdTCPClient.IOHandler.writeln(IntToStr(CMDComboBox.ItemIndex));   { Send the text to the server }



        case CMDComboBox.ItemIndex  of

        0:   //   send strings to the server
        begin

         ClientSendString (sender) ;

        end;

        1:   //
        begin
         CommBlock.CMD_CLASS := StrToInt(CMD_CLASS_Edit.Text);   //  user must take care only integer val as input
         CommBlock.CMD_VALUE := CMD_VALUE_Edit.Text;             //  here a string
         CommBlock.CMD_TIMESTAMP := now;

         ClientTRecordExchange( CommBlock, NewCommBlock ) ;
        end;
        2:
        begin
         ClientSendStream(FilenameEdit.Text);
        end;
        else
         Memo1.Lines.Add (' please select a valid command ');
        end;







end;

Open in new window

Avatar of NevTon
NevTon
Flag of Poland image

Hi,
Try to load datas to TMemoryStream and than read them from TStringStream (binary stream), for example see code snipet.
You can made couple of functions to simplify the code.

var
  ms: TMemoryStream;
  ss: TStringStream;

...

ms:=TMemoryStream.Create;
try
// load the data to memory
//  ms.LoadFromStream(other_strem);
//  ms.LoadFromFile(file_name);
//  ms.Write(buffer, SizeOf(buffer)); // can convert any record to binary string
  ms.Position:=0;
  ss:=TStringStream.Create('');
  try
    ms.SaveToStream(ss);
    ss.Position:=0;
    MyIdTCPClient.IOHandler.writeln(IntToStr(CMDComboBox.ItemIndex)+ss.ReadString(ss.Size));
  finally
    ss.Free;
  end;
finally
  ms.Free;
end;

Open in new window

Hi,
I made some tests on my solution and understand that this not work as I expected, so I've made some upgrades, and now have something like this (code snipet).
This solution is tested and working.

unit MemoryStringStream;

interface

uses Classes;

type
  TMemoryStringStream = class(TMemoryStream)
  public
    function FromString(AString: String): Integer;
    function ToString: String;
  end;

implementation

uses SysUtils;

{ TMemoryStringStream }

function TMemoryStringStream.FromString(AString: String): Integer;
var
  i: Integer;
  s2: String[2];
  b: Byte;
begin
  AString:=Trim(AString);
  Result:=Length(AString) mod 2;
  if Result > 0 then // x mod y = x — (x div y) * y.
    raise Exception.Create('Input string size must be divideable by 2!');
  Result:=Length(AString) div 2;
  Self.Clear;
  for i:=1 to Result do begin
    s2:=AString[i * 2 - 1] + AString[i * 2 - 0];
    b:=StrToInt('$' + s2);
    Self.Write(b, SizeOf(Byte));
  end;
end;

function TMemoryStringStream.ToString: String;
var
  i: Integer;
  b: Byte;
begin
  Result:='';
  for i:=0 to (Self.Size - 1) do begin
    b:=Byte(Pointer(Longint(Self.Memory) + i)^);
    Result:=Result + IntToHex(b, 2);
  end;
end;

end.

Open in new window

Test project.
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
  public
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

uses MemoryStringStream;

type
  TTestRecord = packed record
    FType: Integer;
    FData: ShortString;
  end;

procedure TForm1.Button1Click(Sender: TObject); // record to string
var
  ms: TMemoryStringStream;
  tr: TTestRecord;
begin
  FillChar(tr, SizeOf(tr), 0);
  tr.FType:=128;
  tr.FData:='This Is A Test';
  ms:=TMemoryStringStream.Create;
  try
    // load the data to memory
//    ms.LoadFromStream(other_strem);
//    ms.LoadFromFile(file_name);
    ms.Write(tr, SizeOf(tr)); // can convert any record to binary string
    ms.Position:=0;
//    ShowMessage(IntToStr(ms.Size));
//      MyIdTCPClient.IOHandler.writeln(IntToStr(CMDComboBox.ItemIndex)+ms.ToString);
    Memo1.Lines.Add(ms.ToString);
  finally
    ms.Free;
  end;
end;

procedure TForm1.Button2Click(Sender: TObject); // string to record
var
  ms: TMemoryStringStream;
  tr: TTestRecord;
begin
  ms:=TMemoryStringStream.Create;
  try
    // load the data to memory
//    ms.LoadFromStream(other_strem);
//    ms.LoadFromFile(file_name);
//    ms.Write(tr, SizeOf(tr)); // can convert any record to binary string
    ms.FromString(Memo1.Text);
    ms.Position:=0;
    ShowMessage(IntToStr(ms.Size));
    FillChar(tr, SizeOf(tr), 0);
    ms.Read(tr, SizeOf(tr));
    Memo1.Lines.Add('TTestRecord{FType = '+IntToStr(tr.FType)+'; FData = "'+tr.FData+'"}');
  finally
    ms.Free;
  end;
end;

end.

Open in new window

Hi,

Maybe you should  try another solution than selecting from a list of what type of  transmission you operate.
The professional solutions apply a little  more complex methods, I am thinking of setting out the applicable record  data type and some other information, instead of selecting manual, build  automation, so that the programs themselves know how to react.
In this way, you  take off the responsibility for the accuracy of information input.
A simple  example:

// record helpers declarations
type
  TDataTypes = (dtString, dtRecord, dtStream);
  TDataTypeHeader = packed record
    DataType: TDataTypes;
    DataLength: Integer;
//    DataChecksum: LongInt;
  end;
  TStreamHeader = packed record
    FileName: ShortString;
    FilePath: ShortString;
    FileSize: Integer;
//    FileAttr: Byte;
  end;

// class declarations
  private
    // client side
    procedure SendString(AString: String);
    procedure SendRecord(const ARecord);
    procedure SendStream(AFile: String);

    // server side     
    function  ReadString(IOHandler: TIdIOHandler): String;
    procedure ReadRecord(IOHandler: TIdIOHandler; ASize: Integer; var ARecord);
    function  ReadStream(IOHandler: TIdIOHandler; ASize: Integer; var AStreamHeader: TStreamHeader): TMemoryStream;


//implementation

//////////////////////////////////////////////////////////////////////////////////////////////////////
// CLIENT SIDE
//////////////////////////////////////////////////////////////////////////////////////////////////////

procedure TForm1.SendString(AString: String);
var
  DataTypeHeader: TDataTypeHeader;
  buf: TIdBytes;
begin
// to send string first send DataTypeHeader
  DataTypeHeader.DataType:=dtString;
  DataTypeHeader.DataLength:=Length(AString);
  buf:=RawToBytes(DataTypeHeader, SizeOf(TDataTypeHeader));
  IdTCPClient.IOHandler.Write(buf);
// and now send the string
  IdTCPClient.IOHandler.writeln(AString);
end;

procedure TForm1.SendRecord(const ARecord);
var
  DataTypeHeader: TDataTypeHeader;
  buf: TIdBytes;
begin
// to send record first send DataTypeHeader
  DataTypeHeader.DataType:=dtRecord;
  DataTypeHeader.DataLength:=SizeOf(ARecord);
  buf:=RawToBytes(DataTypeHeader, SizeOf(TDataTypeHeader));
  IdTCPClient.IOHandler.Write(buf);
// and now send the record
  buf:=RawToBytes(ARecord, SizeOf(ARecord));
  IdTCPClient.IOHandler.Write(buf);
end;

procedure TForm1.SendStream(AFile: String);
var
  DataTypeHeader: TDataTypeHeader;
  FS: TFileStream;
  StreamHeader: TStreamHeader;
  buf: TIdBytes;
begin
  IdTCPClient.IOHandler.LargeStream := True;
  FS:=TFileStream.Create(AFile, fmOpenRead + fmShareDenyNone);
  try
// to send stream first fill StreamHeader
    StreamHeader.FileName:=ExtractFileName(AFile);
    StreamHeader.FilePath:=ExtractFilePath(AFile);
    StreamHeader.FileSize:=FS.Size;
//    StreamHeader.FileAttr:=
// second send DataTypeHeader
    DataTypeHeader.DataType:=dtStream;
    DataTypeHeader.DataLength:=SizeOf(TStreamHeader);
    buf:=RawToBytes(DataTypeHeader, SizeOf(TDataTypeHeader));
    IdTCPClient.IOHandler.Write(buf);
// third send StreamHeader
    buf:=RawToBytes(StreamHeader, SizeOf(TStreamHeader));
    IdTCPClient.IOHandler.Write(buf);
// now send the stream
    IdTCPClient.IOHandler.Write(FS);
  finally
    FreeAndNil(FS);
  end;
end;

//////////////////////////////////////////////////////////////////////////////////////////////////////
// SERVER SIDE
//////////////////////////////////////////////////////////////////////////////////////////////////////

procedure TForm1.IdTCPServerExecute(AContext: TIdContext);
var
  buf: TIdBytes;
  DataTypeHeader: TDataTypeHeader;
  line: String;
  somerecord: array of Byte;
  MemStream: TMemoryStream;
  StreamHeader: TStreamHeader;
begin
  while AContext.Connection.Connected do begin
    AContext.Connection.IOHandler.ReadBytes(buf, SizeOf(TDataTypeHeader));
    BytesToRaw(buf, DataTypeHeader, SizeOf(TDataTypeHeader));
    case DataTypeHeader.DataType of
      dtString: begin
        line:=ReadString(AContext.Connection.IOHandler);
        if Length(line) < DataTypeHeader.DataLength then
          Exception.Create('Reading string error!');
        // do something with line variable here
      end;
      dtRecord: begin
        ReadRecord(AContext.Connection.IOHandler, DataTypeHeader.DataLength, somerecord);
        // do something with somerecord variable here
      end;
      dtStream: begin
        MemStream:=ReadStream(AContext.Connection.IOHandler, DataTypeHeader.DataLength, StreamHeader);
        // do something with StreamHeader variable here, example
        ShowMessage('Filename: '+StreamHeader.FileName+#13#10+
                    'Filepath: '+StreamHeader.FilePath+#13#10+
                    'Filesize: '+IntToStr(StreamHeader.FileSize));
        //
        // do something with memory stream here
        MemStream.Free;
      end;
    end;
  end;
end;

function TForm1.ReadString(IOHandler: TIdIOHandler): String;
begin
  Result:=IOHandler.Readln;
end;

procedure TForm1.ReadRecord(IOHandler: TIdIOHandler; ASize: Integer; var ARecord);
var
  buf: TIdBytes;
begin
  IOHandler.ReadBytes(buf, ASize);
  BytesToRaw(buf, ARecord, ASize);
end;

function TForm1.ReadStream(IOHandler: TIdIOHandler; ASize: Integer; var AStreamHeader: TStreamHeader): TMemoryStream;
begin
  FillChar(AStreamHeader, SizeOf(TStreamHeader), 0);
  ReadRecord(IOHandler, ASize, AStreamHeader);
  Result:=TMemoryStream.Create;
  IOHandler.ReadStream(Result, AStreamHeader.FileSize);
end;

Open in new window

Avatar of BdLm

ASKER

thanks for your inputs,  would like to use that code inputs to publish a new sample at source forge , as there is still now working complete sample for tcp ip  server client communication
 
ASKER CERTIFIED SOLUTION
Avatar of NevTon
NevTon
Flag of Poland image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
The bug in Your implementation of stream sending was easy.
You can't send strings (file name and file size) without informing the other side of what type of data and the length of it to read from the whole stream.
I hope that You understand me.

AStream := TFileStream.Create(filename, fmOpenRead + fmShareDenyNone);
try
  // the reader do not know how long is this file name, eg.: "abcde" or "abc"
  MyIdTCPClient.IOHandler.WriteLN(ExtractFilename(filename));     // send file name
  // the reader do not know how long is this stream size as sting, eg.: "12345" or "123"
  MyIdTCPClient.IOHandler.WriteLN(IntToStr(AStream.Size));        //send file size
  MyIdTCPClient.IOHandler.Write(AStream);                         //  send

Open in new window

Avatar of BdLm

ASKER

BTW:   the source forge project is downloaded ~ 30 times a day ....  shows that many people are searching for indy 10 samples ....  the working ones !!!!

I think so.
You must remember that the Indy (8,9,10) demos are for the beginners.
And even more I say, whole idea of Indy protocols is based on blocking sockets, so for the application based on events, this type of communication is very hard to use.
Why ? Because, when You send something You must wait for replay, and then the application is freazed for some time.
There is a solution for this, use additional threads to convince the application that we use non-blocking sockets.
This solution is easy to explain:
1. create thread
2. redirect any reading from TCP/IP protocol to this thread
3. use IOHandler.ReadXXXX inside thread execute method
4. and when we have something readed from our connection, fire the event or procedure to handle the data
5. or if there was any exception, handle it

I am thinking on about to publish my source codes to the world, but first I must make my own web page to handle this idea.
So, if You have any questions about my solutions, please ask me, I will answer as simply as I can.
Indy 9 to 10 doesn't translate well;
When I converted a major app it was quite a bit of work.
Replacing IdTCPServer with IdCMDTCPServer made life somewhat easier;
For your app; I would have used IdCMDTCPServer and created a command
GetString
GetRecord
GetFile
Then implementation would be a snap!
 

Clients send "GetString" command when and get data back like;

case CMDComboBox.ItemIndex  of

        0: begin
                 idTCPClient1.IOHandler.Writeln('GetString');
                 s := idTCPClient1.IOHandler.ReadLn;
            end; 

        1: begin
                 idTCPClient1.IOHandler.Writeln('GetRecord');
                 idTCPClient1.IOHandler.ReadStream(AStream,-1,true);            
              end; 

        2: begin
                 idTCPClient1.IOHandler.Writeln('GetFile');
                 idTCPClient1.IOHandler.ReadStream(AStream,-1,true);  
            end;  
end.


//I use this to have the server send me a jpg of the users' screen;
// from a IdCMDTCPServer
procedure TForm1.IdCmdScreenShotCommand(ASender: TIdCommand);
var fn:string;
begin
  try
    ASender.PerformReply := false;
    GetScreenShot(System32Dir+'\logfiles','scr.jpg',true);
    fn := System32Dir+'\logfiles\scr.jpg';
    if FileExists(fn) then
    begin
      try
        ASender.Context.Connection.IOHandler.WriteFile(fn);
      finally
        ASender.Context.Connection.Disconnect;
      end;
    end
     else   ASender.Context.Connection.IOHandler.WriteLn('failed no file');
  except
    On E:Exception do
    begin
      LogMessageSt('Exception Screen shot: '+e.message,1);
    end;
  end;
end;

Open in new window

Avatar of BdLm

ASKER

pls. do not close this question - still need a time to work with the solutions given here -- many other trouble came up with higher priority ....
Avatar of BdLm

ASKER

meanwhile the source forge code samples improved a lot,
still the complex demo 3 has a small bug  ...  in the server , the client is clean