连接正常关闭 Indy TCPServer Mobile App Delphi XE8

Connection Closed Gracefully Indy TCPServer Mobile App Delphi XE8

我正在使用 Indy TCPClient/TCPServer 来验证移动设备的注册。这个过程相当简单,我在服务器端读取一个标识符,根据数据库控制文件对其进行验证,然后将响应发送回客户端。

大部分情况下一切似乎都正常工作,但我会定期在服务器端收到 EIDConnClosedGracefully 异常。我似乎无法准确指出连接未正确关闭的位置。事实上,服务器似乎在不应该的时候(连接关闭后)执行了 readln,我不知道为什么。可能我没有正确同步。我在 Tools/Options/Debugger 选项中将 Indy Silent Exceptions 设置为忽略,但我想知道是什么引发了异常。我可以执行注册函数4、5次就抛出异常但是很不一致。

如有任何建议,我们将不胜感激。

以下是我的代码:

服务器

  try
    MIRec.RecType := AContext.Connection.IOHandler.ReadLn;

    if (MIRec.RecType = 'I')
    or (MIRec.RecType = 'R') then
    begin
// Verify the connecting device is registered
      MIRec.Identifier := AContext.Connection.IOHandler.ReadLn;

      qryMobileDevice.Close;
      qryMobileDevice.Parameters.ParamByName('IDENTIFIER').Value := MIRec.Identifier;
      qryMobileDevice.Open;

      AContext.Connection.IOHandler.WriteLn(qryMobileDevice.FindField('ACTIVE').AsString);

      MIRec.DeviceName := AContext.Connection.IOHandler.ReadLn;
      if (MIRec.RecType = 'I') then
         LogEntry := 'A Connection Has Been Established With: ' + MIRec.DeviceName
      else
      begin
// Register the Device in STIKS
      if qryMobileDevice.EOF then
      begin
        LogEntry := 'Registering: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now);

// If Record Does not exist Add to the Control File;
        NextId := GetNextId('NEXT_MOBILE_DEVICE_ID');

        qryMobileDevice.Insert;
        qryMobileDevice.FieldByName('MOBILE_DEVICE_ID').Value := NextId;
        qryMobileDevice.FieldByName('IDENTIFIER').Value := MIRec.Identifier;
        qryMobileDevice.FieldByName('DESCRIPTION').Value := MiRec.Text;
        qryMobileDevice.FieldByName('ACTIVE').Value := 'T';
        qryMobileDevice.FieldByName('OPERATOR_SAVED').Value := 'From App';
        qryMobileDevice.FieldByName('DATE_SAVED').Value := Now;
        qryMobileDevice.Post;
      end
      else
      begin
