TCP协议

 

TCP协议格式

 

 

 

确认序号:没有收到就应该重新发送,直到送达。需要不能从1(泛指固定值)开始,假如A发送1、2、3,包超时了,然后A重新发数据包,本次只发送1、2,假如超时的3突然在这时候返回,B就会认为A发过来的数据是1、2、3,则序号的起始序号是随着时间变化的,可以看成一个32位的计数器,每4ms加1,我们知道IP包头部里面一个TTL,也即是生存时间。

窗口大小:tcp要做流量控制,通讯双方各生命一个窗口,标示自己当前能够的处理能力,别发送的太快,也别发的太慢。TCP还会做拥塞控制,控制发送速度。

状态位:SYN发起一个连接;ACK回复; FIN结束连接;RST重新连接;PSH表示有data数据传输;

ACK是可能与SYN,FIN等同时使用的,比如SYN和ACK同时为1时,表示的就是建立连接之后的响应。如果只是一个单独的SYN,表示的只是建立连接。

校验和:目的是为了发现TCP首部和数据在发送端到接收端之间发生的任何改动。如果接收方检测到检验和有差错,则TCP段会被直接丢弃。

紧急指针:有时一些应用程序在某些紧急情况下(如在某些连接中进行强制中断)要求在接收方在没有处理完数据之前就能够发送一些紧急数据,这就使得发送方将CODE字段的URG置为1 即紧急指针字段有效这样可以不必考虑你发送的紧急数据在数据流中的位置,也就是相当于优先级最高。

Option: 这部分最多包含40字节,我们常用的有7种,kind0到kind7。

kind0:放在末尾用于填充,说明首部没有更多的信息了。应用数据在下一个32位处开始。

kind1:空操作,一般用于将TCP选项的总长度填充为4字节的整数倍

kind2:协商最大报文段长度MSS

kind3:TCP连接初始化时,通信双方使用该选项来协商接收窗口的大小。在TCP的头部中,接收窗口大小是用16位表示的,故最大为65535字节,但实际上TCP模块允许的接收窗口大小远不止这个数(为了提高TCP通信的吞吐量)。窗口扩大因子解决了这个问题。 

kind4:选择性确认(SACK)选项

kind5:SACK实际工作的选项该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块,从而让发送端可以据此检查并重发丢失的数据块

kind8:时间戳,为了较为准确的计算通讯双方的回路时间RTT,为TCP的流量控制提供重要信息。

 

顺序问题,稳重不乱

丢包问题,承诺靠谱

连接维护,有始有终

流量控制,把握分寸

拥塞控制,知进知退

 

三次握手,请求->应答->应答之应答

 

 

如果B不乐意建议连接,则A会重试一阵后放弃,如果B乐意建立连接,则A会发送应答给A.

B的请求应答可能也会发送很多次,只要一次到达A,A就认为建立了连接。

 

第一次握手

客户端发送一个TCP的SYN标志位置1的包指明客户打算连接的服务器的端口,以及初始序号ISN为X,保存在包头的序列号字段里。

 

 

第二次握手:

服务器发回确认包(ACK)应答。即SYN标志位和ACK标志位均为1同时,将确认序号设置为客户的ISN加1以.即X+1。

 

 

 

第三次握手:

客户端再次发送确认包(ACK) SYN标志位为0,ACK标志位为1.并且把服务器发来ACK的序号字段+1,放在确定字段中发送给对方.并且在数据段写ISN+1。

 

 

 

--------------------

传输数据的简要过程如下:
1)  发送数据 :服务器向客户端发送一个带有数据的数据包,该数据包中的序列号和确认号与建立连接第三步的数据包中的序列号和确认号相同;
2)  确认收到 :客户端收到该数据包,向服务器发送一个确认数据包,该数据包中,序列号是为上一个数据包中的确认号值,而确认号为服务器发送的上一个数据包中的序列号+所该数据包中所带数据的大小。
数据分段中的序列号可以保证所有传输的数据按照正常的次序进行重组,而且通过确认保证数据传输的完整性。

------------------

 

那 TCP 在三次握手的时候,IP 层和 MAC 层在做什么呢?

TCP 发送每一个消息,都会带着 IP 层和 MAC 层了。因为,TCP 每发送一个消息,IP 层和 MAC 层的所有机制都要运行一遍。而你只看到 TCP 三次握手了,其实,IP 层和 MAC 层为此也忙活好久了。

 

连接建立好后,就开始进行发包。

我们的数据经过七层协议的过程中就像包粽子一样,每过一层就需要增加数据的大小。

