Indy:服务端与客户端连接异常断开时的处理
Indy : Treatment when connection between server and clients will be broken abnormally
各位。
我正在基于 Indy TCP 控件开发 Server/Clients 程序。
现在,我面临一些不确定的问题..
就是连接异常断开了...
让我们假设网络状态意外中断,或者服务器应用程序异常终止,因此客户端无法再与服务器通信......
然后客户会发生异常,如 "connection reset by peer" 或 "connection refused..."
在这些情况下,如何巧妙地处理这些异常?
我希望客户端在恢复服务器状态后自动重新连接并正常通信......
如果您有好的想法,请分享.....
下面是我的代码。我使用了两个定时器控件。一种是发送活动并确认网络状态(5000ms)。如果网络状态正常,那么这个定时器就死了,另一个定时器被启用。第二个定时器是向服务器发送信息(1000ms)
如果在第二个定时器中发生异常则禁用它,并再次启用第一个定时器。
当"connection refused"发生时,try except
块可以捕获它。
但是如果发生了"Connection reset by peer",那么try except
块就抓不住了。
{发送缓冲函数}
function SendBuffer(AClient: TIdTCPClient; ABuffer: TBytes): Boolean; overload;
begin
try
Result := True;
try
AClient.IOHandler.Write(LongInt(Length(ABuffer)));
AClient.IOHandler.WriteBufferOpen;
AClient.IOHandler.Write(ABuffer, Length(ABuffer));
AClient.IOHandler.WriteBufferFlush;
finally
AClient.IOHandler.WriteBufferClose;
end;
except
Result := False;
end;
end;
{活动计时器}
procedure TClientForm.Timer_StrAliveTimer(Sender: TObject);
var
infoStr : string;
begin
if not IdTCPClient_StrSend.Connected then
begin
try
if IdTCPClient_StrSend.IOHandler <> nil then
begin
IdTCPClient_StrSend.IOHandler.InputBuffer.Clear;
IdTCPClient_StrSend.IOHandler.WriteBufferClear;
end;
IdTCPClient_StrSend.Connect;
except on E: Exception do
begin
SAOutMsg := 'connect fail : ' + E.ToString ;
Exit;
end;
end;
SAOutMsg := 'connect success : ';
if IdTCPClient_StrSend.Connected then
begin
IdTCPClient_StrSend.IOHandler.CheckForDisconnect(True, True);
IdTCPClient_StrSend.IOHandler.CheckForDataOnSource(100);
infoStr := MY_MAC_ADDRESS+'|'+MY_COMPUTER_NAME;
try
IdTCPClient_StrSend.IOHandler.WriteLn(infoStr, nil);
except on E: Exception do
begin
SAOutMsg := 'login info send fail : ';
Exit;
end;
end;
SAOutMsg := 'login info send success : ';
try
if IdTCPClient_StrSend.IOHandler.ReadLn() = 'OK' then
begin
Timer_StrAlive.Enabled := False;
Timer_Str.Enabled := True;
end;
except on E: Exception do
begin
SAOutMsg := 'login fail : ' + E.ToString ;
Exit;
end;
end;
SAOutMsg := 'login ok : ' ;
end;
end;
end;
{发送部分}
procedure TClientForm.Timer_StrTimer(Sender: TObject);
var
LBuffer: TBytes;
LClientRecord: TClientRecord;
begin
// IdTCPClient_StrSend.CheckForGracefulDisconnect(False);
if not IdTCPClient_StrSend.Connected then
begin
Timer_Str.Enabled := False;
Timer_StrAlive.Enabled := True;
Exit;
end;
if IdTCPClient_StrSend.Connected then
begin
LClientRecord.data1 := str1;
LClientRecord.data2:= Trim(str2);
LClientRecord.data3 := Trim(str3);
LBuffer := MyRecordToByteArray(LClientRecord);
IdTCPClient_StrSend.IOHandler.CheckForDisconnect(True, True);
IdTCPClient_StrSend.IOHandler.CheckForDataOnSource(100);
if (SendBuffer(IdTCPClient_StrSend, LBuffer) = False) then
begin
SOutMsg := 'info send fail' ;
IdTCPClient_StrSend.Disconnect(False);
if IdTCPClient_StrSend.IOHandler <> nil then
IdTCPClient_StrSend.IOHandler.InputBuffer.Clear;
Timer_Str.Enabled := False;
Timer_StrAlive.Enabled := True;
Exit;
end
与丢失连接相关的异常,如 "connection reset by peer",只有在 OS 检测到丢失连接后执行套接字 read/write 操作才会发生(通常在内部超时之后)期间已经过去)并使套接字连接无效。大多数 Indy 客户端组件不会自动执行此类操作,您必须告诉它们这样做(TIdTelnet
和 TIdCmdTCPClient
是该规则的明显例外,因为它们 运行 内部读取线程)。因此,只需将套接字操作包装在 try/except
块中,如果捕获到 Indy 套接字异常(例如 EIdSocketError
或后代),则可以调用 Disconnect()
和 Connect()
至 re-connect.
"connection refused" 只能在调用 Connect()
时出现。这通常意味着服务器已到达但当时无法接受连接,要么是因为请求的 IP/port 上没有监听套接字,要么是监听套接字的积压中有太多挂起的连接(也可能意味着防火墙阻止了连接)。同样,只需将 Connect()
包装在 try/except
中即可处理错误,这样您就可以再次调用 Connect()
。在执行此操作之前,您应该等待一小段超时时间,以便让服务器有时间可能清除导致它首先拒绝连接的任何情况(假设防火墙不是问题)。
Indy 严重依赖异常来报告错误,在较小程度上依赖于状态报告。因此,在使用 Indy 时通常需要使用 try/except
处理程序。
更新:我发现您的代码中存在一些问题。 SendBuffer()
没有正确实现写入缓冲。大多数对 Connected()
的调用,以及对 CheckForDisconnect()
和 CheckForDataOnSource()
的所有调用,都是多余的,应该完全删除。唯一有意义的保留调用是每个计时器中对 Connected()
的第一次调用。
试试像这样的东西:
{发送缓冲函数}
function SendBuffer(AClient: TIdTCPClient; const ABuffer: TBytes): Boolean; overload;
begin
Result := False;
try
AClient.IOHandler.WriteBufferOpen;
try
AClient.IOHandler.Write(LongInt(Length(ABuffer)));
AClient.IOHandler.Write(ABuffer);
AClient.IOHandler.WriteBufferClose;
except
AClient.IOHandler.WriteBufferCancel;
raise;
end;
Result := True;
except
end;
end;
{活动计时器}
procedure TClientForm.Timer_StrAliveTimer(Sender: TObject);
var
infoStr : string;
begin
if IdTCPClient_StrSend.Connected then Exit;
try
IdTCPClient_StrSend.Connect;
except
on E: Exception do
begin
SAOutMsg := 'connect fail : ' + E.ToString;
Exit;
end;
end;
try
SAOutMsg := 'connect success : ';
infoStr := MY_MAC_ADDRESS+'|'+MY_COMPUTER_NAME;
try
IdTCPClient_StrSend.IOHandler.WriteLn(infoStr);
except
on E: Exception do
begin
E.Message := 'login info send fail : ' + E.Message;
raise;
end;
end;
SAOutMsg := 'login info send success : ';
try
if IdTCPClient_StrSend.IOHandler.ReadLn() <> 'OK' then
raise Exception.Create('not OK');
except
on E: Exception do
begin
E.Message := 'login fail : ' + E.Message;
raise;
end;
end;
SAOutMsg := 'login ok : ' ;
except
on E: Exception do
begin
SAOutMsg := E.ToString;
IdTCPClient_StrSend.Disconnect(False);
if IdTCPClient_StrSend.IOHandler <> nil then
IdTCPClient_StrSend.IOHandler.InputBuffer.Clear;
Exit;
end;
end;
Timer_StrAlive.Enabled := False;
Timer_Str.Enabled := True;
end;
{发送部分}
procedure TClientForm.Timer_StrTimer(Sender: TObject);
var
LBuffer: TBytes;
LClientRecord: TClientRecord;
begin
if not IdTCPClient_StrSend.Connected then
begin
Timer_Str.Enabled := False;
Timer_StrAlive.Enabled := True;
Exit;
end;
LClientRecord.data1 := str1;
LClientRecord.data2:= Trim(str2);
LClientRecord.data3 := Trim(str3);
LBuffer := MyRecordToByteArray(LClientRecord);
if not SendBuffer(IdTCPClient_StrSend, LBuffer) then
begin
SOutMsg := 'info send fail' ;
IdTCPClient_StrSend.Disconnect(False);
if IdTCPClient_StrSend.IOHandler <> nil then
IdTCPClient_StrSend.IOHandler.InputBuffer.Clear;
Timer_Str.Enabled := False;
Timer_StrAlive.Enabled := True;
Exit;
end
...
end;
现在,话虽如此,在主 UI 线程中的计时器内部使用 Indy 并不是使用 Indy 的最佳方式,甚至不是最安全的方式。这种逻辑在工作线程中会工作得更好,例如:
type
TStrSendThread = class(TThread)
private
FClient: TIdTCPClient;
...
protected
procedure Execute; override;
procedure DoTerminate; override;
public
constructor Create(AClient: TIdTCPClient); reintroduce;
end;
constructor TStrSendThread.Create(AClient: TIdTCPClient);
begin
inherited Create(False);
FClient := AClient;
end;
procedure TStrSendThread.Execute;
var
LBuffer: TIdBytes;
...
begin
while not Terminated do
begin
Sleep(ConnectInterval);
if Terminated then Exit;
try
FClient.Connect;
try
// report status to main thread as needed...
FClient.IOHandler.WriteLn(MY_MAC_ADDRESS+'|'+MY_COMPUTER_NAME);
if FClient.IOHandler.ReadLn() <> 'OK' then
raise Exception.Create('error message');
// report status to main thread as needed...
while not Terminated do
begin
Sleep(SendInterval);
if Terminated then Exit;
...
if not SendBuffer(FClient, LBuffer) then
raise Exception.Create('error message');
end;
finally
FClient.FDisconnect(False);
if FClient.IOHandler <> nil then
FClient.IOHandler.InputBuffer.Clear;
end;
except
on E: Exception do
begin
// report error to main thread as needed...
end;
end;
end;
end;
procedure TStrSendThread.DoTerminate;
begin
// report status to main thread as needed...
inherited;
end;
private
Thread: TStrSendThread;
...
// Timer_StrAliveTimer.Active := True;
if Thread = nil then
Thread := TStrSendThread.Create(IdTCPClient_StrSend);
...
// Timer_StrAliveTimer.Active := False;
if Thread <> nil then
begin
Thread.Terminate;
Thread.WaitFor;
FreeAndNil(Thread);
end;
各位。 我正在基于 Indy TCP 控件开发 Server/Clients 程序。 现在,我面临一些不确定的问题.. 就是连接异常断开了... 让我们假设网络状态意外中断,或者服务器应用程序异常终止,因此客户端无法再与服务器通信...... 然后客户会发生异常,如 "connection reset by peer" 或 "connection refused..." 在这些情况下,如何巧妙地处理这些异常? 我希望客户端在恢复服务器状态后自动重新连接并正常通信...... 如果您有好的想法,请分享.....
下面是我的代码。我使用了两个定时器控件。一种是发送活动并确认网络状态(5000ms)。如果网络状态正常,那么这个定时器就死了,另一个定时器被启用。第二个定时器是向服务器发送信息(1000ms)
如果在第二个定时器中发生异常则禁用它,并再次启用第一个定时器。
当"connection refused"发生时,try except
块可以捕获它。
但是如果发生了"Connection reset by peer",那么try except
块就抓不住了。
{发送缓冲函数}
function SendBuffer(AClient: TIdTCPClient; ABuffer: TBytes): Boolean; overload;
begin
try
Result := True;
try
AClient.IOHandler.Write(LongInt(Length(ABuffer)));
AClient.IOHandler.WriteBufferOpen;
AClient.IOHandler.Write(ABuffer, Length(ABuffer));
AClient.IOHandler.WriteBufferFlush;
finally
AClient.IOHandler.WriteBufferClose;
end;
except
Result := False;
end;
end;
{活动计时器}
procedure TClientForm.Timer_StrAliveTimer(Sender: TObject);
var
infoStr : string;
begin
if not IdTCPClient_StrSend.Connected then
begin
try
if IdTCPClient_StrSend.IOHandler <> nil then
begin
IdTCPClient_StrSend.IOHandler.InputBuffer.Clear;
IdTCPClient_StrSend.IOHandler.WriteBufferClear;
end;
IdTCPClient_StrSend.Connect;
except on E: Exception do
begin
SAOutMsg := 'connect fail : ' + E.ToString ;
Exit;
end;
end;
SAOutMsg := 'connect success : ';
if IdTCPClient_StrSend.Connected then
begin
IdTCPClient_StrSend.IOHandler.CheckForDisconnect(True, True);
IdTCPClient_StrSend.IOHandler.CheckForDataOnSource(100);
infoStr := MY_MAC_ADDRESS+'|'+MY_COMPUTER_NAME;
try
IdTCPClient_StrSend.IOHandler.WriteLn(infoStr, nil);
except on E: Exception do
begin
SAOutMsg := 'login info send fail : ';
Exit;
end;
end;
SAOutMsg := 'login info send success : ';
try
if IdTCPClient_StrSend.IOHandler.ReadLn() = 'OK' then
begin
Timer_StrAlive.Enabled := False;
Timer_Str.Enabled := True;
end;
except on E: Exception do
begin
SAOutMsg := 'login fail : ' + E.ToString ;
Exit;
end;
end;
SAOutMsg := 'login ok : ' ;
end;
end;
end;
{发送部分}
procedure TClientForm.Timer_StrTimer(Sender: TObject);
var
LBuffer: TBytes;
LClientRecord: TClientRecord;
begin
// IdTCPClient_StrSend.CheckForGracefulDisconnect(False);
if not IdTCPClient_StrSend.Connected then
begin
Timer_Str.Enabled := False;
Timer_StrAlive.Enabled := True;
Exit;
end;
if IdTCPClient_StrSend.Connected then
begin
LClientRecord.data1 := str1;
LClientRecord.data2:= Trim(str2);
LClientRecord.data3 := Trim(str3);
LBuffer := MyRecordToByteArray(LClientRecord);
IdTCPClient_StrSend.IOHandler.CheckForDisconnect(True, True);
IdTCPClient_StrSend.IOHandler.CheckForDataOnSource(100);
if (SendBuffer(IdTCPClient_StrSend, LBuffer) = False) then
begin
SOutMsg := 'info send fail' ;
IdTCPClient_StrSend.Disconnect(False);
if IdTCPClient_StrSend.IOHandler <> nil then
IdTCPClient_StrSend.IOHandler.InputBuffer.Clear;
Timer_Str.Enabled := False;
Timer_StrAlive.Enabled := True;
Exit;
end
与丢失连接相关的异常,如 "connection reset by peer",只有在 OS 检测到丢失连接后执行套接字 read/write 操作才会发生(通常在内部超时之后)期间已经过去)并使套接字连接无效。大多数 Indy 客户端组件不会自动执行此类操作,您必须告诉它们这样做(TIdTelnet
和 TIdCmdTCPClient
是该规则的明显例外,因为它们 运行 内部读取线程)。因此,只需将套接字操作包装在 try/except
块中,如果捕获到 Indy 套接字异常(例如 EIdSocketError
或后代),则可以调用 Disconnect()
和 Connect()
至 re-connect.
"connection refused" 只能在调用 Connect()
时出现。这通常意味着服务器已到达但当时无法接受连接,要么是因为请求的 IP/port 上没有监听套接字,要么是监听套接字的积压中有太多挂起的连接(也可能意味着防火墙阻止了连接)。同样,只需将 Connect()
包装在 try/except
中即可处理错误,这样您就可以再次调用 Connect()
。在执行此操作之前,您应该等待一小段超时时间,以便让服务器有时间可能清除导致它首先拒绝连接的任何情况(假设防火墙不是问题)。
Indy 严重依赖异常来报告错误,在较小程度上依赖于状态报告。因此,在使用 Indy 时通常需要使用 try/except
处理程序。
更新:我发现您的代码中存在一些问题。 SendBuffer()
没有正确实现写入缓冲。大多数对 Connected()
的调用,以及对 CheckForDisconnect()
和 CheckForDataOnSource()
的所有调用,都是多余的,应该完全删除。唯一有意义的保留调用是每个计时器中对 Connected()
的第一次调用。
试试像这样的东西:
{发送缓冲函数}
function SendBuffer(AClient: TIdTCPClient; const ABuffer: TBytes): Boolean; overload;
begin
Result := False;
try
AClient.IOHandler.WriteBufferOpen;
try
AClient.IOHandler.Write(LongInt(Length(ABuffer)));
AClient.IOHandler.Write(ABuffer);
AClient.IOHandler.WriteBufferClose;
except
AClient.IOHandler.WriteBufferCancel;
raise;
end;
Result := True;
except
end;
end;
{活动计时器}
procedure TClientForm.Timer_StrAliveTimer(Sender: TObject);
var
infoStr : string;
begin
if IdTCPClient_StrSend.Connected then Exit;
try
IdTCPClient_StrSend.Connect;
except
on E: Exception do
begin
SAOutMsg := 'connect fail : ' + E.ToString;
Exit;
end;
end;
try
SAOutMsg := 'connect success : ';
infoStr := MY_MAC_ADDRESS+'|'+MY_COMPUTER_NAME;
try
IdTCPClient_StrSend.IOHandler.WriteLn(infoStr);
except
on E: Exception do
begin
E.Message := 'login info send fail : ' + E.Message;
raise;
end;
end;
SAOutMsg := 'login info send success : ';
try
if IdTCPClient_StrSend.IOHandler.ReadLn() <> 'OK' then
raise Exception.Create('not OK');
except
on E: Exception do
begin
E.Message := 'login fail : ' + E.Message;
raise;
end;
end;
SAOutMsg := 'login ok : ' ;
except
on E: Exception do
begin
SAOutMsg := E.ToString;
IdTCPClient_StrSend.Disconnect(False);
if IdTCPClient_StrSend.IOHandler <> nil then
IdTCPClient_StrSend.IOHandler.InputBuffer.Clear;
Exit;
end;
end;
Timer_StrAlive.Enabled := False;
Timer_Str.Enabled := True;
end;
{发送部分}
procedure TClientForm.Timer_StrTimer(Sender: TObject);
var
LBuffer: TBytes;
LClientRecord: TClientRecord;
begin
if not IdTCPClient_StrSend.Connected then
begin
Timer_Str.Enabled := False;
Timer_StrAlive.Enabled := True;
Exit;
end;
LClientRecord.data1 := str1;
LClientRecord.data2:= Trim(str2);
LClientRecord.data3 := Trim(str3);
LBuffer := MyRecordToByteArray(LClientRecord);
if not SendBuffer(IdTCPClient_StrSend, LBuffer) then
begin
SOutMsg := 'info send fail' ;
IdTCPClient_StrSend.Disconnect(False);
if IdTCPClient_StrSend.IOHandler <> nil then
IdTCPClient_StrSend.IOHandler.InputBuffer.Clear;
Timer_Str.Enabled := False;
Timer_StrAlive.Enabled := True;
Exit;
end
...
end;
现在,话虽如此,在主 UI 线程中的计时器内部使用 Indy 并不是使用 Indy 的最佳方式,甚至不是最安全的方式。这种逻辑在工作线程中会工作得更好,例如:
type
TStrSendThread = class(TThread)
private
FClient: TIdTCPClient;
...
protected
procedure Execute; override;
procedure DoTerminate; override;
public
constructor Create(AClient: TIdTCPClient); reintroduce;
end;
constructor TStrSendThread.Create(AClient: TIdTCPClient);
begin
inherited Create(False);
FClient := AClient;
end;
procedure TStrSendThread.Execute;
var
LBuffer: TIdBytes;
...
begin
while not Terminated do
begin
Sleep(ConnectInterval);
if Terminated then Exit;
try
FClient.Connect;
try
// report status to main thread as needed...
FClient.IOHandler.WriteLn(MY_MAC_ADDRESS+'|'+MY_COMPUTER_NAME);
if FClient.IOHandler.ReadLn() <> 'OK' then
raise Exception.Create('error message');
// report status to main thread as needed...
while not Terminated do
begin
Sleep(SendInterval);
if Terminated then Exit;
...
if not SendBuffer(FClient, LBuffer) then
raise Exception.Create('error message');
end;
finally
FClient.FDisconnect(False);
if FClient.IOHandler <> nil then
FClient.IOHandler.InputBuffer.Clear;
end;
except
on E: Exception do
begin
// report error to main thread as needed...
end;
end;
end;
end;
procedure TStrSendThread.DoTerminate;
begin
// report status to main thread as needed...
inherited;
end;
private
Thread: TStrSendThread;
...
// Timer_StrAliveTimer.Active := True;
if Thread = nil then
Thread := TStrSendThread.Create(IdTCPClient_StrSend);
...
// Timer_StrAliveTimer.Active := False;
if Thread <> nil then
begin
Thread.Terminate;
Thread.WaitFor;
FreeAndNil(Thread);
end;