如何处理 TCPClient 中接收到的数据? (德尔福 - 印地)

Posted

技术标签:

【中文标题】如何处理 TCPClient 中接收到的数据? (德尔福 - 印地)【英文标题】:How to handle received data in the TCPClient ? (Delphi - Indy) 【发布时间】:2011-07-28 15:40:02 【问题描述】:

当我从TCPClientTCPServer 发送消息时,它将使用服务器中的OnExecute 事件进行处理。现在我想在客户端处理收到的消息,但TCPClient 没有任何事件。所以我必须制作一个线程来手动处理它们。我该怎么做?

【问题讨论】:

你看过 Indy 的例子吗? 【参考方案1】:

添加TTimer。 将其Interval 设置为1。 写在OnTimer事件:

procedure TForm1.Timer1Timer(Sender: TObject);
var
s: string;
begin
if not IdTCPClient1.Connected then Exit;
if IdTCPClient1.IOHandler.InputBufferIsEmpty then Exit;
s := IdTCPClient1.IOHandler.InputBufferAsString;
Memo1.Lines.Add('Received: ' + s);
end;

不要将Timer.Interval 设置为其他1。 因为,接收到的数据会在几毫秒后删除。

【讨论】:

【参考方案2】:

这是我用 Delphi 7 读取/写入的代码。使用 Tcp 事件读取。

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ScktComp;

type
  TForm1 = class(TForm)
    ClientSocket1: TClientSocket;
    Button1: TButton;
    ListBox1: TListBox;
    Edit1: TEdit;
    Edit2: TEdit;
    procedure Button1Click(Sender: TObject);
    procedure ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket);
    procedure ClientSocket1Error(Sender: TObject; Socket: TCustomWinSocket;
      ErrorEvent: TErrorEvent; var ErrorCode: Integer);
  private
     Private declarations 
  public
     Public declarations 
  end;

var
  Form1: TForm1;

implementation

$R *.dfm

procedure TForm1.Button1Click(Sender: TObject);
var
 UsePort: Integer;
 UseHost: String;

begin
UseHost := Edit1.Text;
UsePort := STRTOINT(Edit2.Text);
ClientSocket1.Port :=  UsePort;
ClientSocket1.Host :=  UseHost;
ClientSocket1.Active :=  true;
end;

procedure TForm1.ClientSocket1Read(Sender: TObject;
  Socket: TCustomWinSocket);
begin
ListBox1.Items.Add(ClientSocket1.Socket.ReceiveText);

end;

procedure TForm1.ClientSocket1Error(Sender: TObject;
  Socket: TCustomWinSocket; ErrorEvent: TErrorEvent;
  var ErrorCode: Integer);
begin
  ErrorCode:=0;
  ClientSocket1.Active := False;
end;

procedure TForm1.BitBtn1Click(Sender: TObject);
begin
  ClientSocket1.Socket.SendText(Edit1.Text);
end;

end.

【讨论】:

【参考方案3】:

正如其他人在回答您的问题时所说,TCP 不是面向消息的协议,而是流协议。我将向您展示如何写入和读取到一个非常简单的回显服务器(这是我本周为回答其他问题所做的服务器的略微修改版本):

服务器的 OnExecute 方法如下所示:

procedure TForm2.IdTCPServer1Execute(AContext: TIdContext);
var
  aByte: Byte;
begin
  AContext.Connection.IOHandler.Writeln('Write anything, but A to exit');
  repeat
    aByte := AContext.Connection.IOHandler.ReadByte;
    AContext.Connection.IOHandler.Write(aByte);
  until aByte = 65;
  AContext.Connection.IOHandler.Writeln('Good Bye');
  AContext.Connection.Disconnect;
end;

此服务器以欢迎消息开始,然后仅读取每个字节的连接字节。服务器回复相同的字节,直到收到的字节为 65(断开连接命令)65 = 0x41 或 $41。然后服务器以再见消息结束。

您可以在客户端中执行此操作:

