网络通信之TCP协议

TCP是一个巨复杂的协议,在网络通信中他需要解决很多问题,所以这里是带着大家来了解TCP协议的魅力,关于协议的细节,推荐去看W.Richard Stevens的《TCP/IP 详解 卷1:协议》。

一. tcp概念

TCP四元组:[源ip,源端口号,目的ip,目的端口号]


TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

三个概念:

面向连接:必须在建立连接前确认对方链路可达并准备好才开始通信,建立后对状态的保持。(udp直接通信,不会询问接收端是否可达并准备接收数据)

字节流:tcp传输的是byte类型数据。

可靠:保证数据报能够到达接收端(超时重传机制、丢包重传及数据顺序一致)(真的一定可靠吗?)

 

二. tcp如何建立连接

2.1 两军问题

本质:在不可靠的通信链路上试图通过通信达成一致。


扫描二维码关注公众号,回复: 938034 查看本文章

问题描述:一支白军被围困在一个山谷中,山谷的两侧是蓝军。困在山谷中的白军人数多余山谷两侧的任意一支蓝军,而少于两支蓝军的中和。若一支蓝军对白军单独发起进攻,则必败无疑;但若两支蓝军同时发起进攻,则可取胜。两只蓝军希望同时发起进攻,这样他们就要传递消息,以便确定发起进攻的具体时间。假设他们只能派遣士兵穿越白军所在的山谷(唯一的通信信道)来传递消息,那么在穿越山谷是,士兵有可能被俘虏,从而造成消息的丢失。现在的问题是:如何通信,以便蓝军必胜。

通信策略:


最后发出消息的一方永远不知道对方是否收到这个消息,因此不存在使蓝队必胜的通信协议。

结论在不可靠的通信中,不管几次握手都是有风险的。因为永远无法确定最后一次通信送达。通信次数大于3后并不能提升通信的可靠性。这是tcp采用三次握手的原因。

2.2 tcp的报文格式


tcp标志位

ACK:确认标志位,确认可以发送数据,建立连接后act为1。

SYN:建立连接标志位,在连接建立时用来同步序号,syn为1表示这是一个连接请求或连接接收报文。

FIN:结束一个连接的标志位,FIN=1表示释放连接。

URG:当URG=1时,表示报文段中有紧急数据,应尽快传送。

 PSH:当发送端PSH=1时,接收端尽快的交付给应用进程。

 RST:当RST=1时,表明TCP连接中出现严重差错,必须释放连接,再重新建立连接。

其他数据

序   号seq:tcp建立连接是会随机生成一个序号,建立连接后tcp进行通信全部是基于这个序号(也是tcp传输数据顺序一致的保证)。

确认号ack:接受端确认收到数据对seq加一。

窗口:用来控制对方发送的数据量,通知发放已确定的发送窗口上限。

2.3 tcp三次握手


三次握手的过程:

客户端主动打开连接,发送ACT=0,SYN=1表示请求建立连接,产生随机数x为序号seq,给服务器发起第一次握手;

服务器接收到客户端的请求后,发送SYN=1,ACK=1表示连接到接收报文,确认号act=x+1表示知道了客户端的seq,同时产生服务器的随机数y作为序号seq,发送给客户端,即为第二次握手;

最后客户端设置AC=1表示建立请求,设置act=y+1,seq=x+1表示发送的确认消息。即为第三次握手。

经过这三个步骤后,客户端也服务器通过三次握手建立起tcp连接。


三. tcp如何保证可靠性

tcp建立了ip协议之上,ip协议只提供在源地址和目的地址之间传输数据包,并不负责保证数据的可靠性。而tcp通过一系列的机制保证了传输的可靠性。

3.1 数据顺序一致性


TCP报文依赖IP分组传输,IP分组的到达可能失去顺序,tcp通过tcp报文中的序号seq标记了数据的发送顺序,在目的端对接收的tcp报文按照seq重新排序,然后提交给应用层,从而保证了应用层收到的数据是有序一致的。


3.2 重传机制

tcp需要保证所有数据包都可以到达,所以需要提供重传机制。

网络通信中可能会因为网络不好、超时等原因导致出现丢包,如何解决网络丢包问题?tcp采用接收端通过ack来告知发送端前面的ack-1个字节已经被接受了,未被确认了报文在超时后进行重发。

接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,于是回ack 2,然后收到了3、4、5(注意此时2没收到),tcp怎么处理?(tcp不能跳着确认,只能确认最大接收到的连续的包,不然会被认为之前的都收到了)

3.2.1 超时重传机制

