不学不知道,TCP协议竟如此复杂

TCP协议详解

一、TCP报文字段

在这里插入图片描述Source Port和Destination Port(源端口和目的端口):分别占用16位,用于区别主机中的不同进程;而IP地址是用来区分不同的主机的,源端口号和目的端口号配合上IP首部中的源IP地址和目的IP地址就能唯一 的确定一个TCP连接。

Sequence Number(序号):用来标识从TCP发送端向TCP接收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节在数据流中的序号;主要用来解决网络报乱序的问题

Acknowledgment Number(确认序号):32位确认序列号包含发送确认的一端所期望收到的下一个序号,因此,确认序号应当是上次已成功收到数据字节序号加1。不过,只有当标志位中的ACK标志为1时该确认序列号的字段才有效主要用来解决不丢包的问题

ISN即Initial Sequence Number(初始序列号):在三次握手的过程当中,双方会用过SYN报文来交换彼此的 ISN。ISN 并不是一个固定的值,而是每 4 ms 加一,溢出则回到 0,这个算法使得猜测 ISN 变得很困难。那为什么要这么做?

如果 ISN 被攻击者预测到,要知道源 IP 和源端口号都是很容易伪造的,当攻击者猜测 ISN 之后,直接伪造一个 RST 后,就可以强制连接关闭的,这是非常危险的。而动态增长的 ISN 大大提高了猜测 ISN 的难度。

Offset(头部字段):给出首部中32 bit(4字节)的数目,需要这个值是因为任选字段的长度是可变的。这个字段占4bit(最多能 表示15个32bit的字,即4*15=60个字节的首部长度),因此TCP最多有60字节的首部。然而,没有任选字段, 正常的长度是20字节。

标志位字段(U、A、P、R、S、F):占6比特。各比特的含义如下:
它们中的多个可同时被设置为1,主要是用于操控TCP的状态机的,依次 为URG,ACK,PSH,RST,SYN,FIN。

  • URG:此标志表示TCP包的紧急指针域(后面马上就要说到)有效,用来保证TCP连接不被中断,并且督促中间层设备要尽快处理这些数据。
  • ACK:此标志表示应答域有效,就是说前面所说的TCP应答号将会包含在TCP数据包中;有两个取值:0和1, 为1的时候表示应答域有效,反之为0。
  • PSH:这个标志位表示Push操作。所谓Push操作就是指在数据包到达接收端以后,立即传送给应用程序, 而不是在缓冲区中排队。
  • RST:这个标志表示连接复位请求。用来复位那些产生错误的连接,也被用来拒绝错误和非法的数据包。
  • SYN:表示同步序号,用来建立连接。SYN标志位和ACK标志位搭配使用,当连接请求的时候,SYN=1, ACK=0;连接被响应的时候,SYN=1,ACK=1;这个标志的数据包经常被用来进行端口扫描。扫描者发送 一个只SYN的数据包,如果对方主机响应了一个数据包回来 ,就表明这台主机存在这个端口;但是由于这种扫描方式只是进行TCP三次握手的第一次握手,因此这种扫描的成功表示被扫描的机器不很安全,一台安全的主机将会强制要求一个连接严格的进行TCP的三次握手。
    FIN: 表示发送端已经达到数据末尾,也就是说双方的数据传送完成,没有数据可以传送了,发送FIN标志 位的TCP数据包后,连接将被断开。这个标志的数据包也经常被用于进行端口扫描。
URG和PSH的区别

URG(紧急数据标志位):如果URG为1,表示本数据包中包含紧急数据。此时紧急数据指针表示的值有效,它表示在紧急数据之后的第一个字节的偏移值(即紧急数据的总长度)。若URG为0,则紧急指针没有意义。

PSH(推位):当设置为1时,要求把数据尽快的交给应用层,不做处理。当两个应用进程进行交互式的通信时,有时在一端的应用进程希望再键入一个命令后立即就能够收到对方的响应。在这种情况下,TCP就可以使用推送操作。这时,发送方TCP把PSH置1,并立即创建一个报文段发送出去。接收方TCP收到PSH=1的报文段,就尽快交付接收应用进程,而不再等到整个缓存都填满了再向上交付。
虽然应用进程可以选择推送操作,但推送操作还是很少使用。