MTU最大传输单元数据域1500Bytes, 以太网的最大数据帧是1518Bytes。【以太网的帧头148Bytes,一共18B:MAC目的地址6Bytes MAC源地址6Bytes,Type域2Bytes、帧尾校验4Bytes;数据域只剩:1518-8 = 1500Bytes】

IP层最大传输长度:1500B – 20B =1480Bytes,超过此数值,都需要被IP层分片,在到达目的前会自己重组。

MSS最大报文长度,为每次TCP数据包每次传输的最大数据的分段大小,由发送端通知接收端,发送大于MTU就会被分片。

TCP最大数据长度为1460Bytes,MTU- IP头(20B)- TCP头(20B) = 1460B 这也是最大的报文长度

UDP最大数据长度为1472B(UDP数据包 MTU - IP头(20B) - UDP头(8B) = 1472B)

而ip层分片会导致,如果其中的某一个分片丢失,因为tcp层不知道哪个ip数据片丢失,所以就需要重传整个数据段,这样就造成了很大空间和时间资源的浪费,为了解决这个问题,就有了tcp分组和MSS(最长报文大小)概念,利用tcp三次握手建立链接的过程,交互各自的MTU,然后用小的那个MTU-20-20 , 得到MSS,这样就避免在ip层被分片。

备注:我们标注的都是最大长度,不包括各个协议中的可变长度的选项信息等。

 

既然出了网关,那就是在公网上传输数据,公网往往是不可靠的,因而需要很多的机制去保证传输的可靠性,这里需要恒心(重传策略)还需要智慧(大量的算法)。

客户端每发送一个包,服务端都要有回复,如果服务端超过一定的时间没有回复,则客户端会重新发送这个包,直到有回复。

举例:领导交代下属问题(领导交代下属一些事情,需要准备一个本子,领导每交代下属一件事情,双方都要记录一下, 当下属做完一件事情,就回复你办好了,你就在本子上将这个事情划去。同时你的本子上的每件事情都有时限,如果超过了时限下属没有回复,你就要主动交代一下,上次的那件事情,你还没有回复我,咋样啦?既然很多事情一起做,就需要给每个事情编个号,防止弄错了,大部分对于事情的处理都是按照顺序来的,先来的先处理…)

TCP协议为了保证顺序,每一个包都有一个ID,在建立连接的时候会协商起始ID是什么,然后按照ID一个一个的发。为了保证不丢包,对于发送的数据包都要进行应答,这个应答肯定不是一个一个来的,而是会应答某个之前的ID,这种模式就叫累计应答|累计确认。

 

为了记录所有发送的包和接受的包,TCP也需要发送端和接收端分别都有缓存来保存这些记录。发送端的缓存是按照包的ID一个个排列的,如下:

发送端数据结构:

 

 

第一部分:发送了已经确认了,就是交代下属的,并且也做完了,应该划掉的。

第二部分:发送了并且尚未确认的。这部分是交代下属的,但是还没有做完的,需要等到回复后,才能划掉的。

第三部分:没有发送,等待发送了,还没有交代给下属,但是马上能交代的。

第四部分:没有发送,暂时也不能发送的。暂时没有交代给下属的。

 

这里为什么要控制第三部分、第四部分呢?没交代的一下子交代了不就得了?

这就是我们说的“流量控制,把握分寸”。

在TCP里,接收端会给发送报一个窗口的大小,就是AdvertisedWindow=(第二部分+第三部分)超过这个窗口接收端做不过来,就不能发送了。

 

接收端数据结构:

 

 

第一部分:接受并且确认过的,等着上层应用读取的数据。就是领会交代给我的我做完的,并且我也发了ack的。

第二部分:还没有接受,但是马上能接受的,也就是我还能承受的最大工作量。

第三部分:没有办法接受的,实在做不完的,一接受就猝死的工作量。

所以发送端的AdvertisedWindows=(MaxRcvBuffer-A)。

 

顺序与丢包问题

1、2、3没有问题,双方达成了一致。

4、5接收方说ACK了,但是发送发还没有收到,有可能丢了,有可能还在路上。

6、7、8、9肯定都发了,但是8、9已经到了,但是6、7没到,出现了乱序,缓存着但是没有办法ACK。

假如4的确认到了,5的ACK丢了,6、7的数据包丢了,该怎么办?

超时重试

对每一个发送了但是没有ACK的包都设置一个定时器,超过了一定的时间,就要重新尝试。但是这个超时时间如何评估呢?这个时间不宜过短时间必须大于往返时间RTT(Round Trip Time,也就是一个数据包从发出去到回来的时间),否则会引起不必要的重传。也不宜过长,这样超时时间变长影响性能,访问变慢。根据往返时间需要TCP通过采样RTT的时间,然后进行加权求平均,算出一个值,而且这个值还要根据网络的不断的变化而不断变化的,我们称为自适应重传算法。

