Delphi TCP客户端从TCPServer读取字符串

Delphi TCP客户端从TCPServer读取字符串,delphi,Delphi,我需要写一个简单的聊天程序,将由一些客户使用。基本上,有很多客户端连接到服务器,它们一起聊天。服务器工作: 如果需要,请在此输入代码: //CONNECT TO THE SERVER procedure TFormServer.ButtonStartClick(Sender: TObject); begin if not TCPServer.Active then begin try TCPServer.DefaultPort := 8002;

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

如果需要,请在此输入代码:

//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;
如您所见,当服务器收到字符串时,他会立即将其发送回所有客户端。我尝试在此处获取字符串:

procedure TFormClient.Timer1Timer(Sender: TObject);
begin
  Memo1.Lines.Add(TCPClient.IOHandler.ReadLn());
end;
怎么了?我在这里看到了一个类似的问题,答案是我必须使用计时器和
IOHandler.ReadLn()
,这就是我正在做的。我认为问题就在这里。如何修复

计时器的间隔也是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或不锁定?

为您的TCP客户端创建一个线程并将消息同步回UI。

为您的TCP客户端创建一个线程并将消息同步回UI

服务器工作正常

仅供参考,您根本不需要
FClients
变量,尤其是因为您没有真正安全地访问它。至少,使用
TInterlocked
安全访问它。或者切换到
tidthreadsafeinger
。尽管确实如此,您使用它的唯一位置是在
LabelCount
中,您可以从
TIdTCPServers.Contexts
属性获取当前客户端计数

这是客户端:

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

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

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

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

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

没错,但你的做法是错误的

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

或:

或者:

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;
过程TFormClient.Timer1Timer(发送方:TObject);
开始
//Connected()执行读取操作并将缓存
//碰巧收到的任何挂起字节。。。
如果TCP客户端未连接,则退出;
而TCPClient.IOHandler.InputBuffer.IndexOf(字节($0A))-1执行
Memo1.Lines.Add(TCPClient.IOHandler.ReadLn());
终止
我在这里看到了一个类似的问题,答案是我必须使用计时器和
IOHandler.ReadLn()
,这就是我正在做的

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

服务器工作正常

仅供参考,您根本不需要
FClients
变量,尤其是因为您没有真正安全地访问它。至少,使用
TInterlocked
安全访问它。或者切换到
tidthreadsafeinger
。尽管确实如此,您使用它的唯一位置是在
LabelCount
中,您可以从
TIdTCPServers.Contexts
属性获取当前客户端计数

这是客户端:

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

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

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

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

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

没错,但你的做法是错误的

如果您不想使用工作线程(您应该这样做),那么至少要更改
OnTimer
事件处理程序,使其不会阻止t
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;