TCP消息帧+ recv()[linux]:好的约定?

时间:2011-11-16 07:16:12

标签: linux sockets tcp message recv

我正在尝试在Linux上创建一个p2p应用程序,我想尽可能高效地运行。

我遇到的问题是管理数据包。我们知道,recv()缓冲区中可能随时有多个数据包,因此需要使用某种消息帧系统来确保多个数据包不被视为一个大数据包。

所以目前我的数据包结构是:

(u16int Packet Length):(Packet Data)

这需要两次调用recv();一个用于获取数据包大小,一个用于获取数据包。

这有两个主要问题:

1. A malicious peer could send a packet with a size header of 
  something large, but not send any more data. The application will 
  hang on the second recv(), waiting for data that will never come.
2. Assuming that calling Recv() has a noticeable performance penalty
  (I actually have no idea, correct me if I am wrong) calling Recv() twice 
  will slow the program down.

为了最佳效率和稳定性,构建数据包/接收系统的最佳方法是什么?其他应用程序如何做到这一点?你推荐什么?

提前谢谢你。

5 个答案:

答案 0 :(得分:5)

我认为你在TCP流中的消息“框架”是正确的。

您可以考虑在每个帧的前面放置一个“魔术cookie”(例如,除了数据包长度之外,在每个帧头的顶部写入32位int“0xdeadbeef”),这样很明显你的是在每个recv()对的第一个上读取帧头。消息开头没有魔术整数,你已经不同步,需要断开连接。

多次recv()调用可能不会受到性能影响。事实上,由于TCP消息可以以不可预测的方式进行分段,合并和停顿,因此您可能需要在循环中调用recv(),直到获得所需的所有数据。这包括您的两个字节标头以及有效负载字节的较大读取。完全有可能使用2字节缓冲区调用“recv”来读取消息的“大小”,但只返回1字节。 (再次调用recv,你将得到后续的字节)。我告诉团队中的开发人员 - 对网络解析器进行编码,就好像recv一次只能传输1个字节一样。

您可以使用非阻塞套接字和“选择”调用来避免挂起。如果数据未在合理的时间内到达(或者数据到达的数量超出预期 - 这样就无法同步下一条消息),您只需断开连接即可。

我正在研究自己的P2P项目。愿意交易票据。如果你愿意,可以离线关注我。

答案 1 :(得分:2)

我不同意其他人,TCP是一种可靠的协议,因此除非您担心客户端代码不稳定或未经请求的客户端连接到您的端口号,否则数据包魔术标头是无用的。

为每个客户端创建一个缓冲区,并使用非阻塞套接字和select / poll / epoll / kqueue。如果客户端有可用数据,请尽可能多地阅读,如果您阅读更多“数据包”并不重要。然后检查你是否已经阅读了足够的大小字段,如果是,请检查你是否读过整个数据包(或更多)。如果是,请处理数据包。然后,如果有更多数据,您可以重复此过程。如果剩下部分数据包,您可以将其移动到缓冲区的开头,或使用循环缓冲区,这样您就不必执行这些memmove-s。

可以在select / ...循环中处理客户端超时。

如果您正在对收到的数据包数据执行复杂操作,那就是我会使用的。如果您只是将结果写入文件(更大的块),那么sendfile / splice会产生更好的性能。只需读取数据包长度(可能是多次读取),然后使用多次调用sendfile,直到您读取整个数据包(跟踪剩余读取数量)。

答案 2 :(得分:1)

您可以对recv()使用非阻塞调用(通过在套接字上设置SOCK_NONBLOCK),并等待它们准备好在循环中使用select()(带超时)读取数据。

然后,如果文件描述符处于“等待数据”状态的时间太长,您可以关闭套接字。

答案 3 :(得分:1)

TCP是面向流的协议 - 它实际上没有任何数据包概念。因此,除了在一次recv()调用中收到多个应用程序层数据包之外,您还可能只接收应用程序层数据包的部分,其余部分将在未来{{1调用。

这意味着通过在每次recv()调用时接收尽可能多的数据,然后在应用层缓冲区中缓冲该数据,直到您拥有至少一个完整的应用层数据包,即可获得强大的接收器行为。这也可以避免两次调用recv()问题。

要始终在每个recv()收到尽可能多的数据而不阻止,您应该使用非阻塞套接字并调用recv(),直到它返回-1并将recv()设置为{ {1}}。

答案 4 :(得分:0)

正如其他人所说,一个领先的幻数(OT: man file )是一个很好的(99.999999%)解决方案来识别数据报边界,而超时(使用非阻塞recv())是好的用于检测丢失/延迟数据包。

如果你指望攻击者,你应该在你的数据包中加上 CRC 。如果专业攻击者真的想要,他/她迟早会弄清楚你的CRC是如何工作的,但它比创建没有CRC的数据包更难。 (另外,如果安全性至关重要,您将在网上找到SSL库/示例/代码。)