如何为下一个客户端连接正确重启 Indy TCP Server?

How to properly restart Indy TCP Server for the next client connection?

我有一个 Indy TCP 服务器,客户端连接到它,发送信息,服务器接收,创建一个巨大的 TStringList 并发送回客户端。这种情况每天发生数千次,所以我决定将六台服务器放在不同的 .exe 上,在不同的端口上执行相同的操作,并让客户端应用程序每次连接到一个随机端口。

发生了什么:

1) 从客户端第一次尝试连接到他收到他需要的所有信息的那一刻是相当高的,就像他做了两次工作一样。

2) 大约 10% 的时间客户端尝试连接,但服务器似乎忽略了该尝试,并且客户端没有触发 except 重试,然后卡住了。

服务器:

客户端6秒Timer尝试连接到服务器:

  IdDownloadClient.IOHandler.MaxLineLength := MaxInt;
  IdDownloadClient.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
  // Choose a random port
  IdDownloadClient.Connect;
  IdDownloadClient.IOHandler.WriteLn(lat+','+long);
  Timer6.Enabled := True;

客户端Timer6(25 毫秒):

  with IdDownloadClient do
  begin
    try
      if IOHandler.InputBufferIsEmpty then
      begin
        IOHandler.CheckForDataOnSource(0);
        IOHandler.CheckForDisconnect;
        if IOHandler.InputBufferIsEmpty then Exit;
        end;
      end;
      receivedtext := IOHandler.ReadLn;
    except
      Timer6.Enabled := False;
      Exit;
    end;
    if receivedtext = '@' then begin // The server send an '@' to say the complete TStringList has been sent
      IdDownloadClient.IOHandler.InputBuffer.Clear;
      IdDownloadClient.IOHandler.CloseGracefully;
      IdDownloadClient.Disconnect;

服务器OnExecute事件:

begin
try
AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
LatLong := AContext.Connection.IOHandler.ReadLn;
if LatLong <> '' then begin
latF := StrToFloat(StringReplace(Copy(LatLong,0,ansipos(',',LatLong)-1),'.',',',[rfIgnoreCase, rfReplaceAll]));
lonF := StrToFloat(StringReplace(Copy(LatLong,ansipos(',',LatLong)+1,11),'.',',',[rfIgnoreCase, rfReplaceAll]));

// Creates a TStringList from a Memo to send (around half a sec of proccessing)
bufferlist := TStringList.Create;
bufferlist.Add('h-023.64086400000,-046.57425900000 99999999 0300 0301 0001 test|123 test');
  for J := 0 to Memo1.Lines.Count-2 do
    begin
      if ((abs(latF-StrToFloat(StringReplace(Copy(Memo1.Lines[J],2,16),'.',',',[rfIgnoreCase, rfReplaceAll]))) < 0.1) and (abs(lonF-StrToFloat(StringReplace(Copy(Memo1.Lines[J],19,16),'.',',',[rfIgnoreCase, rfReplaceAll]))) < 0.1)) then
      bufferlist.Add(Memo1.Lines[J]);
    end;

///////// Start to send
  for i := 0 to bufferlist.Count-1 do
    begin
      AContext.Connection.IOHandler.WriteLn(bufferlist[i]);
    end;

AContext.Connection.IOHandler.WriteLn('@'); // Send '@' to the client to say the list is over
bufferlist.Free;
end;
except
    if Assigned(bufferlist) then bufferlist.Free;
    Exit;
end;
end;

由于所有连接都来自 3G/4G 手机,我认为其中一些连接不好,这是导致问题的原因,那么我在这段代码中做错了什么?

我可以做些什么来解决这个问题或至少改进它?

您的服务器 OnExecute 正在处理所有引发的异常。当客户端断开连接时,在该客户端的套接字上执行的下一个套接字 I/O 将引发异常,您正在捕获并丢弃该异常。所以服务器不会知道客户端已经断开连接,并且会继续触发 OnExecute 事件。并且由于您的服务器设置为MaxConnections=1,因此在以前的客户端完全释放之前,新客户端无法连接到服务器。您的 OnExecute 代码必须直接调用 AContext.Connection.Disconnect() 或引发未捕获的异常,以释放客户端线程。

