使用 TIdTcpClient 超时接收部分数据
Receive partial data with timeout using TIdTcpClient
如何使用 TIdTcpClient 接收具有以下条件的 100 字节字符串?:
- 如果没有任何内容,Read 调用将被阻塞,线程将永远等待
- 如果收到 100 个字节,读取调用应该 return 字节字符串
- 如果接收到的字节多于 0 个但少于 100 个,Read 调用应该 return 在超时后(例如 1 秒),以便 return 在合理的时间内至少完成某些操作,没有产生超时异常,因为 Delphi IDE 的调试模式中的异常处理还没有变得方便。
我目前不是最优的代码如下:
unit Unit2;
interface
uses
System.Classes, IdTCPClient;
type
TTcpReceiver = class(TThread)
private
_tcpc: TIdTCPClient;
_onReceive: TGetStrProc;
_buffer: AnsiString;
procedure _receiveLoop();
procedure _postBuffer;
protected
procedure Execute(); override;
public
constructor Create(); reintroduce;
destructor Destroy(); override;
property OnReceive: TGetStrProc read _onReceive write _onReceive;
end;
implementation
uses
System.SysUtils, Vcl.Dialogs, IdGlobal, IdExceptionCore;
constructor TTcpReceiver.Create();
begin
inherited Create(True);
_buffer := '';
_tcpc := TIdTCPClient.Create(nil);
//_tcpc.Host := '192.168.52.175';
_tcpc.Host := '127.0.0.1';
_tcpc.Port := 1;
_tcpc.ReadTimeout := 1000;
_tcpc.Connect();
Suspended := False;
end;
destructor TTcpReceiver.Destroy();
begin
_tcpc.Disconnect();
FreeAndNil(_tcpc);
inherited;
end;
procedure TTcpReceiver.Execute;
begin
_receiveLoop();
end;
procedure TTcpReceiver._postBuffer();
var buf: string;
begin
if _buffer = '' then Exit;
buf := _buffer;
_buffer := '';
if Assigned(_onReceive) then begin
Synchronize(
procedure()
begin
_onReceive(buf);
end
);
end;
end;
procedure TTcpReceiver._receiveLoop();
var
c: AnsiChar;
begin
while not Terminated do begin
try
c := AnsiChar(_tcpc.IOHandler.ReadByte());
_buffer := _buffer + c;
if Length(_buffer) > 100 then
_postBuffer();
except
//Here I have to ignore EIdReadTimeout in Delphi IDE everywhere, but I want just to ignore them here
on ex: EIdReadTimeout do _postBuffer();
end;
end;
end;
end.
TCP 是面向流的,而不是像 UDP 那样面向消息。读取没有任何结构的任意字节是糟糕的设计,如果您过早地停止读取然后您想要读取的字节在您停止读取后到达,则很容易破坏您的通信。字节在读取之前不会从套接字中删除,因此下一次读取可能比预期有 more/different 字节。
如果您需要 100 个字节,那么只需读取 100 个字节即可。如果发件人只发送 50 个字节,它需要提前告诉你,以便你可以在收到 50 个字节后停止阅读。如果发件人没有这样做,那么这是一个设计非常糟糕的协议。使用超时来检测传输结束通常是糟糕的设计。网络延迟很容易造成误检
TCP 消息应该被充分地构造,以便接收者确切地知道一条消息在哪里结束以及下一条消息从哪里开始。在 TCP 中有三种方法可以做到这一点:
使用固定长度的消息。接收方可以继续读取,直到到达预期的字节数。
在发送消息本身之前发送消息的长度。接收方可以先读取长度,然后继续读取,直到到达指定的字节数。
使用消息数据中未出现的唯一定界符终止消息。接收方可以继续读取字节,直到该分隔符到达。
也就是说,您所要求的可以在 TCP 中完成(但是不应该在 TCP 中完成!) .而且它可以在根本不使用手动缓冲区的情况下完成,而是使用 Indy 的内置缓冲区。例如:
unit Unit2;
interface
uses
System.Classes, IdTCPClient;
type
TTcpReceiver = class(TThread)
private
_tcpc: TIdTCPClient;
_onReceive: TGetStrProc;
procedure _receiveLoop;
procedure _postBuffer;
protected
procedure Execute; override;
public
constructor Create; reintroduce;
destructor Destroy; override;
property OnReceive: TGetStrProc read _onReceive write _onReceive;
end;
implementation
uses
System.SysUtils, Vcl.Dialogs, IdGlobal;
constructor TTcpReceiver.Create;
begin
inherited Create(False);
_tcpc := TIdTCPClient.Create(nil);
//_tcpc.Host := '192.168.52.175';
_tcpc.Host := '127.0.0.1';
_tcpc.Port := 1;
end;
destructor TTcpReceiver.Destroy;
begin
_tcpc.Free;
inherited;
end;
procedure TTcpReceiver.Execute;
begin
_tcpc.Connect;
try
_receiveLoop;
finally
_tcpc.Disconnect;
end;
end;
procedure TTcpReceiver._postBuffer;
var
buf: string;
begin
with _tcpc.IOHandler do
buf := ReadString(IndyMin(InputBuffer.Size, 100));
{ alternatively:
with _tcpc.IOHandler.InputBuffer do
buf := ExtractToString(IndyMin(Size, 100));
}
if buf = '' then Exit;
if Assigned(_onReceive) then
begin
Synchronize(
procedure
begin
if Assigned(_onReceive) then
_onReceive(buf);
end
);
end;
end;
procedure TTcpReceiver._receiveLoop;
var
LBytesRecvd: Boolean;
begin
while not Terminated do
begin
while _tcpc.IOHandler.InputBufferIsEmpty do
begin
_tcpc.IOHandler.CheckForDataOnSource(IdTimeoutInfinite);
_tcpc.IOHandler.CheckForDisconnect;
end;
while _tcpc.IOHandler.InputBuffer.Size < 100 do
begin
// 1 sec is a very short timeout to use for TCP.
// Consider using a larger timeout...
LBytesRecvd := _tcpc.IOHandler.CheckForDataOnSource(1000);
_tcpc.IOHandler.CheckForDisconnect;
if not LBytesRecvd then Break;
end;
_postBuffer;
end;
end;
end.
附带说明一下,您所说的“Delphi IDE 的调试模式中的异常处理并不方便”只是荒谬的。 Indy 的 IOHandler
具有用于控制异常行为的属性和方法参数,而且如果您不喜欢调试器处理异常的方式,则只需将其配置为忽略它们。您可以将调试器配置为忽略特定的异常类型,或者您可以使用断点告诉调试器跳过处理特定代码块中的异常。
如何使用 TIdTcpClient 接收具有以下条件的 100 字节字符串?:
- 如果没有任何内容,Read 调用将被阻塞,线程将永远等待
- 如果收到 100 个字节,读取调用应该 return 字节字符串
- 如果接收到的字节多于 0 个但少于 100 个,Read 调用应该 return 在超时后(例如 1 秒),以便 return 在合理的时间内至少完成某些操作,没有产生超时异常,因为 Delphi IDE 的调试模式中的异常处理还没有变得方便。
我目前不是最优的代码如下:
unit Unit2;
interface
uses
System.Classes, IdTCPClient;
type
TTcpReceiver = class(TThread)
private
_tcpc: TIdTCPClient;
_onReceive: TGetStrProc;
_buffer: AnsiString;
procedure _receiveLoop();
procedure _postBuffer;
protected
procedure Execute(); override;
public
constructor Create(); reintroduce;
destructor Destroy(); override;
property OnReceive: TGetStrProc read _onReceive write _onReceive;
end;
implementation
uses
System.SysUtils, Vcl.Dialogs, IdGlobal, IdExceptionCore;
constructor TTcpReceiver.Create();
begin
inherited Create(True);
_buffer := '';
_tcpc := TIdTCPClient.Create(nil);
//_tcpc.Host := '192.168.52.175';
_tcpc.Host := '127.0.0.1';
_tcpc.Port := 1;
_tcpc.ReadTimeout := 1000;
_tcpc.Connect();
Suspended := False;
end;
destructor TTcpReceiver.Destroy();
begin
_tcpc.Disconnect();
FreeAndNil(_tcpc);
inherited;
end;
procedure TTcpReceiver.Execute;
begin
_receiveLoop();
end;
procedure TTcpReceiver._postBuffer();
var buf: string;
begin
if _buffer = '' then Exit;
buf := _buffer;
_buffer := '';
if Assigned(_onReceive) then begin
Synchronize(
procedure()
begin
_onReceive(buf);
end
);
end;
end;
procedure TTcpReceiver._receiveLoop();
var
c: AnsiChar;
begin
while not Terminated do begin
try
c := AnsiChar(_tcpc.IOHandler.ReadByte());
_buffer := _buffer + c;
if Length(_buffer) > 100 then
_postBuffer();
except
//Here I have to ignore EIdReadTimeout in Delphi IDE everywhere, but I want just to ignore them here
on ex: EIdReadTimeout do _postBuffer();
end;
end;
end;
end.
TCP 是面向流的,而不是像 UDP 那样面向消息。读取没有任何结构的任意字节是糟糕的设计,如果您过早地停止读取然后您想要读取的字节在您停止读取后到达,则很容易破坏您的通信。字节在读取之前不会从套接字中删除,因此下一次读取可能比预期有 more/different 字节。
如果您需要 100 个字节,那么只需读取 100 个字节即可。如果发件人只发送 50 个字节,它需要提前告诉你,以便你可以在收到 50 个字节后停止阅读。如果发件人没有这样做,那么这是一个设计非常糟糕的协议。使用超时来检测传输结束通常是糟糕的设计。网络延迟很容易造成误检
TCP 消息应该被充分地构造,以便接收者确切地知道一条消息在哪里结束以及下一条消息从哪里开始。在 TCP 中有三种方法可以做到这一点:
使用固定长度的消息。接收方可以继续读取,直到到达预期的字节数。
在发送消息本身之前发送消息的长度。接收方可以先读取长度,然后继续读取,直到到达指定的字节数。
使用消息数据中未出现的唯一定界符终止消息。接收方可以继续读取字节,直到该分隔符到达。
也就是说,您所要求的可以在 TCP 中完成(但是不应该在 TCP 中完成!) .而且它可以在根本不使用手动缓冲区的情况下完成,而是使用 Indy 的内置缓冲区。例如:
unit Unit2;
interface
uses
System.Classes, IdTCPClient;
type
TTcpReceiver = class(TThread)
private
_tcpc: TIdTCPClient;
_onReceive: TGetStrProc;
procedure _receiveLoop;
procedure _postBuffer;
protected
procedure Execute; override;
public
constructor Create; reintroduce;
destructor Destroy; override;
property OnReceive: TGetStrProc read _onReceive write _onReceive;
end;
implementation
uses
System.SysUtils, Vcl.Dialogs, IdGlobal;
constructor TTcpReceiver.Create;
begin
inherited Create(False);
_tcpc := TIdTCPClient.Create(nil);
//_tcpc.Host := '192.168.52.175';
_tcpc.Host := '127.0.0.1';
_tcpc.Port := 1;
end;
destructor TTcpReceiver.Destroy;
begin
_tcpc.Free;
inherited;
end;
procedure TTcpReceiver.Execute;
begin
_tcpc.Connect;
try
_receiveLoop;
finally
_tcpc.Disconnect;
end;
end;
procedure TTcpReceiver._postBuffer;
var
buf: string;
begin
with _tcpc.IOHandler do
buf := ReadString(IndyMin(InputBuffer.Size, 100));
{ alternatively:
with _tcpc.IOHandler.InputBuffer do
buf := ExtractToString(IndyMin(Size, 100));
}
if buf = '' then Exit;
if Assigned(_onReceive) then
begin
Synchronize(
procedure
begin
if Assigned(_onReceive) then
_onReceive(buf);
end
);
end;
end;
procedure TTcpReceiver._receiveLoop;
var
LBytesRecvd: Boolean;
begin
while not Terminated do
begin
while _tcpc.IOHandler.InputBufferIsEmpty do
begin
_tcpc.IOHandler.CheckForDataOnSource(IdTimeoutInfinite);
_tcpc.IOHandler.CheckForDisconnect;
end;
while _tcpc.IOHandler.InputBuffer.Size < 100 do
begin
// 1 sec is a very short timeout to use for TCP.
// Consider using a larger timeout...
LBytesRecvd := _tcpc.IOHandler.CheckForDataOnSource(1000);
_tcpc.IOHandler.CheckForDisconnect;
if not LBytesRecvd then Break;
end;
_postBuffer;
end;
end;
end.
附带说明一下,您所说的“Delphi IDE 的调试模式中的异常处理并不方便”只是荒谬的。 Indy 的 IOHandler
具有用于控制异常行为的属性和方法参数,而且如果您不喜欢调试器处理异常的方式,则只需将其配置为忽略它们。您可以将调试器配置为忽略特定的异常类型,或者您可以使用断点告诉调试器跳过处理特定代码块中的异常。