0%

《计算机网络》TCP 协议

TCP 协议

TCP 协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,TCP 协议数据位于 IP 报文的负载中。与 UDP 协议同级,属于传输层协议

TCP 报文格式

TCP报文格式

  • TCP 源端口(Source Port):16 位的源端口其中包含发送方应用程序对应的端口。源端口和源 IP 地址标示报文发送端的地址。主动发起发通常随机取一个空闲的端口,因此本地端口个数最大只有 65536,端口 0 有特殊含义,不能使用,这样可用端口最多有 65535 个,所以在全部作为 client 端的情况下,同时可以建立的 tcp 连接数最大为 65535。

  • TCP 目的端口(Destination port):16 位的目的端口域定义传输的目的。这个端口指明报文接收计算机上的应用程序地址接口。

  • TCP 序列号(SequenceNumber):32 位的序列号标识了 TCP 报文中第一个 byte 在对应方向的传输中对应的字节序号。当 SYN 出现,SN=ISN (随机值)单位是 byte。比如发送端发送的一个 TCP 包净荷(不包含 TCP 头)为 12byte,SN 为 5,则发送端接着发送的下一个数据包的时候,SN 应该设置为 5+12=17。通过序列号,TCP 接收端可以识别出重复接收到的 TCP 包,从而丢弃重复包,同时对于乱序数据包也可以依靠系列号进行重排序,进而对高层提供有序的数据流。另外如果接收的包中包含 SYN 或 FIN 标志位,逻辑上也占用 1 个 byte,应答号需加 1。

  • TCP 应答号(Acknowledgment Number 简称 ACK Number):32 位的 ACK Number 标识了报文发送端期望接收的字节序列。如果设置了 ACK 控制位,这个值表示一个准备接收的包的序列码,注意是准备接收的包,比如当前接收端接收到一个净荷为 12 byte 的数据包,SN 为 5,则会回复一个确认收到的数据包,如果这个数据包之前的数据也都已经收到了,这个数据包中的 ACK Number 则设置为 12+5=17,表示之前的数据都已经收到了,准备接受 SN=17 的数据包。

  • 头长(Header Length):4 位包括 TCP 头大小,指示 TCP 头的长度,即数据从何处开始。

  • 保留(Reserved):4 位值域,这些位必须是 0。为了将来定义新的用途所保留,其中 RFC3540 将 Reserved 字段中的最后一位定义为 Nonce 标志。后续拥塞控制部分的讲解我们会简单介绍 Nonce 标志位。

  • 标志(Code Bits):8 位标志位,下面介绍。

  1. CWR(Congestion Window Reduce):拥塞窗口减少标志 set by sender,用来表明它接收到了设置 ECE 标志的 TCP 包。并且 sender 在收到消息之后已经通过降低发送窗口的大小来降低发送速率。

  2. ECE(ECN Echo):ECN 响应标志被用来在 TCP 3 次握手时表明一个 TCP 端是具备 ECN 功能的。在数据传输过程中也用来表明接收到的 TCP 包的 IP 头部的 ECN 被设置为 11。注:IP 头部的 ECN 被设置为 11 表明网络线路拥堵。

  3. URG(Urgent):该标志位置位表示紧急 (The urgent pointer) 标志有效。该标志位目前已经很少使用参考后面流量控制和窗口管理部分的介绍。

  4. ACK:取值 1 代表 Acknowledgment Number 字段有效,这是一个确认的 TCP 包,取值 0 则不是确认包。后续文章介绍中当 ACK 标志位有效的时候我们称呼这个包为 ACK 包,使用大写的 ACK 称呼。

  5. PSH(Push):该标志置位时,一般是表示发送端缓存中已经没有待发送的数据,接收端不将该数据进行队列处理,而是尽可能快将数据转由应用处理。在处理 telnet 或 rlogin 等交互模式的连接时,该标志总是置位的。

  6. RST(Reset):用于 reset 相应的 TCP 连接。通常在发生异常或者错误的时候会触发复位 TCP 连接。

  7. SYN:同步序列编号 (Synchronize Sequence Numbers) 有效。该标志仅在三次握手建立 TCP 连接时有效。

  8. FIN(Finish):No more data from sender。当 FIN 标志有效的时候我们称呼这个包为 FIN 包。

  • 窗口大小(Window Size):16 位,该值指示了从 Ack Number 开始还愿意接收多少 byte 的数据量,也即用来表示当前接收端的接收窗还有多少剩余空间,用于 TCP 的流量控制。

  • 校验位(Checksum):16 位 TCP 头。发送端基于数据内容计算一个数值,接收端要与发送端数值结果完全一样,才能证明数据的有效性。接收端 checksum 校验失败的时候会直接丢掉这个数据包。CheckSum 是根据伪头 + TCP 头 + TCP 数据三部分进行计算的。

  • 优先指针(紧急,Urgent Pointer):16 位,指向后面是优先数据的字节,在 URG 标志设置了时才有效。如果 URG 标志没有被设置,紧急域作为填充。

  • 选项(Option):长度不定,但长度必须以是 32bits 的整数倍。常见的选项包括 MSS、SACK、Timestamp 等等。

