使用 Boost ASIO 且适合 MTU 时,有效负载分为两个 TCP 数据包
我在 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之间只有一个交换机。
I have a problem with a boost::asio::ip::tcp::iostream. I am trying to send about 20 raw bytes. The problem is that this 20 byte payload is split into two TCP packets with 1 byte, then 19 bytes. Simple problem, why it is happening I have no idea. I am writing this for a legacy binary protocol that very much requires the payload to fit in a single TCP packet (groan).
Pasting the whole source from my program would be long and overly complex, I've posted the functional issue just within 2 functions here (tested, it does reproduce the issue);
#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();
}
To add to the strange issue, if I send the string "Hello world.", it goes in one packet. If I send 0x1, 0x2, 0x3 (the raw byte values), I get 0x1 in packet 1, then the rest of the data in the next TCP packet. I am using wireshark to look at the packets, there is only a switch between the dev machine and 192.168.1.1.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
我不确定谁会强行要求整个有效负载位于一个 TCP 数据包内。 TCP 本质上是一种流式协议,发送的数据包数量和有效负载大小等许多细节都由操作系统的 TCP 堆栈实现决定。
我会仔细检查这是否是您的协议的实际限制。
I am not sure who would have imposed such a thing as having a requirement that an entire payload be within one TCP packet. TCP by its nature is a streamed protocol and much of the details in number of packets sent and payload size etc. are left up to the TCP stack implementation of the operating system.
I would double check to see if this is an actual restriction of your protocol or not.
我同意User1的回答。您可能多次调用
operator <<
;第一次调用时,它立即通过网络发送第一个字节,然后 Nagle 算法开始发挥作用,因此剩余的数据在单个数据包内发送。然而,即使分包不是问题,即使对小块数据频繁调用套接字发送函数也是一个大问题。在套接字上调用的每个函数都会调用繁重的内核模式事务(系统调用),为每个字节调用
send
简直是疯了!您应该首先在内存中格式化消息,然后发送。对于您的设计,我建议创建一种缓存流,它将在其内部缓冲区中累积数据并将其立即发送到底层流。
I agree with User1's answer. You probably invoke
operator <<
several times; on the first invocation it immediately sends the first byte over the network, then the Nagle's algorithm comes into play, hence the remaining data is sent within a single packet.Nevertheless, even if the packetization was not an issue, the even fact that you invoke a socket sending function frequently on small pieces of data is a big problem. Every function called on a socket invokes a heavy kernel-mode transaction (system call), calling
send
for every byte is simply insane!You should first format your message in the memory, and then send it. For your design I'd suggest creating a sort of a cache stream, that would accumulate the data in its internal buffer and send it at once to the underlying stream.
将通过 TCP 套接字发送的数据视为数据包是错误的。它是一个字节流,如何构建数据是特定于应用程序的。
我建议您实现一个协议,以便接收者知道需要多少字节。实现此目的的一种流行方法是发送固定大小的标头,指示有效负载的字节数。
It is erroneous to think of data sent over a TCP socket as packets. It is a stream of bytes, how you frame the data is application specific.
I suggest you implement a protocol such that the receiver knows how many bytes to expect. One popular way to accomplish this is to send a fixed size header indicating the number of bytes for the payload.
您的代码:
将调用 3 次
operator<<
函数。由于 TCP 的 Nagle 算法,TCP 堆栈将发送可用数据
((char )0x1)
在第一次operator<<
调用之后/期间立即进行对等。因此其余数据(0x2 和 0x3)将进入下一个数据包。
避免1字节TCP段的解决方案:
使用更大的数据量调用发送函数。
Your code:
Will make 3 calls of
operator<<
function.Because of Nagle's algorithm of TCP, TCP stack will send available data
((char)0x1)
to peer immediately after/during the firstoperator<<
call.So the rest of the data (0x2 and 0x3) will go to the next packet.
Solution for avoiding 1 byte TCP segments:
Call sending functions with bigger bunch of data.
别担心,您是唯一遇到此问题的人。肯定有解决办法。事实上,您的旧协议存在两个问题,而且不止一个。
您的旧遗留协议需要一条“应用程序消息”来适应“一个且仅一个 TCP 数据包”(因为它错误地使用 TCP 面向流的协议作为面向数据包的协议)。因此,我们必须确保:
解决方案:
问题 1
您必须立即向套接字提供所有“消息”数据。目前这种情况还没有发生,因为正如其他人所概述的那样,当您使用连续的“<<”时,您使用的升压流 API 将数据以单独的调用放入套接字中。并且操作系统的底层 TCP/IP 堆栈没有足够的缓冲(并且有原因,为了更好的性能)
多种解决方案:
问题 2
你必须激活 TCP_NODELAY 选项。此选项是专门针对此类遗留协议情况而设计的。它将确保操作系统 TCP/IP 堆栈“无延迟”发送您的数据,并且不会将其与您稍后可能发送的其他应用程序消息一起缓冲。
结论
如果你解决了这两个问题,你应该没问题!
操作系统套接字 API,无论是在 Windows 还是 Linux 上,使用起来都有点棘手,但您将获得对其行为的完全控制。 Unix 示例
Don't worry, you are from from the only one to have this problem. There is definitely a solution. In fact, you have TWO problems with your legacy protocol and not only one.
Your old legacy protocol requires one "application message" to fit in "one and only one TCP packet" (because it incorrectly use a TCP stream-oriented protocol as a packet-oriented protocol). So we must make sure that :
The solution :
problem 1
You must feed your socket with all your "message" data at once. This is currently not happening because, as other people have outlined it, the boost stream API you use put data into the socket in separated calls when you use successive "<<" and the underlying TCP/IP stack of your OS doesn't buffer it enough (and with reasons, for better performance)
Multiple solutions :
problem 2
You MUST activate the TCP_NODELAY option on your socket. This option is especially made for such legacy protocol cases. It will ensure that the OS TCP/IP stack send your data "without delay" and doesn't buffer it together with another application message you may send later.
Conclusion
If you solve those two problems, you should be fine !
The OS socket API, either on windows or linux, is a bit tricky to use, but you'll gain full control about its behaviour. Unix example