TCP的数据交互

 

小包的交互

如果我们像telnet那样输入字符,则每次都会触发一个小包的传输,可能只有一个字节的数据内容(数据包是20字节的ip头+20字节的tcp头为41字节)。


当三次握手建立后,客户端像服务端发送数据,分别发送G,E,T。

No.40的TCP内容如下:

Transmission Control Protocol,Src Port:1234, Dst Port:80, Seq:1, Ack:1, Len:1

可以看到数据内容长度为1字节。

序号40的时候客户端发送G,可以看到服务端延迟了接近200毫秒后才回了一个ack。

之后客户端又发送了E和T,这次服务端又延迟确认了,两次都是等待100毫秒左右才发回了ack包。 

服务端不会立即确认收到的数据,而是等待一会,如果在这段时间内有数据要发送,则ack和数据一起发送给客户端,这种现象叫捎带ACK

而如果在这段时间之内没有数据发送,则会等待超时后发回一个ack。一般linux系统为40毫秒,《TCP/IP详解》里面介绍的是早期的linux版本为200毫秒,最大的延迟不应该超过500毫秒。

 

 

 

Nagle算法

因为在广域网上发送1字节内容的小包很不划算浪费带宽。所以该算法规定最多只能有一个未被确认的小分组,在该分组的确认达到之前不能发送其他小分组。相反,TCP手机这些少量的分组,并在确认到来时以一个分组的方式发出去。

该算法的优越之处在于它是自适应的:确认达到的越快,数据发送的越快。

伪代码如下:

if there is new data to send
  if the window size >= MSS and available data is >= MSS
    send complete MSS segment now
  else
    if there is unconfirmed data still in the pipe
      enqueue data in the buffer until an acknowledge is received
    else
      send data immediately
    end if
  end if
end if

 API必须提供TCP_NODELAY选项来关闭nagle算法

对于交互性很强的X窗口来说,应当关闭nagle算法,否则用户会感到明显的延迟。

 

对于

write(head);
write(body);
read();

 这种代码,就会触发nagle算法,第一个write(head)发送后,服务端还没有来得及确认(捎带ACK),需要等回复的的数据+ACK一起确认给客户端,而客户端发送给服务端的数据是不完整的,只有head,没有body。服务端的应用层需要等待body才能处理,这样就导致了延迟确认,其情况类似第一个截图。

其过程是:(如果是debug的话,最终的现象是客户端会在read()那里卡住一小会)

1.客户端发送head

2.服务端只收到了head,等待body

3.客户端继续发送body,但此时没有等待服务端的ack,于是将body放到缓存队列中,延迟发送(如果body

   很大则会立刻发送)

4.服务端等待超时,发送了一个ack确认给客户端(对head的确认)

5.客户端收到确认后,将剩下的body发送给服务端

6.服务端收到body后,应用层可以继续处理逻辑了,处理完后将结果发给客户端

7.客户端收到结果后继续执行或者可以打印出结果

 

如果将两个write()合并在一起,做成一个完成的数据发送给服务端,这样服务端应用层就看以处理了,处理完之后就会返回,这样就不会出现延迟确认了,服务端每次收到的都是完整的数据,可以立刻处理并返回,同样客户端read()的时候也不会卡主了。


这里就没有延迟了

 

 

 

数据的确认和MSS

如果是单独发送一个ack,ack的序号是syn+len,也就是发送方的序列号+发送的字节长度

发送方发送的序号为4206382601,len为13,接收方确认的ack为4206382614

如果是捎带ACK也是一样的。

MSS的含义是最大报文段长度,以太网最大数据长度是1500字节,所以MSS最多只能是1460字节(去掉20字节的IP首部和20字节的TCP首部)。理论上TCP是上层的它不用关心底层是如果分片的,这个分片是由IP层去做的,IP将数据发片后叫给链路层再发送,TCP之所以需要有MSS自己做分片,甚至有些违反了上层知道底层数据大小这么一个细节,是从性能上考虑的。