两者都可理解为处理紧急数据的标志位,只是处理方法不同。URG的紧急数据仅在报文内,而PSH的紧急数据还在接受缓冲区内。

二、TCP的三次握手机制

在这里插入图片描述

过程详解

服务端的TCP进程先创建传输控制块TCB,准备接受客户端进程的连接请求,然后服务端进程处于LISTEN状态,等待客户端的连接请求,如有,则作出响应。
第一次握手:客户端的TCP进程也首先创建传输控制模块TCB,然后向服务端发出连接请求报文段,该报文段首部中的SYN=1,ACK=0,同时选择一个初始序号 seq=x。TCP规定,SYN=1的报文段不能携带数据,但要消耗掉一个序号。这时,TCP客户进程进入SYN—SENT(同步已发送)状态。

第二次握手:服务端收到客户端发来的请求报文后,如果同意建立连接,则向客户端发送确认。确认报文中的SYN=1,ACK=1,确认号ack=x+1,同时为自己选择一个初始序号seq=y。同样该报文段也是SYN=1的报文段,不能携带数据,但同样要消耗掉一个序号。这时,TCP服务端进入SYN—RCVD(同步收到)状态。

第三次握手:TCP客户端进程收到服务端进程的确认后,还要向服务端给出确认。确认报文段的ACK=1,确认号ack=y+1,而自己的序号为seq=x+1TCP的标准规定,ACK报文段可以携带数据,但如果不携带数据则不消耗序号,因此,如果不携带数据,则下一个报文段的序号仍为seq=i+1。这时,TCP连接已经建立,客户端进入ESTABLISHED(已建立连接)状态,可以看出第三次握手客户端已经可以发送携带数据的报文段了。

当服务端收到确认后,也进入ESTABLISHED(已建立连接)状态。

为什么要进行三次而不是两次或者四次呢?

第三次握手看似多余其实不然,这主要是为了防止已失效的请求报文段突然又传送到了服务端而产生连接的误判

比如:客户端发送了一个连接请求报文段A到服务端,但是在某些网络节点上长时间滞留了,而后客户端又超时重发了一个连接请求报文段B该服务端,而后 正常建立连接,数据传输完毕,并释放了连接。但是请求报文段A延迟了一段时间后,又到了服务端,这本是一个早已失效的报文段,但是服务端收到后会误以为客户端又发出了一次连接请求,于是向客户端发出确认报文段,并同意建立连接。那么问题来了,假如这里没有三次握手,这时服务端只要发送了确认,新的 连接就建立了,但由于客户端没有发出建立连接的请求,因此不会理会服务端的确认,也不会向服务端发送数据,而服务端却认为新的连接已经建立了,并在 一直等待客户端发送数据,这样服务端就会一直等待下去,直到超出保活计数器的设定值,而将客户端判定为出了问题,才会关闭这个连接。这样就浪费了很多服务器的资源。而如果采用三次握手,客户端就不会向服务端发出确认,服务端由于收不到确认,就知道客户端没有要求建立连接,从而不建立该连接。

三次握手的目的是确认双方发送和接收的能力,那四次握手可以嘛?
当然可以,100 次都可以。但为了解决问题,三次就足够了,再多用处就不大了。

三次握手过程中可以携带数据么?

第三次握手的时候,可以携带。前两次握手不能携带数据。

如果前两次握手能够携带数据,那么一旦有人想攻击服务器,那么他只需要在第一次握手中的 SYN 报文中放大量数据,那么服务器势必会消耗更多的时间和内存空间去处理这些数据,增大了服务器被攻击的风险。

第三次握手的时候,客户端已经处于ESTABLISHED状态,并且已经能够确认服务器的接收、发送能力正常,这个时候相对安全了,可以携带数据。

说说 TCP 快速打开的原理(TFO)

第一节讲了 TCP 三次握手,可能有人会说,每次都三次握手好麻烦呀!能不能优化一点?

可以啊。今天来说说这个优化后的 TCP 握手流程,也就是 TCP 快速打开(TCP Fast Open, 即TFO)的原理。

