阅读 458

TCP协议

本文的思维导图

问题一:TCP与UDP的区别

———————————————————————————————————————

  • 用户数据报协议(UDP,User Datagram Protocol):特点是无连接、不可靠、快速传输,为应用程序提供了一种无需建立连接就可以发送封装的IP数据包的方法。

  • 传输控制协议 (TCP,Transimission Control Protocol):特点是面向连接、可靠性、面向字节流。

    • 面向连接:在客户端和服务端双方进行通信之前,TCP需要进行三次握手来建立连接,而UDP没有建立连接的过程。
    • 可靠性:为了保证报文传输的可靠,会给每一个包一个序列号(seq),序列号保证了传输至服务端的包的顺序。而服务端会对已成功收到的包进行一个确认(ACK),如果客户端在合理的往返时延(RTT)内未收到确认,那么对应的数据会被重传
    • 面向字节流:UDP的数据传输是基于数据报的,对于应用层发下来的报文不合并也不拆分,只是在其上面加上首部就交给了下面的网络层。而每个TCP套接口有一个发送缓冲区,把应用层发下来的数据看成无结构的字节流来发送,如果字节流太长时,TCP会将其拆分进行发送。当字节流太短时,TCP会等待缓冲区中的字节流达到一定程度时再构成报文发送出去。TCP会根据当前网络的拥塞状态来确定每个报文段的大小。

问题二:TCP的报文格式

——————————————————————————————————————— TCP报文是TCP层传输的数据单元,也叫报文段。
图示

  • 端口号:用来标识同一台计算机的不同应用进程。
    (1)源端口:16位,标识报文的返回地址(报文发送方的地址)
    (2)目的端口:16位,指明接收方计算机上的应用程序接口(报文接收方的地址)

※ TCP报文中的源端口号和目的端口号+IP数据报中的源IP与目的IP确定唯一一条TCP连接。

  • 序列号和确认号:是TCP可靠传输的关键部分。 (1)序列号(Sequence Number),32位。 是本报文段发送的数据包中的第一个字节的序列号。 例子:在TCP传送的流中,每一个字节一个序列号,一个报文段的序列号为300,此报文段数据部分共有100字节,则下一个报文段的序列号为400。所以序列号确保了TCP传输的有序性,并且在 SYN 报文中交换双端的初始序列号。
    (2)确认号(Acknowledge Number),32位。指明下一个期待收到的字节序号,表明该序列号之前的所有数据已经正确无误地收到。确认号只有当ACK标志为1才有效。 在刚开始建立连接时,SYN报文的ACK标志位为0。
  • 标志位:6位 (1)ACK表示Acknowledgment Number字段有意义
    (2)SYN表示SYN报文(在建立TCP连接的时候使用)
    (3)FIN表示没有数据需要发送(在关闭TCP连接的时候使用)
    (4)RST表示复位TCP连接

问题三:TCP协议的三次握手与四次挥手

———————————————————————————————————————

三次握手: 建立TCP连接,需要客户端和服务端总共发送三个包用来确认连接的建立,本质就是要确认通信双方的两样能力: 发送的能力和接收的能力

TCP三次握手过程

  • 第一次握手:客户端发送报文,将标志位SYN置为1,并随机产生一个序列号seq=x, 并将该数据包发送给服务器,客户端进入SYN_SENT状态,等待服务端确认。
  • 第二次握手:服务端收到数据包后,由标志位SYN=1从而得知道客户端请求建立连接,服务端也发送另一个报文,将标志位SYN和ACK都置为1,确认号ack=x+1,并且随机产生一个序列号seq=y, 并将该数据包发送给客户端用于确认连接请求,服务端进入SYN_RCVD状态。
  • 第三次握手:客户端收到确认后,检查确认号ack是否为x+1,ACK是否为1,如果正确则发送第三个报文,将标志位ACK置为1,seq序列号置为x+1(这里不是x+2的原因是因为ACK报文段是不消耗序列号的,而SYN是需要对端的确认,所以要消耗序列号),ack确认号置为y+1, 并将该数据包发送给服务端,服务端检查ack是否为y+1,ACK是否为1,如果正确则建立连接成功。客户端和服务端进入Established状态,完成三次握手。 然后双方可以传输数据。

