计算机网络学习笔记之详解传输层TCP协议

477 阅读17分钟

计算机网络学习笔记

注:本文章的学习内容参考至:https://cyc2018.github.io/CS-Notes

详解传输层TCP协议

概念

传输控制协议 TCP( Transmission Control Protocol )是面向连接的,提供可靠性交付,有流量控制、拥塞控制,提供全双工通信,面向字节流(把应用层协议传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。

TCP 和 UDP 的区别与利弊

  • TCP 是面向连接的,具有可靠性(重传、确认、有序传输)同时还能进行流量控制和拥塞控制的特点。双方的通讯是在一个确定的上下文中进行的。因此,TCP 的协议首部会比较大,而且在每次需要通讯的时候都要有连接和断连的操作,因此 TCP 没有 UDP 传输迅速。
  • UDP 没有一个确定的上下文通讯,是一个不可靠的通讯协议,没有 TCP 的重传、确认、有序控制、流量控制、拥塞控制的特性。只是简单的在 IP 报文的基础上,增加一个8字节的首部,存储的信息有限。但是 UDP 的协议内容简单,因此他适合哪些对时延、丢包都不是特别敏感的场景,例如 DNS 服务。UDP 还会用到多人通讯的场景。

TCP首部格式

TCP 的三次握手

连接过程

假设A是客户端,B是服务端,并且B已经处于监听(LISTEN)状态。

  • A 向 B发送请求报文,SYN = 1 和一个初始的序号x(seq= x),seq对应 TCP 首部格式中序号的部分
  • B 收到 A发送的含有SYN = 1的请求报文后,如果同意连接就会回复响应报文:SYN = 1,ACK = 1 以及 确认号x+1(ack =x + 1)和服务端生成的初始的序号y(seq = y)
  • A 收到B回复的响应报文(含有 SYN = 1,ACK = 1),A 就会向B发送请求报文:ACK = 1 和 确认号 y+1 (ack = y + 1)以及序号x+1(seq = x + 1),代表 A 确认了B的连接
  • A 和 B 成功建立连接

连接解释

在三次握手的过程中,着重体现了TCP首部格式中 SYN、ACK、Seq(序号)、Ack(确认号)的作用。

  • SYN:在连接建立时用来同步序号。简而言之就是开始连接的标识符,当 SYN = 1,ACK = 0 时,就代表要准备建立连接了。
  • ACK:确认标志符。用法很简单,当 ACK = 0时代表未确认,和 SYN 搭配使用就可以代表连接建立的报文。 TCP 规定当连接建立后,所传输的报文段中 ACK 必须置为1;
  • Seq:序号。代表对字节流进行编号,它会存在于所有的 TCP 报文中,建立连接的过程也是在发送 TCP 报文,所以它肯定在,至于他的作用,下面细讲。
  • Ack:确认号。代表期望下一次报文的序号。从而对报文的接收顺序进行了控制。

为何要进行三次握手?

首先要知道一个前提,TCP 协议是一个双工通讯的协议,它在一条线路上面进行双向的通信。就可以类比成一个双端队列,客户端在左侧,服务端在右侧,都可以进行入队和出队。那么为什么要进行三次握手呢?原因是信道具有不可靠性。既然TCP是一个面向连接的可靠性协议,那么在建立连接之前肯定第一步要确认当前通讯的路线是否是可靠的。验证内容就要包括,客户端发送和接收以及服务器端的发送和接收是否正常。

  • 第一步客户端发送SYN可验证客户端的发送
  • 第二步服务端接受客户端带有 SYN = 1 的报文,并且回复了 SYN = 1 、ACK = 1可验证服务端的接收和发送
  • 第三步客户端接收到服务端带有 SYN = 1、ACK = 1 并回复 ACK = 1可验证客户端的接收

综上所述:三次握手是 TCP 针对信道的不可靠性的所作出的应对措施,为何是三次而不是两次或者四次,原因是三次就正好可以验证出信道在建立连接时的可靠状态,少一次不行多一次多余。

如果三次握手中出现断连的情况系统是怎么处理的?

TCP 第一次握手的 SYN 丢包了,会发生了什么?
  • 当客户端发起的 TCP 第一次握手 SYN 包,在超时时间内没收到服务端的 ACK,就会在超时重传 SYN 数据包,每次超时重传的 RTO 是翻倍上涨的,直到 SYN 包的重传次数到达 tcp_syn_retries 值后,客户端不再发送 SYN 包。
TCP 第二次握手的 SYN、ACK 丢包了,会发生什么?
  • 当 TCP 第二次握手 SYN、ACK 包丢了后,客户端 SYN 包会发生超时重传,服务端 SYN、ACK 也会发生超时重传。
  • 客户端 SYN 包超时重传的最大次数,是由 tcp_syn_retries 决定的,默认值是 5 次;服务端 SYN、ACK 包时重传的最大次数,是由 tcp_synack_retries 决定的,默认值是 5 次。
TCP 第三次握手的 ACK 包丢了,会发生什么?
  • 如果第三次握手的 ACK,服务端无法收到,则服务端就会短暂处于 SYN_RECV 状态,而客户端会处于 ESTABLISHED 状态。
  • 由于服务端一直收不到 TCP 第三次握手的 ACK,则会一直重传 SYN、ACK 包,直到重传次数超过 tcp_synack_retries 值(默认值 5 次)后,服务端就会断开 TCP 连接。
  • 客户端会有两种情况:
    • 如果客户端没发送数据包,一直处于 ESTABLISHED 状态,然后经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接,于是客户端连接就会断开连接。
    • 如果客户端发送了数据包,一直没有收到服务端对该数据包的确认报文,则会一直重传该数据包,直到重传次数超过 tcp_retries2 值(默认值 15 次)后,客户端就会断开 TCP 连接。

TCP 的四次挥手

连接过程

前提:在以下的描述中不在考虑序号(Seq)和确认号(Ack)的规则,并且以下报文中 ACK 都是1;

  • A 发送连接释放的报文,FIN = 1,然后 A 进入FIN_WAIT_1 状态
  • B 收到后送 A 的报文后,将回复确认报文,然后 B 就进入 CLOSE_WAIT 状态。此时,TCP将进入半关闭状态,B 能够向 A 发送报文,但是 A 不能 向 B 发送数据。同时,A 收到 B 回复的确认报文后就会进入 FIN_WAIT_2 状态
  • B 处理完剩余的内容就会在向 A 发送 FIN = 1,然后 B 就进入 LAST_ACK 状态
  • A 收到 B 回复的确认报文就会回复确认报文然后进入TIME_WAIT 状态,这也是 A 在 TCP 连接的最后阶段,一般 A 的 TIME_WAIT 状态会持续 2MSL(最长报文生存时间)时间,如果2MSL 时间内没有在收到 B 的报文,A 就会释放套接字资源。而B 收到 A 回复的确认报文后就会释放套接字资源

连接解释

在四次挥手的过程中,主要体现了 FIN 的作用

  • FIN:用来作为连接释放的标志,当 FIN = 1 时,代表此报文段的发送方数据已经发送完毕,并要求释放套接字资源

注意:无论是客户端还是服务器,任何一端都可以发起主动关闭。大多数真实情况是客户端执行主动关闭,你可能不会想到的是,HTTP/1.0 却是由服务器发起主动关闭的。

为何会有四次挥手?

主动关闭端发送了 FIN 连接释放报文后,被动关闭端收到了这个报文,就会进入 CLOSE-WAIT状态。这个状态是为了让被动关闭端发送还未传送完毕的数据,传送完毕后,服务器才会发送 FIN 连接释放报文。

为什么最后主动关闭端要进入TIME_WAIT状态?

在四次挥手的描述中,最后阶段,主动关闭方在回复被动关闭方关闭报文后,会进入时间为 2MSL 的 TIME_WAIT 状态,这么做有以下几个目的:

  • 确保最后一个确认报文能够到达被动关闭端。如果被动关闭端没有收到确认报文,那么被动关闭端会重发连接释放报文,主动关闭端等待一段时间就是为了处理这种情况的发生。
  • 等待一段时间是为了让此次连接持续时间内所产生的所有报文从网络中消失,使得下一个连接不会出现旧的连接请求报文。

注意:2MSL 的时间是从主机 1 接收到 FIN 后发送 ACK 开始计时的

TIME_WAIT 的危害

  • 内存资源占用(目前看来不大严重,基本可以忽略)
  • 对端口资源的占用,一个 TCP 连接至少消耗一个本地端口。

如何优化 TIME_WAIT?

  • 调低 TCP_TIMEWAIT_LEN,重新编译系统(需要重新编译内核,比较复杂)
  • net.ipv4.tcp_tw_reuse (从协议角度理解如果是安全可控的,可以复用处于 TIME_WAIT 的套接字为新的连接所用。)
    • 只适用于连接发起方(C/S 模型中的客户端)
    • 对应的 TIME_WAIT 状态的连接创建时间超过 1 秒才可以被复用。
    • 需要打开对 TCP 时间戳的支持,即net.ipv4.tcp_timestamps=1(默认即为 1)
    • 如果连接双方的时间戳存在差异,可能服务器端就容易丢包
  • SO_REUSEADDR :套接字选项
    • 允许启动绑定在一个端口,即使之前存在一个和该端口一样的连接。
    • 本机服务器如果有多个地址,可以在不同地址上使用相同的端口提供服务。
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

综述: tcp_tw_reuse是为了缩短time_wait的时间,避免出现大量的time_wait链接而占用系统资源,解决的是accept后的问题;SO_REUSEADDR是为了解决time_wait状态带来的端口占用问题,以及支持同一个port对应多个ip,解决的是bind时的问题。

序号和确认号的含义及作用

释义

  • 序号( sequence number ):表示我方(发送方)报文中,本次发送的包的数据部分(应用层的报文)的第一位应该在整个 字节流(data steam)中所在位置(可以理解成偏移量)。(注意这里使用的是‘应该’。因为对于没有数据的传输,如 ACK,虽然它有一个 Seq ,但是这次传输在整个 data steam 中是不占位置的。所以下一个实际有数据的传输,会依旧从上一次发送 ACK 的数据包的 Seq 开始)
  • 确认号( acknowledge number ):表示的是期望对方(接收方)的下一次 Seq 是多少。
  • 注意:SYN/FIN 的传输虽然没有 data ,但是会让下一次传输的 packet Seq 加一。而带有 ACK =1 报文的传输,不会改变 Seq。

三次握手时 Seq 和 Ack 的传输情况

  • 第一次握手(发送):seq为x(x为任意值),无视ack(因为是第一个包,不需要给其他包应答)
  • 第二次握手(发送):seq为y(y为任意值),ack等于接收包seq+1(即x+1)
  • 第三次握手(发送):seq等于上一个本机发送包seq+1(即x+1),也就是1,ack等于接收包seq+1(即y+1)

数据传输过程中 Seq 和 Ack 的传输情况

  • len 代表每次传输的包中 data steam(字节流)的大小
  • 某主机发送的seq和ack是根据上一个接收包的seq、ack和len得到,具体为:seq=ack,ack=seq+len
  • 提醒:如果某一主机连续发了4个包,后三个包的seq和ack和第一个包的一样 seq会单调增大
  • 特别: 如果握手完第一个数据包是客户端发送,第一个数据包的seq和ack和第三次握手的一样 

四次挥手时 Seq 和Ack 的传输情况

  • 如果是服务器发起的挥手,挥手前最后一个包是服务器发送 / 如果是客户端发起的挥手,挥手前最后一个包是客户端发送
    • 第一次挥手(发送):seq为上一个本机发送包seq+len,ack为上一个本机发送包ack
    • 第二个挥手(发送):seq为本次接收包ack,ack为本次接收包seq+1
    • 第三次挥手(发送):和第二次挥手一样
    • 第四次挥手(发送):seq为本次接收包ack,ack为本次接收包seq+1
  • 如果是服务器发起的挥手,挥手前最后一个包是客户端发送 / 如果是客户端发起的挥手,挥手前最后一个包是服务器发送
  • 第一次挥手(发送):seq为本次接收包ack,ack为本次接收包seq+len
  • 第二个挥手(发送):seq为本次接收包ack,ack为本次接收包seq+1
  • 第三次挥手(发送):和第二次挥手一样
  • 第四次挥手(发送):seq为本次接收包ack,ack为本次接收包seq+1

TCP 流量控制

TCP 滑动窗口

前提

为了记录所有发送的包和接收的包, TCP 在发送端和接收端分别都有缓存来保存这些记录。

窗口的由来

在 TCP 中,接收端会给发送端报一个窗口的大小,对应头部里面的 Advertised Window,这个窗口大小是接收端根据自己的缓存和处理情况计算出来的,代表超过这个窗口的大小接收端就忙不过来了,就别发送了。

发送端的缓存是按照包的 ID 一个个排列的,根据处理情况分为下面四个部分:

  1. 发送了并且已经确认了的,这些是应该删掉的
  2. 发送了并且尚未确认的,这些需要等待确认回复
  3. 没有发送的,但是已经等待了的
  4. 没有发送,并且暂时不会发送的

发送端需要保持的数据结构

  • LastByteAcked表示第一部分和第二部分的分界线
  • LastByteSent表示第二部分和第三部分的分界线
  • LastByteAcked + 窗口大小 表示第三部分和第四部分的分界线

接收端需要保持的数据结构

  • MaxRcvBuffer 最大缓存的量
  • LastByteRead之后是已经接收了的,但是还没有被应用层读取的
  • NextByteExcepted是第一部分和第二部分的分界线

Advertised Window 对应的就是图中第二部分的大小,计算方式为 MaxRcvBuffer - (NextByteExcepted与LastByteRead之间的距离)

实际上在第二部分,由于收到的包可能不是连续的,会处出现空挡,如下图所示,LastByteRcvd代表收到包的最后一个位置,因此 Advertised Window = MaxRcvBuffe - LastByteRcvd -1

综述:

TCP 接收端和发送端都定义了基于数据包拆分传递的数据结构来作为缓存,为了避免接收端收到的请求太多处理不过来从而导致网络阻塞的问题。TCP 在首部里面定义了 Advertised Window 字段,可以让接收端将当前可处理的窗口大小告诉发送端,发送端根据接收端告诉的窗口大小来控制传递的数据包的大小,从而在一定程度上面避免网络堵塞。

流量控制

释义:

流量控制就是为了控制发送方发送数据的速率,从而保证接收方可以来得及接收的手段。

利用 TCP 滑动窗口的规则就可以在一定程度上面进行流量控制,接收端通过调整窗口的大小,从而控制发送方发送数据的速率。

拓展:
  • 如果接收方的窗口值设置为0,那么发送方就不在发送数据,但是发送方会发送 ZWP 给接收方,来试探接收方的窗口大小(试探的次数是有限的),如果窗口大小增加了,就会继续发送报文。
  • Silly Window Syndrome(糊涂窗口综合征):如果接收方太忙,来不及上面描述的第一部分的数据,那么就会导致发送方越来越小。到最后,如果接收方腾出几个字节告诉发送方现在有几个字节的 Window,而我们的发送方还会傻傻的发送这几个字节的数据。协议头会有40个字节,只为了发送这几个字节的报文,太浪费带宽了,因此需要避免,方法如下:
    • 如果这个问题是接收端引起的,就会使用 David D Clark’s 方案
    • 如果这个问题是发送端引起的,就会使用 Nagle’s algorithm , 这个算法的思路也是延时处理,他有两个主要的条件:1)要等到 Window Size>=MSS 或是 Data Size >=MSS,2)收到之前发送数据的ack回包,他才会发数据,否则就是在攒数据。