如果过了一段时间,5、6、7都超时了,就会重新发送。接收方发现5原来接受过,就丢弃,6收到了,发送ACK,又发送7,不幸的是7又丢了,当7再次超时的时候,就需要重传,TCP的策略是超时重传加倍两次超时就说明网络环境差,不宜频繁反复发送。

超时重传的问题是,超时周期可能相对较长,又没有更快的方式呢?

有一个快速重传机制,当接收方收到一个序号大于下一个所期望的报文段时,就检测到了数据流中的一个间隔,于是发送3个的ACK,(例如发送如上丢失的7的ACK),客户端接受后,就在定时器超时前,重传丢失的报文段。

还有一个种方式叫SACK,需要在TCP的头部加一个SACK的东西,可以将缓存的地图发送给发送方。例如ACK6、SACK8、SACK9,发送方一下子就知道是7丢了.

 

流量控制问题

我们假设环境良好,窗口不变的情况下,窗口始终为4-12=9,4的确认来了之后,会右移一个,这个时候13号的包也就可以发送了

 

 

 

假设这个时候发送过猛,会将第三部分的10-13全部发送之后停止发送,使未发送的可发送部分为0.

 

 

当对于5的确认到达后,在客户端相当于窗口再滑动一格,这个时候才有更多的包可以发送

 

 

如果接收方处理的实在太慢,导致缓存中没有空间,可以通过确认信息修改窗口的大小甚至可以设置为0,则发送方将暂停发送。假设接收端的应用一直不读取缓存中的数据,当数据包6确认后,窗口就可以缩小一个(9->8)。

 

 

如果接受端的应用一直不去读取数据,则随着确认的包越来越多,窗口越来越小,直到为0,则停止发送。

 

 

 

 

如果这样情况的话,发送方会定时发送窗口的探测数据包,看看是否有机会调整窗口的大小,当接收方比较慢的时候,要防止低能窗口综合征,被空出一个字节就赶快告诉发送方,然后马上又填满了,可以当窗口太小的时候,不更新窗口,直到达到一定大小或者缓冲区的一半为空的时候才更新窗口。

 

拥塞控制问题

拥塞控制问题,通过窗口的大小来控制,前面的滑动窗口rwnd是怕发送发把接收方的缓存塞满,而拥塞控制窗口cwnd,是怕把网络塞满(造成丢包、超时重传)。发送未确认的<=min{cwnd,rwnd},拥塞窗口和滑动串口共同控制发送的速度。

那发送方怎么判断网络是不是满呢?这其实是个挺难的事情,因为对于 TCP 协议来讲,他压根不知道整个网络路径都会经历什么,对他来讲就是一个黑盒。TCP 的拥塞控制就是在不堵塞,不丢包的情况下,尽量发挥带宽。

水管有粗细,网络有带宽,也即每秒钟能够发送多少数据;水管有长度,端到端有时延。在理想状态下,水管里面水的量 = 水管粗细 x 水管长度。对于到网络上,通道的容量 = 带宽 × 往返延迟。

linux3.0以后,采取了Google的建议,把初始拥塞控制窗口调到了10。

 

如果我们设置发送窗口,使得发送但未确认的包为通道的容量,就能够撑满整个管道。

 

 

如图所示,假设往返时间为 8s,去 4s,回 4s,每秒发送一个包,每个包 1024byte。已经过去了 8s,则 8 个包都发出去了,其中前 4 个包已经到达接收端,但是 ACK 还没有返回,不能算发送成功。5-8 后四个包还在路上,还没被接收。这个时候,整个管道正好撑满,在发送端,已发送未确认的为 8 个包,正好等于带宽,也即每秒发送 1 个包,乘以来回时间 8s。

如果我们在这个基础上再调大窗口,使得单位时间内更多的包可以发送,会出现什么现象呢?

我们来想,原来发送一个包,从一端到达另一端,假设一共经过四个设备,每个设备处理一个包时间耗费 1s,所以到达另一端需要耗费 4s,如果发送的更加快速,则单位时间内,会有更多的包到达这些中间设备,这些设备还是只能每秒处理一个包的话,多出来的包就会被丢弃,这是我们不想看到的。

这个时候,我们可以想其他的办法,例如这个四个设备本来每秒处理一个包,但是我们在这些设备上加缓存,处理不过来的在队列里面排着,这样包就不会丢失,但是缺点是会增加时延,这个缓存的包,4s 肯定到达不了接收端了,如果时延达到一定程度,就会超时重传,也是我们不想看到的。

 

