Skip to content
On this page

TCP

文章内容基本来自小林coding博客,仅作为自主学习整理。

TCP基本认识

为什么需要TCP

IP层是不可靠的。不能保证网络包的交付、不保证网络包的按序交付,也不保证网络包中的数据完整性。

什么是TCP

TCP是一个工作在传输层的可靠数据传输服务,它能确保接受端接收的数据是无损坏、无间隔、非冗余和按序的

  • 面向连接连接必须是一对一的,无法像向UDP一样一个主机同时向多个主机发送消息;
  • 可靠的:无论网络链路中出现了怎样的链路变化,TCP都可以保证一个报文一定能到达接收端
  • 字节流:无论消息有多大,都可以进行传输,并且消息是「 有序 」的,即使先收到后面的字节,也不能交给应用层处理,同时会对「 重复 」的报文会自动丢弃。

TCP连接

建立一个TCP连接,需要客户端和服务端达成上述三个信息的共识:

  • Socket:由IP地址和端口号组成
  • 序列号:用来解决乱序的问题
  • 窗口大小:用来做流量控制

唯一确定一个TCP连接:源地址、源端口、目的地址、目的端口(四元组)

源地址和目标地址在IP头部,作用是通过IP协议发送报文给对方主机。

源端口和目的端口是在TCP头部,作用是告诉TCP协议应该把报文发送给哪个进程。

最大TCP连接数 = 客户端的IP数 X 客户端的端口数

对于IPv4,客户端的IP数最多为2^32,客户端的端口最多为2^16,所以服务端单机最大TCP连接数,理论上约为2^48。

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

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

TCP头格式

  • 序列号
    • 建立之初计算机生成的随机数作为初始值,通过SYN包传给接收端主机,每次发送一次数据,就累加一次该数据字节数的大小。用来解决网络包乱序。
  • 确认应答号
    • 指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为这个序号之前的数据都已经被正常吸收,用来解决不丢包的问题。
  • 控制位
    • ACK:该位为1时,「确认应答」的字段变为有效
      • TCP规定,除了最初建立连接时SYN包,该位必须设置为1。
    • RST:该位为1时,表示TCP连接中出现异常必须强制断开连接。
    • SYN:该位为1时,表示希望建立连接,并将「序列号」作为初始值。
    • FIN:该位为1时,表示今后不会再用数据发送,表示断开连接
      • 当通讯结束希望断开连接时,通信双方的主机之间就可以交换FIN位为1的TCP段。

TCP连接建立

三次握手过程和状态变迁

TCP是面向连接的协议,使用TCP之前必须先建立连接,而建立连接是通过三次握手来进行的。

  • 一开始,客户端和服务端都处于Closed状态,先是服务端主动监听某个端口,处于Listen状态
  • 第一次握手:客户端会随机初始化序号(seq = x),填入在头部的「序号」中,同时将SYN设置为1,表示SYN报文。接着发送给服务端,表示向服务端发起连接。该报文不包含应用层数据,之后客户端处于SYN-SENT状态。
  • 第二次握手:服务端接收到客户端SYN报文后,首先服务端也随机初始化自己的序号(seq = y),填入头部的「序号」中,其次「确认应答号」填入「收到的SYN报文中的序号 + 1」,接着把SYNACK都标为1。接着把报文发送给客户端,该报文也不包含应用层数据,之后服务端处于SYN-RCVD状态。
  • 第三次握手:客户端接收到服务端报文后,还需要回应最后一个应答报文,首先,该应答报文TCP首部ACK标志设置为1,其次「确认应答号」填入「收到的SYN报文中的序号 + 1」,最后将报文发送给服务端,这次报文可以携带客户端到服务端的数据,之后客户属于ESTABLISHED状态。
  • 服务端收到报文之后,也进入ESTABLISHED状态。

通过netstat -napt命令,可以查看TCP的连接状态

