【问题标题】:Indy 10.6 using tcpserver on Linux system with admin rights throws Gtk-WARNING when client disconnectsIndy 10.6 在具有管理员权限的 Linux 系统上使用 tcpserver 在客户端断开连接时抛出 Gtk-WARNING
【发布时间】:2017-01-26 18:14:59
【问题描述】:

我正在 Raspberry PI 上使用 Indy 10.6 tcpserver 设置一个新系统,并加载了最新的 Raspbian。我正在通过带有 sudo 的终端 bash 脚本从 GUI 桌面运行该应用程序。在客户端连接之前一切正常,然后当它断开连接时我得到 Gtk-WARNINGs,有时是 Gtk-CRITICALs,我不知道为什么。这是我的代码,它一次只允许一个客户端连接,然后它会停用服务器并在每个连接完成后重新启动它:

Procedure TFK20Elevator.ASpeedBtn1Click(Sender: TObject);
Begin //start the server
  Server.Active := False;
  Server.Bindings.Clear;
  Server.Bindings.Add.IPVersion := Id_IPv4;
  Server.Bindings.Add.IP := LIP;
  Server.Bindings.Add.Port := DefPort + StrToIntDef(UnitID, 0);
  Try
    Server.Active := True;
  Except
    On E: Exception Do
      Memo1.Lines.Add(E.Message);
  End;
  If Not Server.Active Then
    Exit;
  ASpeedBtn1.Enabled := False;
  ASpeedBtn2.Enabled := True;
  AStatus1.SimpleText := 'Server bound to ' + LIP + ':' + IntToStr(DefPort + StrToIntDef(UnitID, 0));
End;

Procedure TFK20Elevator.ServerConnect(AContext: TIdContext);
Begin
  If Connected Then
    Begin
      Abort();
      Exit;
    End;
  AStatus1.SimpleText := 'Connecting to> ' + AContext.Binding.PeerIP + ' - Authenticating...';
  Memo1.Lines.Clear;
  Manager := False;
  EncDecSIdx := 1;
  RetryTimer.Enabled := False;
  RetryTimer.Interval := 3000;
  Authenticating := True;
  AuthTimer.Enabled := True;
  StayAlive.Enabled := True;
End;

Procedure TFK20Elevator.ServerException(AContext: TIdContext; AException: Exception);
Begin
  If AnsiContainsText(AException.Message, 'Gracefully') Then
    AStatus1.SimpleText := 'Server bound to ' + LIP + ':' + IntToStr(DefPort + StrToIntDef(UnitID, 0)) //closed gracefully message
  Else
    Begin //show the exception
      Memo1.Lines.Add('An exception happend! - ' + AException.Message);
      RetryTimer.Enabled := True;
    End;
  Manager := False;
  Authenticating := False;
End;

Procedure TFK20Elevator.ServerExecute(AContext: TIdContext);
//EncStr and DecStr simply encode/decode, respectively, a standard
//  string into/from a key encrypted hex string, i.e. '00' to 'FF'
//  for each character in the string
Var
  S, UserName, Password: String;
  I, N: Integer;
