TCP 协议详解和知识点整理

258 阅读17分钟

TCP 概念

  • TCP 属于网络分层中的传输层协议,介于会话层和网络层中间
  • TCP 协议是用于主机到主机的通信协议,是面向连接的端到端的可靠协议,提供可靠字节流传输和对上层应用提供连接服务
  • TCP 主要通过检验和、序列号、确认应答、重发控制、连接管理以及窗口控制等机制来实现可靠性
  • TCP 对字节流的内容不作任何解释,对字节流的解释由 TCP 连接双方的应用层进行解释
  • TCP 连接的建立和关闭的过程通过三次握手和四次挥手实现,具体细节可以参考上一篇文章TCP 连接状态及相关命令学习

TCP 和 UDP 的区别

  • TCP 是面向连接的、可靠的流协议,通过“顺序控制”或“重发控制”等来提高可靠性
  • UDP 是不具有可靠性的数据报协议,虽然可以确保发送消息的大小,却不能保证消息一定会到达,需要应用层根据自己的需要进行重发处理
  • TCP 用于在传输层有必要实现可靠传输的情况,UDP 主要用于那些对高速传输和实时性有较高要求的通信或广播通信

端口号

  • 端口号用来识别同一台主机中进行通信的不同应用程序
  • 传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确地将数据传输
  • IP 地址 + 端口号 + 协议来唯一识别一个应用程序,也就是不同的协议可以使用相同的端口
  • 知名端口号:分布在 0~1023 之间,例如 HTTP、FTP、TELNET 等广为使用的应用协议中所使用的端口号就是固定的
  • 端口号在 1024~49151 之间,这些端口号可以用于任何的通信用途
  • 一般情况下服务器需要确认端口号,但是客户端在连接服务端时完全不用用户自己指定,由操作系统动态分配 49152~65535 之间的端口号

TCP 的连接队列

  • 正等待连接请求的一端有一个固定长度的连接队列,该队列中保存着已被 TCP 接受的连接(即三次握手已经完成),但还没有被应用层所接受
  • TCP 每接受一个连接,也就是完成三次握手就将其放入这个队列,而应用层每接受一个连接时将其从该队列中移出
  • 应用层将指明该队列的最大长度,这个值通常称为积压值(backlog),它的取值范围是 0~5 之间的整数
  • 当一个连接请求(即 SYN )到达时,该 TCP 监听端口的连接队列中还有空间,TCP 模块将对 SYN 进行确认并完成连接的建立
  • 如果对于新的连接请求,连接队列中已没有空间,TCP 将不理会收到的 SYN,也不发回任何报文段(即不发回 RST),客户的主动打开最终将超时

MSS(Maximum Segment Size)

  • 在建立 TCP 连接的同时会计算两端之间的传输的数据包大小,该数据包的大小被称其为 MSS(最大消息长度)
  • MSS 的大小不包含 TCP Header 和 TCP Option,只包含 TCP Payload
  • 最理想的情况是,MSS 正好是 IP 中不会被分片处理的最大数据长度,减少 IP 层数据分片和重传的消耗
  • TCP 在传送大量数据时,是以 MSS 的大小将数据进行分割发送,进行重发时也是以 MSS 为单位
  • 两端的主机在发出建立连接的请求时,会在 TCP 首部中写入 MSS 选项,告诉对方自己的接口能够适应的 MSS 的大小,然后会在两者之间选择一个较小的值进行使用

通信序列号

  • 序列号是按照顺序给发送数据的每一个字节(8位字节)都标上号码的编号
  • 接收端查询接收数据 TCP 首部中的序列号和数据的长度,将自己下一步应该接收的序列号作为确认应答发送回去
  • 通过序列号和确认应答号,TCP 能够识别是否已经接收数据,又能够判断是否需要接收,从而实现可靠传输
  • 为了解决序列号冲突问题,TCP 每个连接都从不同的序列号开始,这个序号的起始序号是随着时间而变化的

TCP 标志位

TCP 报头信息中有六个控制位:URG,ACK,PSH,RST,SYN,FIN:

  • URG:紧急标志位,如果该位设置为 1,当前报文需要接收端立即处理,并且当前报文不需要经过接收端的缓冲区,直接越过缓冲区,交付给接收端的应用层
  • ACK:确认标志用于确认数据包的成功接收,每次发送一个数据包都要进行确认
  • PSH:
- 通过允许客户应用程序通知其 TCP 设置 PSH 标志,客户进程通知 TCP 在向服务器发送一个报文段时不要因等待额外数据而使已提交数据在缓存中滞留
- PSH 和 URG 不同之处在于:当前的数据还会被发送到接收端的缓冲区,并刷新缓冲区,将当前缓冲区中所有数据都交付给上一层的应用层
- PSH 标志位所表达的是发送方通知接收方传输层应该尽快的将这个报文段交给应用层
- PSH 标志通常设置在文件的最后一段,以防止缓冲区死锁,当用于通过代理发送 HTTP 或其他类型的请求时也可以看到,确保请求得到适当和有效的处理
  • RST:如果主机收到无法匹配的客户端请求,则主机将自动拒绝该请求,并产生 RST 标志回应。产生 RST 的情况主要由以下两种情况:
- 到不存在端口的连接请求
- 在探活过程中,如果对方已经奔溃或者重启,异常终止一个连接,会产生 RST
- 在半打开连接上发送数据
  • SYN:TCP 三次握手中,表示建立连接的标记
  • FIN:TCP 四次挥手时,表示关闭连接的标记

Nagle 算法

  • Nagle 算法要求一个 TCP 连接在任意时刻最多只能有一个没有被 ACK 确认的小段。所谓“小段”指的是小于 MSS 的数据块。
  • Nagle 算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块,进而减少大量小包的发送。
  • 该算法的优越之处在于它是自适应的:确认到达得越快,数据也就发送得越快,并且可以发送更少的分组
  • Nagle算法的实现规则如下:
- 如果包长度达到 MSS,则允许发送;
- 如果该包含有 FIN,则允许发送;
- 如果该包设置了 TCP_NODELAY 选项,则允许发送;
- 未设置 TCP_CORK 选项时,若所有发出去的小数据包均被确认,则允许发送;
- 上述条件都未满足,但发生了超时(一般为200ms),则立即发送。

重发机制

  • TCP 重发超时是指在重发数据之前,等待确认应答到来的那个特定时间间隔
  • 如果超过这个时间仍未收到确认应答,发送端将进行数据重发
  • 最理想的是,找到一个最小时间,它能保证“确认应答一定能在这个时间内返回”
  • 自适应重传算法(Adaptive Retransmission Algorithm):
估计往返时间,需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个值,
而且这个值还是要不断变化的,因为网络状况不断地变化。
除了采样 RTT,还要采样 RTT 的波动范围,计算出一个估计的超时时间
  • 在 BSD 的 Unix 以及 Windows 系统中,超时都以 0.5 秒为单位进行控制,因此重发超时都是 0.5 秒的整数倍。不过,最初其重发超时的默认值一般设置为6秒左右
  • 数据被重发之后若还是收不到确认应答,则进行再次发送。此时,等待确认应答的时间将会以 2 倍、4 倍的指数函数延长
  • 数据也不会被无限、反复地重发,达到一定重发次数之后,如果仍没有任何确认应答返回,就会判断为网络或对端主机发生了异常,强制关闭连接,并且通知应用通信异常强行终止
  • TCP 重传时不一定要重传相同的报文段,可以进行重新分组而发一个更大的报文段

滑动窗口

概念:

如果 TCP 以 1 个段为单位,每发送一个段进行一次确认应答的处理,这样的传输方式有一个缺点,就是包的往返时间越长通信性能就越低。为解决这个问题,TCP 引入了滑动窗口这个概念,确认应答不再是以每个分段,而是以更大的单位进行确认,转发时间将会被大幅地缩短。也就是说,通过滑动窗口的机制,发送端主机在发送了一个段以后不必要一直等待确认应答,而是可以继续发送。

