首页 > 解决方案 > Delphi Indy 客户端发送 64 KB 包,服务器接收 2 个包,共 64 KB

问题描述

使用TIdTCPServerIndy 的组件,一个包分两部分接收,但客户端发送了一个 64 KB 的包。

如何在服务器OnExecute事件中接收完整的包?

现在我放了一个原型(服务器和客户端)代码来重新创建这种情况。

服务器代码

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;

客户代码

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;

如何知道消息是第一个片段还是第二个片段?如何强制接收第二个片段?

标签: delphitcpindy

解决方案


TCP 中的发送和读取之间没有一对一的关系。它可以自由分割数据,但它希望优化网络传输。TCP 只保证数据被传递,并且以相同的顺序发送,但没有关于如何在传输过程中对数据进行分段。TCP 将在接收端重建分片。这就是 TCP 的工作原理,它不是 Indy 独有的。无论使用哪个 TCP 框架,每个 TCP 应用程序都必须处理这个问题。

如果您期望 64KB 的数据,那么只需读取 64KB 的数据,让操作系统和 Indy 在内部为您处理片段。TCP 的这种碎片正是 Indy在将数据拼凑在一起时IOHandler使用 an来收集碎片的原因。InputBuffer

更新:停止关注片段。那是 TCP 层的一个实现细节,你没有在它上面操作。您不需要处理代码中的片段。让 Indy 为您处理。只需专注于您的应用程序级协议即可。

仅供参考,您基本上实现了 ECHO 客户端/服务器解决方案。Indy 有实际的 ECHO 客户端/服务器组件TIdECHOTIdECHOServer你应该看看它们。

无论如何,您的服务器端异常处理是非常有问题的。它不与主 UI 线程同步(OnExecute在工作线程中调用)。但是,更重要的是,TIdTCPServer当客户端连接丢失/断开时,它会阻止处理 Indy 本身发出的任何通知,因此客户端线程将继续运行并且在您停用服务器之前不会停止。不要吞下 Indy 自己的异常(源自EIdException)。如果你需要在你的代码中捕获它们,你应该在完成后重新引发它们,让TIdTCPServer它们处理。try..except但是,在您的示例中,完全删除并使用服务器的OnException事件会更容易。

此外,您尝试使用它的客户端阅读循环是错误的。您没有IBuf正确初始化。但是,更重要的是,您使用的超时时间非常短(TCP 连接可能有延迟),并且一旦任何数据到达或最长 500 毫秒已经过去,您就会中断读取循环,即使还有更多数据即将到来。你应该一直阅读,直到没有任何东西可以阅读。

尝试更多类似的东西:

服务器:

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;

客户:

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;

更好的解决方案是完全不依赖这种逻辑,更明确地说明数据协议的结构,例如<length><data>,例如:

服务器:

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;

客户:

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;

推荐阅读