优化的过程是这样的,还记得我们说 SYN Flood 攻击时会有一个概念叫 SYN Cookie (后面会提到)这个 Cookie 可不是浏览器的Cookie, 用它同样可以实现 TFO。

TFO 流程
  • 首轮三次握手

首先客户端发送SYN给服务端,服务端接收到。

注意哦!现在服务端不是立刻回复 SYN + ACK,而是通过计算得到一个SYN Cookie, 将这个Cookie放到 TCP 报文的 Fast Open选项中,然后才给客户端返回。

客户端拿到这个 Cookie 的值缓存下来。后面正常完成三次握手。

首轮三次握手就是这样的流程。而后面的三次握手就不一样啦!

  • 后面的三次握手

在后面的三次握手中,客户端会将之前缓存的 Cookie、SYN 和HTTP请求(是的,你没看错)发送给服务端,服务端验证了 Cookie 的合法性,如果不合法直接丢弃;如果是合法的,那么就正常返回SYN + ACK。

重点来了,现在服务端能向客户端发 HTTP 响应了!这是最显著的改变,三次握手还没建立,仅仅验证了 Cookie 的合法性,就可以返回 HTTP 响应了。

当然,客户端的ACK还得正常传过来,不然怎么叫三次握手嘛。

流程如下:
注意: 客户端最后握手的 ACK 不一定要等到服务端的 HTTP 响应到达才发送,两个过程没有任何关系。
在这里插入图片描述

TFO 的优势

TFO 的优势并不在与首轮三次握手,而在于后面的握手,在拿到客户端的 Cookie 并验证通过以后,可以直接返回 HTTP 响应,充分利用了1 个RTT(Round-Trip Time,往返时延)的时间提前进行数据传输,积累起来还是一个比较大的优势。

三、TCP的四次挥手

在这里插入图片描述

那又为什么要四次挥手呢?

那四次分手又是为何呢?TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2, 它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文 段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN 报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此 就会愉快的中断这次TCP连接。如果要正确的理解四次分手的原理,就需要了解四次分手过程中的状态变化。

过程详解

第一次挥手: 客户端发送FIN(表示要结束连接)给服务器,客户端状态由 ESTABLISHED 变为 FIN_WAIT_1。

第二次挥手:
服务器收到ACK、,服务器状态由 ESTABLISHED 变为CLOSE_WAIT。
服务器将缓存中没发送的数据完继续发送给客户端,客户端收到ACK后状态由FIN_WAIT_1变为FIN_WAIT_2。

第三次挥手:服务器发送FIN给客户端,这时服务器的状态由CLOSE_WAIT变为 LAST_ACK。

第四次挥手:
客户端收到FIN后返回ACK给服务器,然后客户端的状态由FIN_WAIT_2变为TIME_WAIT。
服务器收到ACK后,状态由LAST_ACK变为CLOSED。
而客户端再经过TIME_WAIT时间后变为CLOSED状态。

PS:TIME_WAIT = 2MSL (maximum segement lifetime 分节在网络中最长生存时间,30秒到2分钟,根据系统实现不同而不同) 2MSL 范围是 1分钟到4分钟。

为什么最后客户端还要等待 2*MSL的时间呢?

MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。

  1. 保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。

  2. 防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。

如果已经建立了连接, 但是客户端突发故障了怎么办?

TCP设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75分钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

四、TCP的特点

TCP相对于UDP协议的特点是:面向连接的、字节流和可靠传输。
  • 面向连接的:使用TCP协议通信的双方必须先建立连接,然后才能开始数据的读写,TCP连接是全双工的,即双方的数据读写可以通过一个连接进行。完成数据交换之后,通信双方都必须断开连接以释放资源。TCP协议的这种连接是一对一的,所以基于广播和多播(目标是多个主机地址)的应用程序不能使用TCP服。而无连接协议UDP则非常适合于广播和多播

  • 流式服务:TCP的字节流服务的表现形式就体现在,发送端执行的写操作数和接收端执行的读操作次数之间没有任何数量关系,当发送端应用程序连续执行多次写操作时,TCP模块先将这些数据放入TCP发送缓冲区中。当TCP模块真正开始发送数据的时候,发送缓冲区中这些等待发送的数据可能被封装成一个或多个TCP报文段发出。

  • UPD的数据报服务:发送端应用程序每执行一次写操作,UDP模块就将其封装成一个UDP数据报并发送之。接收端必须及时针对每一个UDP数据报执行读操作(通过recvfrom系统调用),否则就会丢包(这经常发生在较慢的服务器上)。并且,如果没有指定足够的应用程序缓冲区来读取UDP数据,则UDP数据将被截断。