比如要发送一个10K的数据,作为一个报文段发送,那么最终到链路层会分成很多帧,如果TCP不做分片的话,假设其中一个帧丢失了,那么对方会要求重传,这样发送端不得不重新发送者10K的数据,严重浪费了带宽。而如果TCP自己分片的话,只需要发送丢失的那个报文段即可。

 

 

 

 

滑动窗口


缓冲窗口可以告知发送方,接收方能接收的数据量大小。当窗口为0时,接收方就不能发送数据了。

这里也会引发很多很多,比如糊涂窗口综合征,窗口本来很大,但是接收方处理不过来,告知发送方窗口为一个较小的值,最后发送发每次都发送小包。

使用三个术语来描述窗口左右边沿的运动:

1.称窗口左边沿向右边沿靠近为窗口合拢。这种现象发生在数据被发送和确认时。

2.当窗口右右边沿向右移动时将允许发送更多的数据,我们称之为窗口张开。这种现象发生在另一端的

   接收进程读取已经确认的数据并释放了TCP的接收缓存时。

3.当右边沿向左移动时,我们称之为窗口的收缩,RFC强烈建议不要使用这种方式。

 

这里有一个例子,客户端<---->服务端,客户端发送helloworld,服务端回显helloworld。

同时故意把发送和接收缓冲区设置很小,截图如下:


打红框的是IP尾数为116的机器,116向尾数为133的机器发送helloworld,然后133回显将同样的内容发给166,由于166接收不过来了,可以看到166的滑动窗口慢慢变小,也就是窗口左边慢慢向右移动。如果窗口变为0了则表示接收不过来了,133就无法发送数据了。

 

滑动窗口可以的控制可以分成三种

1.停-等协议(1比特协议),这种情况下发送方和接收方的窗口都是1,每次都必须发送一个字节,然后等待确认再发送,发送方和接收方的状态图如下:


发送方有一个发送超时设置,当一个数据包经过一段时间之后没有收到ACK,就重新发送。
 

2.后退N协议

停等协议无疑效率很低。后退N协议是发送一个报文段后不等待确认,继续发送数据,而且在每发送完一个报文段后都设置一个定时器,在一段时间内收不到确认的ack,就重新发送这个报文段。

比如发送方一口气发送了1--10个报文段,而报文段3接收出错或者没有收到ack,则必须重新发送3-10这几个报文段。在网络不佳的情况下,后退N协议反而会对网络造成更大的拥塞。

 

3.选择重传协议

后退N协议在网络很差的时候会造成网络拥塞,接收方收到1-3这几个报文段,又收到了5-10这几个报文段。此时如果按照后退N协议发送方就会重发4-10这几个报文段。选择重传协议可以让接收方通告发送方,只重传出错的报文段4即可。但是这必须要求接收方有更多的缓冲区可以存下更多的报文段。等这些报文段都达到后一起交给应用层。

 

发送端和接收端的滑动窗口交互过程


 

 

 

 

坚持定时器(persist timer)

如果接收方的窗口已经满了,会通告发送方一个窗口为0的确认报文段,这样发送方将不再发送数据。之后接收方处理完逻辑之后窗口又打开了可以接收数据了,便向发送方发一个非0窗口的确认报文段。如果这个确认报文段丢失会怎样?

发送方等待接收一个非0的窗口确认,这样好发送数据;而接收方此时窗口大于0了可以接收数据了,但是发送方却不知道,双方进入死锁状态。

为了防止这种情况,发送方使用一个坚持定时器(persist timer)来周期性的向接收方查询,以便发现窗口是否已增大。这些从发送方发出的报文段称之为窗口探查(window probe)。


 上图中,发送方75.1每隔一秒都会发送一个窗口探查,接收端75.132会返回一个确认报文,告诉发送方此时窗口大小为0,发送方仍旧不能发送数据。直到窗口大于0发送方才可以继续发送数据。

 

 

 

糊涂窗口综合征(silly window syndrome)