procedure TForm3.Button1Click(Sender: TObject);
var
  AByte: Byte;
begin
  IdTCPClient1.Connect;
  Memo1.Lines.Add(IdTCPClient1.IOHandler.ReadLn);  //we know there must be a welcome message!
  Memo1.Lines.Add('');// a new line to write in!
  AByte := 0;
  while (IdTCPClient1.Connected) and (AByte <> 65) do
  begin
    AByte := NextByte;
    IdTCPClient1.IOHandler.Write(AByte);
    AByte := IdTCPClient1.IOHandler.ReadByte;
    Memo1.Lines[Memo1.Lines.Count - 1] :=  Memo1.Lines[Memo1.Lines.Count - 1] + Chr(AByte);
  end;
  Memo1.Lines.Add(IdTCPClient1.IOHandler.ReadLn);  //we know there must be a goodbye message!
  IdTCPClient1.Disconnect;
end;

下一个字节过程可以是任何你想提供的字节。例如,要从用户那里获取输入,您可以将表单的 KeyPreview 设置为 true,然后编写 OnKeyPress 事件处理程序和 NextByte 函数,如下所示:

procedure TForm3.FormKeyPress(Sender: TObject; var Key: Char);
begin
  FCharBuffer := FCharBuffer + Key;
end;

function TForm3.NextByte: Byte;
begin
  Application.ProcessMessages;
  while FCharBuffer = '' do  //if there is no input pending, just waint until the user adds input
  begin
    Sleep(10);
    //this will allow the user to write the next char and the application to notice that
    Application.ProcessMessages;
  end;  
  Result := Byte(AnsiString(FCharBuffer[1])[1]);  //just a byte, no UnicodeChars support
  Delete(FCharBuffer, 1, 1);
end;

用户在表单中写入的任何内容都将被发送到服务器,然后从那里读取并添加到 memo1。如果输入焦点已经在 Memo1 中,您将看到每个字符两次,一次来自键盘,另一个来自服务器。

因此,为了编写一个从服务器获取信息的简单客户端,您必须知道从服务器可以得到什么。它是一个字符串吗?多个字符串?整数?大批?二进制文件?编码文件?是否有连接结束的标记?如果您要创建自定义服务器/客户端对,这些内容通常由协议或您定义。

在事先不知道从服务器获取什么的情况下编写通用 TCP 是可能的,但由于协议中此级别没有通用消息抽象,因此复杂。

不要对有传输消息的事实感到困惑,但是单个服务器响应可以拆分为多个传输消息,然后重新组装客户端,您的应用程序无法控制这一点。从应用程序的角度来看,套接字是传入字节的流(流)。您将其解释为消息、命令或来自服务器的任何类型的响应的方式取决于您。同样适用于服务器端...例如 onExecute 事件是一张白纸,您也没有消息抽象。

也许您将消息抽象与命令抽象混合在一起......在基于命令的协议上,客户端发送包含命令的字符串,服务器回复包含响应的字符串(然后可能是更多数据)。看看 TIdCmdTCPServer/Client 组件。

编辑

在 cmets OP 状态下,他/她想在线程上进行这项工作,我不确定她/他对此有什么问题,但我正在添加一个线程示例。服务器与之前显示的相同,只是这个简单服务器的客户端部分:

首先,我正在使用的线程类:

type
  TCommThread = class(TThread)
  private
    FText: string;
  protected
    procedure Execute; override;
    //this will hold the result of the communication
    property Text: string read FText;
  end;

procedure TCommThread.Execute;
const
  //this is the message to be sent. I removed the A because the server will close 
  //the connection on the first A sent.  I'm adding a final A to close the channel.
  Str: AnsiString = 'HELLO, THIS IS _ THRE_DED CLIENT!A';
var
  AByte: Byte;
  I: Integer;
  Client: TIdTCPClient;
  Txt: TStringList;