窗口大小:
  • 窗口大小是指无需等待确认应答而可以继续发送数据的最大值
  • 接收方在收到 ACK 时顺带将窗口大小返回给发送方
  • 接收方每次 ACK 的回来的窗口大小不一定是固定的,ACK 只是表示接受到了数据,但是可能这些数据应用程序根本来不及处理,还存在 TCP 缓存区里面,此时滑动窗口的大小就是剩余缓存区的大小
  • 有些情况下可能缓存区已经满了,这时接收方在 ACK 时告诉发送方通告窗口大小为 0,无法接受数据,后面会等待缓存区释放以后,接收方再发送一个 ACK (更新窗口),此时并不确认任何数据,只是用来更新窗口
  • 如果设置为 0 的话,发送方也会定时发送窗口探测数据包,看是否有机会调整窗口的大小
累积确认应答(cumulative acknowledgment):

为了保证不丢包,接收方对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个之前的 ID,表示这个 ID 之前的所有包都收到了,这种模式称为累计确认或者累计应答。通过累积确认应答减少传输次数,提高传输效率。

延时发送 ACK:

通常 TCP 在接收到数据时并不立即发送 ACK,相反它推迟发送,以便将 ACK 与需要沿该方向发送的数据一起发送(有时称这种现象为数据捎带 ACK),绝大多数实现采用的时延为 200 ms,也就是说,TCP 将以最大 200 ms 的时延等待是否有数据一起发送

滑动窗口中的重发控制:
  • 当滑动窗口在一定程度较大时,即使有少部分的确认应答丢失也不会重发,可以通过下一个确认应答进行确认
  • 当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是它就会发送冗余的 ACK,而当客户端收到三个冗余的 ACK 后,就会在定时器过期之前,重传丢失的报文段,这种机制比之前提到的超时管理更加高效,因此也被称为高速重发控制

TCP 拥塞控制

TCP 网络实际传输中,由于包的数量较多,很可能出现网络拥塞的现象,为此 TCP 提供了四种控制拥塞的方法:

TCP 慢启动:

所谓慢启动,也就是 TCP 连接刚建立,一点一点地提速,试探一下网络的承受能力,以免直接扰乱了网络通道的秩序:

1)连接建好的开始先初始化拥塞窗口 cwnd 大小为 1,表明可以传一个 MSS 大小的数据。 
(2)每当收到一个 ACK,cwnd 大小加一,每当过了一个往返延迟时间 RTT,cwnd 大小直接翻倍,乘以 2,呈指数让升。 
(3)还有一个 ssthresh(slow start threshold),是一个上限,发送方取拥塞窗口与通告窗口中的最小值作为发送上限,当 cwnd >= ssthresh 时,就会进入下面讲到的 “拥塞避免算法” 阶段
拥塞避免算法:

如同前边说的,当拥塞窗口大小 cwnd 大于等于慢启动阈值 ssthresh 后,就进入拥塞避免算法阶段:

(1)每收到一个 ACK,则 cwnd = cwnd + 1 / cwnd 
(2)每当过了一个往返延迟时间 RTT,cwnd 大小加一,呈线性增长
(3)过了慢启动阈值后,拥塞避免算法可以避免窗口增长过快导致窗口拥塞,而是缓慢的增加调整到网络的最佳值。
(4)算法通过观察到新分组进入网络的速率应该与另一端返回确认的速率相同而进行工作
拥塞状态时的算法:

该算法是指 TCP 进入拥塞状态后该怎么处理,一般认为丢包的情况下就进入拥塞状态了,丢包有两种判断方式:

  • 超时重传
  • 高速重发控制,也就是收到三个重复确认 ACK

进入拥塞状态后,比较早期的 TCP 处理流程如下:

(1)由于发生丢包,将慢启动阈值 ssthresh 设置为当前 cwnd 的一半,即 ssthresh = cwnd / 2。
(2)然后 cwnd 重置为 1。
(3)由拥塞状态再次进入慢启动过程

由于一丢包就要一切重来,导致 cwnd 又重置为 1,十分不利于网络数据的稳定传递,后面算法又进行了优化:

- cwnd 大小缩小为当前的一半
- ssthresh 设置为缩小后的 cwnd 大小
- 然后进入下一个算法阶段:快速恢复算法
快速恢复算法:
- cwnd = cwnd + 3 * MSS,加 3 * MSS 的原因是因为收到 3 个重复的 ACK
- 重传高速重发控制指定的数据包
- 如果再收到需要高速重发的包,那么 cwnd 大小增加一
- 如果收到新的 ACK,表明重传的包成功了,那么退出快速恢复算法。将 cwnd 设置为 ssthresh,然后进入拥塞避免算法

