TCP协议知识整理(报文、握手、挥手、重传、窗口、拥塞)

1.概念

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

  • 面向连接:一定是「一对一」才能连接,不能像 UDP 协议 可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;

  • 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;

  • 字节流:消息是「没有边界」的,所以无论我们消息有多大都可以进行传输。并且消息是「有序的」,当「前一个」消息没有收到的时候,即使它先收到了后面的字节已经收到,那么也不能扔给应用层去处理,同时对「重复」的报文会自动丢弃。

为什么要使用TCP协议?

网络层的IP协议不可靠,需要传输层的TCP协议来保证。

 


 2.TCP报文内容

IP数据报=IP头部+数据=IP头部+(TCP头部+数据)

源端口号:源计算机上的应用程序的端口号

目的端口号:目标计算机的应用程序端口号

序号Seq:本报文发送的数据组的第一个字节的序号,例如本报文要发送200-399这段200字节的内容,序号就是200,下一次报文的序号就是400,这保证了TCP传输的有序性。

确认号Ack:表示接收方期望收到发送方下一个报文段的第一个字节数据的编号。例如接收方收到了200-399的内容,那么下一次就希望能收到400+的内容,接收方返回的确认号就是400。

头部长度:也叫数据偏移,由于[选项及填充]这部分长度可变,所以整个TCP头部的大小是不确定的,不知道哪个位置是要传送的数据,头部长度指示TCP头部的大小,那么下一个字节开始就是要传输的内容。

保留:为将来定义新的用途保留,现在一般置0。

控制位URG:紧急指针标志,为1时表示紧急指针有效,为0则忽略紧急指针。

控制位ACK:确认序号标志,为1时表示确认号有效,为0表示报文中不含确认信息,忽略确认号字段。

控制位PSH:push标志,为1表示是带有push标志的数据,指示接收方在接收到该报文段以后,应尽快将这个报文段交给应用程序,而不是在缓冲区排队。

控制位RST:重置连接标志,用于重置由于主机崩溃或其他原因而出现错误的连接。或者用于拒绝非法的报文段和拒绝连接请求。

控制位SYN:同步序号,用于建立连接过程,在连接请求中,SYN=1和ACK=0表示该数据段没有使用捎带的确认域,而连接应答捎带一个确认,即SYN=1和ACK=1。

控制位FIN:finish标志,用于释放连接,为1时表示发送方已经没有数据发送了,即关闭本方数据流。

窗口大小:用来告知发送端接受端的缓存大小,以此控制发送端发送数据的速率,从而达到流量控制。窗口大小是一个16字节字段,因而窗口大小最大为65535。

校验和:奇偶校验,此校验和是对整个的 TCP 报文段,包括 TCP 头部和 TCP 数据,以 16 位字进行计算所得。由发送端计算和存储,并由接收端进行验证。

紧急指针:只有当 URG 标志置 1 时紧急指针才有效。紧急指针是一个正的偏移量,和顺序号字段中的值相加表示紧急数据最后一个字节的序号。TCP 的紧急方式是发送端向接收端发送紧急数据的一种方式。

选项及填充:最常见的可选字段是最长报文大小,又称为MSS(Maximum Segment Size),每个连接方通常都在通信的第一个报文段(为建立连接而设置SYN标志为1的那个段)中指明这个选项,它表示本端所能接受的最大报文段的长度。选项长度不一定是32位的整数倍,所以要加填充位,即在这个字段中加入额外的零,以保证TCP头是32的整数倍。然后通过数据偏移来确定数据部分的位置。

数据:这部分也是可选的,例如在建立连接和连接终止时,仅发送TCP首部,不带数据。

 


 3.套接字Socket是个什么东西

套接字=IP地址+端口号。传输层实现的端到端的通信,这里所谓的"端"不是IP地址,也不是端口号,而是套接字。

套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。

在socket编程中,客户端执行connect()时,将触发三次握手。

 


 4.TCP三次握手

就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。

刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。

  • 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN。此时客户端处于 SYN_SENT 状态(在发送连接请求后等待匹配的连接请求)。首部的同步位SYN=1,随机生成初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
  • 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 状态()。在确认报文段中SYN=1,ACK=1,确认号ack=x+1,随机生成初始序号seq=y。
  • 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。

在三次握手的建立中,原本双方都是closed状态,率先发送请求连接报文的 发送端先主动打开(active open),接收端被动打开(passive open)。

客户端:我们在一起吧

服务端:好

客户端:嗯

面试过程中,三次握手衍生出一系列的问题

(1)一个服务器的的最大TCP连接数

理论最大TCP连接数=套接字数量=客户端IP地址数×客户端的端口数

当然,服务端最大并发 TCP 连接数远不能达到理论上限。

  • 首先主要是文件描述符限制,Socket 都是文件,所以首先要通过 ulimit 配置文件描述符的数目;
  • 另一个是内存限制,每个 TCP 连接都要占用一定内存,操作系统是有限的。

(2)半连接队列是什么?

服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。

还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。

(3)丢包情况

客户端没有收到第2个包的情况下 会一直周期性重传第1个包,有两种情况

  • 第1个包丢了,服务端没有收到请求,自然也不会发送第2个包,客户端更不可能收到第2个包
  • 第2个包丢了,服务器收到第1个包然后发送第2个包,但由于半路丢了导致客户端收不到第2个包

服务端没有收到第3个包的情况下,会一直周期性重传第2个包,也是两种情况,第2个包丢了或者第3个包丢了。

周期性重传是个什么概念?每次重传的等待时间逐渐变长,一般是指数式增长。例如1s、2s、4s、8s...过了一定时间就叫超时,不传了。

(4)SYN攻击

服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的。

  • 客户端伪造大量IP地址向服务端 疯狂发送第1个包;
  • 然后服务器以为是正常的请求,会分配资源,并且发送第2个包回去;
  • 然后客户端不鸟它,那么服务端就会周期性超时重传,导致分配资源越来越多,重传的包也越来越多,引起网络拥塞甚至系统瘫痪。

常见抵御SYN攻击的方法

  • 缩短超时时间
  • 增大半连接队列的大小
  • 过滤网关防护
  • SYN cookies技术

(5)三个包的数据携带情况

第1、2个包不可以携带数据,第3个可以。

  • 如果第1个包可以携带数据,那么SYN攻击发送的第1个包携带大量数据浪费服务端的资源去接收
  • 第2个包不可以携带数据没有查到相关资料。个人猜测,既然第1个包为防止SYN攻击不可以携带数据,客户端发起请求的连接 即客户端要发东西给服务端,东西都没发过来服务端带什么数据给它,应该是称为第2个包没必要携带数据。不仅带了也没用,如果遇到SYN攻击更浪费资源。
  • 收到第2个包才会发送第3个包,收到第2个包说明服务器已经准备好要连接了,再发一个包就连接了,所以第3个包顺便带点数据没什么毛病。

(6)两次握手不行吗?

先弄明白三次握手的目的是什么,能不能只用两次握手来达到同样的目的。

  • 第一次握手:客户端发送网络包,服务端收到了。
    这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
  • 第二次握手:服务端发包,客户端收到了。
    这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
  • 第三次握手:客户端发包,服务端收到了。
    这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

因此,需要三次握手才能确认双方的接收与发送能力是否正常。

如果只有两次握手,会出现以下情况:

客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。这样产生的效果就是序列号不同步

(7)第三次握手失败怎么办?

服务端发送很多次第2个包之后没有收到第3个包,直到超时,此时才算是握手失败,不会重传ACK报文,直接发送RST报文然后进入closed状态。这样是防止SYN攻击。(超时即失败,不超时并且没有收到第3个包还会重传)

 


 5.四次挥手

刚开始双方都处于ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:

客户端:我们分手吧!

服务端:等我发完东西先。

服务端:渣男!分手!

客户端:再见了您嘞!