TCP 拥塞控制

释义

如果网络出现拥塞,分组将会丢失,此时发送方在迟迟没有收到接收方的回复后会重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率,这里和流量控制很像,但是流量控制的目的是为了方式接收方处理不过来的情况,而拥塞控制是为了降低整个网络的拥塞程度。

TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。

发送方需要维护一个拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据是发送方窗口。

算法释义

为了便于讨论,做如下假设:

  • 接收方有足够大的接收缓存,因此不会发生流量控制;
  • 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。

慢开始与拥塞避免

发送的最初执行慢开始,令cwnd = 1,发送方只能发送 1 个报文段;当收到确认后,在将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4/8.....指数增加

注意到慢开始每个轮次都会将 cwnd 加倍,这样会让 cwnd 增长的速度非常快,从而使得发送方的发送速率增长过快,网络拥塞的可能性也就更高。因此设置一个慢开始门限 ssthresh ,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加一。

如果出现超时,则令 ssthresh = cwnd/2 ,cwnd = 1,然后重新执行慢开始。

快重传与快恢复

在接收方,要求每次接收到的报文段都应该对最后一个已经收到的有序报文段进行确认。例如已经接收到 M1 和 M2 ,此时收到 M4,则应当发送对 M2 的确认。

在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2 ,则 M3 丢失,立即重传 M3。

在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd/2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。

慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为1,而快恢复 cwnd 设定为 sshthresh。