【TCP协议】---TCP协议详解

一、TCP概念

TCP(Transmission Control Protocol 传输控制协议)是一种面向连接(连接导向)的、可靠的、 基于IP的传输层协议。

首先来看看OSI的七层模型

 

      我们需要知道TCP工作在网络OSI的七层模型中的第四层——传输层,IP在第三层——网络层,ARP 在第二层——数据链路层;同时,我们需要简单的知道,数据从

应用层发下来,会在每一层都会加上头部信息,进行 封装,然后再发送到数据接收端。这个基本的流程你需要知道,就是每个数据都会经过数据的封装和解封 装的过程。

在OSI七层模型中,每一层的作用和对应的协议如下:

 

二、TCP头部结构和字段介绍 

从上面图片可以看出,TCP协议是封装在IP数据包中。

下图是TCP报文数据格式。TCP首部如果不计选项和填充字段,它通常是20个字节。

 

 下面分别对其中的字段进行介绍:

源端口和目的端口

     各占2个字节,这两个值加上IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接。有时一个IP地址和一个端口号也称为socket(插口)。

序号(seq)

     占4个字节,是本报文段所发送的数据项目组第一个字节的序号。在TCP传送的数据流中,每一个字节都有一个序号。例如,一报文段的序号为300,而且数据共100字节,则下一个报文段的序号就是400;序号是32bit的无符号数,序号到达2^32-1后从0开始。

确认序号(ack)

     占4字节,是期望收到对方下次发送的数据的第一个字节的序号,也就是期望收到的下一个报文段的首部中的序号;确认序号应该是上次已成功收到数据字节序号+1。只有ACK标志为1时,确认序号才有效。

数据偏移

      占4比特,表示数据开始的地方离TCP段的起始处有多远。实际上就是TCP段首部的长度。由于首部长度不固定,因此数据偏移字段是必要的。数据偏移以32位为长度单位也就是4个字节,因此TCP首部的最大长度是60个字节。即偏移最大为15个长度单位=1532位=154字节。

保留

     6比特,供以后应用,现在置为0。

6个标志位比特

① URG:当URG=1时,注解此报文应尽快传送,而不要按本来的列队次序来传送。与“紧急指针”字段共同应用,紧急指针指出在本报文段中的紧急数据的最后一个字节的序号,使接管方可以知道紧急数据共有多长。

② ACK:只有当ACK=1时,确认序号字段才有效;

③ PSH:当PSH=1时,接收方应该尽快将本报文段立即传送给其应用层。

④ RST:当RST=1时,表示出现连接错误,必须释放连接,然后再重建传输连接。复位比特还用来拒绝一个不法的报文段或拒绝打开一个连接;

⑤ SYN:SYN=1,ACK=0时表示请求建立一个连接,携带SYN标志的TCP报文段为同步报文段;

⑥ FIN:发端完成发送任务。

窗口

      TCP通过滑动窗口的概念来进行流量控制。设想在发送端发送数据的速度很快而接收端接收速度却很慢的情况下,为了保证数据不丢失,显然需要进行流量控制, 协调好通信双方的工作节奏。所谓滑动窗口,可以理解成接收端所能提供的缓冲区大小。TCP利用一个滑动的窗口来告诉发送端对它所发送的数据能提供多大的缓 冲区。窗口大小为字节数起始于确认序号字段指明的(这个值是接收端正期望接收的字节)。窗口大小是一个16bit字段,因而窗口大小最大为65535字节。

检验和

       检验和覆盖了整个TCP报文段:TCP首部和数据。这是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。

紧急指针

      只有当URG标志置1时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。

 三、TCP流量控制(滑动窗口协议)

在看这里,为了更好的理解,建议先了解TCP三次握手,四次挥手。博客:【TCP协议】---TCP三次握手和四次挥手 

      TCP流量控制主要是针对接收端的处理速度不如发送端发送速度快的问题,消除发送方使接收方缓存溢出的可能性。

      TCP流量控制主要使用滑动窗口协议,滑动窗口是接受数据端使用的窗口大小,用来告诉发送端接收端的缓存大小,以此可以控制发送端发送数据的大小,从而达到流量

控制的目的。这个窗口大小就是我们一次传输几个数据。对所有数据帧按顺序赋予编号,发送方在发送过程中始终保持着一个发送窗口,只有落在发送窗口内的帧才允许被发送;

同时接收方也维持着一个接收窗口,只有落在接收窗口内的帧才允许接收。这样通过调整发送方窗口和接收方窗口的大小可以实现流量控制。

我们可以通过下图来分析:

1、发送方接收到了对方发来的报文 ack = 33, win = 10,知道对方收到了 33 号前的数据,现在期望接收 [33, 43) 号数据。那我们开始发送[33, 43) 号的数据。