不回ack,死等2,当发送端发现收不到2的ack超时后,会重传2,一旦接收方收到2,会ack回6,就意味着2、3、4、5都收到了。但是这也会让发送端认为3、4、5都丢了,导致3、4、5都重传。对此有两个方案:

  1. 是重传timeout的第一个包(重传2) 可以节省带宽,但是慢,因为你不知道是2接收到还是2、3、4、5中还有其他没接收到。
  2. 还是重传接下来所有数据(重传2、3、4、5)?  快一些,但是浪费带宽。
3.2.2 快速重传机制

TCP引入了一种叫Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就ack最后那个可能被丢了的包,如果发送方连续收到3次相同的ack,就重传。


同样是刚才的数据包,收到3、4、5的数据后,接收端给发送端返回ack=2的确认,根据快速重传机制,发送端知道了2还没有到达,于是不等timeout。马上重传2,然后接收端收到了2,由于之前就接收到3、4、5,于是返回ack=6。

Fast Retransmit只解决了一个问题,就是timeout的问题,它依然面临一个艰难的选择,就是,是重传之前的一个还是重传所有的问题。

3.2.3 TCP SACK

对于上面那个问题,TCP提供了Selective Acknowledgment (SACK)方法,这种方式需要在TCP头里加一个SACK的东西,ACK还是Fast Retransmit的ACK,SACK则是汇报收到的数据碎版。

接收端的ack仍然返回连续最大的确认值,在sack中返回接收端接收到的大于ack的信息,如上图,300-499的数据没有接收到,但是接收到500-699的数据,此时接收端发送ack=300,sack为500-700的返回消息,当返回消息三次都为ack=300时,发送端就可以知道300-499的数据没有接收到,发送端就可以只重传300-499的数据了。

此外,还有ack丢包,网络延时的问题。

3.3 流量控制

解决了传输可靠性的问题,接下来还有一个很大的问题,如何在一个网络中根据不同的情况来动态调整自己的发包速度。


从tcp的重传机制中,我们知道Fast Retransmit和timeout是决定重传的两个因素。Fast Retransmit好定制,但是timeout的设置就很麻烦,因为在不同的网络环境下,timeout本身就不是一个定值,只能动态的设置,为了动态设置timeout,tcp引入了RTT(Round Trip Time)算法。Round Trip Time就是一个数据包从发出去到回来的时间。这样发送端就大约知道需要多少的时间,从而可以方便地设置Timeout——RTO(Retransmission TimeOut),以让我们的重传机制更高效。 

3.3.1 RTT算法
  • 1)首先,先采样RTT,记下最近好几次的RTT值。
  • 2)然后做平滑计算SRTT( Smoothed RTT),公式为:(其中的 α 取值在0.8 到 0.9之间,这个算是加权移动平均)
         SRTT = ( α * SRTT ) + ((1- α) * RTT)
  • 3)开始计算RTO。公式如下:
         RTO = min [ UBOUND,  max [ LBOUND,   (β * SRTT) ]  ]

UBOUND是最大的timeout时间,上限值

LBOUND是最小的timeout时间,下限值

β 值一般在1.3到2.0之间。

3.3.2 Karn / Partridge 算法

但是RTT算法在重传的时候会出有一个终极问题——你是用第一次发数据的时间和ack回来的时间做RTT样本值,还是用重传的时间和ACK回来的时间做RTT样本值?


一种情况是ack没回来,所以重传。如果你计算第一次发送和ACK的时间,那么,明显算大了。另外一种情况是ack回来慢了,但是导致了重传,但刚重传不一会儿,之前ACK就回来了。如果你是算重传的时间和ACK回来的时间的差,就会算短了。

Karn / Partridge 算法对这个问题的解决方法忽略重传,不把重传的RTT做采样。而只要一发生重传,就对现有的RTO值翻倍(这就是所谓的 Exponential backoff)。

3.3.3 Jacobson / Karels 算法

Karn / Partridge 算法发生重传就翻倍的方式对RTT的估算也不准确,而“加权移动平均”如果RTT有一个大的波动的话,很难被发现,因为被平滑掉了。Jacobson / Karels 算法引入了最新的RTT的采样和平滑过的SRTT的差距做因子来计算。 

  • SRTT = SRTT + α (RTT – SRTT) :计算平滑RTT;
  • DevRTT = (1-β)*DevRTT + β*(|RTT-SRTT|) :计算平滑RTT和真实的差距(加权移动平均);
  • RTO= μ * SRTT + ∂ *DevRTT 

在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4 ,而这个算法在被用在今天的TCP协议中。

3.4 滑动窗口

确定了tcp的超时重传时间timeout后,tcp需要解决的是网络实际的数据处理速度。这样才不会引起网络拥塞,导致丢包。