基于窗口流量控制方案,可能会导致接收方每次都通告一个小窗口,比如窗口大小为1,这样发送方仍然发送数据,但是报文段长41(20字节的IP头和20字节的TCP首部以及1字节的数据)。这样对带宽浪费很大,这种现象可以在两端中的任何一端

1.如果是接收端引起的;那么如果接收到的数据导致窗口小于某个值,直接发送一个窗口为0的ack,这样就阻止发送端继续发数据了。等到接收端处理完一些数据了,窗口size大于等于MSS或者,缓冲区中有一半为空,就可以发送一个非0窗口让发送方继续发送数据。

2.如果是发送端引起的;那么使用nagle算法,有两个条件:

    1)等到窗口size大于等于MSS或者发送数据size大于等于MSS

    2)等待时间超过200毫秒

    当满足两个条件之一时就发送数据,否则就继攒数据,到一定量再发送。

 

 

 

紧急指针

    TCP提供了"紧急方式"(urgent mode),它使一段可以告诉另一端有些具有某种方式的"紧急数据"已经放置在普通的数据流中。另一端被通知这个紧急数据已被放置在普通数据流中,由接收方决定如何处理。

    可以通过设置TCP首部中的两个字段来发出这种从一端到另一端的紧急数据已经被放置在数据流中的通知。URG比特被置为1,并且一个16bit的紧急指针被置为一个正的偏移量,该偏移量必须与TCP首部中的序号字段相加,以便得出紧急数据的最后一个字节的序号。

    只要从接收方当前读取位置到紧急数据指针之间有数据存在,就认为应用程序处于“紧急方式”。在紧急指针通过之后,应用程序便转回到正常方式。

   不幸的是,需要实现不正确的称TCP紧急方式为带外数据(out-of-band data).

   在接收端通告了一个0窗口后,发送方不能发送任何数据,但是可以发送紧急指针和URG标志。

 

 

PUSH标志

对于客户端来说,这个标志通告TCP在向服务端发送一个报文段时,不要因等待额外数据而使已提交数据在

  缓存中滞留。

对于服务端来说,这个标志通告TCP需要立即将这些数据递交给服务端进程而不能等待判断是否还会有额外

  的数据达到。

通过程序模拟一个GET请求给baidu,由于返回的HTML内容远超过了MSS大小,所以数据需要分组,在客户端收到之后需要将多个小分组合并。


TCP自己有MSS(Maximum Segment Size 最大报文段长度),这个长度是小于MTU的(Maximum Transmission Unit最大传输单元)。

这里相当于服务端发送一个大数据,超过了MSS,于是在TCP这层就做了分片,将数据拆成了若干个小包,每个小包都是小于等于MSS的,注意这里并不会使用到IP层的分片,对于IP来说每个数据包都是完成的,只有UDP需要IP层做分片,TCP层自己就可以做了。

对于UDP的组装是需要IP层去组装的,而TCP层是不需要IP层组装,TCP层自己有一个PSH标志,当最后一个分片的数据被接收到后,带有了PSH标志的数据会告知不要缓存全部交给应用层处理。而TCP在发送的时候每个序列号都是递增的,所以可以根据序列号确定谁在前谁在后。

当收到了A1,A2,A3,B1,B2,B3,A4这些数据包后,因为A是先发送的,所以A4的序列号一定是小于B1的,而A4的数据包中还会附带PSH标志,这样就之前收到的A1,A2,A3和A4一起交给了应用层。


从这个截图可以看到,IP层没有偏移量,所以不是IP层分片的,同时这是A这个大数据的最后一个分片,所以TCP层带了一个PSH标志。

 

 

 

参考:

Nagle's algorithm

Nagle算法

Nagle算法TCP_NODELAY和TCP_CORK

网络编程中Nagle算法和Delayed ACK的测试

TCP滑动窗口演示

Sliding Window演示

TCP滑动窗口

TCP/IP中的拥塞窗口

滑动窗口

TCP的那些事儿(上)

TCP的那些事儿(下)

 

 

 

猜你喜欢

转载自xxniao.iteye.com/blog/2076457