Begin
  S := AContext.Connection.IOHandler.ReadLn(IndyTextEncoding_OSDefault, IndyTextEncoding_OSDefault); //get the data
  If S = Heart Then //if message is the client heart beat, return to client
    Begin //just a heart beat, reset timer
      StayAlive.Enabled := False;
      AContext.Connection.IOHandler.WriteLn(Heart, IndyTextEncoding_OSDefault, IndyTextEncoding_OSDefault);
      StayAlive.Enabled := True;
      Exit;
    End;
  S := PCommon.DecStr(S, EncDecStr, EncDecSIdx); //not heart beat, decompress
  If Authenticating Then
    Begin //test log in
      If Length(S) > 3 Then
        Begin
          I := Pos('|', S);
          If (I > 1) And (Length(S) > I) Then
            Begin
              UserName := Copy(S, 1, I - 1);
              Password := Copy(S, I + 1, Length(S) - I);
              If UserName = ManUser Then
                Begin
                  If Password = ManPass Then
                    Begin
                      AuthTimer.Enabled := False;
                      Manager := True;
                      Authenticating := False;
                      AContext.Connection.IOHandler.WriteLn(EncStr(AContext.Binding.PeerIP +
                                                            ':' + IntToStr(DefPort + StrToIntDef(UnitID, 0)) + 'M',
                                                            EncDecStr, EncDecSIdx), IndyTextEncoding_OSDefault,
                                                            IndyTextEncoding_OSDefault);
                      AStatus1.SimpleText := 'Connecting to> ' + AContext.Binding.PeerIP + ' as Manager';
                      Connected := True;
                    End
                  Else
                    AuthTimerTimer(Self);
                End
              Else If UserName = GenUser Then
                Begin
                  If Password = GenPass Then
                    Begin
                      AuthTimer.Enabled := False;
                      Authenticating := False;
                      AContext.Connection.IOHandler.WriteLn(EncStr(AContext.Binding.PeerIP +
                                                            ':' + IntToStr(DefPort + StrToIntDef(UnitID, 0)) + 'U',
                                                            EncDecStr, EncDecSIdx), IndyTextEncoding_OSDefault,
                                                            IndyTextEncoding_OSDefault);
                      AStatus1.SimpleText := 'Connecting to> ' + AContext.Binding.PeerIP + ' as General User';
                      Connected := True;
                    End
                  Else
                    AuthTimerTimer(Self);
                End
              Else
                AuthTimerTimer(Self);
            End
          Else
            AuthTimerTimer(Self);
        End
      Else
        AuthTimerTimer(Self);
    End
  Else
    Begin //test for commands
      If Copy(S, 1, Length(AssignID)) = AssignID Then
        Begin //command to assign a new unit id
          NewLoc := DefLocation;
          NewUnit := DefUnitNum;
          I := Pos('-', S, 1);
          If (I > 0) And (I < Length(S)) Then
            Begin
              N := Pos('-', S, I + 1);
              If (N > 0) And (N < Length(S)) Then
                Begin
                  NewLoc := Copy(S, I + 1, N - I - 1);
                  NewUnit := Copy(S, N + 1, Length(S) - N);
                End;
            End;
          Label15.Caption := NewLoc;
          Label16.Caption := NewUnit;
          FmtStr(LIP, '%.3d', [StrToInt(NewUnit)]);
          LIP := '192.168.6' + Copy(LIP, 1, 1) + '.' + Copy(LIP, 2, 2); //wifi ip
          Memo1.Lines.Add('--> ' + S + '-' + LIP);
          AContext.Connection.IOHandler.WriteLn(PCommon.EncStr(Rebooting, EncDecStr, EncDecSIdx),
                                                IndyTextEncoding_OSDefault, IndyTextEncoding_OSDefault);
          Memo1.Lines.Add('<-- ' + Rebooting);
          TestTimer.Enabled := True;
        End;
    End;
End;

Procedure TFK20Elevator.ASpeedBtn2Click(Sender: TObject);
Begin //shut down the server with optional restart if not rebooting
  AuthTimer.Enabled := False;
  RetryTimer.Enabled := False;
  StayAlive.Enabled := False;
  TestTimer.Enabled := False;
  DropClient;
  Try
    Server.Active := False;
  Except
    On E: Exception Do
      Memo1.Lines.Add('Error disconnecting server - ' + E.Message);
  End;
  If Server.Active Then
    Exit;
  ASpeedBtn1.Enabled := True;
  ASpeedBtn2.Enabled := False;
  AStatus1.SimpleText := 'Server not running...';
  Manager := False;
  Authenticating := False;
  Connected := False;
  RetryTimer.Enabled := Not SysReboot;
End;

Procedure TFK20Elevator.ServerDisconnect(AContext: TIdContext);
Begin
  StayAlive.Enabled := False;
  RetryTimer.Enabled := False;
  DropClient;
  AStatus1.SimpleText := 'Client disconnected...';
  Manager := False;
  Authenticating := False;
  Connected := False;
  RetryTimer.Enabled := Not SysReboot;
End;

Procedure TFK20Elevator.DropClient; //make sure buffers are cleared
Var
  I: Integer;
  SC: TIdContext;
Begin
  If Server.Active Then
    Begin
      Application.ProcessMessages;
      With Server.Contexts.LockList Do
        Try
          Memo1.Lines.Add('Disconnecting...');
          For I := Count - 1 DownTo 0 Do
            Begin
              SC := TIdContext(Items[I]);
              If SC = Nil Then
                Continue;
              SC.Connection.IOHandler.WriteBufferClear;
              SC.Connection.IOHandler.InputBuffer.Clear;
              SC.Connection.IOHandler.Close;
              If SC.Connection.Connected Then
                SC.Connection.Disconnect;
              Memo1.Lines.Add('Disconnecting client ' + IntToStr(I + 1) + ' of ' + IntToStr(Count));
            End;
        Finally
          Server.Contexts.UnlockList;
          Memo1.Lines.Add('Disconnected');
        End;
    End;