2、[33, 43) 号的数据你是已经发送了,但接受方并没有接受到[36,37]数据。所以接收方发送回对报文段 A 的确认:ack = 35, win = 10。

3、发送方收到了 ack = 35, win = 10,对方期望接收 [35, 45) 号数据。那么发送方在发送[35, 45) 

    这里面需要思考一个问题?

    第一步发送了[33, 43),如果这次发送[35, 45),那中间重叠部分不是发送了两次,所以这里要思考: 是全部重新发送还是只发送接收端没有收到的数据,如果全部发送那么重复发送的数据接收端怎么处理。这个下面快速重传会讲。

4、接收方接收到了报文段 [35, 41),接收方发送:ack = 41, win = 10. (这是一个累积确认)

5、发送方收到了 ack = 41, win = 10,对方期望接收 [41, 51) 号数据。

6、.......

 这样一直传输数据,直到数据发送完成。这么一来就保证数据数据的可靠性,因为如果某数据没有获取到,那么ack永远不会跳过它。

 这里也要思考一个问题,如果某一数据一只没有获取到,总不能一直这样堵塞在这里吧,这里就要讲接下来有关堵塞的解决方法。

 四、TCP拥塞控制

流量控制是通过接收方来控制流量的一种方式;而拥塞控制则是通过发送方来控制流量的一种方式。

TCP发送方可能因为IP网络的拥塞而被遏制,TCP拥塞控制就是为了解决这个问题(注意和TCP流量控制的区别)。

TCP拥塞控制的几种方法:慢启动,拥塞避免,快重传和快恢复。

这里先理解一个概念: 拥塞窗口

      拥塞窗口:发送方维持一个叫做拥塞窗口 cwnd的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态变化。

      发送方的让自己的发送窗口=min(cwnd,接受端接收窗口大小)。说明: 发送方取拥塞窗口与滑动窗口的最小值作为发送的上限。

      发送方控制拥塞窗口的原则是:只要网络没有出现拥塞,拥塞窗口就增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数。

下面将讨论拥塞窗口cwnd的大小是怎么变化的

1、慢启动

     TCP在连接过程的三次握手完成后,开始传数据,并不是一开始向网络通道中发送大量的数据包。因为假如网络出现问题,很多这样的大包会积攒在路由器上,很容易导致网络中路由器缓存空间耗尽,从而发生拥塞。因此现在的TCP协议规定了,新建立的连接不能够一开始就发送大尺寸的数据包,而只能从一个小尺寸的包开始发送,在发送和数据被对方确认的过程中去计算对方的接收速度,来逐步增加每次发送的数据量(最后到达一个稳定的值,进入高速传输阶段。相应的,慢启动过程中,TCP通道处在低速传输阶段),以避免上述现象的发生。这个策略就是慢启动。

画个简单的图从原理上粗略描述一下

 

我们思考一个慢启动引起的性能问题?

       在海量用户高并发访问的大型网站后台,有一些基本的系统维护需求。比如迁移海量小文件,就是从一些机器拷贝海量小碎文件到另一些机器,来完成一些系统维护的基本需求。

慢启动为什么会对拷贝海量小文件的需求造成重大性能损失?

      举个简单的例子,我们对每个文件都采用独立的TCP连接来传输(循环使用scp拷贝就是这个例子的实际场景,很常见的用法)。那么工作过程应该是,每传输一个文件建立一个连接,然后连接处于慢启动阶段,传输小文件,每个小文件几乎都处于独立连接的慢启动阶段被传输,这样传输过程所用的TCP包的总量就会增多。更细致的说一说这个事,如果在慢启动过程中传输一个小文件,我们可能需要2至3个小包,而在一个已经完成慢启动的TCP通道中(TCP通道已进入在高速传输阶段),我们传输这个文件可能只需要1个大包。网络拷贝文件的时间基本上全部消耗都在网络传输的过程中(发数据过去等对端ACK,ACK确认归来继续再发,这样的数据来回交互相比较本机的文件读写非常耗时间),撇开三次握手和四次握手那些包,如果文件的数量足够大,这个总时间就会被放大到需求难以忍受的地步。因此,在迁移海量小文件的需求下,我们不能使用“对每个文件都采用独立的TCP连接来传输(循环使用scp拷贝)“这样的策略,它会使每个文件的传输都处于在一个独立TCP的慢启动阶段。

如何避免慢启动,进而提升性能?

       很简单,尽量把大量小文件放在一个TCP连接中排队传输。起初的一两个文件处于慢启动过程传输,后续的文件传输全部处于高速通道中传输,用这样的方式来减少发包的数目,进而降低时间消耗。同样,实际上这种传输策略带来的性能提升的功劳不仅仅归于避免慢启动,事实上也避免了大量的3次握手和四次握手,这个对海量小文件传输的性能消耗也非常致命。

