计算机网络基础之TCP/IP

TCP包头格式:

这里写图片描述

源端口号和目标端口号和UDP一样,是不可少的,因为少了的话就不知道应该发给哪个应用了。

接下来是包的序号,这是为了解决乱序的问题

然后是确认号,发出去的包应该有确认,要不然不知道对方收到了。

接下来是一些状态位,SYN是发起一个连接,ACK是回复,RST是重新连接,FIN是结束连接等。TCP是面向连接的,因为双方都要维护连接的状态,这些带状态位的包的发送,会引起双方状态的更变。

窗口大小:TCP要做流量控制,通信的双方各声明一个窗口,标识自己当前能够处理的能力,别发得太快,也不要发得太慢。

通过TCP头的解析,我们要重点关注一下几个问题: 
- 顺序问题 
- 丢包问题 
- 连接维护 
- 流量控制 
- 拥塞控制

TCP的三次握手

对于所有的问题,首先都要建立一个连接,TCP的连接建立,常常称为三次握手: 
这里写图片描述

一开始客户端和服务端都处于CLOSED状态,先是客户端主动监听某一个端口,处于LISTEN状态,然后客户端主要发起连接SYN,带上了序列号,之后处于SYN_SENT状态,服务端收到发起的连接后,返回SYN,并且带上了ACK和序列号seq和值为客户端seq+1的ack,之后处于SYN_RCVD状态。客户端收到服务端发来的SYN和ACK后,发送ACK并带上seq和ack,之后处于ESTABLISHED状态,服务端收到了ACK的ACK之后,也处于ESTABLISHED状态

这样连接就建立好了,就可以进行数据传输了

为什么是三次,不是两次呢,按理说两个人打招呼,一来一回就可以了,使用三次是为了可靠,确保对方收到了我的回复。 
那么为什么不是四次呢? 
假设现在的这个通路不靠谱,A向B发起了一个连接,当发起了第一个请求杳无信息的时候,会有很多可能性,如果第一个请求的包丢了,或者是饶了弯路,超时了,还有B没有响应,不想和A连接。

但是A不确定结果,于是A就继续发,终于有一个包到达了B,但是B收到了请求包,A不知道,于是A可能还会发。

B收到请求包后,就知道了A的存在,并且知道A要跟它建立连接,如果B不愿意,A在重试一阵子后就会放弃,连接建立失败;如果B是愿意跟A建立连接的,那么就会发应答包给A。 
对于B来说,这个应答包发出去后,也不知道会不会达到A,这个时候B是不认为连接建立好了,因为应答包仍然会丢,会绕弯,或者A已经挂了。

而且还有一个现象是,如果只是两次握手,A和B建立连接后,进行了简单的通信,结束了连接,我们前面说过,A在请求连接的时候,可能会发很多次请求包,如果这个时候,那些绕路的请求包又回来了,B收到了连接,就会认为这是一个正常的请求,于是建立了连接,但是我们知道,这个连接不会进行下去的,也没有个终止的时候,纯属B单相思了,所以两次握手是肯定不行的。

现在回到正常的连接,B发送应答包后,很可能发很多次,但是只要有一个到达了A,A就认为连接已经建立好了,因为对于A来说,它的消息有去有回,这个时候A就会给B发送应答之应答,B也在等这个消息,只有等到这个消息,才能确认建立了连接,对于B来说,它的消息也是有去有回。

这样连接就建立成功了。

当然A的应答之应答也可能会丢,会绕路,甚至B挂了,按理说,还应该有一个应答之应答之应答,但是这样下次就没有底了,所以四次握手也可以,甚至四百次也可以,但都不能保证完全的可靠。只要双方的消息都有去有回,就可以了。

好在大部分情况下,A和B的连接建立好后,A会马上发送数据,一旦A发送数据,则很多问题就解决了,例如A发给B的应答丢了,当A后续发送的数据达到的时候,B可以认为这个连接已经建立,如果B挂了,那么A发送数据的时候,就会报错,说B不可达,这个时候A就知道B出事了。

如果连接建立好后,如果A不发送数据,那么连接就空着,这个时候,有依欧格keepalive机制,即使没有真实的数据包,也有探活包。

三次握手除了双方建立连接外,主要还为了沟通,就是TCP包的序号问题。

A要告诉B,我这里发起的包的起始序号是从哪个号开始的,B同样也要告诉A。那么序号为什么不能都从1开始呢?因为会出现冲突。

例如:A连上B后,发送了1,2,3,三个包,但是发送3的时候,中间丢了,或者绕路了,于是就重新发送,后来A掉线了,重新连接上B,序号又会1,2,开始,但是不想发3了,但是上次绕路的那个3又回来了,B就收到了,B就自然认为这是下一个包,于是就发生了错误。

因而,每个连接都要有不同的序号,这个序号的起始序号是随时间改变的,可以看成一个32位的计数器,每4ms加一,如果重复到最大,需要4个小时,这个时候那个绕路的包早就死了,因为我们知道IP包头里面有一个TTL,即生存时间。