End;

Procedure TFK20Elevator.StayAliveTimer(Sender: TObject);
Begin //server reset timer if client stops sending heart beat
  StayAlive.Enabled := False;
  AStatus1.SimpleText := 'Client timed out!';
  If ASpeedBtn2.Enabled Then
    ASpeedBtn2Click(Self);
End;

Procedure TFK20Elevator.AuthTimerTimer(Sender: TObject);
Begin //login authorization timeout timer
  AuthTimer.Enabled := False;
  ASpeedBtn2Click(Self);
  Application.ProcessMessages;
  ASpeedBtn1Click(Self);
End;

【问题讨论】:

  • 也获得了很多 Pango-CRITICALs。尝试在 ServerDisconnect 中禁用 DropClient,但没有帮助。
  • 部分留言:
  • Pango-CRITICAL **:pango_layout_get_context:断言“布局!= NULL”失败 Pango-CRITICAL **:pango_context_get_language:断言“上下文!= NULL”失败 Pango-CRITICAL **:pango_context_get_metrics:断言' PANGO_IS_CONTEXT (context)' 失败 Pango-CRITICAL **: pango_font_metrics_get_approximate_char_width: 断言'metrics != NULL' 失败 Pango-CRITICAL **: pango_font_metrics_get_approximate_digit_width: 断言'metrics != NULL' 失败等等。
  • 客户端程序实际上在 Windows 10 下运行,使用 Delphi 7 编写,也使用 Indy 10.6。确认登录后,客户端每 3 秒发送一次简单的心跳代码,如果服务器未返回该代码,则重置其连接。
  • 另外,第二个条件,如果客户端已连接,但突然断开连接(我在调试模式下重置程序),服务器向我发出客户端超时的信号,但随后程序冻结。

标签: linux gtk indy tcpserver disconnect


【解决方案1】:
 Server.Bindings.Add.IPVersion := Id_IPv4;
 Server.Bindings.Add.IP := LIP;
 Server.Bindings.Add.Port := DefPort + StrToIntDef(UnitID, 0);

这是您的代码中的一个错误。您不只是打开 1 个监听套接字,而是实际上打开了 3 个监听套接字!对Bindings.Add() 的每次调用都会告诉TIdTCPServer 创建一个单独的监听套接字,并且每个绑定对象都有自己的IP/端口设置。

您对上述代码的真正作用是:

  1. 在端口 TIdTCPServer.DefaultPort 上的 IP 0.0.0.0 上创建 IPv4 绑定。

  2. 使用 Indy 的默认 IP 版本在端口 TIdTCPServer.DefaultPort 上创建另一个 IP LIP 绑定(恰好是 IPv4,除非您使用在 IdCompilerDefines.inc 中定义的 IdIPv6 重新编译 Indy)。

  3. 根据 Indy 的默认 IP 版本,在端口 DefPort+UnitID 上创建另一个 IP 0.0.0.0::1 绑定。

对于您正在尝试做的事情,您只需要致电Bindings.Add()一次,例如:

var
  Binding : TIdSocketHandle;

Binding := Server.Bindings.Add;
Binding.IPVersion := Id_IPv4;
Binding.IP := LIP;
Binding.Port := DefPort + StrToIntDef(UnitID, 0);

话虽如此,TIdTCPServer 是一个多线程组件。它的各种事件(OnConnectOnDisconnectOnExecuteOnExceptionOnListenException)在TIdTCPServer 内部创建的工作线程上下文中触发。您的事件处理程序从主 UI 线程的上下文之外直接访问 UI 控件。这会导致各种问题,绝对不能这样做。

如果您的事件处理程序需要访问您的 UI,它们必须与主 UI 线程同步,例如与 TThread.Synchronize()TThread.Queue(),或 Indy 自己的 TIdSyncTIdNotify 类,或任何其他交互您选择的线程同步机制。

此外,当您使用 DropClient() 手动删除客户端时,它会在没有业务的情况下执行操作。无论如何,您甚至都不需要手动删除客户端,因为 TIdTCPServer 在停用时会为您处理。

