Delphi Indy 客户端发送 64 KB 包,服务器收到 2 个包,共 64 KB
Delphi Indy client sends 64 KB package and the Server receives 2 packages totaling 64 KB
使用 Indy 的 TIdTCPServer
组件,一个包被分成两部分,但客户发送了一个 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;
如何知道一条消息是第一个片段还是第二个片段?
如何强制接收第二个片段?
TCP 中的发送和读取之间没有一对一的关系。它可以自由分割数据,但它希望优化网络传输。 TCP 只保证数据被传送,并且以与发送相同的顺序,但没有关于数据在传输过程中如何碎片化的信息。 TCP 将在接收端重建碎片。这就是 TCP 的工作原理,它不是 Indy 独有的。无论使用哪个 TCP 框架,每个 TCP 应用程序都必须处理这个问题。
如果您期望 64KB 的数据,那么只需读取 64KB 的数据,然后让 OS 和 Indy 在内部为您处理片段。 TCP 的这种碎片正是 Indy 的 IOHandler
在将数据拼接在一起时使用 InputBuffer
来收集碎片的原因。
更新:不再关注片段。这是 TCP 层的实现细节,您不在该层进行操作。您不需要处理代码中的片段。让 Indy 为您处理。只需专注于您的应用程序级协议即可。
仅供参考,您实际上已经实施了 ECHO client/server 解决方案。 Indy 有实际的 ECHO client/server 组件,TIdECHO
和 TIdECHOServer
,你应该看看它们。
不管怎样,你的服务端异常处理是很有问题的。它不与主 UI 线程同步(OnExecute
在工作线程中调用)。但是,更重要的是,当客户端连接为 lost/disconnected 时,它阻止 TIdTCPServer
处理 Indy 本身发出的任何通知,因此客户端线程将保持 运行 并且在您停用之前不会停止服务器。不要吞下 Indy 自己的异常(源自 EIdException
)。如果你需要在你的代码中捕获它们,你应该在完成后重新引发它们,让 TIdTCPServer
处理它们。但是,在您的示例中,完全删除 try..except
并改用服务器的 OnException
事件会更容易。
此外,您的客户端阅读循环对于您尝试使用它所做的事情是错误的。您没有正确初始化 IBuf
。但是,更重要的是,您使用的超时时间非常短(TCP 连接可能有延迟),并且一旦 any 数据到达或最多 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, );
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, );
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;
使用 Indy 的 TIdTCPServer
组件,一个包被分成两部分,但客户发送了一个 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;
如何知道一条消息是第一个片段还是第二个片段? 如何强制接收第二个片段?
TCP 中的发送和读取之间没有一对一的关系。它可以自由分割数据,但它希望优化网络传输。 TCP 只保证数据被传送,并且以与发送相同的顺序,但没有关于数据在传输过程中如何碎片化的信息。 TCP 将在接收端重建碎片。这就是 TCP 的工作原理,它不是 Indy 独有的。无论使用哪个 TCP 框架,每个 TCP 应用程序都必须处理这个问题。
如果您期望 64KB 的数据,那么只需读取 64KB 的数据,然后让 OS 和 Indy 在内部为您处理片段。 TCP 的这种碎片正是 Indy 的 IOHandler
在将数据拼接在一起时使用 InputBuffer
来收集碎片的原因。
更新:不再关注片段。这是 TCP 层的实现细节,您不在该层进行操作。您不需要处理代码中的片段。让 Indy 为您处理。只需专注于您的应用程序级协议即可。
仅供参考,您实际上已经实施了 ECHO client/server 解决方案。 Indy 有实际的 ECHO client/server 组件,TIdECHO
和 TIdECHOServer
,你应该看看它们。
不管怎样,你的服务端异常处理是很有问题的。它不与主 UI 线程同步(OnExecute
在工作线程中调用)。但是,更重要的是,当客户端连接为 lost/disconnected 时,它阻止 TIdTCPServer
处理 Indy 本身发出的任何通知,因此客户端线程将保持 运行 并且在您停用之前不会停止服务器。不要吞下 Indy 自己的异常(源自 EIdException
)。如果你需要在你的代码中捕获它们,你应该在完成后重新引发它们,让 TIdTCPServer
处理它们。但是,在您的示例中,完全删除 try..except
并改用服务器的 OnException
事件会更容易。
此外,您的客户端阅读循环对于您尝试使用它所做的事情是错误的。您没有正确初始化 IBuf
。但是,更重要的是,您使用的超时时间非常短(TCP 连接可能有延迟),并且一旦 any 数据到达或最多 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, );
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, );
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;