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 客户端组件不会自动执行此类操作,您必须告诉它们这样做(TIdTelnetTIdCmdTCPClient 是该规则的明显例外,因为它们 运行 内部读取线程)。因此,只需将套接字操作包装在 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;