说了这么多,试试这样的:

interface

uses
  Classes, Form, SysUtils, StdCtrls, ExtCtrls, Buttons, IdTCPServer, IdContext;

type
  TFK20Elevator = class(TForm)
    Server: TIdTCPServer;
    ASpeedBtn1: TSpeedButton;
    ASpeedBtn2: TSpeedButton;
    Memo1: TMemo;
    AStatus1: TStatusBar;
    AuthTimer: TTimer;
    RetryTimer: TTimer;
    StayAlive: TTimer;
    TestTimer: TTimer;
    ...
    procedure ASpeedBtn1Click(Sender: TObject);
    procedure ASpeedBtn2Click(Sender: TObject);
    procedure StayAliveTimer(Sender: TObject);
    procedure AuthTimerTimer(Sender: TObject);
    procedure ServerConnect(AContext: TIdContext);
    procedure ServerDisconnect(AContext: TIdContext);
    procedure ServerException(AContext: TIdContext; AException: Exception);
    procedure ServerExecute(AContext: TIdContext);
    ...
  private
    DefPort: Integer;
    UnitID: string;
    Manager: Boolean;
    Authenticating: Boolean;
    EncDecSIdx: Integer;
    ...
    procedure DropClient;
    procedure ConnectedNotify(const APeerIP: string);
    procedure DisconnectedNotify;
    procedure ErrorNotify(const AMessage: string);
    procedure HeartNotify;
    procedure ManagerLoggedInNotify(const APeerIP: string);
    procedure GeneralUserLoggedInNotify(const APeerIP: string);
    procedure FailedAuthNotify;
    procedure RebootNotify(const Data: string);
    ...
  end;

var
  FK20Elevator: TFK20Elevator;

implementation

uses
  IdGlobal, IdSync;

const
  Heart: string = ...;
  AssignID: string = ...;
  ...

procedure TFK20Elevator.ASpeedBtn1Click(Sender: TObject);
var
  Binding: TIdSocketHandle;
begin
  //start the server

  Server.Active := False;
  Server.Bindings.Clear;
  Binding := Server.Bindings.Add;
  Binding.IPVersion := Id_IPv4;
  Binding.IP := LIP;
  Binding.Port := DefPort + StrToIntDef(UnitID, 0);
  Server.MaxConnections := 1;

  try
    Server.Active := True;
  except
    on E: Exception do
    begin
      Memo1.Lines.Add('Error activating server - ' + E.Message);
      Exit;
    end;
  end;

  AStatus1.SimpleText := 'Server bound to ' + Binding.IP + ':' + IntToStr(Binding.Port);

  ASpeedBtn1.Enabled := False;
  ASpeedBtn2.Enabled := True;
end;

procedure TFK20Elevator.ASpeedBtn2Click(Sender: TObject);
begin
  //shut down the server with optional restart if not rebooting

  AuthTimer.Enabled := False;
  RetryTimer.Enabled := False;
  StayAlive.Enabled := False;
  TestTimer.Enabled := False;

  try
    Server.Active := False;
  except
    on E: Exception do
    begin
      Memo1.Lines.Add('Error deactivating server - ' + E.Message);
      Exit;
    end;
  end;

  Manager := False;
  Authenticating := False;

  AStatus1.SimpleText := 'Server not running...';
  ASpeedBtn1.Enabled := True;
  ASpeedBtn2.Enabled := False;

  RetryTimer.Enabled := not SysReboot;
end;

procedure TFK20Elevator.StayAliveTimer(Sender: TObject);
begin
  //client stopped sending heart beats
  StayAlive.Enabled := False;
  Memo1.Lines.Add('Client timed out!');
  DropClient;
end;

procedure TFK20Elevator.AuthTimerTimer(Sender: TObject);
begin
  //login authorization timeout
  AuthTimer.Enabled := False;
  Memo1.Lines.Add('Authentication timed out!');
  DropClient;
end;

procedure TFK20Elevator.DropClient;
begin
  with Server.Contexts.LockList do
  try
    if Count > 0 then
      TIdContext(Items[0]).Connection.Disconnect;        
  finally
    Server.Contexts.UnlockList;
  end;
end;

type
  TMyNotifyMethod = procedure(const AStr: string) of object;

  TMyNotify = class(TIdNotify)
  protected
    FMethod: TMyNotifyMethod;
    FStr: string;
    procedure DoNotify; override;
  public
    class procedure NotifyStr(AMethod: TMyNotifyMethod; const AStr: string);
  end;