当连接建立好后,客户端和服务端都处于ESTABLISHED状态。

TCP四次挥手

这里写图片描述

当要断开连接的时候,A先给B说“不玩了”,发送一个FIN包并带上seq,就进入了FIN_WAIT_1的状态,B收到A的“不玩了”的消息后,就发送一个ACK包并带上ack表示说“我知道了”,然后B进入CLOESD_WAIT状态,当A收到B的ACK包后,就进入了FIN_WAIT_2的状态。

如果这个时候B直接跑路,则A将一直保持这个状态。TCP协议里面并没有对这个状态的处理,但是liunx有,可以调整tcp_fin_timeout这个参数,设置一个超时时间。

如果B没有跑路,处理完所有事情后,B也发送了“不玩了”的请求的,也就是一个FIN+ACK包,并带了sep和ack,状态变为LAST_WAIT ,当A收到这个FIN+ACK后,A就发送一个ACK包,结束了FIN_WAIT_2的状态,表示“知道B也不玩了”,按理说这个时候A就可以直接跑路了,但是万一最后这个ACK包B收不到呢,则B就会重新发一个FIN+ACK包表示“我也不玩了”,如果这个时候A已经跑路了,则B就会永远收不到这个ACK了,于是在TCP协议中,要求A最后等待一段时间TIME_WAIT,这个时间足够长,长到如果B没有收到ACK的话,B说“不玩了”会重发,A也会重新发送一个ACK并且足够时间达到B。这个时间也能保证B发过来的包都死翘翘,才会空出端口。

等待的时间为2MSL,MSL是Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间,报文都会被丢弃。因为TCP是基于IP协议的,而IP协议中有一个TTL值,是IP数据报可以经过的最大路由数,每经过一个处理它的路由器此值就减1,当此值为0时,则数据包将被丢弃,同时发送ICMP报文通知源主机。 
协议规定MSL为2分支,实际应用中常用的是30s,一分钟和2分钟等。

还有一个异常情况就是,B超过了2MSL的时间,依然没有收到A发送的的ACK,按照TCP原理,B会重新发送FIN+ACK,但是这个时候A再收到这个包时,A就不认了,就会发送一个RST,于是B就知道A早跑了。

TCP状态机:

这里写图片描述

其中黑线加粗的部分,是主要流程,其中阿拉伯数字的序号,是连接过程中的序号,大写的中文数字序号,是连接断开过程的中的顺序。加粗的实线是客户端A的状态变迁,加粗的虚线是服务器B的状态变迁

TCP协议为了保证顺序,每一个包都有一个id,在建立连接的时候,会商定起始ID是什么,然后按照ID一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个之前的ID,表示都收到了,这种模式称为累计确认或者累计应答。

为了记录所有发送的包和接收的包,TCP需要发送端和接收端分别都有缓存在保存这些词记录。发送端的缓存是按照包的ID一个个排列根据处理的情况分为了四个部分:

第一部分:发送了并且已经确认的 
第二部分:发送了并且尚未确认的 
第三部分:没有发送的,但是已经等待发送的 
第四部分:没有发送,并且暂时还不会发送的。

为什么不一次性发送完呢?还记得上面提到的“流浪控制”吗,在TCP里,接收端会给发送端报一个窗口一个大小,叫Advertised window,这个窗口的大小等于上面的第二部分加上第三部分。,超过了这个窗口的,接收端处理不过来,就不能发送了。

这里写图片描述

对于接收端来讲,它的缓存里面记录的内容要简单一些:

第一部分:接收并且确认过的 
第二部分:还没接收,但是马上就能接收的,也就是能够接收的最大工作量 
第三部分:还没接收,也没法接收的,也就是超出工作量的部分

这里写图片描述

  • MaxRcvBuffer:最大缓存的量;
  • LastByteRead之后是已经接收了,但是还没有被应用层读取的。

第二部分的窗口大小为:MaxRcvBuffer-(NextByteExpected-LastByteRead)

其中第二部分里面,由于收到的包可能不是顺序的,会出现空挡,只有和第一部分连续的,可以进行回复,中间空着的部分需要等待,哪怕后面的已经来了。

顺序问题与丢包问题

结合上面接收端和发送端的图, 
在发送端看来,1,2,3已经发送并确认,4,5,6,7,8,9都是发送了还没有确认的,10,11,12是还没有发出去的,13,14,15是接收方没有空间,不准备发的。

在接收端看来,1,2,3,4,5是已经完成ACK,但是没有读取的,6,7是等待接收的,8,9是已经接收,但是没有ACK的

发送端和接收端当前状态如下:

  • 1,2,3没有问题,双方达成一致
  • 4,5接收方说ACK了,但是发送方还没有收到,有可能丢了,有可能在路上。
  • 6,7,8,9肯定是发送了,但是6,7还没有到,但是8,9到了,出现了乱序,缓存着但是没法ACK

假设4的ACK到了,但是5的ACK丢了,6,7的数据包也丢了,该怎么办呢?

