Indy写缓冲/高效TCP通信

8

我知道,我问了很多问题...但作为一个新的Delphi开发人员,我总是遇到这些问题 :)

这个问题涉及使用Indy 10进行TCP通信。为了使通信更有效率,我将客户端操作请求编码为单个字节(在大多数情况下后面跟随其他数据字节,但在这种情况下只有一个单独的字节)。问题是

var Bytes : TBytes;
...
SetLength (Bytes, 1);
Bytes [0] := OpCode;
FConnection.IOHandler.Write (Bytes, 1);
ErrorCode := Connection.IOHandler.ReadByte;

该字节不会立即发送(至少服务器执行处理程序未被调用)。例如,如果我将“1”更改为“9”,则一切正常。我假设Indy缓冲传出字节,并尝试通过以下方式禁用写入缓冲:

FConnection.IOHandler.WriteBufferClose;

但是这并没有帮助。我该如何发送单个字节并确保它立即被发送?另外,我在这里添加另一个小问题-使用indy发送整数的最佳方法是什么?不幸的是,我无法在TIdTCPServer的IOHandler中找到类似于WriteInteger的函数...

WriteLn (IntToStr (SomeIntVal))

我觉得这样做效率不高。如果我连续使用多个写命令,或者把它们打包在一个字节数组中一次发送,这会有什么区别吗?

非常感谢您的回答!

编辑:由于读写过程似乎发生了重大变化,我添加了一个提示,我正在使用 Indy 10。


请注意,发送1个字节并不比发送更多字节更有效。您应该详细了解TCP/IP、数据包大小、传输开销等内容。尽管使用文本会导致数据尺寸较大,但大多数旧的互联网协议仍然使用文本是有原因的。 - mghie
这个命令是一个例外。大多数命令会为命令参数添加更多字节。我认为尽可能紧密地打包东西不会有坏处。或者我在这里错了吗? - jpfollenius
只要您的完整数据帧适合一个TCP数据包,它不应该有太大影响。另一方面,基于文本的协议在调试方面非常有帮助。 - mghie
我在 http://stackoverflow.com/questions/2440926/strange-rare-out-of-order-data-received-using-indy 上发布了一个问题,可能与此相关,所以我想提到它,以防有人见过类似的情况。 - Jim
3个回答

6

默认情况下,写入缓冲是禁用的。您可以通过测试fConnection.IOHandler.WriteBufferingActive属性来检查写入缓冲是否处于活动状态。

至于发送整数的最佳方法...这取决于您的协议和总体目标。具体而言,请使用FConnection.IOHandler.Write(),因为它有重载的方法可以写入几乎任何类型的数据,包括整数。

摘自IdIOHandler:

// Optimal Extra Methods
//
// These methods are based on the core methods. While they can be
// overridden, they are so simple that it is rare a more optimal method can
// be implemented. Because of this they are not overrideable.
//
//
// Write Methods
//
// Only the ones that have a hope of being better optimized in descendants
// have been marked virtual
procedure Write(const AOut: string; const AEncoding: TIdEncoding = enDefault); overload; virtual;
procedure WriteLn(const AEncoding: TIdEncoding = enDefault); overload;
procedure WriteLn(const AOut: string; const AEncoding: TIdEncoding = enDefault); overload; virtual;
procedure WriteLnRFC(const AOut: string = ''; const AEncoding: TIdEncoding = enDefault); virtual;
procedure Write(AValue: TStrings; AWriteLinesCount: Boolean = False; const AEncoding: TIdEncoding = enDefault); overload; virtual;
procedure Write(AValue: Byte); overload;
procedure Write(AValue: Char; const AEncoding: TIdEncoding = enDefault); overload;
procedure Write(AValue: LongWord; AConvert: Boolean = True); overload;
procedure Write(AValue: LongInt; AConvert: Boolean = True); overload;
procedure Write(AValue: SmallInt; AConvert: Boolean = True); overload;
procedure Write(AValue: Int64; AConvert: Boolean = True); overload;
procedure Write(AStream: TStream; ASize: Int64 = 0; AWriteByteCount: Boolean = False); overload; virtual;

您提出的另一个问题是:“我是否可以在一行中使用多个写命令,还是将它们打包到字节数组中并一次性发送有所区别?” 对于大多数情况而言,是有区别的。对于高度压力的服务器,您需要更深入地了解如何发送和接收字节,但在这个级别上,您应该将您的发送抽象成一个单独的协议类型类,该类构建要发送的数据并以突发方式发送它,并具有一个接收协议,该协议接收一堆数据并将其作为完整单元处理,而不是将其分解成发送/接收整数、字符、字节数组等。
以下是一个非常简单的例子:
TmyCommand = class(TmyProtocol)
private
  fCommand:Integer;
  fParameter:String;
  fDestinationID:String;
  fSourceID:String;
  fWhatever:Integer;
