用于实时数据和元数据的 TCP 通信协议
TCP communication protocols for realtime data plus metadata
我对网络协议设计还很陌生,希望对我的问题提出建议。
我的要求
- 我需要将实时数据从 TCP 套接字客户端流式传输到服务器。 我这里没有选择UDP.
- 数据需要元数据描述符才能在
服务器端。此描述符可以代表客户端用户在 运行 时间更改。
- 我需要偶尔从客户端向服务器发送控制命令以进行数据操作。
设计A
- 设置两对套接字服务器和客户端,一对用于需求#1,另一对用于需求#2和#3;现在称他们为
Data Tunnel
和 Metadata Tunnel
。
优点
- 概念清晰,易于在实现中隔离元数据和数据模块。
- 实时数据,即#1,可以按字节发送,不需要额外的数据封装。
缺点
- 两个隧道之间需要同步,以避免由于控制更改而导致的解码错误或故障。
- 每个套接字客户端对可能需要两个线程以避免阻塞应用程序的主线程:每个隧道一个线程。
设计 B
- 只使用一个隧道。
- 分别为实时数据和元数据设计至少两种数据包数据结构。实时数据包将包含其解释。
- 让数据和元数据共享唯一的隧道。
优点
- 每个实时数据包都与元数据捆绑在一起,将保证正确解码。
- 只使用一个线程。
缺点
- 接收器数据处理的更复杂实现,例如,必须在接收数据包时区分数据和元数据。
- 单隧道交通繁忙。我打赌控制消息不会阻塞实时数据消耗,但我真的不知道。
我倾向于 Design A
"realtime" 保证比正确解码更安全。但我想与专家仔细检查我的想法。
我知道那里有成百上千种通信协议,但老实说,看着随机的协议文档让我头晕目眩。毕竟我的应用程序相对轻量级,所以自己动手似乎更有意义。
顺便说一句,我使用Google Protobuf
进行协议设计。我认为它的性能是实时就绪的。
如有任何建议,我们将不胜感激。
正如@RonMaupin 在评论中提到的,TCP 和 "real time" 并不是特别兼容,因为 TCP 优先于 "getting the bytes there correctly" 而不是 "getting the bytes there in a particular time frame"。
就是说,您 可以 使用 TCP 实现的是 "getting the bytes there as quickly as possible",只要您同意在某些情况下(例如网络是丢弃大量数据包),"as quickly as possible" 可能不会那么快。
关于使用多少 TCP 流,与做出该决定相关的 TCP 质量是 TCP 流始终强制in-order/FIFO 传送该流中的字节。也就是说,您 send()
发送到 TCP 套接字的所有字节都将按照接收程序的确切顺序 recv()
——这种行为可能对您有利,也可能对您不利,因此您想要以适合您的方式设计您的程序。特别是,在决定是使用一个还是多个 TCP 连接时,问问自己,"does the data I'm sending need to be received in the same order it was sent, or does that not matter?" 如果严格的 FIFO 顺序很重要,那么单个 TCP 流是可行的方法; OTOH 如果你例如有两种类型的数据并且 type-B 数据在逻辑上独立于 type-A 数据,那么您可以考虑为 type-B 数据提供其自己的独立 TCP 流,以便 dropped-packet-of-A-data 不会减慢 B-data.
的传输速度
在任何情况下,您都希望至少有一些最小的 protocol/framing(例如,在每个 data-message) 这样接收方就不必猜测它正在接收的字节的含义。 (即使你一开始不需要它们,你也会希望在你的第二个版本中使用它们来帮助维护 backwards-compatibility 与以前版本的协议)
其他一些使您的 TCP-data 尽可能达到 fast/low-latency 的建议:
禁用 Nagle 的算法(在您完成 send()
-ing 特定的数据突发后永久或至少暂时禁用)——否则您将获得 200 多毫秒的不必要时间大部分时间都有延迟。
假设您的程序 运行 在具有大量 RAM 的平台上运行,setsockopt()
使用 SO_SNDBUF
和 SO_RCVBUF
选项来发送和接收socket-buffers尽可能大;这减少了缓冲区填满和数据包因 no-space-available-in-a-buffer.
而被丢弃的可能性
如果可能,请将您的发送算法设计为仅在最后可能的时刻生成 data-to-send,而不是 queue 占用大量 data-to-be-sent进步。例如,如果(由于某些 trigger-event)您的代码决定它需要尽快通过 TCP 套接字发送特定 data-structure 的当前状态,而不是序列化数据结构和 enqueueing (and/or send()
-ing) 马上序列化的字节,只要设置一个 dirty-flag 表示需要发送结构。那么,下一次socket表示是ready-for-write,即是时候将数据结构序列化,发送给socket。好处是,如果您收到例如快速连续 10 trigger-events,使用这种 dirty-flag 设计,您仍然最终只发送一次最终版本数据结构,而不是连续发送 10 次。第二个好处是,这限制了可以 queue 等待发送的数据积压,从而减少了 data-updates.
的平均延迟
在接收端,让 recv()
调用由一个专用的 high-priority 线程在一个紧密的循环中完成,除了尽可能快地接收数据外,它做的很少可能然后 enqueue 它进行进一步处理。这里的想法是尽量减少接收 TCP 套接字的 incoming-data-buffer 变满的可能性,因为如果它确实变满,一些传入的 TCP 数据包可能会被丢弃,迫使 TCP backoff-and-resend,这会减慢速度传输.
我对网络协议设计还很陌生,希望对我的问题提出建议。
我的要求
- 我需要将实时数据从 TCP 套接字客户端流式传输到服务器。 我这里没有选择UDP.
- 数据需要元数据描述符才能在 服务器端。此描述符可以代表客户端用户在 运行 时间更改。
- 我需要偶尔从客户端向服务器发送控制命令以进行数据操作。
设计A
- 设置两对套接字服务器和客户端,一对用于需求#1,另一对用于需求#2和#3;现在称他们为
Data Tunnel
和Metadata Tunnel
。
优点
- 概念清晰,易于在实现中隔离元数据和数据模块。
- 实时数据,即#1,可以按字节发送,不需要额外的数据封装。
缺点
- 两个隧道之间需要同步,以避免由于控制更改而导致的解码错误或故障。
- 每个套接字客户端对可能需要两个线程以避免阻塞应用程序的主线程:每个隧道一个线程。
设计 B
- 只使用一个隧道。
- 分别为实时数据和元数据设计至少两种数据包数据结构。实时数据包将包含其解释。
- 让数据和元数据共享唯一的隧道。
优点
- 每个实时数据包都与元数据捆绑在一起,将保证正确解码。
- 只使用一个线程。
缺点
- 接收器数据处理的更复杂实现,例如,必须在接收数据包时区分数据和元数据。
- 单隧道交通繁忙。我打赌控制消息不会阻塞实时数据消耗,但我真的不知道。
我倾向于 Design A
"realtime" 保证比正确解码更安全。但我想与专家仔细检查我的想法。
我知道那里有成百上千种通信协议,但老实说,看着随机的协议文档让我头晕目眩。毕竟我的应用程序相对轻量级,所以自己动手似乎更有意义。
顺便说一句,我使用Google Protobuf
进行协议设计。我认为它的性能是实时就绪的。
如有任何建议,我们将不胜感激。
正如@RonMaupin 在评论中提到的,TCP 和 "real time" 并不是特别兼容,因为 TCP 优先于 "getting the bytes there correctly" 而不是 "getting the bytes there in a particular time frame"。
就是说,您 可以 使用 TCP 实现的是 "getting the bytes there as quickly as possible",只要您同意在某些情况下(例如网络是丢弃大量数据包),"as quickly as possible" 可能不会那么快。
关于使用多少 TCP 流,与做出该决定相关的 TCP 质量是 TCP 流始终强制in-order/FIFO 传送该流中的字节。也就是说,您 send()
发送到 TCP 套接字的所有字节都将按照接收程序的确切顺序 recv()
——这种行为可能对您有利,也可能对您不利,因此您想要以适合您的方式设计您的程序。特别是,在决定是使用一个还是多个 TCP 连接时,问问自己,"does the data I'm sending need to be received in the same order it was sent, or does that not matter?" 如果严格的 FIFO 顺序很重要,那么单个 TCP 流是可行的方法; OTOH 如果你例如有两种类型的数据并且 type-B 数据在逻辑上独立于 type-A 数据,那么您可以考虑为 type-B 数据提供其自己的独立 TCP 流,以便 dropped-packet-of-A-data 不会减慢 B-data.
在任何情况下,您都希望至少有一些最小的 protocol/framing(例如,在每个 data-message) 这样接收方就不必猜测它正在接收的字节的含义。 (即使你一开始不需要它们,你也会希望在你的第二个版本中使用它们来帮助维护 backwards-compatibility 与以前版本的协议)
其他一些使您的 TCP-data 尽可能达到 fast/low-latency 的建议:
禁用 Nagle 的算法(在您完成
send()
-ing 特定的数据突发后永久或至少暂时禁用)——否则您将获得 200 多毫秒的不必要时间大部分时间都有延迟。假设您的程序 运行 在具有大量 RAM 的平台上运行,
setsockopt()
使用SO_SNDBUF
和SO_RCVBUF
选项来发送和接收socket-buffers尽可能大;这减少了缓冲区填满和数据包因 no-space-available-in-a-buffer. 而被丢弃的可能性
如果可能,请将您的发送算法设计为仅在最后可能的时刻生成 data-to-send,而不是 queue 占用大量 data-to-be-sent进步。例如,如果(由于某些 trigger-event)您的代码决定它需要尽快通过 TCP 套接字发送特定 data-structure 的当前状态,而不是序列化数据结构和 enqueueing (and/or
send()
-ing) 马上序列化的字节,只要设置一个 dirty-flag 表示需要发送结构。那么,下一次socket表示是ready-for-write,即是时候将数据结构序列化,发送给socket。好处是,如果您收到例如快速连续 10 trigger-events,使用这种 dirty-flag 设计,您仍然最终只发送一次最终版本数据结构,而不是连续发送 10 次。第二个好处是,这限制了可以 queue 等待发送的数据积压,从而减少了 data-updates. 的平均延迟
在接收端,让
recv()
调用由一个专用的 high-priority 线程在一个紧密的循环中完成,除了尽可能快地接收数据外,它做的很少可能然后 enqueue 它进行进一步处理。这里的想法是尽量减少接收 TCP 套接字的 incoming-data-buffer 变满的可能性,因为如果它确实变满,一些传入的 TCP 数据包可能会被丢弃,迫使 TCP backoff-and-resend,这会减慢速度传输.