TCP头里有一个字段叫Window,又叫Advertised-Window,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。

tcp缓冲区结构


接收端LastByteRead指向了TCP缓冲区中读到的位置,NextByteExpected指向的地方是收到的连续包的最后一个位置,LastByteRcved指向的是收到的包的最后一个位置,我们可以看到中间有些数据还没有到达,所以有数据空白区。
发送端的LastByteAcked指向了被接收端Ack过的位置(表示成功发送确认),LastByteSent表示发出去了,但还没有收到成功确认的Ack,LastByteWritten指向的是上层应用正在写的地方。

接收端在给发送端回ACK中会汇报自己的AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1。

发送端滑动窗口示意图


send window就是滑动窗口

  • 1. Sent and Acknowledged:这些数据表示已经发送成功并已经被确认的数据,比如图中的前31个bytes,这些数据其实的位置是在窗口之外了,因为窗口内顺序最低的被确认之后,要移除窗口,实际上是窗口进行合拢,同时打开接收新的带发送的数据
  • 2. Send But Not Yet Acknowledged:这部分数据称为发送但没有被确认,数据被发送出去,没有收到接收端的ACK,认为并没有完成发送,这个属于窗口内的数据。
  • 3. Not Sent,Recipient Ready to Receive:这部分是尽快发送的数据,这部分数据已经被加载到缓存中,也就是窗口中了,等待发送,其实这个窗口是完全有接收方告知的,接收方告知还是能够接受这些包,所以发送方需要尽快的发送这些包
  • 4. Not Sent,Recipient Not Ready to Receive: 这些数据属于未发送,同时接收端也不允许发送的,因为这些数据已经超出了发送端所接收的范围

接收端的数据有3个分类,因为接收端并不需要等待ACK所以它没有类似的接收并确认了的分类

情况如下

  • 1.  Received and ACK Not Send to Process:这部分数据属于接收了数据但是还没有被上层的应用程序接收,也是被缓存在窗口内
  • 2.  Received  Not ACK: 已经接收并,但是还没有回复ACK,这些包可能输属于Delay ACK的范畴了
  • 3.  Not Received:有空位,还没有被接收的数据。

3.5 拥塞控制

有了重传机制和流量控制,tcp协议已经很完善了。但是还有一个问题,如果网络上的延时突然增加,那么,TCP对这个事做出的应对只有重传数据,但是,重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,于是,这个情况就会进入恶性循环被不断地放大。试想一下,如果一个网络内有成千上万的TCP连接都这么行事,那么马上就会形成“网络风暴”,TCP这个协议就会拖垮整个网络。

所以,tcp需要知道网络上发生的事情,不能无脑的重发数据,对整个网络造成更大的伤害。将TCP设计成一个无私的协议,当拥塞发生的时候,要做自我牺牲。就像交通阻塞一样,每个车都应该把路让出来,而不要再去抢路了。

拥塞控制主要是四个算法:

  • 1)慢启动;
  • 2)拥塞避免;
  • 3)拥塞发生;
  • 4)快速恢复。
 3.5.1 慢启动算法

慢启动的算法如下(cwnd全称Congestion Window):

1)连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据。

2)每当收到一个ACK,cwnd++; 呈线性上升

3)每当过了一个RTT,cwnd = cwnd*2; 呈指数让升

4)还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”。


3.5.2 拥塞避免算法

当cwnd >= ssthresh时,就会进入“拥塞避免算法”。当cwnd达到这个值时后,算法如下:

1)收到一个ACK时,cwnd = cwnd + 1/cwnd

2)当每过一个RTT时,cwnd = cwnd + 1

这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。

3.5.3 拥塞发生算法

当丢包的时候,会有两种情况:

1)等到RTO超时,重传数据包。TCP认为这种情况太糟糕,反应也很强烈。

    • sshthresh =  cwnd /2
    • cwnd 重置为 1
    • 进入慢启动过程

2)Fast Retransmit算法,也就是在收到3个duplicate ACK时就开启重传,而不用等到RTO超时。

    • TCP Tahoe的实现和RTO超时一样。
    • TCP Reno的实现是:
      • cwnd = cwnd /2
      • sshthresh = cwnd
      • 进入快速恢复算法——Fast Recovery

上面我们可以看到RTO超时后,sshthresh会变成cwnd的一半,这意味着,如果cwnd<=sshthresh时出现的丢包,那么TCP的sshthresh就会减了一半,然后等cwnd又很快地以指数级增涨爬到这个地方时,就会成慢慢的线性增涨。我们可以看到,TCP是怎么通过这种强烈地震荡快速而小心得找到网站流量的平衡点的。