(1)挥手为什么要4次?

因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。

但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,”你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。

(2)半关闭状态

发送FIN的一端单方面宣布本端不会再发数据了,但可以收数据,这就是半关闭状态。

(3)2MSL等待状态

TIME_WAIT状态也称为2MSL等待状态。每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。这个2MSL等待状态会更新的,每发出一次第4个包,都会重置2MSL。

(4)为什么要等2MSL时间?

  • 如果客户端在发送第4个包后直接关闭,如果这个包丢了,则服务端会再发送第3个包,但此时客户端已经关闭了,没办法收到包也就不会再发第4个包,服务端就一直重发,超过一定次数应该就不会重发,浪费资源。  等待的效果就是如果第4个包丢了,服务端再发一个过来,客户端能扛到那个时候,再发一次第4个包。每发一次第4个包就会重新计时等待,发出去的ACK包丢了的话还能扛到下一次服务端发来FIN包。
  • 如果没有这个2MSL的话,客户端这边的套接字直接关闭了又重新发起连接,套接字相同,原本滞留在网络中的包会对新的连接造成影响。2MSL保证本次连接的所有数据都从网络中消失。

(5)2MSL时间有多长?

以下仅属于个人猜测,查阅许多博客都是一笔带过,没有想要的答案

假设一个包在客户端和服务端之间传递的单程时间是X,服务端重传第3个包的等待时间是Y。模拟最后两次挥手

服务端发送FIN包给客户端,此时重传时间Y开始计时;客户端收到了并且发回ACK包,此时2MSL开始计时,重传时间还剩Y-X。对于这个ACK包做出假设:

ACK包没有丢,传到了服务端,那么就是一次成功,服务端不会再发送ACK包,即重传时间还没到,Y-X-X>0,Y>2X

ACK包丢了,都说2MSL是为了防止ACK包丢了的情况,那这个2MSL一定能扛到下一次包发来。服务端没有收到,在重传时间Y后再发一次FIN包,此时在客户端2MSL里过了Y-X,要扛到FIN包发来,至少再过X,2MSL>Y-X+X,2MSL>Y>2X。

如果服务端第2次发送的FIN包丢了咋办?

那么2MLS要扛到Y+Y,2MSL>2Y>4X。感觉不太可能,丢包本来就少见,连续丢包更少见,为了这个"更少见"的情况延长2MSL,还不如规定服务端重传2、3次就异常自动关闭。

 


6.重传机制

TCP 实现可靠传输的方式之一,是通过序列号与确认应答,针对数据包丢失的情况,会用重传机制解决。

(1)超时重传

没有收到应答就再发一次包,没有收到应答的情况有两种丢包情况(或者网络滞留,如果滞留重传数据包,则接收的一方会自动忽略重复的包)

  • 发出去的包半路丢了使得对方没有发应答包过来
  • 发出去的包没丢但对方的应答包丢了

例子在三次握手里,时间设置要权衡往返时间,公式看着挺麻烦的就不记录了。

(2)快速重传

不以时间为驱动,而是以数据驱动重传。直接盗图

如果发送方连续收到3个相同的ACK=2则会知道seq=2丢了,但是这3个ACK=2是(1,3,4))还是(1,4,5)或者(1,3,5)发来的就不清楚了,那要重发哪些包?

于是有了SACK

(3)SACK 带选择确认的重传

即返回的ACK异常时,顺便把收到了哪些包的信息也发回去,这些信息放在报头的 "选项及填充" 里面。

(4)D-SACK

使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。

 

 

D-SACK 有这么几个好处:

可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;

可以知道是不是「发送方」的数据包被网络延迟了;

可以知道网络中是不是把「发送方」的数据包给复制了;

 


 7.滑动窗口机制

最初始的信息传递就是一问一答,一个seq回应一个ack,然后再发seq,这样效率不高。为了提高效率,几个seq一起发,这就是滑动窗口。

滑动窗口是在缓冲区的基础上建立的,一方发一方接,为什么要发?是为了让对方接。所以窗口大小是根据接收方缓冲区大小来确定的。

(1)发送端滑动窗口

(2)接收端滑动窗口

滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系

(3)流量控制

如果接收窗口初始360,由于接收端繁忙,收到200的数据放在缓冲区没有读取,此时接收窗口剩下160,发个包回应发送端"我现在只能接160了",但发送端还没收到这个消息就发了大小200的包过去,此时接收端 没有能力接收 就导致包丢了。 

为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样就可以避免了丢包情况。(避免发送方的数据填满了接收方的缓存)

(4)窗口关闭

为了解决这个死锁问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器

如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。

窗口探测

  • 如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器;
  • 如果接收窗口不是 0,那么死锁的局面就可以被打破了。

窗口探查探测的次数一般为 3 此次,每次次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接。

 


8.拥塞控制

(1)概念

在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大。这就是拥塞。

为了避免这样的悲剧发生,要及时控制数据包的发送量。这就是拥塞控制。

有这么几个概念:慢启动、拥塞避免、拥塞发生、快速恢复

(2)拥塞窗口

这是发送方维护的一个状态变量,会根据网络的拥塞情况动态变化,如果拥塞加重,窗口减小,拥塞减缓,窗口变大。

有了拥塞窗口之后,发送窗口 = min(拥塞窗口,接受窗口);

(3)慢启动

每收到一个ACK则拥塞窗口大小+1

三次握手建立TCP连接后,第一次发1个数据包,收到1个ACK后;第二次发2个数据包,收到2个ACK;第三次发4个数据包,收到4个ACK;第四次发8个数据包... 指数式增长,试探网络的底线。

(4)拥塞避免

指数式增长无疑会使得拥塞窗口会越来越大,当它达到一个阈值(ssthresh)时,不再以指数式增长,而是每次+1,线性增长,不然一下子就爆炸了,这就是拥塞避免。

(5)拥塞发生

即使线性增长,每轮包数量慢慢变多,总会有网络扛不住的时候,此时重传计时器超时/连续收到3个相同ACK,系统就判断是网络拥塞了,这就是拥塞发生。

(6)快速恢复

根据超时重传和快速重传的区别,快速恢复有2种方式。(同时使用)

超时重传的快速恢复,下一次发送数据包只发1个,重新回到慢启动,并且阈值设为 拥塞时窗口大小的一半(ssthresh=cwnd/2)。一夜回到解放前,这种方式太激进,会造成卡顿,已废弃。

(《图解TCP》第五版215页:TCP通信开始时是没有设置慢启动阈值。而是在超时重发时,才会设置为当时拥塞窗口大小的一半。)

快速重传的快速恢复,当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh 和 cwnd 变化如下:

  • cwnd = cwnd/2;
  • ssthresh = cwnd;

然后进入快速恢复算法

  • cwnd = ssthresh + 重传ack个数(一般为3)
  • 重传丢失的数据包
  • 如果再收到重复的 ACK,那么 cwnd 增加 1
  • 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的ssthresh的值,原因是该ACK确认了新的数据,说明丢失的数据都已收到。快速恢复算法结束,进入拥塞避免阶段。
  • 缺点:这个算法依赖3个重传的ack包,但3个重传的ack不代表只丢了一个包,可能丢了好多好多个,但这个算法只会重传一个,剩下的那些只能等到RTO超时。于是,超时一个阻塞窗口减半,多个超时会超成TCP的传输速度呈级数下降,而且也不会触发快速恢复算法了。

再贴一张图

 

 

 


参考&引用

https://yuanrengu.com/2020/77eef79f.html

http://cdn.yuanrengu.com/img/040315115571.png

https://blog.csdn.net/paincupid/article/details/79726795

https://www.cnblogs.com/xiaolincoding/p/12638546.html

https://www.cnblogs.com/xiaolincoding/p/12732052.html

posted @ 2020-05-11 13:05  守林鸟  阅读(2645)  评论(4编辑  收藏  举报