【问题标题】:Delphi TCPClient read string from TCPServerDelphi TCPClient 从 TCPServer 读取字符串
【发布时间】:2018-08-07 21:26:15
【问题描述】:

我需要编写一个简单的聊天程序,供一些客户使用。基本上,有很多客户端连接到服务器并一起聊天。服务器工作:

如果需要,这里的代码:

//CONNECT TO THE SERVER
procedure TFormServer.ButtonStartClick(Sender: TObject);
begin
  if not TCPServer.Active then
    begin
      try
        TCPServer.DefaultPort := 8002;
        TCPServer.Bindings[0].IP := LIP.Text;
        TCPServer.Bindings[0].Port := StrToInt(LPort.Text);
        TCPServer.MaxConnections := 5;
        TCPServer.Active := true;

        Memo1.Lines.Add(TimeNow + 'Server started.');
      except
        on E: Exception do
          Memo1.Lines.Add(sLineBreak + ' ====== INTERNAL ERROR ====== ' +
            sLineBreak + ' > ' + E.Message + sLineBreak);
      end;
    end;
end;

//DISCONNECT
procedure TFormServer.ButtonStopClick(Sender: TObject);
begin
  if TCPServer.Active then
    begin
      TCPServer.Active := false;
      Memo1.Lines.Add(TimeNow + 'Server stopped.');
    end;
end;

//IF CLOSE THE APP DONT FORGET TO CLOSE SERVER!!
procedure TFormServer.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  ButtonStopClick(Self);
end;

procedure TFormServer.FormCreate(Sender: TObject);
begin
  FClients := 0;
end;

//When a client connects I write a log
procedure TFormServer.TCPServerConnect(AContext: TIdContext);
begin
  Inc(FClients);
  TThread.Synchronize(nil, procedure
                           begin
                             LabelCount.Text := 'Connected sockets: ' + FClients.ToString;
                             Memo1.Lines.Add(TimeNow + ' Client connected @ ' + AContext.Binding.IP + ':' + AContext.Binding.Port.ToString);
                           end);
end;

//Same, when a client disconnects I log it
procedure TFormServer.TCPServerDisconnect(AContext: TIdContext);
begin
  Dec(FClients);
  TThread.Synchronize(nil, procedure
                           begin
                             LabelCount.Text := 'Connected sockets: ' + FClients.ToString;
                             Memo1.Lines.Add(TimeNow + ' Client disconnected');
                           end);
end;

//WHAT I DO HERE:
//I receive a message from the client and then I send this message to EVERYONE that is connected here. It is a global chat
procedure TFormServer.TCPServerExecute(AContext: TIdContext);
var
  txt: string;
begin
  txt := AContext.Connection.IOHandler.ReadLn();
  AContext.Connection.IOHandler.WriteLn(txt);
  TThread.Synchronize(nil, procedure
                           begin
                             Memo1.Lines.Add(TimeNow + txt);
                           end);
end;

服务器代码非常简单且最少,但它可以满足我的需要。这是客户端:

这里有代码,很简单:

//CONNECT TO THE SERVER
procedure TFormClient.ConnectClick(Sender: TObject);
begin

  if Length(Username.Text) < 4 then
    begin
      Memo1.Lines.Clear;
      Memo1.Lines.Add('ERROR: Username must contain at least 4 characters');
      Exit;
    end;

  if not TCPClient.Connected then
    begin
      try
        Username.Enabled := false;
        Memo1.Lines.Clear;

        TCPClient.Host := '127.0.0.1';
        TCPClient.Port := 8002;
        TCPClient.ConnectTimeout := 5000;
        TCPClient.Connect;

        Connect.Text := 'Disconnect';
      except
        on E: Exception do
          Memo1.Lines.Add(' ====== ERROR ======' + sLineBreak +
            ' > ' + E.Message + sLineBreak);
      end;
    end
  else
    begin
      TCPClient.Disconnect;
      Username.Enabled := true;
      Connect.Text := 'Connect';
    end;
end;

//IF YOU FORGET TO DISCONNECT WHEN APP IS CLOSED
procedure TFormClient.FormDestroy(Sender: TObject);
begin
  if TCPClient.Connected then
    TCPClient.Disconnect;
end;

//Here I send a string to the server and it's good
procedure TFormClient.SendClick(Sender: TObject);
begin
  if TCPClient.Connected then
    begin
      TCPClient.IOHandler.WriteLn(Username.Text + ': ' + EditMessage.Text);
      EditMessage.Text := '';
    end
  else
    begin
      Memo1.Lines.Add('ERROR: You aren''t connected!');
    end;
end;

//Problems here
procedure TFormClient.Timer1Timer(Sender: TObject);
begin
  Memo1.Lines.Add(TCPClient.IOHandler.ReadLn());
end;

问题始于最后一个过程Timer1Timer。我发现 TCPServer 使用线程,这就是为什么我调用 Synchronize 来更新 UI。相反,TCPClient 不使用线程,我必须手动检查服务器。请看这段代码:

    procedure TFormServer.TCPServerExecute(AContext: TIdContext);
    var
      txt: string;
    begin
      txt := AContext.Connection.IOHandler.ReadLn();
      AContext.Connection.IOHandler.WriteLn(txt);
      TThread.Synchronize(nil, procedure
                               begin
                                 Memo1.Lines.Add(TimeNow + txt);
                               end);
    end;