相关问题

  • 为什么是三次握手?不是两次或者四次?

    • 防止旧的重复连接初始化造成混乱(首要原因)
      • 假如说存在网络阻塞,第一个包由于没发出去,导致超时重新发送,那么旧的报文应该失效,那如果服务端还是先收到了旧的报文,然后返回了第二个报文,那么客户端根据上下文比较发现「确认应答号」不符合预期,那么就会发送RST报文终止连接。接着新的报文到达服务端,正确的进行了三次握手,建立了连接。
      • 如果只有两次握手的话,无法判断当前连接是否是历史连接,而三次握手则可以在客户端准备发送报文的时候,有足够的上下文去判断当前连接是否是历史连接。
    • 同步双方初始序列号
      • TCP协议的通信双方,都必须维护一个「序列号」,序列号是可靠传输的一个关键因素
        • 接收方可以去除重复的数据
        • 接收方可以根据数据包的序号按序接收
        • 可标识发送出去的包,哪些是已经被对方接收的
      • 四次握手也可以可靠的同步双方的初始化序号,但第二步和第三部已经优化成一步,也就成了「三次握手」,而两次握手只能保证一方的初始化序列号被对方成功接收,无法保证双方的初始序列号都被确认接收。
    • 避免资源浪费
      • 回到网络阻塞的例子,如果只有两次握手,那么服务端不清楚客户端是否收到自己发送出去的ACK确认信号,所以只能每收到一个SYN报文就主动建立一个连接,导致多个冗余的无效连接,造成不必要的资源浪费。
  • 不使用「两次握手」和「四次握手」的原因:

    • 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
    • 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
  • 为什么每次建立TCP连接,初始化的序列号都要求不一样呢?

    • 为了避免历史报文被下一个相同的四元组的连接接收;
      • 过程如下:客户端与服务端建立了一个连接,此时客户端发送的数据被被网络阻塞了,而此时服务器又发生了重启,就会发送RST报文断开连接;紧接着,客户端与服务端建立了一个和上次相同四元组的连接,刚好,上一个连接中被阻塞的数据包正好抵达客户端,被正常接收,导致数据发生错乱。
    • 为了安全性,防止黑客伪造的相同序列号的TCP报文被对方接收;
  • 初始化序列号如何产生?

    • 起始 ISN 是基于时钟的,每 4 微秒 + 1,转一圈要 4.55 个小时。
    • RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)
      • M 是一个计时器,这个计时器每隔 4 微秒加 1。
      • F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。
  • IP层能分片,为什么TCP还需要MSS(最大报文长度)

  • MTU:一个网络包的最大长度,以太网中一般为1500字节;

  • MSS:出去IP和TCP头部之后,一个网络包所能容纳的TCP数据的最大长度。

  • IP层本身没有超时重传机制,它由TCP来负责超时和重传。

  • 如果接收方发现某个分片丢失,就不会响应ACK给对方,那么发送方在超时后,就会重发「整个TCP报文」,然后IP层又因为数据过大进行分片,这样子效率是非常低的

  • 为了达到最佳的传输效能,TCP在建立连接时通常协商双方的MSS值,当TCP层发现数据超过MSS时,就会先进行切片,当然由它形成的IP报文长度也不会大于MTU,自然也不需要IP分片了。

  • 如果TCP分片后,发生丢失,进行重发的也是以MSS为单位,不用重传所有的部分,大大增加了重传的效率

  • 第一、二、三次握手丢失,会发生生什么?

    • 第一次握手:当客户端迟迟收不到服务端发送的SYN-ACK报文,就会触发「超时重传」,重传SYN报文。超时时间和重发次数由系统的内核参数来决定。
      • 超时时间是写死的,修改比较麻烦;
      • 在linux中,重发次数是5次(默认值,由tcp_synack_retries内核参数决定),每次超时时间是上次的两倍,第一次是1s后重发,下一次就是2s后重发,知道第五次超时重传后,继续等待32s,如果服务端还是没有回应ACK,客户端则不再发送,然后断开TCP连接。总耗时大约1+2+4+8+16+32 = 63,约1分钟。
    • 第二次握手:客户端和服务端都会重传
      • 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 tcp_syn_retries内核参数决定;
      • 服务端会重传 SYN-AKC 报文,也就是第二次握手,最大重传次数由 tcp_synack_retries 内核参数决定。
    • 第三次握手:此时服务端希望接收到客户端的ACK报文,迟迟没有收到,触发超时重传,重新发送SYN-ACK报文,直到收到第三次握手或者达到最大重传次数
      • 注意:ACK报文不会重传,当ACK丢失时,就由对方重传对应的报文。
  • 什么是SYN攻击,如何避免?

    • SYN攻击:攻击者短时间伪造不同IP地址的报文,服务端每接受到一个SYN报文,就进入SYN_RCVD状态,但发出去的SYN-ACK报文无法得到回应,久而久之就换占满服务端的半连接队列,使得服务器不能正常用户服务。
    • 避免方式:
      • 修改内核参数,控制队列大小和队满时做什么操作
        • 当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值:net.core.netdev_max_backlo
        • YN_RCVD 状态连接的最大个数:`net.ipv4.tcp_max_syn_backlog
        • 超出处理能时,对新的 SYN 直接回报 RST,丢弃连接:`net.ipv4.tcp_abort_on_overflow
      • 发送SYN cookie的方式
        • 当 「 SYN 队列」满之后,后续服务器收到 SYN 包,不进入「 SYN 队列」;
        • 计算出一个 cookie 值,再以 SYN + ACK 中的「序列号」返回客户端,
        • 服务端接收到客户端的应答报文时,服务器会检查这个 ACK 包的合法性。如果合法,直接放入到「Accept 队列」。
        • 最后应用通过调用 accpet() socket 接口,从「Accept 队列」取出的连接。

