5.3.1.3 PUSH
在上一小节中,我们讲述了 TCP 为了提升网络传输效率以及避免一些不必要的网络拥塞,而不惜付出些许时延的代价,以将多个小包攒成1个大包进行发送。
本小节讲述的“PUSH”,其目标则恰恰相反,它是为了减少“时延”。PUSH 本身是 TCP 报文头中的一个 Flag,如图5-90所示:
图5-90 TCP 报文结构
图5-90中的“P”标记位,代表的就是 PUSH(有时候简写为 PSH),占用1个 bit。当 PSH = 1 时,它从一定意义上来讲,就可以减少“时延”。
为什么说是“从一定意义上来讲”,为什么“时延”两个字又打了引号呢?这首先要从 TCP 接收端与它的上层之间的数据提交模型说起,如图5-91所示:
图5-91 TCP 接收端与上层之间的内存模型
图5-91中,TCP 接收方收到数据以后,首先是将数据存储在自己的接收缓存中,然后再提交给上层(应用层)相关缓存中。那么问题来了,TCP 接收方何时将这些数据提交给应用层呢?这分为两种提交模型,如图5-92、5-93所示:
图5-92 满泻提交模型
图5-93 直流提交模型
图5-92中,T0、T1 时刻,TCP接收方虽然接收了一些数据,但是其缓存并没有满,所以它暂时还不把所接收到的数据提交给应用层。待到 T3 时刻,TCP 接收方又接收了一些数据,此时其缓存已满,它才将缓存中所有的数据一次性提交给应用层。正如张学友的一首歌所唱的那样:
便爱你多些再多些至满泻
所以,我们把TCP 的这种提交模型称为“满泻提交模型”:接受缓存满了以后,才提交给应用层。
古老的磁带里藏着谁的青春
图5-93则没有图5-92那么复杂,TCP 接收方在任意时刻(Ti),只要接收到数据,就直接从接收缓存提交给应用层。我们把这种提交模型称为“直流提交模型”:直接提交过去。
之所以会有这两种模型,是因为 TCP 认为直流提交模型的效率并不高,所以期望自己先缓存一下,待缓存满了以后再一次新提交,这样提交的效率会高些。
客观地说,所谓的满泻提交模型的效率会高些,似乎并没有多大意义,毕竟有的 TCP 实现(比如 BSD(Berkeley Software Distribution)),就是采用直流提交模型,这么多年来也没觉得有什么效率问题。
而且,让人沮丧的是,满泻提交模型对于效率提升没有什么显著作用,但是它的坏处——时延——却是非常显著的。想象一下图5-92中,如果从 T2 时刻到 T3 时刻,中间如果经历了很长时间(比如几秒、甚至几小时),那么 TCP 几乎就是不可用了。
为此,TCP 提出了“PUSH”方案来解决这个问题。我们再回到前面所提的问题:为什么“时延”要打引号,这是因为这个时延并不是一般意义上的网络时延,而是 TCP 接收方与应用层之间的时延;为什么说是“从一定意义上来讲”,是因为这个时延原本可以避免,比如采用直流提交模式。
唉......说什么好呢?反正笔者感觉“满泻提交模式”就是一个车祸现场。好吧,我们就从车祸现场说起,看看 PUSH 是怎么拯救这个车祸现场的,如图5-94所示:
图5-86 PUSH:拯救车祸现场
图5-94表达的是满泻提交模式与 PUSH 相遇的故事。T0 时刻的报文,其 PSH = 0(图5-92、5-93中的 PSH 都是0),所以此时数据(data0,3个字节)并不会被 TCP提交给应用层。T1 时刻的报文,PSH = 1,此时 TCP 会将已接收未提交的数据全部提交给应用层缓存:data1(4个字节) + data0(3个字节)。
也就是说,对于接收方而言,TCP 只要看到 PSH = 1,无论其接收缓存是否已满,它都会将缓存中所接收的所有数据(包括本次接收和历史接收),全部一次性提交给应用层。
由此看来,PSH 标签(等于1),对于 TCP 接收方来说,其含义就是:立刻马上全部提交(PUSH)接收缓存中的数据。也正是源于此,PSH 标签可以有效地减少或者避免 TCP 接收方缓存提价的时延。
PSH 标签作用如此明显,那么它是怎么来的呢?当然,是由发送方打上的——发送方决定 PSH 等于0或者等于1。
这里的发送方指的是两个角色:TCP 发送方、用户(调用 TCP 接口发送数据,从程序的角度,是应用层。当然,应用层的代码也是人编写的,所以把用户理解为“人”,也无不可)。
我们先说用户这个角色。用户设置 PSH = 0 或者 PSH = 1,是一个主动行为。也就是说,用户根据不同的场景、不同的应用特点,决定 PSH 的值是多少。这个我们就不展开说了,毕竟这里没有绝对的规则——当然,一般来说,如果要追求低时延,报文中最好还是打上 PSH 标签。
PSH = 1,不仅可以是 TCP 接收方立刻提交数据,也可以使 TCP 发送方立刻发送数据。在“5.3.1.2 窗口大小与发送效率”中,我们讲过,为了提高发送效率,TCP 发送方可能会延迟发送(把多个小包攒成1个大包)。但是这样做的缺点也是明显的:在发送方造成了时延。PSH 标签正是为了解决此问题,如图5-95所示:
图5-87 PUSH:立刻发送数据
图5-95中,T0 时刻,用户(应用层)调用 TCP 的 SEND 接口,发送了 data0(3个字节),此时 PSH = 0,TCP 发送方并不会立刻发送 data0(假设 TCP 采用了 Nagle 算法)。T1 时刻,用户又调用 SEND 接口,发送了 data1(4个字节)。此时,data1 + data0,一共也才7个字节,仍然是属于小包,但是由于用户设置了 PSH 标签(PSH = 1),TCP 会立刻将其发送缓存中的数据发送出去(当然发送本身,仍然要受发送窗口、MSS 等约束)。
所以,PSH 标签,对于 TCP 发送方而言,其目的就是:立刻马上全部发送发送缓存中的数据。也正是源于此,PSH 标签也可以有效地减少或者避免 TCP 发送方缓存发送的时延。
前面我们一直揶揄 TCP 的“满泻提交模式”,说它是车祸现场。现在来看,造成时延的,不仅仅是 TCP 接收方的不太必要数据延迟提交(满泻提交模式),也有 TCP 发送方的延迟发送。所以如果说 PUSH 仅仅是为了拯救 TCP 发送方的“车祸”,那是不公平的,^_^
这个锅一人一半
PSH 标签,不仅用户可以主动打上,TCP 发送方(TCP 协议栈)也会自动打上,即:就算用户没有打上 PSH 标签,TCP 发送方也会根据具体情形,自动打上 PSH 标签。一个典型的场景就是“发送缓存空了(each write empties the sender buffer)”,如图5-96所示:
图5-88 发送缓存为空
图5-96中,TCP 发送方将一批 data 发送出去以后,会发现自己发送缓冲区空了——未发送已允许,没有数据;未发送未允许,没有数据——也就说没有数据可以发送了。于是 TCP 在发送这批数据时,会自动打上 PSH 标签(PSH = 1)。
自动打上 PSH 标签,并不是为了发送,因为无论打不打标签,TCP 都会发送。打上标签是为了 TCP 接收方。前文我们说过,TCP 接收方可能会采用“满泻提交模式”,当这批数据发送过去,接收方收到这批数据以后,接收方的缓存很有可能是未满的,那么 TCP 接收方就会在那里等待——等待下一批数据的到来......
但是,TCP 接收方又能等到什么呢?对于发送方来说,TCP 发现自己的发送缓存已经空了,它自己都不知道下一批数据何时到来,它能让对方等待吗?
国足如果赢得世界冠军,我就娶你
最感人的誓言不是等到海枯石烂,而是等到中国足球队赢得世界冠军。TCP 的爱情没有誓言,而是放手。既然自己都不知道下一批数据在哪里,那就赶紧通知对方收到这批数据以后,马上提交个应用层,不必再等待了。
莫等闲,白了少年头,空悲切
如果我们把“莫等闲”的意思篡改一下,整句话的意思就可以形象地解释“如果发送缓存为空,TCP 发送方会自动在发送报文里打上 PSH 标签(PSH = 1)”:我已经很闲了(发送缓存为空),你莫要等待了(收到数据赶紧提交给应用层吧),否则恐怕会白了少年头,空悲切!
除了“发送缓存空”这个场景,还有一个场景,TCP 发送方也会自动打上 PSH 标签,那就是:用户调用 CLOSE 接口,以关闭一个 TCP 连接。这个时候 TCP 会发送1个 FIN 报文,并且为这个报文打上 PSH 标签(PSH = 1)。
这很好理解,既然都决定要关闭连接了,那就更有必要通知接收方收到报文后,马上提交给应用层,因为自己再也不会发送数据了——这下彻底空了。
世界那么空
我和谁相拥