Client/Server 申请

Client/Server application

我正在编写客户端/服务器应用程序。有一台服务器和几个客户端。

连接客户端时,任务是将其IP地址添加到ListBox中,断开客户端时,将其从ListBox中移除。然后在客户端和服务器之间交换消息。

出现三个问题:当客户端连接时,它的IP地址被添加到ListBox中,但是当断开连接时,它并没有从那里删除,这里是代码:

type
  TSimpleClient = class(TObject)
    DNS,
    Name        : String;
    ListLink    : Integer;
    Thread      : Pointer;
  end;

procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
var
  Client: TSimpleClient;
begin
  Client := TSimpleClient.Create;
  Client.DNS := AContext.Connection.Socket.Binding.PeerIP;
  Client.ListLink := ListBox1.Items.Count;
  Client.Thread := AContext;
  ListBox1.Items.Add(Client.DNS);
  AContext.Data := Client;
  Clients.Add(Client);
end;

procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
var
  Client : TSimpleClient;
begin
  sleep(2000);
  Client :=Pointer (AContext.Data);
  Clients.Delete(Client.ListLink);
  ListBox1.Items.Delete(ListBox1.Items.IndexOf(Client.DNS));
  Client.Free;
  AContext.Data := nil;
end;

第二个问题,在交换消息时,西里尔字母给出的是“???”,全部Google都通过了,没发现错误。

还有第三个问题,客户端上有一个定时器,监听服务端的消息,当定时器开启时​​,客户端应用程序挂得很紧,把这些都放到流中也是一样的麻烦,代码:

if not IdTCPClient1.Connected then
  Exit;
s := IdTCPClient1.Socket.ReadLn;
if s <> '' then
  Label1.Text := s;

我发现你的代码有很多问题。

在服务器端,您需要去掉 TSimpleClient.ListLink 字段。您滥用了它,导致您的代码出现不良行为,因为您没有像客户端 added/removed 那样对其进行更新。想一想当您连接了 2 个客户端时会发生什么,其中 ListLink 分别为 0 和 1,然后第一个客户端断开连接。第二个客户端的 ListLink 将变得无效,因为您没有将它从 1 递减到 0。

此外 TIdTCPServer 是一个多线程组件,它的事件是在工作线程的上下文中触发的,但是您的事件处理程序代码不是线程安全的。从工作线程访问 UI 控件时,您必须与主 UI 线程同步,并且必须保护您的 Clients 列表免受跨线程边界的并发访问。在这种情况下,您真的不需要自己的 Clients 列表作为开头,因为 TIdTCPServer 有自己的线程安全 Contexts 列表,您可以使用它来访问连接的客户端。

您也根本没有处理 Unicode。默认情况下,Indy 对 Unicode 字符串的默认字节编码是 US-ASCII,这就是为什么非 ASCII 字符得到 ? 的原因。您可以使用 IOHandler 的 DefStringEncoding 属性 设置不同的字节编码,例如 IndyTextEncoding_UTF8 (如果您使用的是 Delphi 2007 或更早版本,您可能还需要使用IOHandler 的 DefAnsiEncoding 属性 指定你的 ANSI 字符串如何转换 to/from Unicode。默认设置为 IndyTextEncoding_OSDefault).

试试像这样的东西:

type
  TSimpleClient = class(TObject)
    DNS,
    Name            : String;
    Thread          : Pointer;
    OutgoingMsgs    : TIdThreadSafeStringList;
    HasOutgoingMsgs : Boolean;

    constructor Create;
    destructor Destroy; override;

    procedure Queue(const Msg: string);
    procedure FlushMsgs;
  end;

constructor TSimpleClient.Create;
begin
  inherited;
  OutgoingMsgs := TIdThreadSafeStringList.Create;
end;

destructor TSimpleClient.Destroy;
begin
  OutgoingMsgs.Free;
  inherited;
end;

procedure TSimpleClient.Queue(const Msg: string);
var
  List: TStringList;