五、TCP可靠性传输

实现TCP可靠传输的具体机制见下文

TCP通过下列方式来提供可靠性:

  1. TCP没有报文长度字段,只有序号字段,应用数据通过序号被分割成TCP认为最适合发送的数据块,应用程序产生的数据报长度将保持不变

  2. 超时重发——当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。

  3. 对于收到的请求,给出确认响应——当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒 。 (之所以推迟,是因为延时确认可以更有效利用接收端的缓冲区)

  4. TCP将保持它首部和数据的检验和——这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段。 (校验出包有错,丢弃报文段,不给出响应,TCP发送数据端,超时时会重发数据)

  5. 失序重排——既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。如果必要,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层。

  6. 丢弃重复数据——既然IP数据报会发生重复,TCP的接收端必须丢弃重复的数据。

  7. TCP还能提供流量控制——TCP连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送接收端缓冲区所能接纳 的数据。这将防止较快主机致使较慢主机的缓冲区溢出。(TCP可以进行流量控制,防止较快主机致使较慢主机的缓冲区溢出)TCP使用的流量控制协议是可变大小的滑动窗口协议。

TCP中的时间问题

两个概念:

  • 超时重传时间(Retransmission TimeOut, 简称RTO)
  • 往返时延 RTT(Round-Trip Time)
TCP报文中时间戳的作用?

timestamp是 TCP 报文首部的一个可选项,一共占 10 个字节,格式如下:

kind(1 字节) + length(1 字节) + info(8 个字节)

其中 kind = 8, length = 10, info 有两部分构成: timestamp和timestamp echo,各占 4 个字节。

那么这些字段都是干嘛的呢?它们用来解决那些问题?

接下来我们就来一一梳理,TCP 的时间戳主要解决两大问题:

  • 计算往返时延 RTT(Round-Trip Time)

  • 防止序列号的回绕问题

计算往返时延 RTT

在没有时间戳的时候,计算 RTT 会遇到的问题如下图所示:
在这里插入图片描述

如果以第一次发包为开始时间的话,就会出现左图的问题,RTT 明显偏大,开始时间应该采用第二次的;

如果以第二次发包为开始时间的话,就会导致右图的问题,RTT 明显偏小,开始时间应该采用第一次发包的。

实际上无论开始时间以第一次发包还是第二次发包为准,都是不准确的。

那这个时候引入时间戳就很好的解决了这个问题。

比如现在 a 向 b 发送一个报文 s1,b 向 a 回复一个含 ACK 的报文 s2 那么:

  • step 1: a 向 b 发送的时候,timestamp 中存放的内容就是 a 主机发送时的内核时刻 ta1。

  • step 2: b 向 a 回复 s2 报文的时候,timestamp 中存放的是 b 主机的时刻 tb, timestamp echo字段为从 s1 报文中解析出来的 ta1。

  • step 3: a 收到 b 的 s2 报文之后,此时 a 主机的内核时刻是 ta2, 而在 s2 报文中的 timestamp echo 选项中可以得到 ta1, 也就是 s2 对应的报文最初的发送时刻。然后直接采用 ta2 - ta1 就得到了 RTT 的值。

防止序列号回绕问题

现在我们来模拟一下这个问题。

序列号的范围其实是在0 ~ 2 ^ 32 - 1, 为了方便演示,我们缩小一下这个区间,假设范围是 0 ~ 4,那么到达 4 的时候会回到 0。

在这里插入图片描述
假设在第 6 次的时候,之前还滞留在网路中的包回来了,那么就有两个序列号为1 ~ 2的数据包了,怎么区分谁是谁呢?这个时候就产生了序列号回绕的问题。

那么用 timestamp 就能很好地解决这个问题,因为每次发包的时候都是将发包机器当时的内核时间记录在报文中,那么两次发包序列号即使相同,时间戳也不可能相同,这样就能够区分开两个数据包了。