TCP 工作原理

下面是 curl baidu.com ,嗅探到的整个过程中的所有数据包

get请求完整过程

TCP 连接三次握手 SYN-(ACK-SYN)-ACK

TCP 三次握手

  1. A 打开 socket 并向 B 发送 SYN (Synchronize Sequence Numbers)。传递生成的序列号 seq=x(seq就相当于一个暗号,对方要对上答案:seq+1)

SYN

  1. B 打开 socket 并回复 A SYN_ACK(对 A SYN 的 ACK 和 B 的 SYN 合成到了一个包内)。传递 ack=x+1 以及生成的序列号 seq=y 。这一步的作用是 B 确认序列号并回复 ACK 包

SYN_ACK

  1. A回复B ACK。传递ack=y+1。这一步的作用是A确认序列号(协商数据)并回复ACK包(消除SYN_ACK包的网络问题)

ACK

问题来了

第一步完成后,B 因为网络原因没有收到 SYN 怎么办?B 没有收到 SYN 那么 B 就不会回复 ACK,那么 A 就会重发 SYN (A 在发送之前会启动一个计时器,到达计时器还没有收到回复就会重发)

那如果第二步完成后,A 因为网络原因没有收到 B 发送的 SYN_ACK 怎么办?那么 A 就不会回复 ACK 了,那么 B 就会重发 SYN_ACK

那如果第三步完成后,因为网络原因B没有收到怎么办?B 收不到 ACK 就会重发 SYN_ACK,然而 A 前面已经处理过了,就会忽略这个包重新发送 ACK

两军问题

Two Generals Problem 两将军问题是这么一个思维性实验问题: 有两支军队,它们分别有一位将军领导,现在准备攻击一座修筑了防御工事的城市。这两支军队都驻扎在那座城市的附近,分占一座山头。一道山谷把两座山分隔开来,并且两位将军唯一的通信方式就是派各自的信使来往于山谷两边。不幸的是,这个山谷已经被那座城市的保卫者占领,并且存在一种可能,那就是任何被派出的信使通过山谷是会被捕。 请注意,虽然两位将军已经就攻击那座城市达成共识,但在他们各自占领山头阵地之前,并没有就进攻时间达成共识。两位将军必须让自己的军队同时进攻城市才能取得成功。因此,他们必须互相沟通,以确定一个时间来攻击,并同意就在那时攻击。如果只有一个将军进行攻击,那么这将是一个灾难性的失败。 这个思维实验就包括考虑他们如何去做这件事情。下面是我们的思考:

  1. 第一位将军先发送一段消息“让我们在上午9点开始进攻”。然而,一旦信使被派遣,他是否通过了山谷,第一位将军就不得而知了。任何一点的不确定性都会使得第一位将军攻击犹豫,因为如果第二位将军不能在同一时刻发动攻击,那座城市的驻军就会击退他的军队的进攻,导致他的军对被摧毁。
  2. 知道了这一点,第二位将军就需要发送一个确认回条:“我收到您的邮件,并会在9点的攻击。”但是,如果带着确认消息的信使被抓怎么办?所以第二位将军会犹豫自己的确认消息是否能到达。
  3. 于是,似乎我们还要让第一位将军再发送一条确认消息——“我收到了你的确认”。然而,如果这位信使被抓怎么办呢?
  4. 这样一来,是不是我们还要第二位将军发送一个“确认收到你的确认”的信息。

于是你会发现,这事情很快就发展成为不管发送多少个确认消息,都没有办法来保证两位将军有足够的自信自己的信使没有被敌军捕获。这个问题是无解的

这个实验意在阐明:试图通过建立在一个不可靠的连接上的交流来协调一项行动的隐患和设计上的巨大挑战。

从工程上来说,一个解决两个将军问题的实际方法是使用一个能够承受通信信道不可靠性的方案(比如重试处理),并不试图去消除这个不可靠性。

TCP 连接的 3 次握手就是利用 ACK 回复包进行重试处理

TCP 连接握手的意义