begin
  try
    Client := TIdTCPClient.Create(nil);
    try
      Client.Host := 'localhost';
      Client.Port := 1025;
      Client.Connect;
      Txt := TStringList.Create;
      try
        Txt.Add(Client.IOHandler.ReadLn);  //we know there must be a welcome message!
        Txt.Add('');// a new line to write in!
        AByte := 0;
        I := 0;
        while (Client.Connected) and (AByte <> 65) do
        begin
          Inc(I);
          AByte := Ord(Str[I]);
          Client.IOHandler.Write(AByte);
          AByte := Client.IOHandler.ReadByte;
          Txt[Txt.Count - 1] :=  Txt[Txt.Count - 1] + Chr(AByte);
        end;
        Txt.Add(Client.IOHandler.ReadLn);  //we know there must be a goodbye message!
        FText := Txt.Text;
      finally
        Txt.Free;
      end;
      Client.Disconnect;
    finally
      Client.Free;
    end;
  except
    on E:Exception do
      FText := 'Error! ' + E.ClassName + '||' + E.Message;
  end;
end;

然后,我将这两种方法添加到表单中:

//this will collect the result of the thread execution on the Memo1 component.
procedure TForm3.AThreadTerminate(Sender: TObject);
begin
  Memo1.Lines.Text := (Sender as TCommThread).Text;
end;

//this will spawn a new thread on a Create and forget basis. 
//The OnTerminate event will fire the result collect.
procedure TForm3.Button2Click(Sender: TObject);
var
  AThread: TCommThread;
begin
  AThread := TCommThread.Create(True);
  AThread.FreeOnTerminate := True;
  AThread.OnTerminate := AThreadTerminate;
  AThread.Start;
end;

【讨论】:

感谢您的完整回复。但我想使用 Thread 来做到这一点。 @Kermia 在您的问题中,您在哪里说明了这一点?是什么让你认为你不能在一个线程中做到这一点? 正如我所说:i have to make a thread to handle them manually. how can i do it ?。因为 Indy 正在使用阻塞模式,并且没有任何额外的线程,我的应用程序将冻结。 而使用Application.ProcessMessages; 是不正确的。你可以编辑你的代码并给我看一个使用额外线程的例子吗? @Kermia 我添加了一个使用线程的示例客户端实现。【参考方案4】:

如果您需要 Indy 客户端处理传入的“消息”(“消息”的定义取决于所使用的协议),我建议您看一下 TIdTelnet 在protocols\IdTelnet 单元中的实现。

该组件使用基于 TIdThread 的接收线程,该线程异步接收来自 Telnet 服务器的消息,并将它们传递给消息处理程序例程。如果您有类似的协议,这可能是一个很好的起点。

更新:更具体地说,IdTelnet.pas 中的 procedure TIdTelnetReadThread.Run; 是异步客户端“神奇”发生的地方,正如您所看到的,它使用 Synchronize 在主线程中运行数据处理 - 当然是您的应用程序也可以在接收线程中进行数据处理,或者将其传递给工作线程以保持主线程不受影响。该过程不使用循环,因为循环/暂停/重启是在IdThread中实现的。

【讨论】:

【参考方案5】:

TCP 不处理消息。那是基于流的接口。因此,不要指望您会在接收器上收到“消息”。相反,您从套接字读取传入的数据流并根据您的高级协议对其进行解析。

【讨论】:

好的,如何使用外部线程处理TCPClient中接收到的流? @Kermia 如果你从根本上改变问题,我建议你发布一个单独的问题。

以上是关于如何处理 TCPClient 中接收到的数据? (德尔福 - 印地)的主要内容,如果未能解决你的问题,请参考以下文章

如何处理反序列化变化的 JSON 数据?

通量/反应:如何处理商店中过滤的 api 数据

如何处理共享同一通道的多个 goroutine

当应用程序关闭或不活动时,如何处理 iOS 应用程序中的推送通知?

如何处理C++编译webrtc无法成功获取sdp的问题?

CakePhp 1.3 如何处理作为 HTTP post 请求接收的数据?