一种方法是超时重试,即对每一个发送了,但是没有ACK的包,都有设一个定时器,超过了一定的时间,就重新尝试。这个超时时间不能过短,必须大于往返时间RTT ,否则就会引起不必要的重传,也不能过长,这个访问就变慢了。

估计往返时间,需要TCP通过采样RTT的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状况不断的改变。除了RTT,还要采样RTT的波动范围,计算出一个估计的超时时间,由于重传时间是不断变化的,我们称为自适应重传算法。

如果过了一段时间,5,6,7都超时了,就会重新发送,接收方发现5原来接收过,于是就丢弃,6收到了,就发送6的ACK,要求下一个是7,但是7不幸又丢了,当7再次超时的时候,有需要重传的时候,TCP的策略是超时时间加倍。没当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。

还有一个快速重传的机制,当接收方收到一个序号大于下一个所期望的序号时,就会检测到数据流冲有一个间隔,于是发送三个冗余的ACK,客户端收到后,就在定时器过期之前,重传丢失的报文段。

例如,接收方发现6。8。9都已经接收到了,就是7没有来,那肯定是丢了,于是发送三个6的ACK,要求下一个是7。客户端收到这3个,就会发现7丢了,不等超时,马上重发。

还有一种方式是SACK,这种方法需要在TCP头里加一个SACK的东西,可以将缓存的地图发送给发送方。例如,发送ACK6,SACK8,SACK9,有了地图,发送方一下子就看出来是7丢了。

流量控制问题

对于包的确认中,同时会携带一个窗口大小。

假设窗口不变,窗口始终为9。对于发送端,4的确认来的时候,会右移一个,这个时候第13个包也可以发送了。

这里写图片描述

这个时候,假设发送端发送过猛,会将第三部分的10,11,12,13全部发送完毕后,之后就停止发送了,未发送可发送部分为0。

这里写图片描述

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

这里写图片描述

如果接收方实在处理得太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为0,则发送放暂时停止发送。

假设一个极端情况,接收端的应用一直不读取缓存中的数据,对于发送端来说,当数据包6确认后,窗口大小就不能再是9了,就要缩小一个变为8

这里写图片描述

这个新的窗口大小通过6个确认消息到达发送端时,可以发现窗口没有平行右移,而是仅仅左边右移了,窗口大小由9改为了8

这里写图片描述

如果接受端还是一直不处理数据,则随着确认的包越来越多,窗口越来越小,直到0:

这里写图片描述

当这个窗口通过包14的确认达到发送端的时候,发送端的窗口也调整为0,停止发送。

这里写图片描述

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

这就是流量控制。

拥塞控制问题

拥塞控制,也是通过控制窗口的大小来实现的。前面的滑动窗口hwnd是怕发送方把接收方缓存塞满,而拥塞窗口cwnd,是怕把网络塞满。

这里有一个公式lastByteSent-LastByAcked<=min(cwnd,hwnd)是拥塞窗口和滑动窗口共同控制发送的速度。

那么发送方是怎么判断网络是不是满的呢?

对于网络上,通道的容量= 带宽 x 往返延迟,TCP的拥塞控制就是在不堵塞,不丢包的情况下,尽量发挥带宽。

TCP的拥塞控制主要来避免两种现象:包丢失和超时重传。一旦出现了这些现象就说明发送的速度太快了,要慢一点。

一条TCP连接开始,cwnd设置一个报文段,一次只能发送一个,当收到一个确认的时候,cwnd加一,于是一次能够发两个,当这个两个确认到来的时候,每个确认,cwnd加一,也是一次能够发四个。加到什么时候是个头呢?有一个值ssthresh为65535个字节,当超出这个值的时候,就要小心一点,不能发那么快了,要慢下来。

于是每收到一个确认,cwnd增长1/cwnd,接着上面的,当可以一次性发送8个的时候,这8个确认都到了的时候,每个确认增长1、8,8个一共让cwnd增长1,于是一次能发9个,变成了线性增长。

可以线性增长还是增长,还是会塞满网络,就出现了拥塞,这个时候,一般就会一下子降低发送的速度,等待网络不那么拥堵。

拥塞的一种表现是丢包,需要超时重传,这个时候将sshresh设为cwnd/2,将cwnd设为1,重新开始慢启动。但是这种方式太激进,会造成网络卡顿。

之前有说过快速重传,当接收端发现中间有一个包丢了的时候,发送三次前一个包的ACK,于是发送端就会快速重传,不必等待超时重传,这个时候,cwnd减半,cwnd = cwnd/2,sshresh = cwnd,当三个包返回的时候,cwnd = sshresh+3,呈线性增长。

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

  1. 丢包并不代表网络满了
  2. TCP的拥塞控制要等到中间设备都填满了,才发生丢包,从而降低速度,但是这个时候已经晚了。

为了优化这两个问题,后来有了TCP BBR拥塞算法,呀通过不断的加快发送速度,将管道填满,但是不填满中间设备的缓存。

这里写图片描述

本文参考: 

猜你喜欢

转载自blog.csdn.net/zxh2075/article/details/82144930