2、拥塞避免

先补充下: 慢启动中拥塞窗口的cwnd值,开始是1,接下开是指数型增涨的。1、2、4、8、16.....这样涨太快了吧。那么就有了堵塞避免。

cwnd不能一直这样无限增长下去,一定需要某个限制。TCP使用了一个叫慢启动门限(ssthresh)的变量,一旦cwnd>=ssthresh(大多数TCP的实现,通常大小都是65536),慢启动过程结束,拥塞避免阶段开始;

拥塞避免:cwnd的值不再指数级往上升,开始加法增加。此时当窗口中所有的报文段都被确认时,cwnd的大小加1,cwnd的值就随着RTT开始线性增加,这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。(它逻辑很简单就是到一定值后,cwnd不在是指数增长,而是+1增长。这样显然慢多了)。

非ECN环境下的拥塞判断,发送方RTO超时,重传了一个报文段,它的逻辑如下:

      1)把ssthresh降低为cwnd值的一半。

      2)把cwnd重新设置为1。

      3)重新进入慢启动过程。

 上面的图还是蛮好理解的。

3、快速重传

    TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。

注意:  接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到)此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。

3.1)超时重传机制

         一种是不回ack,死等3,当发送方发现收不到3的ack超时后,会重传3。一旦接收方收到3后,会ack 回 4——意味着3和4都收到了。

但是,这种方式会有比较严重的问题,那就是因为要死等3,所以会导致4和5即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到Ack,所以,发送方可能会悲观地认为也丢了,所以有可能也会导致4和5的重传。

对此有两种选择:

① 一种是仅重传timeout的包。也就是第3份数据。

② 另一种是重传timeout后所有的数据,也就是第3,4,5这三份数据。

这两种方式有好也有不好。第一种会节省带宽,但是慢,第二种会快一点,但是会浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等timeout,timeout可能会很长。

3.2)快速重传机制

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

比如:如果发送方发出了1,2,3,4,5份数据,第一份先到送了,于是就ack回2,结果2因为某些原因没收到,3到达了,于是还是ack回2,后面的4和5都到了,但是还是ack回2因为2还是没有收到,于是发送端收到了三个ack=2的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了,于是ack回6。示意图如下

 

 Fast Retransmit只解决了一个问题,就是timeout的问题,它依然面临一个艰难的选择,就是重转之前的一个还是重装所有的问题。对于上面的示例来说,是重传#2呢还是重传#2,#3,#4,#5呢?因为发送端并不清楚这连续的3个ack(2)是谁传回来的?也许发送端发了20份数据,是#6,#10,#20传来的呢。这样,发送端很有可能要重传从#2到#20的这堆数据(这就是某些TCP的实际的实现)。可见,这是一把双刃剑。

 总结: 不管是超时重传还是快速重传确实能保证数据的可靠性,但它无法解决的问题就是:比如发送端发了1、2、3、4、5,而接收端收到了1、3、4、5,那么这个时候它发送的ack是2。那么发送端发送的是重传#2呢还是重传#2,#3,#4,#5的问题。如果在发送#2,#3,#4,#5,本身资源是一种浪费,因为接受方#3,#4,#5已经缓存下来,只需#2,所以在发一遍是无意义的。

 五、TCP粘包、拆包

1.简介

 

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:

      1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包

     2)服务端一次接受到了两个数据包,D1和D2粘合在一起,称之为TCP粘包

     3)服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这称之为TCP拆包

     4)服务端分两次读取到了数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。

特别要注意的是,如果TCP的接受滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种情况,即服务端分多次才能将D1和D2包完全接受,期间发生多次拆包。

 2、 粘包、拆包发生原因

      产生原因主要有这3种:滑动窗口、MSS/MTU限制、Nagle算法

2.1、滑动窗口

      TCP流量控制主要使用滑动窗口协议,滑动窗口是接受数据端使用的窗口大小,用来告诉发送端接收端的缓存大小,以此可以控制发送端发送数据的大小,从而达到流量控制的目的。这个窗口大小就是我们一次传输几个数据。对所有数据帧按顺序赋予编号,发送方在发送过程中始终保持着一个发送窗口,只有落在发送窗口内的帧才允许被发送;

同时接收方也维持着一个接收窗口,只有落在接收窗口内的帧才允许接收。这样通过调整发送方窗口和接收方窗口的大小可以实现流量控制。

现在来看一下滑动窗口是如何造成粘包、拆包的?