图示

  • 从最开始双方都处于CLOSED状态。然后服务端开始监听某个端口,进入了LISTEN状态。
  • 然后客户端主动发起连接,发送 SYN , 自己变成了SYN-SENT状态。
  • 服务端接收到,返回SYN和ACK(对应客户端发来的SYN),自己变成了SYN-RCVD。
  • 之后客户端再发送ACK给服务端,自己变成了ESTABLISHED状态;服务端收到ACK之后,也变成了ESTABLISHED状态。

———————————————————————————————————————

TCP四次挥手:断开TCP连接。

TCP四次挥手过程

  • 第一次挥手:客户端想要释放连接,向服务端发送一段TCP报文,标志位FIN=1,表示"请求释放连接",序列号seq=U,然后客户端进入FIN-WAIT-1阶段,即半关闭阶段。并且停止在客户端到服务端上发送数据,但是客户端仍然能接受从服务端传输过来的数据。

※ 这里不发送的是正常连接时传输的数据(非确认报文),而不是一切数据,所以客户端仍然能发送ACK确认报文。

  • 第二次挥手:服务端接收到客户端发出的TCP报文之后,确认了客户端想要释放连接,随后服务端结束Established状态,进入CLOSE-WAIT阶段(半关闭状态) 并返回一段TCP报文,标志位ACK=1,表示"接收到客户端发送的释放连接的请求",序列号seq=V,确认号ack=U+1,随后服务端开始准备释放服务端到客户端方向上的连接。客户端收到从服务端发出的TCP报文之后,确认了服务器收到了客户端发出的释放连接信号,随后客户端结束FIN-WAIT-1阶段,进入FIN-WAIT-2阶段。

前"两次挥手"既让服务器端知道了客户端想要释放连接,也让客户端知道了服务器端了解了自己想要释放连接的请求。于是,可以确认关闭客户端到服务器端方向上的连接了。

  • 第三次挥手:服务端自从发出ACK确认报文之后,经过CLOSED-WAIT阶段,做好了释放服务端到客户端方向上的连接准备,再次向客户端发出一段TCP报文标志位为FIN=1,ACK=1,表示"已经准备好释放连接了"。序列号seq=W,确认号为ack=U+1,表示在收到客户端报文的基础上,将其序列号seq的值加1作文本段报文确认号ack的值。随后服务端结束CLOSE-WAIT阶段,进入LAST-ACK阶段。 并且停止在服务端到客户端到客户端的方向上发送数据,但是服务端仍然能接受从客户端传输过来的数据。
  • 第四次挥手:客户端收到从服务端发出的TCP报文,确认了服务端已经做好了释放连接的准备,结束FIN-WAIT-2阶段,进入TIME-WAIT阶段, 并且向服务端发送一段报文,标志位ACK=1,表示"接收到服务器准备好释放连接的信号",确认号ack=W+1。

Time-Wait状态:假设最后的ACK丢失,服务端将重发FIN,客户端必须维护TCP状态信息以便可以重发最后的ACK,否则将会发送RST,结果服务端认为发生错误。TCP实现必须可靠地终止连接的两个方向,所以客户端必须进入TIME_WAIT状态。

※ 这个时候,客户端需要等待2 个 MSL(Maximum Segment Lifetime,报文最大生存时间) ,在这段时间内如果客户端没有收到服务端的重发请求,那么表示 ACK 成功到达,挥手结束,否则客户端重发 ACK。

对于2MSL的解释: 最后一次挥手时,主动关闭端发送ACK以确认自己是收到对方想要释放连接的信号,但是自己却不能确定对方是否收到。所以有两种情况:

  • 对方并没有收到自己的ACK,这个时候被动关闭端会超时重新传输FIN。等待对方的ACK。
  • 对方已经收到自己的ACK,但是不会再发送任何消息,包括对端的ACK。

所以,无论是哪一种情况,主动关闭端都需要等待,取这两种情况等待时间的最大值,以应对最坏情况的发生。

所以最坏情况是:最后的ACK报文的MSL+重传的FIN报文的MSL

1MSL 确保四次挥手中主动关闭方最后的ACK报文最终能到达对端
1MSL 确保对端没有收到ACK时,服务端重传的FIN报文可以到达客户端

RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等

如果不等待2MSL,主动关闭端发送了ACK就直接释放连接,如果服务端还有报文需要发送的时候(ACK丢失,服务端重传FIN),客户端的端口被新应用占用了,就收到了很多无关的数据包,造成数据包混乱。所以,最安全的方法是等服务端发来的数据包都消失再启动新的应用。等待2MSL时间主要目的是怕最后一个ACK包对方没收到,那么对方在超时后将重发第三次握手的FIN包,主动关闭端接到重发的FIN包后可以再发一个ACK应答包。

图示

问题四:为什么不能是两次握手或者四次握手,为什么不能是三次挥手

———————————————————————————————————————

  • 不能是两次握手:无法确认客户端的接受能力。 如果是两次,当客户端发送SYN报文想建立连接,但是因为网络问题导致该报文段滞留在网络中,超时重传,两次握手就建立了连接。但是当该次连接关闭后,第一次滞留在网络中的报文到达了服务端,如果是两次握手,服务端就建立了连接,但是客户端已经断开,会导致资源的浪费。 如果此时是三次握手,当第二次握手,没有收到客户端的ACK,则此次连接是无效的。
  • 三次握手就已经可以确认了双方的发送能力和接受能力,四次握手、五次握手...都可以,但是浪费资源。
  • 不能是三次挥手:这里的三次只能是第二次挥手和第三次挥手合并,即同时发送ACK确认报和FIN终止报。如果服务端仍有报文需要发送,则需要等待服务器端把数据都发送完毕后,再发送FIN。 如果是三次挥手,长时间的延迟会导致客户端误以为FIN没有到达服务端,从而客户端会不断重发FIN,所以要先发一个ACK表示已经接收到客户端的FIN,但是要等到自己的数据都发完才发FIN。

问题五:三次握手的过程中可以携带数据吗

———————————————————————————————————————
第一次和第二次是不可以携带数据的,但是第三次是可以携带数据的。 假如第一次握手可以携带数据的话,那对于服务器就太危险了,有人如果恶意攻击服务器,每次都在第一次握手中的SYN报文中放入大量数据。而且频繁重复发SYN报文,服务器会花费很多的时间和内存空间去接收这些报文。 第三次握手,此时客户端已经处于ESTABLISHED状态。对于客户端来说,他已经建立起连接了,并且已经知道服务器的接收和发送能力是正常的。所以也就可以携带数据了。

问题六:半连接队列与全连接队列、SYN攻击

———————————————————————————————————————
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:

  • 半连接队列,也称 SYN 队列;
  • 全连接队列,也称 ACCEPT 队列;

服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到全连接队列,等待进程调用 accept 函数时把连接取出来。
图示

不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,内核会直接丢弃,或返回 RST 包。
当服务端并发处理大量请求时,如果 TCP 全连接队列过小,就容易溢出。发生 TCP 全连接队溢出的时候,后续的请求就会被丢弃。

SYN攻击:

  • 概念:客户端在短时间内伪造大量不存在的 IP 地址,并向服务端疯狂发送SYN包,

这其实也就是所谓的 SYN 洪泛、SYN 攻击、DDos 攻击。

  • 后果:

服务端处理大量的SYN包并返回对应的ACK到伪造的IP地址,服务端一直等不到第三次握手的ACK包,导致大量的连接处于SYN_RCVD状态,从而占满整个半连接队列,无法处理正常请求。

  • 应对办法:
    • 增加半连接队列的容量
    • 降低SYN timeout时间,使得主机尽快释放半连接的占用
    • 利用 SYN Cookie技术,在服务端接收到SYN后不立即分配连接资源,而是根据这个SYN计算出一个Cookie,连同第二次握手回复给客户端,在客户端回复ACK的时候带上这个Cookie值,服务端验证 Cookie 合法之后才分配连接资源。

问题七:TCP的快速打开(TFO)