begin
  List := OutgoingMsgs.Lock;
  try
    List.Add(Msg);
    HasOutgoingMsgs := True;
  finally
    OutgoingMsgs.Unlock;
  end;
end;

procedure TSimpleClient.FlushMsgs;
var
  List: TStringList;
begin
  List := OutgoingMsgs.Lock;
  try
    while List.Count > 0 do
    begin
      TIdContext(Thread).Connection.IOHandler.WriteLn(List[0]);
      List.Delete(0);
    end;
    HasOutgoingMsgs := False;
  finally
    OutgoingMsgs.Unlock;
  end;
end;

procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
var
  PeerIP: string;
  Client: TSimpleClient;
begin
  PeerIP := AContext.Binding.PeerIP;

  Client := TSimpleClient.Create;
  Client.DNS := PeerIP;
  Client.Thread := AContext;
  AContext.Data := Client;

  TThread.Queue(nil,
    procedure
    begin
      ListBox1.Items.AddObject(PeerIP, Client);
    end
  );

  AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
end;

procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
var
  Client : TSimpleClient;
begin
  Client := TSimpleClient(AContext.Data);
  try
    TThread.Queue(nil,
      procedure
      var
        Index: Integer;
      begin
        Index := ListBox1.Items.IndexOfObject(Client);
        if Index <> -1 then
          ListBox1.Items.Delete(Index);
      end;
    );
  finally
    { The anonymous procedure being passed to TThread.Queue() above captures
      the Client variable itself, not its value.  On ARC platforms, we need to
      prevent Free() setting the variable to nil before it can be passed to
      IndexOfObject(), and also because IndexOfObject() expects a live object
      anyway. ARC will free the object when the anonymous procedure exits. On
      non-ARC platforms, it is OK to Free() the object here, the variable will
      not change value, and IndexOfObject() does not need a live object... }
    {$IFNDEF AUTOREFCOUNT}
    Client.Free;
    {$ENDIF}
    AContext.Data := nil;
  end;
end;

procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
  Client: TSimpleClient;
begin
  Client := TSimpleClient(AContext.Data);

  if Client.HasOutgoingMsgs then
    Client.FlushMsgs
  else
    Sleep(100);
end;

procedure TForm1.SendMessageToClient(Client: TSimpleClient; const Msg: string);
var
  List: TIdContextList;
begin
  List := IdTCPServer1.Contexts.LockList;
  try
    if List.IndexOf(TIdContext(Client.Thread)) <> -1 then // still connected?
      Client.Queue(Msg);
  finally
    IdTCPServer1.Contexts.UnlockList;
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Index: Integer;
  Msg: string;
  Client: TSimpleClient;
begin
  Index := ListBox1.ItemIndex;
  if Index = -1 then Exit;

  Msg := Edit1.Text;
  if Msg = '' then Exit;

  Client := TSimpleClient(ListBox1.Items.Objects[Index]);
  SendMessageToClient(Client, Msg);
end;

或者,您可以从 TIdServerContext 派生 TSimpleClient 并完全删除 Thread 字段:

type
  TSimpleClient = class(TIdServerContext)
    DNS,
    Name            : String;
    OutgoingMsgs    : TIdThreadSafeStringList;
    HasOutgoingMsgs : Boolean;

    constructor Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TIdContextThreadList = nil); override;
    destructor Destroy; override;

    procedure Queue(const Msg: string);
    procedure FlushMsgs;
  end;

constructor TSimpleClient.Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TIdContextThreadList = nil);
begin
  inherited Create(AConnection, AYarn, AList);
  OutgoingMsgs := TIdThreadSafeStringList.Create;
end;

destructor TSimpleClient.Destroy;
begin
  OutgoingMsgs.Free;
  inherited;
end;

procedure TSimpleClient.Queue(const Msg: string);
var
  List: TStringList;
