为什么当客户端连接到 Indy 中的服务器时 IOHandler.ReadStream 会阻塞线程?

发布于 2024-09-11 07:00:08 字数 1018 浏览 1 评论 0原文

今天,我在使用 Indy 10(Delphi 2010 附带)时遇到了奇怪的行为。问题如下:

假设我们的客户端中有一个 IdTcpClient,服务器应用程序中有一个 IdTcpServer,并且 IdTcpServer 的 OnExecute 事件处理程序中的代码如下:

procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
  AStream: TStringStream;
  S: string;
begin
  AStream := TStringStream.Create;
  try
    AContext.Connection.IOHandler.ReadStream(AStream);
    S := AStream.DataString;
  finally
    AStream.Free;
  end;
end;

现在,当客户端尝试连接到服务器时,使用 TIdTcpClient.Connect ;在服务器上,调用 TIdTcpServer.OnExecute,并且当执行到达 AContext.Connection.IOHandler.ReadStream(AStream) 行时,在 OnExecute 事件处理程序内运行的线程将被阻止!

当我跟踪代码时,问题是在 ReadStream 内部调用 ReadLongInt 来获取字节数时引起的。 ReadLongInt 调用 ReadBytes。在 ReadBytes 内部,FInputBuffer.Size 为零。在那里,循环调用 ReadFromSource,最终执行到 TIdSocketListWindows.FDSelect,它从 WinSock2 调用“select”函数,执行在此停止,并且不会从该客户端连接接收任何内容。我也尝试为 AByteCount 和 AReadUntilDisconnect 参数赋予值,但它并没有改变行为。

如果我用ReadLn替换ReadStream,那么连接到服务器不会阻止代码执行,并且从客户端发送的数据被服务器读取。

代码有什么问题吗?或者这是一个错误?

问候

Today I faced a weird behavior using Indy 10 (shipped with Delphi 2010). Here is the problem:

Suppose we have a IdTcpClient in our client, and a IdTcpServer in our server app, and this code in OnExecute event-handler for our IdTcpServer:

procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
  AStream: TStringStream;
  S: string;
begin
  AStream := TStringStream.Create;
  try
    AContext.Connection.IOHandler.ReadStream(AStream);
    S := AStream.DataString;
  finally
    AStream.Free;
  end;
end;

Now, when the client tries to connect to the server, using TIdTcpClient.Connect; on the server, TIdTcpServer.OnExecute is invoked, and the thread running inside OnExecute event-handler is blocked when execution reaches AContext.Connection.IOHandler.ReadStream(AStream) line!

When I trace the code, the problem is caused when ReadLongInt is called inside ReadStream to get bytes count. ReadLongInt calls ReadBytes. Inside ReadBytes, FInputBuffer.Size is zero. There, in a loop ReadFromSource is called, and eventually execution reaches to TIdSocketListWindows.FDSelect which calls "select" function from WinSock2, and execution stops here, and nothing will be received from that client connection. I tried giving value to AByteCount and AReadUntilDisconnect parameters too, but it did not change the behavior.

If I replace ReadStream with ReadLn, then connecting to server does not block code execution, and the data sent from client is read by server.

Is there anything wrong with the code? Or is this a bug?

Regards

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

明媚如初 2024-09-18 07:00:08

问题出在您的代码中,而不是在 ReadStream() 中。它按照设计运行。

它接受 3 个参数输入:

procedure ReadStream(AStream: TStream; AByteCount: TIdStreamSize = -1; AReadUntilDisconnect: Boolean = False); virtual;

您只需为第一个参数提供值,因此其他两个参数使用默认值。

AByteCount 参数设置为 -1 且 AReadUntilDisconnect 参数设置为 False 时,ReadStream() 设计为假设前 4 个接收到的字节(如果 IOHandler.LargeStream 属性设置为 True,则为 8 字节)是正在发送的数据的长度,后面跟着实际数据。这就是 ReadStream() 调用 ReadLongInt() 的原因。这不仅告诉 ReadStream() 何时停止读取,而且还允许 ReadStream() 在接收数据之前预先调整目标 TStream 的大小,以便更好地进行内存管理。

如果客户端实际上并未在其数据之前发送 4 字节(或 8 字节)长度值,则 ReadStream() 仍会将实际数据的开始字节解释为长度。这通常(但并非总是如此,具体取决于数据)会导致 ReadLongInt() (或 ReadInt64())返回一个大整数值,从而导致 ReadStream() 预期永远不会真正到达的大量数据,从而无限期地阻塞读取(或者直到发生超时,如果 IOHandler.ReadTimeout 属性设置为非-无限超时)。