于是 TCP 的拥塞控制主要来避免两种现象,包丢失和超时重传。一旦出现了这些现象就说明,发送速度太快了,要慢一点。但是一开始我怎么知道速度多快呢,我怎么知道应该把窗口调整到多大呢?

如果我们通过漏斗往瓶子里灌水,我们就知道,不能一桶水一下子倒进去,肯定会溅出来,要一开始慢慢的倒,然后发现总能够倒进去,就可以越倒越快。这叫作慢启动。

一条 TCP 连接开始,cwnd 设置为一个报文段,一次只能发送一个;当收到这一个确认的时候,cwnd 加一,于是一次能够发送两个;当这两个的确认到来的时候,每个确认 cwnd 加一,两个确认 cwnd 加二,于是一次能够发送四个;当这四个的确认到来的时候,每个确认 cwnd 加一,四个确认 cwnd 加四,于是一次能够发送八个。可以看出这是指数性的增长。

涨到什么时候是个头呢?有一个值 慢启动阀值(sshresh) 为 65535 个字节,当超过这个值的时候,就要小心一点了,不能倒这么快了,可能快满了,再慢下来。

每收到一个确认后,cwnd 增加 1/cwnd,我们接着上面的过程来,一次发送八个,当八个确认到来的时候,每个确认增加 1/8,八个确认一共 cwnd 增加 1,于是一次能够发送九个,变成了线性增长。

但是线性增长还是增长,还是越来越多,直到有一天,水满则溢,出现了拥塞,这时候一般就会一下子降低倒水的速度,等待溢出的水慢慢渗下去。

拥塞的一种表现形式是丢包,需要超时重传,这个时候,将 sshresh 设为 (cwnd/2),将阀值减半,然后将 cwnd 设为 1,重新开始慢启动。这真是一旦超时重传,马上回到解放前。但是这种方式太激进了,将一个高速的传输速度一下子停了下来,会造成网络卡顿。

 

前面我们讲过快速重传算法。当接收端发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速的重传,不必等待超时再重传。TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,cwnd 减半为 cwnd/2,然后 sshthresh = cwnd,当三个包返回的时候,cwnd = sshthresh + 3,也就是没有一夜回到解放前,而是还在比较高的值,呈线性增长。

就像前面说的一样,正是这种知进退,使得时延很重要的情况下,反而降低了速度。但是如果你仔细想一下,

 

 

TCP 的拥塞控制主要来避免的两个现象都是有问题的。

第一个问题是丢包并不代表着通道满了,也可能是管子本来就漏水。例如公网上带宽不满也会丢包,这个时候就认为拥塞了,退缩了,其实是不对的。

第二个问题是 TCP 的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实 TCP 只要填满管道就可以了,不应该接着填,直到连缓存也填满。

为了优化这两个问题,后来有了TCP BBR 拥塞算法。它企图找到一个平衡点,就是通过不断的加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡。

 

 

 

四次挥手

A:B啊,我不想玩了…

B:那好吧,我知道了…

这个时候B能不能在ack的时候直接关闭呢?当然不可以了,很有可能是A发完了最后的数据就不玩了,但是B还没有做完自己的事情,还是可以发送数据的,成为半封闭的状态。

B:A啊,我也不玩了…

A:好的,拜拜…

 

A说完不玩了,超时后,假如没有收到回复,A会重新发送不玩了,这回合结束后时候出现异常了….

假如:

1、A说完不玩了,直接跑路,B还没有发起结束,如果A跑路,B发送结束,也没有应答….

2、A说完不玩了,B直接跑路….

 

TCP设置了复杂的状态机来解决,TCP协议要求A最后等待一段时间Time_wait

 

 

 

TIME-WAIT是2MSL(MSL报文最大生产时间,协议规定是2分钟,实际应用设为30s,60s,120s)它是任何报文在网络上存在的最长时间,超过这个时间,则报文被丢弃。TCP报文是基于IP协议的,而IP头中有一个TTL(生存时间值)域,是IP数据包可以经过的最大路由数,每经过一个路由时就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。假如超过2MSL后,B依然没有收到FIN的ACK,则B肯定还会重新发送FIN,这个时候超时后,A就直接发送RST,B就知道A早就跑了….

 

 

怎么样出网关?出网关后怎么样路由下一跳?如何防止拓扑结构中的环路问题?

下次分享,我们再见

posted @ 2018-12-13 16:41  奈特yi  阅读(160)  评论(0)    收藏  举报