粘包:假设发送方的每256 bytes表示一个完整的报文,接收方由于数据处理不及时,这256个字节的数据都会被缓存到SO_RCVBUF(接收缓存区)中。如果接收方的SO_RCVBUF中缓存了多个报文,那么对于接收方而言,这就是粘包。

拆包:考虑另外一种情况,假设接收方的窗口只剩了128,意味着发送方最多还可以发送128字节,而由于发送方的数据大小是256字节,因此只能发送前128字节,等到接收方ack后,才能发送剩余字节。这就造成了拆包。

2.2、MSS和MTU分片

MSS: 是Maximum Segement Size缩写,表示TCP报文中data部分的最大长度,是TCP协议在OSI五层网络模型中传输层对一次可以发送的最大数据的限制。

MTU: 最大传输单元是Maxitum Transmission Unit的简写,是OSI五层网络模型中链路层(datalink layer)对一次可以发送的最大数据的限制。当需要传输的数据大于MSS或者MTU时,数据会被拆分成多个包进行传输。由于MSS是根据MTU计算出来的,因此当发送的数据满足MSS时,必然满足MTU。

为了更好的理解,我们先介绍一下在5层网络模型中应用通过TCP发送数据的流程:

对于应用层来说,只关心发送的数据DATA,将数据写入socket在内核中的发送缓冲区SO_SNDBUF即返回,操作系统会将SO_SNDBUF中的数据取出来进行发送。

    传输层会在DATA前面加上TCP Header,构成一个完整的TCP报文。

    当数据到达网络层(network layer)时,网络层会在TCP报文的基础上再添加一个IP Header,也就是将自己的网络地址加入到报文中。

    到数据链路层时,还会加上Datalink Header和CRC。

     当到达物理层时,会将SMAC(Source Machine,数据发送方的MAC地址),DMAC(Destination Machine,数据接受方的MAC地址 )和Type域加入。

可以发现数据在发送前,每一层都会在上一层的基础上增加一些内容,下图演示了MSS、MTU在这个过程中的作用。

     MTU是以太网传输数据方面的限制,每个以太网帧都有最小的大小64bytes最大不能超过1518bytes。刨去以太网帧的帧头 (DMAC目的MAC地址48bit=6Bytes+SMAC源MAC地址48bit=6Bytes+Type域2bytes)14Bytes和帧尾 CRC校验部分4Bytes(这个部分有时候大家也把它叫做FCS),那么剩下承载上层协议的地方也就是Data域最大就只能有1500Bytes这个值 我们就把它称之为MTU。

由于MTU限制了一次最多可以发送1500个字节,而TCP协议在发送DATA时,还会加上额外的TCP Header和Ip Header,因此刨去这两个部分,就是TCP协议一次可以发送的实际应用数据的最大大小,也就是MSS。

      MSS长度=MTU长度-IP Header-TCP Header

TCP Header的长度是20字节,IPv4中IP Header长度是20字节,IPV6中IP Header长度是40字节,因此:在IPV4中,以太网MSS可以达到1460byte;在IPV6中,以太网MSS可以达到1440byte。需要注意的是MSS表示的一次可以发送的DATA的最大长度,而不是DATA的真实长度。发送方发送数据时,当SO_SNDBUF中的数据量大于MSS时,操作系统会将数据进行拆分,使得每一部分都小于MSS,这就是拆包,然后每一部分都加上TCP Header,构成多个完整的TCP报文进行发送,当然经过网络层和数据链路层的时候,还会分别加上相应的内容。

需要注意:  默认情况下,与外部通信的网卡的MTU大小是1500个字节。而本地回环地址的MTU大小为65535,这是因为本地测试时数据不需要走网卡,所以不受到1500的限制。

2.3、 Nagle算法

       TCP/IP协议中,无论发送多少数据,总是要在数据(DATA)前面加上协议头(TCP Header+IP Header),同时,对方接收到数据,也需要发送ACK表示确认。即使从键盘输入的一个字符,占用一个字节,可能在传输上造成41字节的包,其中包括1字节的有用信息和40字节的首部数据。这种情况转变成了4000%的消耗,这样的情况对于重负载的网络来是无法接受的。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此,TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据)。

Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。

Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有

收到对方发送的ACK确认该数据已收到。

Nagle算法的规则:

      1)如果SO_SNDBUF(发送缓冲区)中的数据长度达到MSS,则允许发送;

      2)如果该SO_SNDBUF中含有FIN,表示请求关闭连接,则先将SO_SNDBUF中的剩余数据发送,再关闭;

      3)设置了TCP_NODELAY=true选项,则允许发送。TCP_NODELAY是取消TCP的确认延迟机制,相当于禁用了Nagle 算法。

      4)未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;

      5)上述条件都未满足,但发生了超时(一般为200ms),则立即发送。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值