六、TCP的各类机制

ACK机制

在这里插入图片描述
TCP将每个字节的数据都进行了编号, 即为序列号;每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你要从哪里开始发。
比如, 客户端向服务器发送了1005字节的数据, 服务器返回给客户端的确认序号是1003, 那么说明服务器只收到了1~1002的数据,1003, 1004, 1005都没收到,此时客户端就会从1003开始重发。

超时重传机制

在这里插入图片描述

在这里插入图片描述

主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B,如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发,但是主机A没收到确认应答也可能是ACK丢失了,这种情况下, 主机B会收到很多重复数据,这时候利用前面提到的序列号, 就可以很容易做到去重

超时时间如何确定?

最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”。但是这个时间的长短, 随着网络环境的不同, 是有差异的。
如果超时时间设的太长, 会影响整体的重传效率; 如果超时时间设的太短, 有可能会频繁发送重复的包。

TCP为了保证任何环境下都能保持较高性能的通信, 因此会动态计算这个最大超时时间

Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍。

如果重发一次之后, 仍然得不到应答, 等待 2500ms 后再进行重传. 如果仍然得不到应答, 等待 4500ms 进行重传。依次类推, 以指数形式递增. 累计到一定的重传次数, TCP认为网络异常或者对端主机出现异常, 强制关闭连接。

滑动窗口机制

刚才我们讨论了确认应答机制, 对每一个发送的数据段, 都要给一个ACK确认应答.,收到ACK后再发送下一个数据段。这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返时间较长的时候。
那么我们可不可以一次发送多个数据段呢?
在这里插入图片描述

  • 窗口:窗口大小指的是无需等待确认应答就可以继续发送数据的最大值,上图的窗口大小就是4000个字节 (四个段)。
    发送前四个段的时候, 不需要等待任何ACK, 直接发送。收到第一个ACK确认应答后, 窗口向后移动, 继续发送第五六七八段的数据。

  • 因为这个窗口不断向后滑动, 所以叫做滑动窗口。
    操作系统内核为了维护这个滑动窗口, 需要开辟发送缓冲区来记录当前还有哪些数据没有应答,只有ACK确认应答过的数据, 才能从缓冲区删掉。
    在这里插入图片描述
    在滑动窗口下,如果出现丢包,进行重传的方法如下:

数据包到了,可是回送的ACK丢失

在这里插入图片描述这种情况下, 部分ACK丢失并无大碍, 因为还可以通过后续的ACK来确认对方已经收到了哪些数据包。
个人理解,窗口大小是协商好的,当第一个窗口段的ACK丢包,主机收到第二个窗口段的ACK时,发现确认序号是2001,那表明1001~2000的字节的数据已经收到了,可是主机并没有收到确认序号为1001的ACK,那么就要对1—1000字节的数据启用重传机制。

数据包丢失

在这里插入图片描述
当某一段报文丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 “我想要的是 1001”。
如果发送端主机连续三次(是固定三次还是和窗口大小有关我不太清楚)收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送。
这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了,因为2001 - 7000接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中
这种机制被称为 “高速重发控制” ( 也叫 “快重传” )

选择性重传

如上所示,比如第 2 个包丢了,即使第 3、4 个包到达的接收端,接收端也一律返回第 2个包的 ACK。当发送端收到 3 个重复的 ACK 时,意识到丢包了,于是马上进行重传,不用等到一个 RTO 的时间到了才重传。这就是快速重传,它解决的是是否需要重传的问题。

那你可能会问了,既然要重传,那么只重传第 2 个包还是第2、3、4 个包都重传呢?

当然第 3、4个都已经到达了,TCP 的设计者也不傻,已经传过去干嘛还要传?干脆记录一下哪些包到了,哪些没到,针对性地重传。

在收到发送端的报文后,接收端回复一个 ACK 报文,那么在这个报文首部的可选项中,就可以加上SACK这个属性,通过left edge和right edge告知发送端已经收到了哪些区间的数据报。因此,即使第 2 个包丢包了,当收到第 3、4个包之后,接收端依然会告诉发送端,这两个包到了。剩下第 2个包没到,就重传这个包。这个过程也叫做选择性重传(SACK,Selective Acknowledgment),它解决的是如何重传的问题。