begin
  List := OutgoingMsgs.Lock;
  try
    List.Add(Msg);
    HasOutgoingMsgs := True;
  finally
    OutgoingMsgs.Unlock;
  end;
end;

procedure TSimpleClient.FlushMsgs;
var
  List: TStringList;
begin
  List := OutgoingMsgs.Lock;
  try
    while List.Count > 0 do
    begin
      Self.Connection.IOHandler.WriteLn(List[0]);
      List.Delete(0);
    end;
    HasOutgoingMsgs := False;
  finally
    OutgoingMsgs.Unlock;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  IdTCPServer1.ContextClass := TSimpleClient;
end;

procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
var
  PeerIP: string;
  Client: TSimpleClient;
begin
  PeerIP := AContext.Binding.PeerIP;

  Client := TSimpleClient(AContext);
  Client.DNS := PeerIP;

  TThread.Queue(nil,
    procedure
    begin
      ListBox1.Items.AddObject(PeerIP, Client);
    end
  );

  AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
end;

procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
var
  Client : TSimpleClient;
begin
  Client := TSimpleClient(AContext);

  TThread.Queue(nil,
    procedure
    var
      Index: Integer;
    begin
      Index := ListBox1.Items.IndexOfObject(Client);
      if Index <> -1 then
        ListBox1.Items.Delete(Index);
    end;
  );
end;

procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
  Client: TSimpleClient;
begin
  Client := TSimpleClient(AContext);

  if Client.HasOutgoingMsgs then
    Client.FlushMsgs
  else
    Sleep(100);
end;

procedure TForm1.SendMessageToClient(Client: TSimpleClient; const Msg: string);
var
  List: TIdContextList;
begin
  List := IdTCPServer1.Contexts.LockList;
  try
    if List.IndexOf(TIdContext(Client)) <> -1 then // still connected?
      Client.Queue(Msg);
  finally
    IdTCPServer1.Contexts.UnlockList;
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Index: Integer;
  Msg: string;
  Client: TSimpleClient;
begin
  Index := ListBox1.ItemIndex;
  if Index = -1 then Exit;

  Msg := Edit1.Text;
  if Msg = '' then Exit;

  Client := TSimpleClient(ListBox1.Items.Objects[Index]);
  SendMessageToClient(Client, Msg);
end;

在客户端,您正在从主UI线程中的套接字读取,但Indy使用阻塞套接字,因此它的读取方法将阻塞调用线程,直到请求的数据到达。不要阻塞主线程 UI!只读如果确实有可读的东西,或者将阅读移动到一个单独的工作线程中。例如:

IdTCPClient1.Connect;
IdTCPClient1.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
...
IdTCPClient1.Disconnect;

...

procedure TForm1.Timer1Timer(Sender: TObject);
var
  s: string;
begin
  if IdTCPClient1.Connected and (not IdTCPClient1.IOHandler.InputBufferIsEmpty) then
  begin
    s := IdTCPClient1.IOHandler.ReadLn;
    if s <> '' then
      Label1.Text := s;
  end;
end;

或者:

type
  TReadingThread = class(TThread)
  protected
    procedure Execute; override;
  end;

procedure TReadingThread.Execute;
var
  s: String;
begin
  while not Terminated do
  begin
    s := Form1.IdTCPClient1.IOHandler.ReadLn;
    if s <> '' then
    begin
      TThread.Queue(nil,
        procedure
        begin
          Form1.Label1.Text := s;
        end
      );
    end;
  end;
end;

...

var
  ReadingThread: TReadingThread = nil;

...

IdTCPClient1.Connect;
IdTCPClient1.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
ReadingThread := TReadingThread.Create(False);
...
ReadingThread.Terminate;
try
  IdTCPClient1.Disconnect;
finally
  ReadingThread.WaitFor;
  ReadingThread.Free;
end;

非常感谢 Remy,您的回答确实帮助我解决了我的问题。我针对 Windows 和 Android 平台。我稍微修改了你的代码,它对我有用:

type
  TSimpleClient = class(TObject)
    DNS,
    Name            : String;
    Thread          : Pointer;
    OutgoingMsgs    : TIdThreadSafeStringList;
    HasOutgoingMsgs : Boolean;

    constructor Create;
    destructor Destroy; override;

    procedure Queue(const Msg: string);
    procedure FlushMsgs;
  end;

constructor TSimpleClient.Create;
begin
  inherited;
  OutgoingMsgs := TIdThreadSafeStringList.Create;
end;

destructor TSimpleClient.Destroy;
begin
  OutgoingMsgs.Free;
  inherited;
end;

procedure TSimpleClient.Queue(const Msg: string);
var
  List: TStringList;
  Client: TSimpleClient;
begin
  List := OutgoingMsgs.Lock;
  try
    List.Add(Msg);
    HasOutgoingMsgs := True;
    Client.FlushMsgs;
  finally
    OutgoingMsgs.Unlock;
  end;
end;

procedure TSimpleClient.FlushMsgs;
var
  List: TStringList;
begin
  List := OutgoingMsgs.Lock;
  try
    while List.Count > 0 do
    begin
      TIdContext(Thread).Connection.IOHandler.WriteLn(List[0]);
      List.Delete(0);
    end;
    HasOutgoingMsgs := False;
  finally
    OutgoingMsgs.Unlock;
  end;
end;

procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
var
  PeerIP: string;
  Client: TSimpleClient;
begin
  PeerIP := AContext.Binding.PeerIP;

  Client := TSimpleClient.Create;
  Client.DNS := PeerIP;
  Client.Thread := AContext;
  AContext.Data := Client;

  TThread.Queue(nil,
    procedure
    begin
      ListBox1.Items.AddObject(PeerIP, Client);
    end
  );

  AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
end;

procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
var
  Client : TSimpleClient;
begin
  Client := TSimpleClient(AContext.Data);
  try
    TThread.Queue(nil,
      procedure
      var
        Index: Integer;
      begin
        Index := ListBox1.Items.IndexOfObject(Client);
        if Index <> -1 then
          ListBox1.Items.Delete(Index);
      end;
    );
  finally
    { The anonymous procedure being passed to TThread.Queue() above captures
      the Client variable itself, not its value.  On ARC platforms, we need to
      prevent Free() setting the variable to nil before it can be passed to
      IndexOfObject(), and also because IndexOfObject() expects a live object
      anyway. ARC will free the object when the anonymous procedure exits. On
      non-ARC platforms, it is OK to Free() the object here, the variable will
      not change value, and IndexOfObject() does not need a live object... }
    {$IFNDEF AUTOREFCOUNT}
    Client.Free;
    {$ENDIF}
    AContext.Data := nil;
  end;
end;

procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
  Client: TSimpleClient;
begin
  Client := TSimpleClient(AContext.Data);

  if Client.HasOutgoingMsgs then
    Client.FlushMsgs
  else
    Sleep(100);
end;

procedure TForm1.SendMessageToClient(Client: TSimpleClient; const Msg: string);
var
  List: TIdContextList;
begin
  List := IdTCPServer1.Contexts.LockList;
  try
    if List.IndexOf(TIdContext(Client.Thread)) <> -1 then // still connected?
      Client.Queue(Msg);
  finally
    IdTCPServer1.Contexts.UnlockList;
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Index: Integer;
  Msg: string;
  Client: TSimpleClient;
begin
  Index := ListBox1.ItemIndex;
  if Index = -1 then Exit;

  Msg := Edit1.Text;
  if Msg = '' then Exit;

  Client := TSimpleClient(ListBox1.Items.Objects[Index]);
  SendMessageToClient(Client, Msg);
end;

我从 TSimpleClient.Queue 过程中添加了对 FlushMsgs 方法的调用,消息开始发送,每次客户端连接和断开连接时都会更新客户端列表,并且服务器停止挂起。再次感谢雷米,你帮了我很多,加快了开发速度,金人。