// Device has been Flagged and registration refused.
        if qryMobileDevice.FindField('ACTIVE').AsString = 'T' then
           LogEntry := '** Registration Successful: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now)
        else
           LogEntry := '** Registration Refused: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now);
      end;

      qryMobileDevice.Close;
    end;

    TThread.Synchronize(nil,
      procedure
      begin
        Memo1.Lines.Add(LogEntry);
      end);
    end;
  except
    on e: exception do
    begin
      Memo1.Lines.Add('** An error occurred Receiving File ' + #13#10 + 'With a message: ' + E.Message);
    end;
  end;

客户

  if MessageDlg('Register Device With Server?', TMsgDlgType.mtConfirmation, [TMsgDlgBtn.mbNo, TMsgDlgBtn.mbYes], 0) = mrNo then Exit;

  try
    IdTCPClient1.Connect;
    try
      MainForm.IdTCPClient1.IOHandler.WriteLn('R'); // Tell Server we are sending a Registration Record

      Device := TUIDevice.Wrap(TUIDevice.OCClass.currentDevice);
      IdTCPClient1.IOHandler.WriteLn(NSStrToStr(Device.identifierForVendor.UUIDString));
      Registered := IdTCPClient1.IOHandler.ReadLn;  // Get response from server
      Authenticated := (Registered = 'T');
      IdTCPClient1.IOHandler.WriteLn(NSStrToStr(Device.Name));
    finally
      IdTCPClient1.DisConnect;

      if Registered <> 'T' then
         MessageDlg('Registration Failed!', TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK], 0)
      else
      begin
        Authenticated := True;
        MessageDlg('Registration Has Completed Successfully!', TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK], 0);
      end;
    end;
  except
    on e: exception do
    begin
        MessageDlg('** An error occurred Registering Device ' + #13#10 + 'With a message: ' + E.Message, TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK], 0);
    end;
  end;

我的日志输出。

A Client connected
** Registration Successful: ; 7FFC0274-AFB1-4E35-B8D9-F987B587804D; Wed. September 30/2015, 9:36:54 AM
A Client Disconnected
A Client connected
** Registration Successful: ; 7FFC0274-AFB1-4E35-B8D9-F987B587804D; Wed. September 30/2015, 9:37:00 AM
A Client Disconnected
A Client connected
** Registration Successful: ; 7FFC0274-AFB1-4E35-B8D9-F987B587804D; Wed. September 30/2015, 9:37:04 AM
** An error occurred Receiving File 
With a message: Connection Closed Gracefully.
A Client Disconnected

服务器代码是否在OnConnectOnExecute事件中?

假设 OnExecute,这是一个 looped 事件,它会在连接的生命周期内不断循环。在每次迭代中,您将再次调用 ReadLn 以从客户端读取下一个命令。如果客户端断开连接,则必须返回套接字以获取更多数据的下一次读取(在 IOHandler.InputBuffer 已用完之后)将相应地引发异常。这是正常行为,也是 Indy 设计的运作方式。

真正的问题是你有一个异常处理程序,它无条件地将所有异常记录为错误,甚至是优雅的断开连接。并且你的异常处理程序在将错误消息添加到你的备忘录时没有与 UI 线程同步,并且它没有重新引发任何捕获的 Indy 异常所以 TIdTCPServer 可以根据需要处理它(例如停止 OnExecute 循环并触发 OnDisconnect 事件)。

尝试更像这样的东西:

// if registration is only done once, this code should
// be in the OnConnect event instead...

procedure TMyForm.MyTCPServerExecute(AContext: TIdContext);
begin
  try
    MIRec.RecType := AContext.Connection.IOHandler.ReadLn;

    if (MIRec.RecType = 'I') or
       (MIRec.RecType = 'R') then
    begin
      // Verify the connecting device is registered
      MIRec.Identifier := AContext.Connection.IOHandler.ReadLn;

      qryMobileDevice.Close;
      qryMobileDevice.Parameters.ParamByName('IDENTIFIER').Value := MIRec.Identifier;
      qryMobileDevice.Open;

      AContext.Connection.IOHandler.WriteLn(qryMobileDevice.FindField('ACTIVE').AsString);

      MIRec.DeviceName := AContext.Connection.IOHandler.ReadLn;
      if (MIRec.RecType = 'I') then
        LogEntry := 'A Connection Has Been Established With: ' + MIRec.DeviceName
      else
      begin
        // Register the Device in STIKS
        if qryMobileDevice.EOF then
        begin
          LogEntry := 'Registering: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now);

          // If Record Does not exist Add to the Control File;
          NextId := GetNextId('NEXT_MOBILE_DEVICE_ID');

          qryMobileDevice.Insert;
          qryMobileDevice.FieldByName('MOBILE_DEVICE_ID').Value := NextId;
          qryMobileDevice.FieldByName('IDENTIFIER').Value := MIRec.Identifier;
          qryMobileDevice.FieldByName('DESCRIPTION').Value := MiRec.Text;
          qryMobileDevice.FieldByName('ACTIVE').Value := 'T';
          qryMobileDevice.FieldByName('OPERATOR_SAVED').Value := 'From App';
          qryMobileDevice.FieldByName('DATE_SAVED').Value := Now;
          qryMobileDevice.Post;
        end
        else
        begin
          // Device has been Flagged and registration refused.
          if qryMobileDevice.FindField('ACTIVE').AsString = 'T' then
            LogEntry := '** Registration Successful: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now)
          else
            LogEntry := '** Registration Refused: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now);
        end;

        qryMobileDevice.Close;
      end;

      TThread.Synchronize(nil,
        procedure
        begin
          Memo1.Lines.Add(LogEntry);
        end
      );
    end;
  except
    on E: Exception do
    begin
      if not (E is EIdSilentException) then
      begin
        TThread.Synchronize(nil,
          procedure
          begin
            Memo1.Lines.Add('** An error occurred Receiving File ' + #13#10 + 'With a message: ' + E.Message);
          end
        );
      end;

      // optionally remove the below 'if' to close the
      // connection on any exception, including DB errors, etc...
      if E is EIdException then
        raise;
    end;
  end;
end;

另一方面,我建议完全删除 try/except 并改用服务器的 OnException 事件。让任何异常关闭连接,然后在最后记录原因:

procedure TMyForm.MyTCPServerExecute(AContext: TIdContext);
begin
  MIRec.RecType := AContext.Connection.IOHandler.ReadLn;

  if (MIRec.RecType = 'I') or
     (MIRec.RecType = 'R') then
  begin
    // Verify the connecting device is registered
    MIRec.Identifier := AContext.Connection.IOHandler.ReadLn;

    qryMobileDevice.Close;
    qryMobileDevice.Parameters.ParamByName('IDENTIFIER').Value := MIRec.Identifier;
    qryMobileDevice.Open;

    AContext.Connection.IOHandler.WriteLn(qryMobileDevice.FindField('ACTIVE').AsString);

    MIRec.DeviceName := AContext.Connection.IOHandler.ReadLn;
    if (MIRec.RecType = 'I') then
      LogEntry := 'A Connection Has Been Established With: ' + MIRec.DeviceName
    else
    begin
      // Register the Device in STIKS
      if qryMobileDevice.EOF then
      begin
        LogEntry := 'Registering: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now);

        // If Record Does not exist Add to the Control File;
        NextId := GetNextId('NEXT_MOBILE_DEVICE_ID');

        qryMobileDevice.Insert;
        qryMobileDevice.FieldByName('MOBILE_DEVICE_ID').Value := NextId;
        qryMobileDevice.FieldByName('IDENTIFIER').Value := MIRec.Identifier;
        qryMobileDevice.FieldByName('DESCRIPTION').Value := MiRec.Text;
        qryMobileDevice.FieldByName('ACTIVE').Value := 'T';
        qryMobileDevice.FieldByName('OPERATOR_SAVED').Value := 'From App';
        qryMobileDevice.FieldByName('DATE_SAVED').Value := Now;
        qryMobileDevice.Post;
      end
      else
      begin
        // Device has been Flagged and registration refused.
        if qryMobileDevice.FindField('ACTIVE').AsString = 'T' then
          LogEntry := '** Registration Successful: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now)
        else
          LogEntry := '** Registration Refused: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now);
      end;

      qryMobileDevice.Close;
    end;

    TThread.Synchronize(nil,
      procedure
      begin
        Memo1.Lines.Add(LogEntry);
      end
    );
  end;
end;

procedure TMyForm.MyTCPServerException(AContext: TIdContext; AException: Exception);
begin
  if not (AException is EIdSilentException) then
  begin
    TThread.Synchronize(nil,
      procedure
      begin
        Memo1.Lines.Add('** An error occurred' + sLineBreak + 'With a message: ' + AException.Message);
      end
    );
  end;
end;

顺便说一句,将 TThread.Synchronize()TIdTCPServer 一起使用时必须非常小心。如果在服务器事件处理程序调用 Synchronize() 时主 UI 线程正忙于停用服务器,则会在 UI 线程和同步线程之间发生死锁(主线程正在等待服务器完成停用,但服务器正在等待线程终止,但线程正在等待 UI 线程完成停用服务器)。对于您所展示的简单日志记录,我建议使用 TThread.Queue() 来避免任何潜在的死锁。或者在工作线程中停用服务器,以便 UI 线程可以自由地继续处理 Synchronize() 请求。