当使用Boost ASIO时,有效载荷拆分为两个TCP数据包,当它适合MTU时

Payload split over two TCP packets when using Boost ASIO, when it fits within the MTU

本文关键字:两个 MTU TCP 数据包 Boost ASIO 有效 拆分      更新时间:2023-10-16

我有一个boost::asio::ip::tcp::iostream的问题。我正在尝试发送大约20个原始字节。问题是这个20字节的有效负载被分成两个TCP数据包,分别是1字节和19字节。问题很简单,为什么会这样我也不知道。我写这篇文章是为了一个遗留的二进制协议,它非常需要有效负载能够容纳在单个TCP数据包中(呻吟)。

从我的程序粘贴整个源代码会很长,过于复杂,我已经发布了功能问题只是在2个函数在这里(经过测试,它确实再现的问题);

#include <iostream>
// BEGIN cygwin nastyness
// The following macros and conditions are to address a Boost compile
// issue on cygwin. https://svn.boost.org/trac/boost/ticket/4816
//
/// 1st issue
#include <boost/asio/detail/pipe_select_interrupter.hpp>
/// 2nd issue
#ifdef __CYGWIN__
#include <termios.h>
#ifdef cfgetospeed
#define __cfgetospeed__impl(tp) cfgetospeed(tp)
#undef cfgetospeed
inline speed_t cfgetospeed(const struct termios *tp)
{
    return __cfgetospeed__impl(tp);
}
#undef __cfgetospeed__impl
#endif /// cfgetospeed is a macro
/// 3rd issue
#undef __CYGWIN__
#include <boost/asio/detail/buffer_sequence_adapter.hpp>
#define __CYGWIN__
#endif
// END cygwin nastyness.
#include <boost/array.hpp>
#include <boost/asio.hpp>
#include <iostream>
typedef boost::asio::ip::tcp::iostream networkStream;
void writeTestingData(networkStream* out) {
        *out << "Hello world." << std::flush;
//      *out << (char) 0x1 << (char) 0x2 << (char) 0x3 << std::flush;
}
int main() {
        networkStream out("192.168.1.1", "502");
        assert(out.good());
        writeTestingData(&out);
        out.close();
}

添加一个奇怪的问题,如果我发送字符串"Hello world.",它会在一个包中。如果我发送0x1、0x2、0x3(原始字节值),我在数据包1中得到0x1,然后在下一个TCP数据包中得到其余的数据。我正在使用wireshark来查看数据包,在开发机器和192.168.1.1之间只有一个开关。

代码:

out << (char) 0x1 << (char) 0x2 << (char) 0x3;

将调用3次operator<<函数。

由于TCP的Nagle算法,TCP栈将在第一次operator<<调用之后/期间立即将可用数据((char)0x1)发送给对等体。因此,其余的数据(0x2和0x3)将进入下一个数据包。

避免1字节TCP段的解决方案:使用更大的数据集调用发送函数。

别担心,你是唯一一个有这个问题的人。肯定有解决办法。实际上,您的遗留协议存在两个问题,而不仅仅是一个。

您的旧遗留协议需要一个"应用程序消息"来适应"一个且只有一个TCP数据包"(因为它错误地将TCP面向流的协议用作面向数据包的协议)。所以我们必须确保:

  1. 没有"应用程序消息"被分割成多个TCP数据包(你看到的问题)
  2. 没有TCP数据包包含多个"应用程序消息"(你没有看到这个,但它肯定会发生)

解决方案:

<标题>问题1

你必须一次给你的套接字提供所有的"消息"数据。这目前还没有发生,因为,正如其他人所概述的那样,当您使用连续的"<<"并且操作系统的底层TCP/IP堆栈没有足够的缓冲(并且有理由,为了更好的性能)时,您使用的boost流API将数据放入分隔调用的套接字中

多个解:

  • 你传递一个字符缓冲区而不是单独的字符,这样你只调用一次<<
  • 你忘记了boost,打开一个OS套接字并在一个调用中发送它()(在windows上,查找"winsock2"API,或者在unix/cygwin上查找"sys/socket.h")
<标题>问题2 h1> 必须在套接字上激活TCP_NODELAY选项。这个选项特别适用于这种遗留协议的情况。它将确保操作系统TCP/IP堆栈"无延迟"地发送你的数据,并且不会将它与你可能稍后发送的另一个应用程序消息一起缓冲。
  • 如果你坚持使用Boost,寻找TCP_NODELAY选项,它在doc
  • 如果你使用OS套接字,你必须在套接字上使用setsockopt()函数。
结论

如果你解决了这两个问题,你就没事了!

操作系统套接字API,无论是在windows还是linux上,使用起来都有点棘手,但你将完全控制它的行为。Unix示例

我不确定谁会强加这样的要求,即整个有效负载在一个TCP数据包内。TCP本质上是一个流协议,发送的数据包数量和有效负载大小等许多细节都留给了操作系统的TCP堆栈实现。

我会仔细检查一下,看看这是否是您的协议的实际限制。

我同意User1的回答。您可能多次调用operator <<;在第一次调用时,它立即通过网络发送第一个字节,然后内格尔算法开始发挥作用,因此剩余的数据在单个数据包中发送。

然而,即使打包不是问题,对小块数据频繁调用套接字发送函数这一事实也是一个大问题。在套接字上调用的每个函数都会调用一个繁重的内核模式事务(系统调用),为每个字节调用send简直是疯了!

你应该先在内存中格式化你的信息,然后再发送。对于您的设计,我建议创建一种缓存流,它将在其内部缓冲区中积累数据并立即将其发送到底层流。

认为通过TCP套接字发送的数据是数据包是错误的。它是一个字节流,你如何构建数据是应用程序特定的。

有什么建议吗?

我建议你实现一个协议,这样接收者知道需要多少字节。一种常用的方法是发送固定大小的头,指示有效负载的字节数。