———————————————————————————————————————
TCP快速打开(TCP FAST OPEN - TFO):
原理:通过握手开始时的SYN包中的TFO cookie(一个TCP选项)来验证一个之前连接过的客户端。如果验证成功,它可以在三次握手最终的ACK包收到之前就开始发送数据,这样便跳过了一个绕路的行为,更在传输开始时就降低了延迟。这个加密的Cookie被存储在客户端,在一开始的连接时被设定好。然后每当客户端连接时,这个Cookie被重复返回。
具体过程:

  • 请求Fast Open Cookie
    • 客户端发送SYN数据包,该数据包包含Fast Open选项,且该选项的Cookie为空,这表明客户端请求Fast Open Cookie;
    • 支持TCP Fast Open的服务器生成Cookie,并将其置于SYN-ACK数据包中的Fast Open选项以发回客户端;
    • 客户端收到SYN-ACK后,缓存Fast Open选项中的Cookie。
  • 实施TCP Fast Open
    • 以下描述假定客户端在此前的TCP连接中已完成请求Fast Open Cookie的过程并存有有效的Fast Open Cookie。
    • 客户端发送SYN数据包,该数据包包含数据(对于非TFO的普通TCP握手过程,SYN数据包中不包含数据)以及此前记录的Cookie
    • 支持TCP Fast Open的服务器会对收到Cookie进行校验:如果Cookie有效,服务器将在SYN-ACK数据包中对SYN和数据进行确认(Acknowledgement),服务器随后将数据递送至相应的应用程序(确认成功就直接发送数据而不用再三次握手建立连接);否则,服务器将丢弃SYN数据包中包含的数据,且其随后发出的SYN-ACK数据包将仅确认(Acknowledgement)SYN的对应序列号(不成功则仍要建立);
    • 如果服务器接受了SYN数据包中的数据,服务器可在握手完成之前发送数据
    • 客户端将发送ACK确认服务器发回的SYN以及数据,但如果客户端在初始的SYN数据包中发送的数据未被确认,则客户端将重新发送数据
    • 此后的TCP连接和非TFO的正常情况一致。

问题八:TCP的超时重传

———————————————————————————————————————
超时重传是TCP协议保证数据可靠性的另一个重要机制,其原理是在发送某一个数据以后就开启一个计时器,在一定时间内如果没有得到发送的数据报的ACK报文,那么就重新发送数据,直到发送成功为止 两个基本概念:

  • 重传超时时间(Retransmission TimeOut - RTO)
  • 传输往返时间(Round Trip Time - RTT)

问题九:TCP的流量控制

———————————————————————————————————————
概念:双方在通信的时候,发送方的速率与接收方的速率是不一定相等,如果发送方的发送速率太快,会导致接收方处理不过来,这时候接收方只能把处理不过来的数据存在缓存区里。
如果缓存区满了发送方还在疯狂着发送数据,接收方只能把收到的数据包丢掉,大量的丢包会极大着浪费网络资源,因此,我们需要控制发送方的发送速率,让接收方与发送方处于一种动态平衡才好。
对发送方发送速率的控制,我们称之为流量控制。
原理:滑动窗口
概念:

  • 发送方的发送缓存内的数据都可以被分为4类:
  1. 已发送,已收到ACK
  2. 已发送,未收到ACK
  3. 未发送,但允许发送
  4. 未发送,但不允许发送

图示 其中类型2和3都属于发送窗口。

  • 接收方的缓存数据分为3类:
  1. 已接收
  2. 未接收但准备接收
  3. 未接收而且不准备接收

其中类型2属于接收窗口。
图示

REV 即 receive,NXT 表示下一个接收的位置,WND 表示接收窗口大小。

流量控制过程:

过程一

首先是第一次发送数据这个时候的窗口大小是根据链路带宽的大小来决定的。我们假设这个时候窗口的大小是3。这个时候接受方收到数据以后会对数据进行确认告诉发送方我下次希望手到的是数据是多少。这里我们看到接收方发送的ACK=3(这是发送方发送序列2的回答确认,下一次接收方期望接收到的是3序列信号)。这个时候发送方收到这个数据以后就知道我第一次发送的3个数据对方只收到了2个。就知道第3个数据对方没有收到。下次在发送的时候就从第3个数据开始发。这个时候窗口大小就变成了2 。

过程二

这个时候发送方发送2个数据。

过程三