如您所见,当服务器收到一个字符串时,他立即将其发送回所有客户端。我尝试在这里获取字符串:

procedure TFormClient.Timer1Timer(Sender: TObject);
begin
  Memo1.Lines.Add(TCPClient.IOHandler.ReadLn());
end;

怎么了?我在这里看到了一个类似的问题,答案说我必须使用计时器和IOHandler.ReadLn(),这就是我正在做的事情。我认为问题就在这里。如何解决?

还有timer的间隔是200,是不是太短了?


我已阅读 Remy Lebeau 在答案中所说的内容,并生成了以下简单代码:

procedure TFormClient.Timer1Timer(Sender: TObject);
begin
  if not(TCPClient.Connected) then
    Exit;
  if TCPClient.IOHandler.InputBufferIsEmpty then
    Exit;
  Memo1.Lines.Add(TCPClient.IOHandler.InputBufferAsString());
end;

表单中有一个Timer1 组件。这按我的预期工作,但它仍然可以锁定 UI 吗?

【问题讨论】:

    标签: delphi


    【解决方案1】:

    服务器工作

    仅供参考,您根本不需要FClients 变量,尤其是因为您并没有真正安全地访问它。至少,使用TInterlocked 安全地访问它。或切换到TIdThreadSafeInteger。虽然说真的,你唯一使用它的地方是LabelCount,你可以从TIdTCPServers.Contexts 属性中获取当前的客户端数量。

    这是客户端:

    ...

    问题从最后一个过程Timer1Timer开始。

    那是因为您使用的是基于 UI 的 TTimer,并且(就像 Indy 中的大多数东西一样)IOHandler.ReadLn() 方法会阻塞直到完成。您在 UI 线程的上下文中调用它,因此它会阻塞 UI 消息循环,直到从套接字到达整行。

    解决阻塞 UI 的一种方法是将 Indy TIdAntiFreeze 组件放置到您的表单上。然后 UI 将在 ReadLn() 阻塞时保持响应。但是,使用TTimer 会有点危险,因为您最终会遇到OnTimer 重入问题,这可能会破坏IOHandler 的数据。

    确实,最好的解决方案是根本不在 UI 线程中调用 IOHandler.ReadLn()。而是在工作线程中调用它。成功Connect()到服务器后启动线程,断开连接时终止线程。甚至将Connect() 本身移动到线程中。无论哪种方式,您都可以使用 Indy 的 TIdThreadComponent,或者编写自己的 T(Id)Thread 派生类。

    相反,TCPClient 不使用线程,我必须手动检查服务器。

    正确,但你这样做的方式是错误的。

    如果您不想使用工作线程(您应该这样做),那么至少更改您的 OnTimer 事件处理程序,使其不再阻塞 UI,如下所示:

    或者:

    procedure TFormClient.Timer1Timer(Sender: TObject);
    begin
      if TCPClient.IOHandler.InputBufferIsEmpty then
      begin
        TCPClient.IOHandler.CheckForDataOnSource(0);
        TCPClient.IOHandler.CheckForDisconnect(False);
        if TCPClient.IOHandler.InputBufferIsEmpty then Exit;
      end;
      // may still block if the EOL hasn't been received yet...
      Memo1.Lines.Add(TCPClient.IOHandler.ReadLn);
    end;
    

    或者:

    procedure TFormClient.Timer1Timer(Sender: TObject);
    begin
      // Connected() performs a read operation and will buffer
      // any pending bytes that happen to be received...
      if not TCPClient.Connected then Exit;
      while TCPClient.IOHandler.InputBuffer.IndexOf(Byte($0A)) <> -1 do
        Memo1.Lines.Add(TCPClient.IOHandler.ReadLn());
    end;
    

    我在这里看到了一个类似的问题,答案说我必须使用计时器和IOHandler.ReadLn(),这就是我正在做的。

    谁说错了,或者你误解了要求。如果使用得当,在 UI 中使用计时器是一种可能的解决方案,但它不是一个很好的解决方案。

    【讨论】:

    • 非常感谢,我理解我的错误。我已经用我创建的一些有效的代码编辑了主要问题。根据您的问题,我应该编写一个 TThread 子类并在那里运行计时器吗?
    • @RaffaeleRossi 如果您使用线程,则根本不需要计时器,只需在连接的生命周期内循环调用ReadLn(就像您的服务器一样)。此外,您编写的新代码将不再阻塞 UI,但也不能保证每次都能获得完整的行。至少,如果你要使用InputBufferAsString,那么我不建议使用Memo1.Lines.Add(),我建议使用Memo1.SelText,例如:Memo1.SelStart := Memo1.GetTextLen; Memo1.SelLength := 0; Memo1.SelText := TCPClient.IOHandler.InputBufferAsString;
    • @RaffaeleRossi 注意,如果缓冲区位于编码的代码单元边界之间,这将可能破坏非 ASCII 文本。我会坚持在线程的循环中调用ReadLn,这是一个更好的选择。 Socket I/O 根本不属于你的 UI 线程。
    【解决方案2】:

    为您的 tcpclient 创建一个线程并将消息同步回 UI。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-01-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-05
      • 1970-01-01
      • 2019-08-26
      • 2016-11-04
      相关资源
      最近更新 更多