procedure TMyNotify.DoNotify;
begin
  FMethod(FStr);
end;

class procedure TMyNotify.NotifyStr(AMethod: TMyNotifyMethod; const AStr: string);
begin
  with Create do
  begin
    FMethod := AMethod;
    FStr := AStr;
    Notify;
  end;
end;

procedure TFK20Elevator.ConnectedNotify(const APeerIP: string);
begin
  if not Server.Active then Exit;
  AStatus1.SimpleText := 'Connecting to> ' + APeerIP + ' - Authenticating...';
  Memo1.Lines.Clear;
  RetryTimer.Enabled := False;
  RetryTimer.Interval := 3000;
  AuthTimer.Enabled := True;
  StayAlive.Enabled := True;
end;

procedure TFK20Elevator.DisconnectedNotify;
begin
  StayAlive.Enabled := False;
  RetryTimer.Enabled := False;

  if Server.Active then
  begin
    with Server.Bindings[0] do
      AStatus1.SimpleText := 'Client Disconnected. Server bound to ' + IP + ':' + IntToStr(Port);
  end;

  RetryTimer.Enabled := Not SysReboot;
end;

procedure TFK20Elevator.ErrorNotify(const AMessage: string);
begin
  Memo1.Lines.Add('An exception happened! - ' + AMessage);
  RetryTimer.Enabled := True;
end;

procedure TFK20Elevator.HeartNotify;
begin
  StayAlive.Enabled := False;
  StayAlive.Enabled := True;
end;

procedure TFK20Elevator.ManagerLoggedInNotify(const APeerIP: string);
begin
  AuthTimer.Enabled := False;
  AStatus1.SimpleText := 'Connecting to> ' + APeerIP + ' as Manager';
end;

procedure TFK20Elevator.GeneralUserLoggedInNotify(const APeerIP: string);
begin
  AuthTimer.Enabled := False;
  AStatus1.SimpleText := 'Connecting to> ' + APeerIP + ' as General User';
end;

procedure TFK20Elevator.FailedAuthNotify;
begin
  //login authorization failed
  AuthTimer.Enabled := False;
end;

procedure TFK20Elevator.RebootNotify(const Data: string);
var
  Tmp, S, NewLoc, NewUnit, LIP: string;