流量控制

接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被填满, 这个时候如果发送端继续发送, 就会造成丢包, 进而引起丢包重传等一系列连锁反应。
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)

在这里插入图片描述

那么接收端如何把窗口大小告诉发送端呢?

我们的TCP首部中, 有一个16位窗口大小字段, 就存放了窗口大小的信息,16位数字最大表示65536,通过ACK通知发送端,窗口大小越大, 说明网络的吞吐量越高;
接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端。
发送端接受到这个窗口大小的通知之后, 就会减慢自己的发送速度;
如果接收端缓冲区满了, 就会将窗口置为0,这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 让接收端把窗口大小再告诉发送端。

那么TCP窗口最大就是65536字节么?

实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是窗口字段的值左移 M 位(左移一位相当于乘以2)。

拥塞控制

虽然TCP有了滑动窗口这个大杀器, 能够高效可靠地发送大量数据。但是如果在刚开始就发送大量的数据,仍然可能引发一些问题。因为网络上有很多计算机, 可能当前的网络状态已经比较拥堵,在不清楚当前网络状态的情况下, 贸然发送大量数据, 很有可能雪上加霜。

因此, TCP引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态以后, 再决定按照多大的速度传输数据。

在这里插入图片描述
在此引入一个概念“拥塞窗口(cwnd)”
PS:在考虑拥塞的时候我们一般不考虑rwnd的值

  • 发送开始的时候, 定义拥塞窗口大小为1;
  • 每次收到一个ACK应答, 拥塞窗口加1;
    cwnd增加1也就是相当于字节数增加1个MSS(最大报文段)大小
  • 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口。TCP的真正的发送窗口=min(rwnd接收窗口, cwnd拥塞窗口)

(可以尝试回顾一下,和上述的滑动窗口机制、流量控制机制联系起来,叙述一下TCP数据传输的过程。)

像上面这样的拥塞窗口增长速度,是指数级别的,“慢启动” 只是指初始时慢, 但是增长速度非常快。
为了不增长得那么快, 此处引入一个名词叫做慢启动的阈值(ssthresh), 当拥塞窗口的大小超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长。

当TCP开始启动的时候, 慢启动阈值有一个初始值
无论是慢启动算法还是拥塞避免算法,只要判断网络出现拥塞,就要把慢启动开始门限(ssthresh)设置为设置为发送窗口的一半(>=2),拥塞窗口(cwnd)设置为1。在这里插入图片描述

快恢复算法

快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时才进行捎带确认。(不能使用捎带应答,下文提到)

其过程有以下两个要点:

  • 当发送方连续收到三个重复确认,就执行“乘法减小”算法,把慢开始门限ssthresh减半。这是为了预防网络发生拥塞。请注意:接下去不执行慢开始算法。

  • 由于发送方现在认为网络很可能没有发生拥塞,因此与慢开始不同之处是现在不执行慢开始算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。

    下图给出了快重传和快恢复的示意图,并标明了“TCP Reno版本”。

在这里插入图片描述

区别:新的 TCP Reno 版本在快重传之后采用快恢复算法而不是采用慢开始算法。

也有的快重传实现是把开始时的拥塞窗口cwnd值再增大一点,即等于 ssthresh + 3 X MSS 。这样做的理由是:既然发送方收到三个重复的确认,就表明有三个分组已经离开了网络。这三个分组不再消耗网络 的资源而是停留在接收方的缓存中。可见现在网络中并不是堆积了分组而是减少了三个分组。因此可以适当把拥塞窗口扩大了些。

在采用快恢复算法时,慢开始算法只是在TCP连接建立时和网络出现超时时才使用。

采用这样的拥塞控制方法使得TCP的性能有明显的改进。

少量的丢包, 我们仅仅是触发超时重传;大量的丢包, 我们就认为是网络拥塞。
当TCP通信开始后, 网络吞吐量会逐渐上升;随着网络发生拥堵, 吞吐量会立刻下降。

Nagle 算法(延迟发送)