TCP 传输可不可以直接传输数据,不进行握手。答案是不行,因为 TCP 协议进行可靠传输需要双方约定一些参数,而握手的过程主要是约定这些参数。

  1. MSS 最大报文段长度协商,告诉双方自己网卡能接受的最大报文长度。MSS = MTU - 20 字节 TCP 报头 - 20 字节 IP 报头(在 ip 层,数据会被按照 MTU 的限制分段传输,MTU 的限制来源于网络层的硬件,tcp 传输层每次传输的数据量肯定不能大于网络层每次传输量的上限,不然会被 ip 层胡乱切割),以太网中 MTU 默认是 1500。如果双方中间的路由器网卡配置的 MTU 是 1000,则路由器会修改双方的协商数据。最终 A 和 B 协商的结果 MSS 值应该是整个链路中所有网卡的 MTU 值中的最小值 - 20 字节 TCP 报头 - 20 字节 IP 报头,这样的话数据才能在 A 和 B 之间正常传输,不会被抛弃。

  2. 序列号的交换。client 和 server 端把自己的初始序列号 isn(一个随机数)告诉对方。

  3. 交换其他的参数。比如窗口比例因子 (Window Scale Factor) 等等

TCP 断开连接四次握手 FIN-ACK-FIN-ACK

  1. A 向 B 发送 FIN,此时 A 的 socket 不能关,还要继续接收数据,收到 B 的下次回复才能关闭(这是第 3 次存在的原因)

FIN

  1. B 回复 ACK 告诉 A 已经收到了(A 没收到 ACK 就会重复第一步), 进入 CLOSE_WAIT 状态, 等待上层应用程序完成传输任务(flush 网络缓冲区的全部数据)然后调用 close 操作,此时 A 的 socket 没有关,还是可以接收数据的

ACK

  1. B 发送 FIN 并关闭 socket,表示自己该发送的数据都发送完了,通知客户端可以关闭了(作用是告诉客户端可以关闭 socket 了,需要等待 ACK)

FIN

  1. A 收到后,回复 ACK,变成 time-wait 状态,之后自动关闭 socket。B 因为网络原因没收到 ACK 就会重发第 3 步(这就是 A 需要回复 ACK 的原因)

ACK

为什么断开时 2、3 不能合成一步

TCP 连接时的第 2 步其实就是断开时的第 2 步和第 3 步,断开时,服务端可能还有数据没有传输完成,当然不能发 FIN ,所以就是先 ACK 客户端的 FIN ,然后等传输完了再 FIN ,这样就成了两步

断开时第四次的意义以及客户端 time-wait 的意义

第四次就是发送 ACK 包,告诉服务端已经收到了。如果不告诉服务端收到了,客户端网络原因没有收到服务端的 FIN 怎么办

所以第四次的意义就是:消除第三次FIN包的网络问题

客户端 time-wait 是指发起方 A 收到 B 的 FIN 后还会再等待 2MSL(MSL 是报文最大生存时间,一般是 2min)的时间才会关闭连接,也就是等 4 分钟

当 A 发送的 ACK 由于网络原因丢了,B 就收不到 A 的确认,B 就会超时重发 FIN,A 的 time-wait 就是为了接收这个重发,如果没有 time-wait,那么 A 就会把 B 的 FIN 当成错误包,返回给 B 错误信息,B 收到后就会产生错误,最后导致 B 关不了连接,这种情况多了最终会导致 B 资源耗尽

所以 time-wait 的意义就是:避免服务端重发 FIN 时接收到错误信息导致服务端资源耗尽

为什么连接时要来回确认总共 3 次握手,两次为啥不行

如果只是前两握手的话,这里举个例子:

A 要和 B 对话,A 派出 A1 传话 SYN,A1 是乌龟,走得很慢,结果 A1 还没将话传到,A 就发现超时了,于是又派了 A2 去传话,A2 同样也是乌龟,A 又发现超时,又派了 A3 去传话,A3 是兔子,顺利把话带到,B 收到后派出兔子 B1 带话 ACK,A 顺利收到 ACK,连接建立。

但是这个时候 A1 和 A2 将话带到了,B 以为 A 又要建立两个连接,于是纷纷安排资源(开辟 socket 等等)并回复 ACK,结果 B 安排了 3 份资源,但是 A 看不到后面两次连接

但如果是 3 次握手,后面 A 的两次 SYN 不会造成连接建立(B 回复 ACK 给 A,A 不会理睬,第三步不可能完成),B 就不会安排资源,所以不会有资源的浪费

TCP 如何保障可靠传输以及有序性

TCP 数据包中的序列号(Sequence Number)不是以报文段来进行编号的,而是将连接生存周期内传输的所有数据当作一个字节流,序列号就是整个字节流中每个字节的编号。

一个 TCP 数据包中包含多个字节流的数据(即数据段),而且每个 TCP 数据包中的数据大小不一定相同。