简单的经验法则 - 不要吞下例外!如果您捕获到一个您不知道如何处理的异常,您应该重新引发它,因为它可能在调用堆栈的更高层进行处理。在 TIdTCPServer 方面,永远不要吞下从 EIdException 派生的 Indy 异常,让服务器处理它。您使用 try/except 只是为了释放 bufferlist 应该替换为 try/finally

事实上,我建议取消用于收集响应数据的 TStringList,因为它实际上并没有帮助您,它会阻碍您的服务器性能。在整个 TStringList 完全构建之前,客户端不会收到服务器的任何响应,这可能会导致客户端超时。最好在创建时将每一行文本发送给客户端,这样客户端就知道服务器实际上正在做某事而不是 dead/frozen.

我看到您的 OnExecute 代码的另一个问题是它直接访问 UI 控件(TMemo)而不与 UI 线程同步。 TIdTCPServer 是一个多线程组件,它的事件在工作线程中触发。在工作线程中访问 UI 控件时,您 必须 与 UI 线程同步。

最后,您对 Copy()StringReplace() 的过度使用是一个主要的问题,并且使代码通常难以维护。

尝试更像这样的东西:

procedure IdTCPServer1Connect(AContext: TIdContext);
begin
  // do this assignment one time, not on every OnExecute loop iteration
  AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
end;

procedure IdTCPServer1Execute(AContext: TIdContext);
var
  s: string;
  latF, lonF: Double;
  fmt: TFormatSettings;
  lines: TStringList;
  j: Integer;
begin
  s := AContext.Connection.IOHandler.ReadLn;
  if s = '' then Exit;

  fmt := TFormatSettings.Create;
  fmt.DecimalSeparator := '.';

  latF := StrToFloat(Fetch(s, ','), fmt);
  lonF := StrToFloat(s, fmt);

  AContext.Connection.IOHandler.WriteLn('h-023.64086400000,-046.57425900000 99999999 0300 0301 0001 test|123 test');

  lines := TStringList.Create;
  try
    TThread.Synchronize(nil,
      procedure
      begin
        lines.Assign(Memo1.Lines);
      end
    );

    for J := 0 to lines.Count-2 do
    begin
      s := lines[J];
      if (abs(latF-StrToFloat(Copy(s, 2, 16), fmt)) < 0.1) and (abs(lonF-StrToFloat(Copy(s, 19, 16), fmt)) < 0.1) then
        AContext.Connection.IOHandler.WriteLn(s);
    end;
  finally
    lines.Free;
  end;

  AContext.Connection.IOHandler.WriteLn('@'); // Send '@' to the client to say the list is over
end;

现在,在客户端,你所做的通常没问题(尽管我建议在主 UI 线程中使用工作线程而不是计时器),但我会说你这样做不需要调用 InputBuffer.Clear() 来正常断开连接,只需要异常断开连接(即,将其移动到您的 except 处理程序中),并且您根本不应该调用 CloseGracefully()

此外,由于您使用唯一分隔符终止响应,我建议一旦客户端检测到服务器响应开始到达,它应该使用 TIdIOHandler.Capture() 一次性读取整个响应,而不是调用 ReadLn() 来读取每个计时器事件的每一行。这将大大简化和加快客户端接收完整响应的速度。

procedure Timer6Timer(Sender: TObject);
var
  response: TStringList;
begin
  with IdDownloadClient do
  begin
    try
      if IOHandler.InputBufferIsEmpty then
      begin
        IOHandler.CheckForDataOnSource(0);
        IOHandler.CheckForDisconnect;
        if IOHandler.InputBufferIsEmpty then Exit;
      end;

      response := TStringList.Create;
      try
        // The server send an '@' to say the complete response has been sent
        IOHandler.Capture(response, '@', False);

        // use response as needed...
      finally
        response.Free;
      end;
    except
      IOHandler.InputBuffer.Clear;
    end;
    Disconnect;
  end;
  Timer6.Enabled := False;
end;