TCP连接断开

  • 客户端打算关闭连接,就会把TCP首部的FIN标志位标上1,即FIN报文,之后客户端进入FIN_WAIT_1阶段
  • 服务端接收到报文后,给客户端发送ACK报文,接着进入CLOSE_WAIT阶段
  • 客户端接收到ACK报文之后,进入FIN_WAIT_2阶段
  • 等待服务端处理完数据之后,也给客户端发送FIN报文,之后进入LAST_ACK状态
  • 客户端接收到FIN报文之后,也会发送ACK报文,之后进入TIME_WAIT状态
  • 服务端收到ACK报文之后,就会进入CLOSED状态,至此服务端已经完成连接的关闭
  • 客户端在经过2MSL一段时间后,自动进入CLOSED状态,至此客户端也完成连接的关闭

两方各自发送一个FIN和一个ACK,通常称为四次握手。

只有主动关闭连接的,才会有TIME_WAIT状态。

相关问题

  • 为什么需要四次握手
    • 客户端发送FIN报文,仅代表客户端不再发送数据,但还能接收数据。
    • 服务器收到FIN报文之后,先应答一个ACK报文,但服务器可能还有数据需要处理和发送,等到服务器不再发送数据了,才会发送FIN报文给客户端,来表示同意现在关闭连接。
    • 所以,服务端通常需要等待完成数据的发送和处理,所以服务端的ACK和FIN需要分开发送,因此比三次握手多了一个环节。
  • 第一次挥手丢失,会发生什么
    • 第一次是客户端的FIN报文。如果丢失会触发超时重传机制,重传FIN报文,直到超过最大重发次数tcp_orphan_retries,不再发送FIN报文,直接进入close状态
  • 第二次挥手丢失,会发生什么
    • 第二次是服务端的ACK报文,但ACK报文是不能重传的,所以如果服务端的第二次挥手丢失,客户端会触发超时重传机制,直到收到服务端的第二次挥手,或者达到最大重传次数。
    • 对于客户端来说,如果收到ACK报文之后,会处于FIN_WAIT_2阶段,但这个阶段不能会持续太久,由tcp_fin_timeout控制,默认是60秒,如果超过这个时间,客户端的链接就会直接关闭
  • 第三次挥手丢失,会发生什么
    • 服务端接收到客户端的FIN报文时,内核会自动回复ACK,之后内核必须等待进程主动调用close函数(它无权代替进程关闭连接)当触发close函数之后,内核会发出FIN报文,进入LAST_WAIT状态,等待客户端返回ACK来确认连接关闭。
    • 与客户端重发FIN报文的重发次数控制方式是一样的。
  • 第四次挥手丢失,会发生什么
    • 如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制
  • 为什么TIME_WAIT等待的时间是2MSL(TODO)
    • MSL报文最大生存时间,这里还涉及到一个IP层的概念TTL:经过路由跳数。每经过一个处理他的路由器,此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。
    • 所以MSL应该要大于TTL消耗为0的时间,以确保报文已自然消亡。
    • TTL 的值一般是 64,Linux 将 MSL 设置为 30 秒,意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒,如果超过了,就认为报文已经消失在网络中了
    • TIME_WAIT等待2倍的MSL,比较合理的解释是:网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待2倍的时间