在建立 TCP 连接的三次握手过程中,通信双方各自已确定了初始的序号 x 和 y,TCP 每次传送的报文段中的序号字段值表示所要传送本报文中的第一个字节的序号。

TCP 的报文到达确认(ACK),是对接收到的数据的最高序列号的确认,并向发送端返回一个下次接收时期望的 TCP 数据包的序列号(Ack Number)。

例如,主机 A 发送的当前数据序号是 400,数据长度是 100,则接收端收到后会返回一个确认号是 501 的确认号给主机 A。

TCP 提供的确认机制,可以在通信过程中可以不对每一个 TCP 数据包发出单独的确认包(Delayed ACK 机制),而是在传送数据时,顺便把确认信息传出,这样可以大大提高网络的利用率和传输效率。

同时,TCP 的确认机制,也可以一次确认多个数据报,例如,接收方收到了 201,301,401 的数据报,则只需要对 401 的数据包进行确认即可,对 401 的数据包的确认也意味着 401 之前的所有数据包都已经确认,这样也可以提高系统的效率。

若发送方在规定时间内没有收到接收方的确认信息,就要将未被确认的数据包重新发送。

接收方如果收到一个有差错的报文,则丢弃此报文,并不向发送方发送确认信息。

因此,TCP 报文的重传机制是由设置的超时定时器来决定的,在定时的时间内没有收到确认信息,则进行重传。

这个定时的时间值的设定非常重要,太大会使包重传的延时比较大,太小则可能没有来得及收到对方的确认包发送方就再次重传,会使网络陷入无休止的重传过程中。

接收方如果收到了重复的报文,将会丢弃重复的报文,但是必须发回确认信息,否则对方会再次发送。

TCP 协议应当保证数据报按序到达接收方。如果接收方收到的数据报文没有错误,只是未按序号,这种现象如何处理呢?

TCP 协议本身没有规定,而是由 TCP 协议的实现者(都是由操作系统实现,然后通过系统调用暴露给应用层)自己去确定。

通常有两种方法进行处理:

  1. 对没有按序号到达的报文直接丢弃
  2. 将未按序号到达的数据包先放于缓冲区内,等待它前面的序号包到达后,再将它交给应用进程。

后一种方法将会提高系统的效率。

例如,发送方连续发送了每个报文中 100 个字节的 TCP 数据报,其序号分别是 1,101,201,…,701。

假如其它 7 个数据报都收到了,而 201 这个数据报没有收到,则接收端应当对 1 和 101 这两个数据报进行确认,并将数据递交给相关的应用进程,301 至 701 这 5 个数据报则应当放于缓冲区,

等到 201 这个数据报到达后,然后按序将 201 至 701 这些数据报递交给相关应用进程,并对 701 数据报进行确认,确保了应用进程级的 TCP 数据的按序到达。

TCP 粘包

TCP 是一个流式协议,每一份数据不会有边界,也不会立马发送出去,先放入缓存中,然后一批一批发送出去

接收方也有缓存,是按流的方式接收数据

比如 A 要发送给 B “aa” 和 “BB” 两个数据,如果 A 的缓存区大小大于 4 个字符的长度,则 aaBB 会一起发送给 B,B 如果读取全部缓存数据,读到的就是 “aaBB”,这就是 TCP 粘包

解决方式:

  1. 定义一个应用层协议,最基本的就是,前面 8 个字节表示数据包的长度,后面是内容。这样的话,A 按照协议一次发送包长度以及包内容,而 B 可以按照协议先读取 8 个字节,确定这个包的内容的长度,假设是 c,接着读取长度为 c 的数据

  2. 固定长度。A 和 B 约定一个固定长度,比如 10。A 如果发长度是 6 的数据,则需要再填充长度为 4 的数据,然后发出去,B 则按照 10 个的长度依次读取

TCP 的性能问题

  1. TCP 的拥塞控制在发生丢包时会进行退让,减少能够发送的数据段数量,但是丢包并不一定意味着网络拥塞,更多的可能是网络状况较差;
  2. TCP 的三次握手带来了额外开销,这些开销不只包括需要传输更多的数据,还增加了首次传输数据的网络延迟;
  3. TCP 的重传机制在数据包丢失时可能会重新传输已经成功接收的数据段,造成带宽的浪费;

业界的两种解决方案

  1. 使用 UDP 构建性能更加优异、更灵活的传输协议,例如:HTTP/3 使用的 QUIC 协议等;
  2. 通过不同的手段优化 TCP 协议的性能,例如:选择性 ACK(Selective ACK, SACK),TCP 快开启(TCP Fast Open, TFO);



微信关注我,及时接收最新技术文章