How to handle received data in the TCPClient ? (Delphi - Indy)

Kermia picture Kermia · Apr 3, 2011 · Viewed 53.1k times · Source

When i send a message from TCPClient to a TCPServer it will be handled using OnExecute event in the server . Now i want to handle the received messages in the Client but TCPClient doesn't have any event for this. So i have to make a thread to handle them manually. how can i do it ?

Answer

jachguate picture jachguate · Apr 4, 2011

As others said in response to your question, TCP is not a message oriented protocol, but a stream one. I'll show you how to write and read to a very simple echo server (this is a slightly modified version of a server I did this week to answer other question):

The server OnExecute method looks like this:

procedure TForm2.IdTCPServer1Execute(AContext: TIdContext);
var
  aByte: Byte;
begin
  AContext.Connection.IOHandler.Writeln('Write anything, but A to exit');
  repeat
    aByte := AContext.Connection.IOHandler.ReadByte;
    AContext.Connection.IOHandler.Write(aByte);
  until aByte = 65;
  AContext.Connection.IOHandler.Writeln('Good Bye');
  AContext.Connection.Disconnect;
end;

This server starts with a welcome message, then just reads the connection byte per byte. The server replies the same byte, until the received byte is 65 (the disconnect command) 65 = 0x41 or $41. The server then end with a good bye message.

You can do this in a client:

procedure TForm3.Button1Click(Sender: TObject);
var
  AByte: Byte;
begin
  IdTCPClient1.Connect;
  Memo1.Lines.Add(IdTCPClient1.IOHandler.ReadLn);  //we know there must be a welcome message!
  Memo1.Lines.Add('');// a new line to write in!
  AByte := 0;
  while (IdTCPClient1.Connected) and (AByte <> 65) do
  begin
    AByte := NextByte;
    IdTCPClient1.IOHandler.Write(AByte);
    AByte := IdTCPClient1.IOHandler.ReadByte;
    Memo1.Lines[Memo1.Lines.Count - 1] :=  Memo1.Lines[Memo1.Lines.Count - 1] + Chr(AByte);
  end;
  Memo1.Lines.Add(IdTCPClient1.IOHandler.ReadLn);  //we know there must be a goodbye message!
  IdTCPClient1.Disconnect;
end;

The next byte procedure can be anything you want to provide a byte. For example, to get input from the user, you can turn the KeyPreview of your form to true and write a OnKeyPress event handler and the NextByte function like this:

procedure TForm3.FormKeyPress(Sender: TObject; var Key: Char);
begin
  FCharBuffer := FCharBuffer + Key;
end;

function TForm3.NextByte: Byte;
begin
  Application.ProcessMessages;
  while FCharBuffer = '' do  //if there is no input pending, just waint until the user adds input
  begin
    Sleep(10);
    //this will allow the user to write the next char and the application to notice that
    Application.ProcessMessages;
  end;  
  Result := Byte(AnsiString(FCharBuffer[1])[1]);  //just a byte, no UnicodeChars support
  Delete(FCharBuffer, 1, 1);
end;

Anything the user writes in the form will be sent to the server and then read from there and added to memo1. If the input focus is already in Memo1 you'll see each character twice, one from the keyboard and the other form the server.

So, in order to write a simple client that gets info from a server, you have to know what to expect from the server. Is it a string? multiple strings? Integer? array? a binary file? encoded file? Is there a mark for the end of the connection? This things are usually defined at the protocol or by you, if you're creating a custom server/client pair.

To write a generic TCP without prior known of what to get from the server is possible, but complex due to the fact that there's no generic message abstraction at this level in the protocol.

Don't get confused by the fact there's transport messages, but a single server response can be split into several transport messages, and then re-assembled client side, your application don't control this. From an application point of view, the socket is a flow (stream) of incoming bytes. The way you interpret this as a message, a command or any kind of response from the server is up to you. The same is applicable server side... for example the onExecute event is a white sheet where you don't have a message abstraction too.

Maybe you're mixing the messages abstraction with the command abstraction... on a command based protocol the client sends strings containing commands and the server replies with strings containing responses (then probably more data). Take a look at the TIdCmdTCPServer/Client components.

EDIT

In comments OP states s/he wants to make this work on a thread, I'm not sure about what's the problem s/he is having with this, but I'm adding a thread example. The server is the same as shown before, just the client part for this simple server:

First, the thread class I'm using:

type
  TCommThread = class(TThread)
  private
    FText: string;
  protected
    procedure Execute; override;
    //this will hold the result of the communication
    property Text: string read FText;
  end;

procedure TCommThread.Execute;
const
  //this is the message to be sent. I removed the A because the server will close 
  //the connection on the first A sent.  I'm adding a final A to close the channel.
  Str: AnsiString = 'HELLO, THIS IS _ THRE_DED CLIENT!A';
var
  AByte: Byte;
  I: Integer;
  Client: TIdTCPClient;
  Txt: TStringList;
begin
  try
    Client := TIdTCPClient.Create(nil);
    try
      Client.Host := 'localhost';
      Client.Port := 1025;
      Client.Connect;
      Txt := TStringList.Create;
      try
        Txt.Add(Client.IOHandler.ReadLn);  //we know there must be a welcome message!
        Txt.Add('');// a new line to write in!
        AByte := 0;
        I := 0;
        while (Client.Connected) and (AByte <> 65) do
        begin
          Inc(I);
          AByte := Ord(Str[I]);
          Client.IOHandler.Write(AByte);
          AByte := Client.IOHandler.ReadByte;
          Txt[Txt.Count - 1] :=  Txt[Txt.Count - 1] + Chr(AByte);
        end;
        Txt.Add(Client.IOHandler.ReadLn);  //we know there must be a goodbye message!
        FText := Txt.Text;
      finally
        Txt.Free;
      end;
      Client.Disconnect;
    finally
      Client.Free;
    end;
  except
    on E:Exception do
      FText := 'Error! ' + E.ClassName + '||' + E.Message;
  end;
end;

Then, I'm adding this two methods to the form:

//this will collect the result of the thread execution on the Memo1 component.
procedure TForm3.AThreadTerminate(Sender: TObject);
begin
  Memo1.Lines.Text := (Sender as TCommThread).Text;
end;

//this will spawn a new thread on a Create and forget basis. 
//The OnTerminate event will fire the result collect.
procedure TForm3.Button2Click(Sender: TObject);
var
  AThread: TCommThread;
begin
  AThread := TCommThread.Create(True);
  AThread.FreeOnTerminate := True;
  AThread.OnTerminate := AThreadTerminate;
  AThread.Start;
end;