public
  property Command:Integer read fCommand write fCommand;
  ...

  function Serialize;
  procedure Deserialize(Packet:String);
end;

function TmyCommand.Serialize:String;
begin
  //you'll need to delimit these to break them apart on the other side
  result := AddItem(Command) + 
            AddItem(Parameter) + 
            AddItem(DestinationID) + 
            AddItem(SourceID) + 
            AddItem(Whatever);
end; 
procedure TMyCommand.Deserialize(Packet:String);
begin
   Command := StrToInt(StripOutItem(Packet));
   Parameter := StripOutItem(Packet);
   DesintationID := StripOutItem(Packet); 
   SourceID := StripOutItem(Packet);
   Whatever := StrToInt(StripOutItem(Packet));
end;

然后通过以下方式发送:

  FConnection.IOHandler.Write(myCommand.Serialize());

另一方面,您可以通过Indy接收数据,然后

  myCommand.Deserialize(ReceivedData);

如果您使用Indy的内置写入缓冲区,则不需要手动序列化数据。无论如何,特别是在使用Indy 10及其新的Unicode功能时,绝对不应该使用字符串进行序列化。如果要手动序列化,请改用TIdBytes。 - Remy Lebeau
Unicode 在我和很多美国人之间的通信中不是很有用。此外,我宁愿使用字符串进行序列化,而不是 TidBytes。我从未在重要事情上使用过Indy 10 - 如果使用 Indy,我会使用 Indy9。 - Darian Miller

4

我不熟悉Indy,但你可能需要查找其API以获取TCP_NODELAY选项(你可能需要在Indy源代码中搜索类似的内容 - 不区分大小写的“delay”应该就可以了)。

编辑:Rob Kennedy指出了我所指的属性是TIdIOHandlerSocket.UseNagle - 谢谢!

问题在于TCP的本质。TCP确保数据按照发射顺序传递,但不能保证消息边界。换句话说,源、目标和任何路由器的操作系统都可以任意合并连接的数据包或将其分片。您必须将TCP传输视为流,而不是一系列单独的数据包。因此,您将不得不实现一种机制,通过其中您可以定界各个消息(例如,使用一个魔术字节,如果它也可能出现在消息数据中,则必须对其进行转义),或者您可以首先发送随后消息的长度,然后发送实际消息。

当我需要发送消息时,消息边界很重要,例如你的情况,我总是使用UDP结合一种简单的ACK /重传方案。也许要考虑这一点。 UDP更适合命令消息。


你说的是 Nagle,对吗?这个可以通过 Indy 的 TIdIOHandlerSocket.UseNagle 属性进行控制。 - Rob Kennedy
好的,Nagle - 谢谢,Rob。我已经相应地编辑了帖子。 - Mihai Limbășan

0

看起来你需要清空缓存。试试这个:

TIdTCPConnection.FlushWriteBuffer;

如果您不想使用写缓冲区,请使用以下代码:
TIdTCPConnection.CancelWriteBuffer;

根据帮助文件,首先调用ClearWriteBuffer来清除缓冲区,然后再调用CloseWriteBuffer。
使用Indy 10发送整数的最佳方法是使用TIdIOHandler.Write(根据Indy 10帮助文件,Write被重载以处理不同类型的数据,包括整数)。

我试图刷新缓冲区,但没有任何变化。根据帮助文档,调用WriteBufferClose应该释放写缓冲区(WriteBufferCancel也是如此),所以根本不应该有写缓冲区存在。 - jpfollenius
关于您使用WriteInteger进行编辑的问题:我认为您正在使用Indy 9。我的IOHandler中没有这样的方法。 - jpfollenius
尝试使用TIdTCPConnection的写入方法,而不是直接使用IOHandler,也许会有所不同。我假设在写入数据之后并在读取响应之前调用FlushWriteBuffer。 - The_Fox
这是因为我没有使用IOHandler,而是使用Connection类(在你的情况下是FConnection),是的,我正在查看Indy 9的帮助文档。 - The_Fox
你的推断是正确的 ;) 在Indy 10中,除了WriteHeader和WriteRFCStrings之外,TCP连接中没有写入方法。 - jpfollenius

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接