试想一个场景,发送端不停地给接收端发很小的包,一次只发 1 个字节,那么发 1 千个字节需要发 1000 次。这种频繁的发送是存在问题的,不光是传输的时延消耗,发送和确认本身也是需要耗时的,频繁的发送接收带来了巨大的时延。

而避免小包的频繁发送,这就是 Nagle 算法要做的事情。

具体来说,Nagle 算法的规则如下:
当第一次发送数据时不用等待,就算是 1byte 的小包也立即发送,后面发送满足下面条件之一就可以发了:

  • 数据包大小达到最大段大小(Max Segment Size, 即 MSS)

  • 之前所有包的 ACK 都已接收到

延迟应答

前言:
窗口越大, 网络吞吐量就越大, 传输效率就越高。
TCP的目标是在保证网络不拥堵的情况下尽量提高传输效率。

如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小.
假设接收端缓冲区为1M. 一次收到了500K的数据,如果立刻应答, 返回的窗口大小就是500K。
但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区处理掉了; 在这种情况下, 接收端其实可以再次接受1M的数据,可他返回的窗口大小只有500K,处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来。
如果接收端稍微等一会儿再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M。

那么所有的数据包都可以延迟应答么?

有两个限制:

  1. 数量限制: 每隔N个包就应答一次
  2. 时间限制: 超过最大延迟时间就应答一次

具体的数量N和最大延迟时间, 依操作系统不同也有差异
一般 N 取2, 最大延迟时间取200ms

有一些场景是不能延迟确认的,收到了就要马上回复:

  1. 接收到了大于一个 frame 的报文,且需要调整窗口大小
  2. TCP 处于 quickack 模式(通过tcp_in_quickack_mode设置)
  3. 发现了乱序包

※ 延迟发送和延迟接收两者一起使用会怎样?

会造成更大的延迟,产生性能问题。

捎带应答

在延迟应答的基础上, 我们发现, 很多情况下,客户端和服务器在应用层也是 “一发一收” 的。
意味着客户端给服务器说了 “How are you”,服务器也会给客户端回一个 “Fine, thank you”,那么这个时候ACK就可以搭顺风车, 和服务器回应的 “Fine, thank you” 一起发送给客户端 。
在这里插入图片描述

面向字节流

创建一个TCP的socket, 同时在内核中创建一个发送缓冲区和一个接收缓冲区。调用write时, 数据会先写入发送缓冲区中:

  • 如果发送的字节数太大, 会被拆分成多个TCP的数据包发出。
  • 如果发送的字节数太小, 就会先在缓冲区里等待, 等到缓冲区大小差不多了, 或者到了其他合适的时机再发送出去。

接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区,然后应用程序可以调用read从接收缓冲区拿数据。

TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区,那么对于这一个连接, 既可以读数据, 也可以写数据, 这个概念叫做全双工。

由于缓冲区的存在, 所以TCP程序的读和写不需要一一匹配
例如:

  • 写100个字节的数据, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节。
  • 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次。
粘包问题

首先要明确, 粘包问题中的 “包”, 是指应用层的数据包
在TCP的协议头中, 没有如同UDP一样的 “报文长度” 字段,但是有一个序号字段。站在传输层的角度, TCP是一个一个报文传过来的. 按照序号排好序放在缓冲区中,站在应用层的角度, 看到的只是一串连续的字节数据
那么应用程序看到了这一连串的字节数据, 就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包?此时数据之间就没有了边界, 就产生了粘包问题。

那么如何避免粘包问题呢?
明确两个包之间的边界

  • 对于定长的包
    保证每次都按固定大小读取即可,例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可。

  • 对于变长的包
    可以在数据包的头部, 约定一个数据包总长度的字段, 从而就知道了包的结束位置。
    还可以在包和包之间使用明确的分隔符来作为边界(应用层协议, 是程序员自己来定的, 只要保证分隔符不和正文冲突即可)。

对于UDP协议来说, 是否也存在 “粘包问题” 呢?

不会。对于UDP, 如果还没有向上层交付数据, UDP的报文长度仍然存在。同时, UDP是一个一个把数据交付给应用层的, 就有很明确的数据边界。
站在应用层的角度, 使用UDP的时候,要么收到完整的UDP报文, 要么不收,不会出现收到 “半个” 的情况。