3.5.4 快速恢复算法

TCP Reno

快速重传和快速恢复算法一般同时使用。快速恢复算法是认为,你还有3个Duplicated Acks说明网络也不那么糟糕,所以没有必要像RTO超时那么强烈。 注意,正如前面所说,进入Fast Recovery之前,cwnd 和 sshthresh已被更新:

  • cwnd = cwnd /2
  • sshthresh = cwnd

然后,真正的Fast Recovery算法如下:

  • cwnd = sshthresh  + 3 * MSS (3的意思是确认有3个数据包被收到了)
  • 重传Duplicated ACKs指定的数据包
  • 如果再收到 duplicated Acks,那么cwnd = cwnd +1
  • 如果收到了新的Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法了。

如果你仔细思考一下上面的这个算法,你就会知道,上面这个算法也有问题,那就是——它依赖于3个重复的Acks。注意,3个重复的Acks并不代表只丢了一个数据包,很有可能是丢了好多包。但这个算法只会重传一个,而剩下的那些包只能等到RTO超时,于是,进入了恶梦模式——超时一个窗口就减半一下,多个超时会超成TCP的传输速度呈级数下降,而且也不会触发Fast Recovery算法了。

通常来说,正如我们前面所说的,SACK或D-SACK的方法可以让Fast Recovery或Sender在做决定时更聪明一些,但是并不是所有的TCP的实现都支持SACK(SACK需要两端都支持),所以,需要一个没有SACK的解决方案。而通过SACK进行拥塞控制的算法是FACK(后面会讲)

TCP New Reno

于是,1995年,TCP New Reno(参见 RFC 6582 )算法提出来,主要就是在没有SACK的支持下改进Fast Recovery算法的——

  • 当sender这边收到了3个Duplicated Acks,进入Fast Retransimit模式,开发重传重复Acks指示的那个包。如果只有这一个包丢了,那么,重传这个包后回来的Ack会把整个已经被sender传输出去的数据ack回来。如果没有的话,说明有多个包丢了。我们叫这个ACK为Partial ACK。
  • 一旦Sender这边发现了Partial ACK出现,那么,sender就可以推理出来有多个包被丢了,于是乎继续重传sliding window里未被ack的第一个包。直到再也收不到了Partial Ack,才真正结束Fast Recovery这个过程

我们可以看到,这个“Fast Recovery的变更”是一个非常激进的玩法,他同时延长了Fast Retransmit和Fast Recovery的过程。

 

 

四. Wireshark抓包工具

 Wireshark 是最著名的网络通讯抓包分析工具。功能十分强大,可以截取各种网络封包,显示网络封包的详细信息。


抓到的一个包,tcp报文对应关系

4.1 Wireshark 中的三次握手 

第一次握手



第二次握手



第三次握手



4.2 Wireshark 中的请求过程

发起http请求


服务器ack确认


服务器返回数据


客户端的ack确认


五. tcp传输数据

tcp缓冲区简单模型


socket如何通过tcp读写数据

当应用希望写数据时,并不是直接向网卡驱动发数据,而是先放入到一个缓冲区中,然后根据一定算法(达到一定数量或者调用flush之后),缓冲区中的数据就经发送到网卡中了(这里说的不准确,实际上,是网卡主动从缓冲区中拷贝出来的,但并不影响我们理解)。

 

当网卡收到数据时,数据包要先经过如下几步:

  1. 数据包要先经过网卡校验正确与否。
  2. 数据链路层根据报头的类型传给不同的上层类型(IP层或者其他),并移除了数据链路层报头。
  3. IP层也需要先校验,然后根据IP报头选择不同的类型(TCP或者UDP),然后移除IP层报头,并将剩余的数据发送到相应的处理程序(tcp或udp)
  4. 到了tcp层,处理程序此时根据tcp首部中的端口号选择一个socket,并将其载荷数据拷贝进去。

tcp发送数据的结构图


六. udp与tcp

UDP协议全称是用户数据报协议 ,是一种无连接的协议。不提供数据包分组、组装和不能对数据包进行排序。

每个UDP socket都有一个接收缓冲区,没有发送缓冲区,从概念上来说就是只要有数据就发,不管对方是否可以正确接收,所以不缓冲,不需要发送缓冲区。

UDP:当套接口接收缓冲区满时,新来的数据报无法进入接收缓冲区,此数据报就被丢弃。UDP是没有流量控制的;快的发送者可以很容易地就淹没慢的接收者,导致接收方的UDP丢弃数据报。

q:我们能否用udp实现可靠的网络通信?




猜你喜欢

转载自blog.csdn.net/ningdunquan/article/details/79908937