TCP BBR 拥塞算法:

TCP BBR 算法是 google 公司研究出来的拥塞控制算法,相对于传统的拥塞控制算法,TCP BBR 算法致力于解决两个问题:

  • 在有一定丢包率的网络链路上充分利用带宽。
  • 降低网络链路上的 buffer 占用率,从而降低延迟。

TCP BBR 不再使用丢包作为拥塞的信号,也不使用 “加性增,乘性减” 来维护发送窗口大小,而是分别估计极大带宽和极小延迟,把它们的乘积作为发送窗口大小。它企图找到一个平衡点,就是通过不断地加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡。

TCP 中的定时器

TCP 一共有四种不同的定时器:

重传定时器:

TCP 发送报文段时,创建该特定报文段的重传计时器,可能发生两种情况:

  • 若在计时器截止时间到之前收到了对此特定报文段的确认 ACK,则撤销此计时器
  • 若在收到了对此特定报文段的确认之前计时器截止期到,则重传此报文段,并将计时器复位
坚持定时器:
  • 坚持定时器专门用于对付滑动窗口大小为零窗口通知而设立。
  • 当发送端收到零窗口的确认时,就启动坚持计时器,当坚持计时器截止期到时,发送端 TCP 就发送一个特殊的报文段,叫探测报文段,这个报文段只有一个字节的数据。
  • 探测报文段有序号,但序号永远不需要确认,甚至在计算对其他部分数据的确认时这个序号也被忽略。探测报文段提醒接收端 TCP,确认已丢失,必须重传。
保活定时器:
  • 假定客户打开了到服务器的连接,传送了一些数据,然后就保持静默了,也许这个客户出故障了。在这种情况下,这个连接将永远地处于打开状态。
  • 为了防止出现这种状态,TCP 使用保活计时器来检查连接状态。
  • 要解决这种问题,在大多数的实现中都是使服务器设置保活计时器,每当服务器收到客户的信息,就将计时器复位,超时通常默认设置为 2 小时。
  • 若服务器过了 2 小时还没有收到客户的信息,它就发送探测报文段,若发送了 9 个探测报文段,每一个相隔 75 秒,还没有响应就假定客户出了故障,因而就终止该连接。
  • keepalive 相关参数配置:
- tcp_keepalive_time:默认两小时,在没有数据传输的情况下,超时多久开始发送探测报文
- tcp_keepalive_probes:探活次数,默认为 9 次
- tcp_keepalive_intal:重试的时间间隔
2MSL 定时器:
  • 当 TCP 关闭连接时,并不认为这个连接就真正关闭了,在时间等待期间,连接还处于一种中间过渡状态。
  • 2MSL 定时器的设置主要是为了确保发送的最后一个 ACK 报文段能够到达对方,最后一次挥手数据很有可能丢失,维持这个状态主要为了可以继续重发 ACK。
  • 2MSL 定时器阶段,该 TCP 连接处于 TIME—WAIT 状态,通常为2MSL(报文段寿命的两倍)。

关于 Socket

  • 一个 TCP 连接对应一个 Socket
  • 一个 Socket 的唯一标识是: {SRC-IP, SRC-PORT, DEST-IP, DEST-PORT, PROTOCOL}
  • 监听的 Socket 和真正用来传数据的 Socket 是两个,一个叫作监听 Socket,一个叫作已连接 Socket
  • 一个处于监听状态的 TCP 服务可以同时接受来自多个客户端的 Socket,TCP Socket 就是一个文件流,Socket 在 Linux 中就是以文件的形式存在的
  • 不同进程可以监听同一个端口,如果他们的协议(TCP/UDP)不同
  • 一个进程可以打开和关闭多个 Socket
  • 子进程可以继承所有的文件描述符(FD)从父进程上,所以不同的进程或者线程之间如果有父子关系,可以使用同一个Socket
  • 一个处于监听状态的 TCP 服务只需要一个监听端口,但可以建立多个 Socket
  • 服务器一个端口可以创建的 socket 连接数理论上是没有上限的,取决于系统的内存大小和可以创建的文件描述符的上限,可以通过修改文件描述符上限进行设置

参考文献: