Search code examples
delphitcpindy

Delphi Indy client sends 64 KB package and the Server receives 2 packages totaling 64 KB


With the TIdTCPServer component of Indy, a package is received in two fractions but the client sent one with 64 KB.

How do I receive the complete package in the Server OnExecute event?

Now I put a prototype (Server and Client) code to recreate the situation.

Server Code

procedure TFrmServer.IdTCPServer1Execute(AContext: TIdContext);
Var
  ReceivedBytesTCP : Integer;
  IBuf : TIdBytes;
begin
  if Not AContext.Connection.IOHandler.InputBufferIsEmpty then Begin
    Try
      ReceivedBytesTCP := AContext.Connection.IOHandler.InputBuffer.Size;
      SetLength(IBuf,ReceivedBytesTCP);
      AContext.Connection.IOHandler.ReadBytes(IBuf,ReceivedBytesTCP,False);
      AContext.Connection.IOHandler.Write(IBuf,Length(IBuf),0);
    Except
      On E : Exception Do Begin
        Memo1.Lines.Add('Except Server TCP: ' + E.Message);
      End;
    End;
  End Else Begin
    Sleep(1);
  End;
end;

Client Code

procedure TFrm_TCP_Client.BtnSendClick(Sender: TObject);
Var
  IBuf,RBuf : TIdBytes;
  I         : Integer;
  LenPacket : Integer;
begin
  LenPacket := StrToInt(EdtLength.Text);
  if IdTCPClient1.Connected then Begin
    SetLength(IBuf,LenPacket);
    for I := 1 to LenPacket do
      IBuf[I] := 1;
    IdTCPClient1.IOHandler.Write(IBuf,Length(IBuf),0);
    I := 0;
    Repeat
      IdTCPClient1.IOHandler.CheckForDataOnSource(50);
      Inc(I);
    Until Not IdTCPClient1.IOHandler.InputBufferIsEmpty or (I >= 10);
    If Not IdTCPClient1.IOHandler.InputBufferIsEmpty Then Begin
      SetLength(RBuf,IdTCPClient1.IOHandler.InputBuffer.Size);
      IdTCPClient1.IOHandler.ReadBytes(RBuf,IdTCPClient1.IOHandler.InputBuffer.Size,False);
      if Length(RBuf) = Length(IBuf) then
        Memo1.Lines.Add('Response Received OK: '+IntToStr(Length(RBuf)))
      Else
        Memo1.Lines.Add('Response Received With Different Length: '+IntToStr(Length(RBuf)));
      if Not IdTCPClient1.IOHandler.InputBufferIsEmpty then
        Memo1.Lines.Add('Llego otro Mensaje');
    End Else Begin
      Memo1.Lines.Add('NO Response Received');
    End;
  End;
end;

How to know that a message is the first or the second fragment? How to force the receive of second fragment?


Solution

  • There is no 1-to-1 relationship between sends and reads in TCP. It is free to fragment data however it wants to optimize network transmissions. TCP guarantees only that data is delivered, and in the same order it was sent, but nothing about HOW data is fragmented during transmission. TCP will reconstruct the fragments on the receiving end. This is simply how TCP works, it is not unique to Indy. Every TCP app has to deal with this issue regardless of which TCP framework is used.

    If you are expecting 64KB of data, then simply read 64KB of data, and let the OS and Indy handle the fragments internally for you. This fragmentation of TCP is exactly why Indy's IOHandler uses an InputBuffer to collect the fragments when piecing data back together.

    Update: stop focusing on fragments. That is an implementation detail at the TCP layer, which you are not operating at. You don't need to deal with fragments in your code. Let Indy handle it for you. Just focus on your application level protocol instead.

    And FYI, you have essentially implemented an ECHO client/server solution. Indy has actual ECHO client/server components, TIdECHO and TIdECHOServer, you should have a look at them.

    In any case, your server-side exception handling is very problematic. It is not syncing with the main UI thread (OnExecute is called in a worker thread). But, more importantly, it as preventing TIdTCPServer from processing any notifications issued by Indy itself when the client connection is lost/disconnected, so the client thread will keep running and not stop until you deactivate the server. DO NOT swallow Indy's own exceptions (which are derived from EIdException). If you need to catch them in your code, you should re-raise them when done, let TIdTCPServer process them. But, in your example, it would be easier to remove the try..except altogether and use the server's OnException event instead.

    Also, your client-side reading loop is wrong for what you are attempting to do with it. You are not initializing IBuf correctly. But, more importantly, you are using a very short timeout (TCP connections may have latency), and you are breaking your reading loop as soon as any data arrives or 500ms max have elapsed, even if there is more data still coming. You should be reading until there is nothing left to read.

    Try something more like this instead:

    Server:

    procedure TFrmServer.IdTCPServer1Execute(AContext: TIdContext);
    var
      IBuf : TIdBytes;
    begin
      AContext.Connection.IOHandler.ReadBytes(IBuf, -1);
      AContext.Connection.IOHandler.Write(IBuf);
    end;
    
    procedure TFrmServer.IdTCPServer1Exception(AContext: TIdContext, AException: Exception);
    var
      Msg: string;
    begin
      if AException <> nil then
        Msg := AException.Message
      else
        Msg := 'Unknown';
    
      TThread.Queue(nil,
        procedure
        begin
          Memo1.Lines.Add('Except Server TCP: ' + Msg);
        end
      );
    end;
    

    Client:

    procedure TFrm_TCP_Client.BtnSendClick(Sender: TObject);
    Var
      IBuf,RBuf : TIdBytes;
      LenPacket : Integer;
    begin
      if not IdTCPClient1.Connected then Exit;
      LenPacket := StrToInt(EdtLength.Text);
      if LenPacket < 1 then Exit;
      SetLength(IBuf, LenPacket);
      FillBytes(IBuf, LenPacket, $1);
      try
        IdTCPClient1.IOHandler.InputBuffer.Clear;
        IdTCPClient1.IOHandler.Write(IBuf);
      except
        Memo1.Lines.Add('Request Send Error');
        Exit;
      end;
      try
        while IdTCPClient1.IOHandler.CheckForDataOnSource(500) do;
        if not IdTCPClient1.IOHandler.InputBufferIsEmpty then
        begin
          IdTCPClient1.IOHandler.ReadBytes(RBuf, IdTCPClient1.IOHandler.InputBuffer.Size, True);
          if Length(RBuf) = Length(IBuf) then
            Memo1.Lines.Add('Response Received OK: ' + IntToStr(Length(RBuf)))
          else
            Memo1.Lines.Add('Response Received With Different Length. Expected: ' + IntToStr(Length(IBuf)) + ', Got: ' + IntToStr(Length(RBuf)));
        end
        else
          Memo1.Lines.Add('NO Response Received');
      except
        Memo1.Lines.Add('Response Receive Error');
      end;
    end;
    

    A better solution would be to not rely on such logic at all, be more explicit about the structure of your data protocol, for instance <length><data>, eg:

    Server:

    procedure TFrmServer.IdTCPServer1Execute(AContext: TIdContext);
    var
      IBuf : TIdBytes;
      LenPacket : Int32;
    begin
      LenPacket := AContext.Connection.IOHandler.ReadInt32;
      AContext.Connection.IOHandler.ReadBytes(IBuf, LenPacket, True);
      AContext.Connection.IOHandler.Write(LenPacket);
      AContext.Connection.IOHandler.Write(IBuf);
    end;
    
    procedure TFrmServer.IdTCPServer1Exception(AContext: TIdContext, AException: Exception);
    var
      Msg: string;
    begin
      if AException <> nil then
        Msg := AException.Message
      else
        Msg := 'Unknown';
    
      TThread.Queue(nil,
        procedure
        begin
          Memo1.Lines.Add('Except Server TCP: ' + Msg);
        end
      );
    end;
    

    Client:

    procedure TFrm_TCP_Client.BtnSendClick(Sender: TObject);
    Var
      IBuf,RBuf : TIdBytes;
      LenPacket : Int32;
    begin
      if not IdTCPClient1.Connected then Exit;
      LenPacket := StrToInt(EdtLength.Text);
      if LenPacket < 1 then Exit;
      SetLength(IBuf, LenPacket);
      FillBytes(IBuf, LenPacket, $1);
      try
        IdTCPClient1.IOHandler.InputBuffer.Clear;
        IdTCPClient1.IOHandler.Write(LenPacket);
        IdTCPClient1.IOHandler.Write(IBuf);
      except
        Memo1.Lines.Add('Request Send Error');
        Exit;
      end;
      try
        IdTCPClient1.IOHandler.ReadTimeout := 5000;
        LenPacket := IdTCPClient1.IOHandler.ReadInt32;
        IdTCPClient1.IOHandler.ReadBytes(RBuf, LenPacket, True);
      except
        Memo1.Lines.Add('Response Receive Error');
        Exit;
      end;
      Memo1.Lines.Add('Response Received OK');
    end;