begin
  Tmp := Data;

  S := Fetch(Tmp, #10);
  NewLoc := Fetch(Tmp, #10);
  NewUnit := Tmp;

  Label15.Caption := NewLoc;
  Label16.Caption := NewUnit;

  FmtStr(LIP, '%.3d', [StrToInt(NewUnit)]);
  LIP := '192.168.6' + Copy(LIP, 1, 1) + '.' + Copy(LIP, 2, 2); //wifi ip

  Memo1.Lines.Add('--> ' + S + '-' + LIP);
  Memo1.Lines.Add('<-- ' + Rebooting);

  TestTimer.Enabled := True;
end;

procedure TFK20Elevator.ServerConnect(AContext: TIdContext);
begin
  Manager := False;
  Authenticating := True;
  EncDecSIdx := 1;

  TMyNotify.NotifyStr(@ConnectedNotify, AContext.Binding.PeerIP);

  // Note: OSDefault is platform-specific. On Linux, it is UTF-8, so
  // you should use UTF-8 explicitly instead, so as to provide
  // better compatibility across platforms, especially if you ever
  // move this server code to another platform in the future...
  //
  AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_OSDefault; // IndyTextEncoding_UTF8
  AContext.Connection.IOHandler.DefAnsiEncoding := IndyTextEncoding_OSDefault; // IndyTextEncoding_UTF8
end;

procedure TFK20Elevator.ServerDisconnect(AContext: TIdContext);
begin
  Manager := False;
  Authenticating := False;
  TIdNotify.NotifyMethod(@DisconnectedNotify);
end;

procedure TFK20Elevator.ServerException(AContext: TIdContext; AException: Exception);
begin
  if not (AException is EIdConnClosedGracefully) then
    TMyNotify.NotifyStr(@ErrorNotify, AException.Message);
end;

procedure TFK20Elevator.ServerExecute(AContext: TIdContext);
var
  S, Tmp, UserName, Password: String;
begin
  S := AContext.Connection.IOHandler.ReadLn; //get the data
  if S = Heart then
  begin
    //just a heart beat, return to client and reset timer
    AContext.Connection.IOHandler.WriteLn(Heart);
    TIdNotify.NotifyMethod(@HeartNotify);
    Exit;
  end;

  //not heart beat, decompress
  //EncStr and DecStr simply encode/decode, respectively, a standard
  //  string into/from a key encrypted hex string, i.e. '00' to 'FF'
  //  for each character in the string

  S := PCommon.DecStr(S, EncDecStr, EncDecSIdx);

  if Authenticating then
  begin
    //test log in
    UserName := Fetch(S, '|');
    Password := S;

    if (UserName = ManUser) and (Password = ManPass) then
    begin
      Authenticating := False;
      Manager := True;
      AContext.Connection.IOHandler.WriteLn(EncStr(AContext.Binding.PeerIP + ':' + IntToStr(AContext.Binding.Port) + 'M', EncDecStr, EncDecSIdx));
      TMyNotify.NotifyStr(@ManagerLoggedInNotify, AContext.Binding.PeerIP);
    end
    else if (UserName = GenUser) and (Password = GenPass) then
    begin
      Authenticating := False;
      AContext.Connection.IOHandler.WriteLn(EncStr(AContext.Binding.PeerIP + ':' + IntToStr(AContext.Binding.Port) + 'U', EncDecStr, EncDecSIdx));
      TMyNotify.NotifyStr(@GeneralUserLoggedInNotify, AContext.Binding.PeerIP);
    end else
    begin
      TIdNotify.NotifyMethod(@FailedAuthNotify);
      AContext.Connection.Disconnect;
    end;
    Exit;
  end;

  //test for commands
  if TextStartsWith(S, AssignID) then
  begin
    //command to assign a new unit id

    Tmp := S;
    Fetch(Tmp, '-');
    NewLoc := Fetch(Tmp, '-');
    NewUnit := Tmp;

    if (NewLoc = '') or (NewUnit = '') then
    begin
      NewLoc := DefLocation;
      NewUnit := DefUnitNum;
    end;

    AContext.Connection.IOHandler.WriteLn(PCommon.EncStr(Rebooting, EncDecStr, EncDecSIdx));

    TMyNotify.NotifyStr(@RebootNotify, S + #10 + NewLoc + #10 + NewUnit);
  end;
end;

最后,我建议您考虑从主 UI 线程中删除所有全局变量和心跳/身份验证计时器。在OnExecute 事件本身内部进行超时处理,例如使用客户端的ReadTimeout 属性和/或CheckForDataOnSource() 方法。使用TIdContext.Data 属性(或从TIdServerContext 派生一个新类并将其分配给TIdTCPServer.ContextClass 属性)来跟踪每个连接的值,例如上次接收到心跳的时间,或者客户端是否仍然身份验证(实际上,您应该在OnExecute甚至开始运行之前处理OnConnect中的身份验证),或者客户端是否以管理员身份登录等。这将减少需要与主同步的事情的数量UI 线程,并避免同步处理延迟引入的任何时间问题。

【讨论】:

  • 太好了,谢谢 Remy,我明天会第一时间实施并通知您。
  • 我修改了代码,学到了很多东西,但是我不熟悉使用通知类这样的类,也不知道如何为编译器编写它们,所以当我编译时,它在每个通知过程中告诉我“错误:预期的类标识符”。
  • @user7475089 你有一个TFK20Elevator 类。您是否在单元的interface 中为该类的声明添加了其他方法?
  • 我使用后在接口下添加了这个:Type TMyNotifyMethod = Procedure(Const AStr: String) Of Object; TMyNotify = Class(TIdNotify) 受保护的 FMethod: TMyNotifyMethod; FStr:字符串;程序DoNotify;覆盖;公共类过程 NotifyStr(AMethod: TMyNotifyMethod; Const AStr: String);结尾; { TFK20Elevator } TFK20Elevator = Class(TForm)
  • 然后这个私有下:Procedure ConnectedNotify(Const APeerIP: String);程序断开通知;过程 ErrorNotify(Const AMessage: String);程序心脏通知;过程 ManagerLoggedInNotify(Const APeerIP: String);过程 GeneralUserLoggedInNotify(Const APeerIP: String);程序FailedAuthNotify;过程重启通知(常量数据:字符串);
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-04-21
  • 1970-01-01
  • 2023-03-30
  • 2022-01-24
  • 2013-06-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多