TCP 异常情况
  • 进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别。
  • 机器重启: 和进程终止的情况相同。
  • 机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行 reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放。

另外, 应用层的某些协议, 也有一些这样的检测机制:
例如HTTP长连接中, 也会定期检测对方的状态
例如QQ, 在QQ断线之后, 也会定期尝试重新连接

七、说一说 TCP 和 UDP 的区别

首先概括一下基本的区别:

TCP是一个面向连接的、可靠的、基于字节流的传输层协议。

而UDP是一个面向无连接的传输层协议。(就这么简单,其它TCP的特性也就没有了)。

具体来分析,和 UDP 相比,TCP 有三大核心特性:

  • 面向连接。所谓的连接,指的是客户端和服务器的连接,在双方互相通信之前,TCP 需要三次握手建立连接,而 UDP 没有相应建立连接的过程。

  • 可靠性。TCP 花了非常多的功夫保证连接的可靠,这个可靠性体现在哪些方面呢?一个是有状态,另一个是可控制。

    TCP 会精准记录哪些数据发送了,哪些数据被对方接收了,哪些没有被接收到,而且保证数据包按序到达,不允许半点差错。这是有状态。

    当意识到丢包了或者网络环境不佳,TCP 会根据具体情况调整自己的行为,控制自己的发送速度或者重发。这是可控制。

    相应的,UDP 就是无状态, 不可控的。

  • 面向字节流。UDP 的数据传输是基于数据报的,这是因为仅仅只是继承了 IP 层的特性,而 TCP 为了维护状态,将一个个 IP 包变成了字节流。

说说半连接队列和 SYN Flood 攻击的关系

三次握手前,服务端的状态从CLOSED变为LISTEN, 同时在内部创建了两个队列:半连接队列和全连接队列,即SYN队列和ACCEPT队列。

半连接队列

当客户端发送SYN到服务端,服务端收到以后回复ACK和SYN,状态由LISTEN变为SYN_RCVD,此时这个连接就被推入了SYN队列,也就是半连接队列。

全连接队列

当客户端返回ACK, 服务端接收后,三次握手完成。这个时候连接等待被具体的应用取走,在被取走之前,它会被推入另外一个 TCP 维护的队列,也就是全连接队列(Accept Queue)。

SYN Flood 攻击原理

SYN Flood 属于典型的 DoS/DDoS 攻击。其攻击的原理很简单,就是用客户端在短时间内伪造大量不存在的 IP 地址,并向服务端疯狂发送SYN。对于服务端而言,会产生两个危险的后果:

  • 处理大量的SYN包并返回对应ACK, 势必有大量连接处于SYN_RCVD状态,从而占满整个半连接队列,无法处理正常的请求。

  • 由于是不存在的 IP,服务端长时间收不到客户端的ACK,会导致服务端不断重发数据,直到耗尽服务端的资源。

如何应对 SYN Flood 攻击?
  • 增加 SYN 连接,也就是增加半连接队列的容量。

  • 减少 SYN + ACK 重试次数,避免大量的超时重发。

  • 利用 SYN Cookie 技术,在服务端接收到SYN后不立即分配连接资源,而是根据这个SYN计算出一个Cookie,连同第二次握手回复给客户端,在客户端回复ACK的时候带上这个Cookie值,服务端验证 Cookie 合法之后才分配连接资源。

八、TCP协议总结

为什么TCP这么复杂?

因为既要保证可靠性, 同时又要尽可能提高性能。

保证可靠性的机制
  • 校验和
  • 序列号(按序到达)
  • 确认应答
  • 超时重传
  • 连接管理
  • 流量控制
  • 拥塞控制
提高性能的机制
  • 滑动窗口
  • 快速重传
  • 延迟应答
  • 捎带应答
基于 TCP 的应用层协议

HTTP、HTTPS、SSH、Telnet、FTP、SMTP等

TCP 和 UDP 的应用领域

TCP和UDP之间的优点和缺点, 不能简单绝对地进行比较
TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景
UDP用于对高速传输和实时性要求较高的通信领域
例如, 早期的QQ, 视频传输等. 另外UDP可以用于广播

部分内容参考博客:rugu_xxx

  • 8
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值