TCP的一些特性

重传机制

TCP实现可靠传输的方式之一,是通过序列号和确认应答。

  • 超时重传
  • 快速重传
  • SACK
  • D-SACK

超时重传

分为两种情况:

  • 超时时间(RTO)应该怎么设置
    • 先理解两个概念:RTT和RTO
      • RTT是数据从网络一端到另一端的时间
      • RTO是超时重传的时间
    • 如果RTO过短,可能会导致不必要的重传;如果过长,可能会降低网络传输的效率
    • 所以,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值
    • 但由于网络情况复杂,所以其实这个值是一直动态变化的

快速重传

这种方式不以时间为驱动,而是以数据驱动重传。

在上图,发送方发出了 1,2,3,4,5 份数据:

  • 第一份 Seq1 先送到了,于是就 Ack 回 2;
  • 结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;
  • 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;
  • 发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。
  • 最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。

所以,快速重传的工作方式就是当收到三个相同的ACK报文时,会在定时器过期之前,重传丢失的报文段

快速重传机制只解决了一个问题,就是超时时间的问题,还有一个问题,是重传丢失的那一个,还是重传所有,根据具体的实现,两种都是有可能的。

SACK方法

Selective Acknowledgment 选择性确认

在TCP头部选项中加一个SACK的东西,他可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些没收到,就可以根据这些信息,只重传丢失的数据。

如果要支持 SACK,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。

Duplicate SACK

Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。

D-SACK 有这么几个好处:

  1. 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
  2. 可以知道是不是「发送方」的数据包被网络延迟了;
  3. 可以知道网络中是不是把「发送方」的数据包给复制了;

在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能(Linux 2.4 后默认打开)。

滑动窗口

我们都知道 TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。这样的传输方式有一个缺点:数据包的往返时间越长,通信的效率就越低

为解决这个问题,TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。

那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。

窗口大小由哪一边决定

TCP 头里有一个字段叫 Window,也就是窗口大小。

这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。

所以,通常窗口的大小是由接收方的窗口大小来决定的。

发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。

发送方的滑动窗口

分为四部分:

  • #1 是已发送并收到 ACK确认的数据:1~31 字节
  • #2 是已发送但未收到 ACK确认的数据:32~45 字节
  • #3 是未发送但总大小在接收方处理范围内(接收方还有空间):46~51字节
  • #4 是未发送但总大小超过接收方处理范围(接收方没有空间):52字节以后

接收方的滑动窗口

  • #1 + #2 是已成功接收并确认的数据(等待应用进程读取);
  • #3 是未收到数据但可以接收的数据;
  • #4 未收到数据并不可以接收的数据;

接收窗口和发送窗口的大小是相等的吗? 并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。

流量控制

为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。

拥塞控制

拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。

拥塞窗口

拥塞窗口 cwnd 变化的规则:

  • 只要网络中没有出现拥塞,cwnd 就会增大;
  • 但网络中出现了拥塞,cwnd 就减少;

有哪些控制算法?

拥塞控制主要是四个算法:

  • 慢启动
  • 拥塞避免
  • 拥塞发生
  • 快速恢复

#

TCP Vs UDP

MIT Licensed | Copyright © 2021 - 2022