为了有效地使用 ReadStream(),它需要知道何时停止读取,或者通过提前告知预计有多少数据(即:AByteCount >= 0) code>),或者要求发送方在发送数据后断开连接(即:AReadUtilDisconnect = True)。当长度直接在流中编码时,AByteCount = -1AReadUtilDisconnect = False 的组合是一种特殊情况。这主要用于(但不限于)当发送方调用 IOHandler.Write(TStream) 并将其 AWriteByteCount 参数设置为 True(默认情况下为 False)时使用。

处理非文本数据时,尽可能在实际数据之前发送数据长度始终是一个好主意。它优化了读取操作。

ReadStream() 的不同参数组合得出以下逻辑:

  1. AByteCount = -1,AReadUtilDisconnect = False:读取 4/8 字节,解释为长度,然后继续读取,直到收到该长度。

  2. AByteCount < -1, AReadUtilDisconnect = False: 假设AReadUntilDisconnect为True并继续读取直到断开连接。

  3. AByteCount > -1、AReadUtilDisconnect = False:预先调整目标TStream的大小并继续读取,直到接收到AByteCount个字节。

  4. AByteCount <= -1, AReadUtilDisconnect = True: 继续读取直到断开连接。

  5. AByteCount > -1、AReadUtilDisconnect = True:预先调整目标TStream的大小并继续读取直到断开连接。

根据客户端首先实际发送到服务器的数据类型,ReadStream() 可能不是读取该数据的最佳选择。 IOHandler 有许多不同类型的可用读取方法。例如,如果客户端正在发送分隔文本(特别是使用 IOHandler.WriteLn() 发送),则 ReadLn() 是更好的选择。

The problem is in your code, not in ReadStream(). It is acting as designed.

It accepts 3 parameters for input:

procedure ReadStream(AStream: TStream; AByteCount: TIdStreamSize = -1; AReadUntilDisconnect: Boolean = False); virtual;

You are only providing a value for the first parameter, so the other two parameters use default values.

When the AByteCount parameter is set to -1 and the AReadUntilDisconnect parameter is set to False, ReadStream() is designed to assume that the first 4 bytes received (or 8 bytes, if the IOHandler.LargeStream property is set to True) are the length of the data being sent, followed by the actual data afterwards. That is why ReadStream() is calling ReadLongInt(). Not only does this tell ReadStream() when to stop reading, but it also allows ReadStream() to pre-size the target TStream for better memory management before receiving the data.

If the client is not actually sending a 4-byte (or 8-byte) length value ahead of its data, then ReadStream() will still interpret the beginning bytes of the real data as a length. This typically (but not always, depending on the data) results in ReadLongInt() (or ReadInt64()) returning a large integer value, which would then cause ReadStream() to expect a huge amount of data that will never actually arrive, thus blocking the reading indefinately (or until a timeout occurs, if the IOHandler.ReadTimeout property is set to a non-infinite timeout).

In order to use ReadStream() effectively, it needs to know when to stop reading, either by being told how much data to expect ahead of time (ie: AByteCount >= 0), or by requiring the sender to disconnect after sending its data (ie: AReadUtilDisconnect = True). The combination of AByteCount = -1 and AReadUtilDisconnect = False is a special case, when the length is encoded directly in the streaming. This is primarily used (but not limited to) when the sender calls IOHandler.Write(TStream) with its AWriteByteCount parameter set to True (it is False by default).

When dealing with non-textual data, it is always a good idea to send the data length ahead of the actual data whenever possible. It optimizes reading operations.

The different parameter combinations of ReadStream() work out to the following logic:

  1. AByteCount = -1, AReadUtilDisconnect = False: read 4/8 bytes, interpret as a length, then keep reading until that length is received.

  2. AByteCount < -1, AReadUtilDisconnect = False: assume AReadUntilDisconnect is True and keep reading until disconnected.

  3. AByteCount > -1, AReadUtilDisconnect = False: pre-size the target TStream and keep reading until AByteCount number of bytes are received.

  4. AByteCount <= -1, AReadUtilDisconnect = True: keep reading until disconnected.

  5. AByteCount > -1, AReadUtilDisconnect = True: pre-size the target TStream and keep reading until disconnected.

Depending on the kind of data the client is actually sending to the server in the first place, chances are that ReadStream() is likely not the best choice for reading that data. The IOHandler has many different kinds of reading methods available. For instance, if the client is sending delimited text (especially if it is being sent with IOHandler.WriteLn()), then ReadLn() is a better choice.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文