普通TCP上的排序数据包

2020-12-24 21:24:15

对于与低级网络协议gubbins有关的公认的,不是很有用的技术黑客来说,这是一个粗糙的想法。如果您不想知道这一点,请立即移开视线。

是(哦-我会警告您,我在整个文档中说的是包而不是段的意思是这样的)。我将假设您知道几乎普遍使用的POSIXsocket API是什么。您可能会或可能不知道的是我今天要谈论的两件事:套接字API中的这些已排序数据包(aka SOCK_SEQPACKET)套接字类型和TCP规范中的紧急指针。套接字API支持多种类型的套接字。 '类型'不是指套接字后面的协议,而是指与套接字关联的语义。最著名的套接字类型是流(SOCK_STREAM)和数据报(SOCK_DGRAM);前者提供了一对连接到单个远程套接字的全双工,可靠,有序,无结构的字节流,而后者则提供了一个孔,通过该孔可以将不可靠,无序,较小的字节束发送到多个远程套接字。但是,还有其他套接字类型。最有趣的是排序数据包套接字(SOCK_SEQPACKET)。套接字API规范的实际文本中说明了顺序数据包套接字:

SOCK_SEQPACKET套接字类型类似于SOCK_STREAM类型,并且也是面向连接的。这些类型之间的唯一区别是使用SOCK_SEQPACKETtype可以维护记录边界。可以使用一个或多个输出操作发送一条记录,并使用一个或多个输入操作接收一条记录,但是单个操作绝不传输多条记录的一部分。接收者可以通过recvmsg()函数返回的已接收消息标志中的MSG_EOR标志来查看记录边界。是否强加最大记录大小是特定于协议的。

因此,基本上,它要么是一系列可靠且有序地传送的数据包,要么是其中包含记录边界的可靠,有序的流,具体取决于您如何看待它。无论哪种方式,它显然位于流(即TCP)套接字和数据报(即UDP)套接字之间的中间位置。有关MSG_EOR的内容是如何标记消息的结尾;像流数据一样读取和写入消息,但有一些细微差别:在发送时标记消息的结尾,在lastsend()中设置MSG_EOR标志,并在接收时检测消息的结尾,请使用recvmsg()和在msghdr.msg_flags字段中查找MSG_EOR标志。

(顺便说一句,说“一条记录可以使用一个或多个输出操作发送并使用一个或多个输入操作接收记录”的行有点奇怪; recv()和recvmsg()的规范说&#34 ;基于格式的套接字(例如SOCK_DGRAM和SOCK_SEQPACKETET),必须在单个操作中读取整个消息。OpenGroup邮件列表上有一个帖子。

关于MSG_WAITALLflag在序列化数据包套接字的上下文中的语义,这表明序列化数据包套接字上的消息确实可以通过多个recv()调用来传递,这确实更有意义;然后,您可以使用MSG_WAITALL强制执行一次读取,尽管如果消息大于您的缓冲区则行不通。)此外,排序数据包套接字可能是对程序员最友好的一种:它们具有很强的可靠性和排序保证力(例如TCP但与UDP不同),它们在协议级别(例如UDP,但与TCP不同)提供记录分界;因此,它们应该是网络程序员的关键工具。但是,据我所知,尚无通用的互联网传输层协议对数据包的语义进行排序。因此,SOCK_SEQPACKET不是老旧可用的套接字类型!当您直接在ATM或IR或其他合适的链路层上运行连接时,却没有人关心。

但是,一切都不会丢失!我想出了一种使用完全普通的TCP来提供序列化数据包语义的方法,它利用了该协议的一个很少使用但历史悠久的功能,我认为该功能是专门为该目的而设计的:紧急指针。

TCP规范非常简短,因此请通读并了解紧急指针的含义。基本上,它是一种设施,TCP连接的一端可以通过该功能向另一端指示在流中某处存在紧急数据。从客户端(即应用程序层协议)的角度来看,每个套接字的输出流旁边都有一个按钮,另一个套接字的输入流旁边有一个红灯,都标记为“ URGENT” !!!&#39 ;;一端写一些它认为紧急的数据,然后按一下按钮:另一端的灯立即亮起,并且一旦客户端读取了发件人在按下按钮之前写的所有数据,该灯便熄灭。 TCP模块通过其数据包的两个组件来处理此问题:“紧急指针字段有效”。 (aka URG)控制位和紧急指针(UP)。按下按钮后发送的所有数据包中的URG位置1,直到包含紧急数据的最后一个字节的URG位;数据包中存在设置的URG位,因此向接收方TCP模块指示它应打开紧急灯。 UP指向紧急数据的最后一个字节(RFC 793对此感到困惑-请参阅RFC 1122

,第4.2.2.4节,进行更正);客户端读取该字节后,接收模块可以关闭紧急灯。为了完整起见,我应该指出TCPheader中的UP字段实际上只有16位长。它将UP表示为与数据包序列号的偏移量。 RFC 793对UP大于当前序列号大于65535时的情况保持谨慎态度,但RFC2147尤其指出:

当要发送带有紧急指针的TCP数据包时(即设置了URG位),请首先计算从序列号到紧急指针的偏移量。如果偏移量小于65535,请填写“紧急”字段,然后继续进行正常的TCP处理。如果偏移量大于65535,并且偏移量大于或等于TCP数据的长度(除非您正在处理超jumbograms,否则偏移量将为该长度),请用65535填充紧急指针,然后继续正常TCP处理。

无论如何,我建议滥用紧急机制来标记消息的结尾:紧急指针只是指向当前消息的结尾。这些支持qqpacket的TCP模块没有将其连接到按钮和指示灯,而是对MSG_EOR进行了正确的处理:在send()中设置MSG_EOR会导致设置UP,而在执行recvmsg()时单击UP会导致它设置MSG_EOR。简单!

有一个小问题,那就是您不能将多个消息打包到一个TCP数据包中,因为您将无法使UP指向所有消息的末尾。这是一个耻辱,但没什么大不了的。整个排序的数据包是关于相当大的数据包。如果感觉每个数据包急需多个消息,则可以指定一个可协商的TCP选项来保存其他消息末尾指针。

我描述的机制与TCP的push函数之间存在某种程度的交互作用。推送功能的部分指定是为了使应用程序可以在它们到达数据块末尾时向TCP模块进行指示,从而不会浪费时间等待更多数据;在具有显式消息边界的情况下,它可以正常工作本身。当且仅当发信号通知消息端时,TCP模块才应发送小于MTU的数据包。其余时间,他们应该等待客户端发送足够的数据以填充完整的数据包,而不会发生任何超时。除了使push函数过时之外,显式的消息边界也使Nagle的算法变得不必要。但是,可以重新使用推送功能:可以应用显式推送来指示应该尽快传递消息(应该在传递消息结束指示的同一发送中对推送进行信号通知)。然后,TCP模块应尽其所能快速传递消息,并应向用户指示消息已被推送。与用户的这两种交互都可以通过send()或recvmsg()标志中的MSG_PUSH标志(也可能是inrecvmsg()中的MSG_PUSH_PENDING标志,以指示前面某条消息已被推送)来进行调解。像现在一样,通过PSH标志的设置来传输TCP数据包。我们甚至可以按顺序发送推送的消息,但这将非常奇怪。从本质上讲,我们正在交换push和紧急函数的语义!