看到接收方发送的ACK是5就表示他下一次希望收到的数据是5,发送方就知道我刚才发送的2个数据对方收了这个时候开始发送第5个数据。 这就是滑动窗口的工作机制,当链路变好了或者变差了这个窗口还会发生变化,并不是第一次协商好了以后就永远不变了。

滑动窗口协议优势:

  • 允许发送方在停止并等待确认前可以连续发送多个分组。由于发送方不必每发送每确认,因此该协议可以加速数据的传输。
  • 在接收窗口向前滑动时(与此同时也发送了确认),发送窗口也会同步向前滑动,收发两端的窗口按照以上规律不断地向前滑动 ,可以动态调整窗口大小

问题十:TCP的拥塞控制

———————————————————————————————————————
概念:TCP拥塞控制是传输控制协议避免网络拥塞的算法,使用多种拥塞控制策略来避免雪崩式拥塞。TCP会为每条连接维护一个“拥塞窗口”来限制可能在端对端间传输的未确认分组总数量。这类似TCP流量控制机制中使用的滑动窗口。

核心:

  • 拥塞窗口cwnd

发送方维持一个拥塞窗口cwnd(congestion window)的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。 发送方让自己的发送窗口等于拥塞窗口。发送方控制拥塞窗口的原则是:只要网络没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数。

  • 慢开始门限ssthresh

拥塞控制的方法:

  • 慢开始
  • 拥塞避免
  • 快速重传
  • 快速恢复

慢开始

当主机开始发送数据时,如果立即所大量数据字节注入到网络,那么就有可能引起网络拥塞,因为现在并不清楚网络的负荷情况。因此,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值。 通常在刚刚开始发送报文段时,先把拥塞窗口 cwnd设置为一个最大报文段MSS的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。用这样的方法逐步增大发送方的拥塞窗口 cwnd ,可以使分组注入到网络的速率更加合理。

每经过一个传输轮次,拥塞窗口 cwnd 就加倍。一个传输轮次所经历的时间其实就是往返时间RTT。 不过“传输轮次”更加强调:把拥塞窗口cwnd所允许发送的报文段都连续发送出去,并收到了对已发送的最后一个字节的确认。

另外,慢开始的“慢”并不是指cwnd的增长速率慢,而是指在TCP开始发送报文段时先设置cwnd=1,使得发送方在开始时只发送一个报文段(目的是试探一下网络的拥塞情况),然后再逐渐增大cwnd。

为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量(如何设置ssthresh)。慢开始门限ssthresh的用法如下:

当 cwnd < ssthresh 时,使用上述的慢开始算法。

当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。

当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。

拥塞避免

让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口cwnd按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。

无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送方窗口值的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。

快速重传

如果发送方设置的超时计时器时限已到但还没有收到确认,那么很可能是网络出现了拥塞,致使报文段在网络中的某处被丢弃。这时,TCP马上把拥塞窗口 cwnd 减小到1,并执行慢开始算法,同时把慢开始门限值ssthresh减半。这是不使用快重传的情况。
快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时(不用等到重传超时时间RTO)才进行捎带确认。

接收方收到了M1和M2后都分别发出了确认。现在假定接收方没有收到M3但接着收到了M4。显然,接收方不能确认M4,因为M4是收到的失序报文段。根据可靠传输原理,接收方可以什么都不做,也可以在适当时机发送一次对M2的确认。但按照快重传算法的规定,接收方应及时发送对M2的重复确认,这样做可以让发送方及早知道报文段M3没有到达接收方。 发送方接着发送了M5和M6。接收方收到这两个报文后,也还要再次发出对M2的重复确认。这样,发送方共收到了接收方的四个对M2的确认,其中后三个都是重复确认。快重传算法还规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段M3 ,而不必继续等待M3设置的重传计时器到期。由于发送方尽早重传未被确认的报文段,因此采用快重传后可以使整个网络吞吐量提高约20%。

快速恢复

发送方一旦受到三个重复确认,就知道现在只是丢失了个别的报文段。于是不启用慢开始算法,而执行快速恢复算法。

  • 发送方将慢开始门限值ssthresh和拥塞窗口cwnd值调整为当前窗口的一半
  • 开始执行拥塞避免算法

图示整个拥塞控制算法

文章分类
前端
文章标签