oldme 博客

兼听则明,偏信则暗

计网 - 探寻 TCP 建立断开连接的原理 (二)

oldme create: 2024-10-14

一网飞架瀛海,天堑变通途。神女应无恙,当惊世界殊。TCP 连接通过三次挥手与四次挥手,可靠的建立连接与断开连接。

什么是 TCP 协议

在正式了解 TCP 协议之前最好先熟知 IP 协议,点此了解:计网 - 探寻 IP 地址,子网掩码与划分 (一)

IP 协议是一种不可靠的传输方式,TCP 行之有效的解决了 IP 协议的不可靠,所以 TCP 的中文名称叫做 传输控制协议(TRANSMISSION CONTROL PROTOCOL)。TCP/IP 协议族是互联网运作的基石,是人类智慧的结晶,也是问倒各路好汉的面试题。

TCP 被 RFC 793 定义,是一种面向连接的,基于字节流的一种传输层协议。

TCP 数据包

刚看见 TCP 数据包可能会一头雾水,可以先跳过此部分,结合后续的建立连接/断开连接,数据传输部分来循序渐进的理解。

偏移 字节 0 1 2 3
字节 比特 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
0 0 源端口号 目的端口号
4 32 序列号
8 64 确认号(当ACK设置)
12 96 资料偏移 保留
0 0 0
N
S
C
W
R
E
C
E
U
R
G
A
C
K
P
S
H
R
S
T
S
Y
N
F
I
N
窗口大小
16 128 校验和 紧急指针(当URG设置)
20
...
160
...
选项(如果资料偏移 > 5,需要在结尾添加0。)
...

TCP头部的标准大小是20字节,但如果包含选项字段,可以更大。TCP头部的各字段如下:

  1. Source Port (16 bits): 源端口号
  2. Destination Port (16 bits): 目的端口号
  3. Sequence Number (32 bits): 序列号
  4. Acknowledgment Number (32 bits): 确认号
  5. Data Offset (4 bits): 数据偏移,表示TCP头部的长度
  6. Reserved (3 bits): 保留位,3个0
  7. Flags (9 bits): 标志位,包括以下标志
    • NS—ECN-nonce。ECN 显式拥塞通知(Explicit Congestion Notification)是对 TCP 的扩展,定义于 RFC 3540 (2003)。ECN 允许拥塞控制的端对端通知而避免丢包。ECN 为一项可选功能,如果底层网络设施支持,则可能被启用 ECN 的两个端点使用。在 ECN 成功协商的情况下,ECN 感知路由器可以在 IP 头中设置一个标记来代替丢弃数据包,以标明阻塞即将发生。数据包的接收端回应发送端的表示,降低其传输速率,就如同在往常中检测到包丢失那样。
    • CWR—Congestion Window Reduced,定义于 RFC 3168(2001)。
    • ECE—ECN-Echo有两种意思,取决于 SYN 标志的值,定义于 RFC 3168(2001)。
    • URG—为1表示高优先级数据包,紧急指针字段有效。
    • ACK—为1表示确认号字段有效
    • PSH—为1表示是带有 PUSH 标志的数据,指示接收方应该尽快将这个报文段交给应用层而不用等待缓冲区装满。
    • RST—为1表示出现严重差错。可能需要重新建立 TCP 连接。还可以用于拒绝非法的报文段和拒绝连接请求。
    • SYN—为1表示这是连接请求或是连接接受请求,用于建立连接和使顺序号同步
    • FIN—为1表示发送方没有数据要传输了,要求释放连接。
  8. Window Size (16 bits): 窗口大小
  9. Checksum (16 bits): 校验和
  10. Urgent Pointer (16 bits): 紧急指针
  11. Options (Variable length): 最多40字节。每个选项的开始是1字节的kind字段,说明选项的类型。
    • 选项表结束(1字节)
    • 无操作(1字节)用于选项字段之间的字边界对齐。
    • 最大报文段长度(4字节,Maximum Segment Size,MSS)通常在建立连接而设置SYN标志的数据包中指明这个选项,指明本端所能接收的最大长度的报文段。通常将MSS设置为(MTU-40)字节,携带TCP报文段的IP数据报的长度就不会超过MTU(MTU最大长度为1518字节,最短为64字节),从而避免本机发生IP分片。只能出现在同步报文段中,否则将被忽略。
    • 窗口扩大因子(3字节,wscale),取值0-14。用来把TCP的窗口的值左移的位数,使窗口值乘倍。只能出现在同步报文段中,否则将被忽略。这是因为现在的TCP接收数据缓冲区(接收窗口)的长度通常大于65535字节。
    • sackOK—发送端支持并同意使用SACK选项。
    • SACK实际工作的选项。
    • 时间戳(10字节,TCP Timestamps Option,TSopt)发送端的时间戳(Timestamp Value field,TSval,4字节)时间戳回显应答(Timestamp Echo Reply field,TSecr,4字节)
    • MD5摘要,将TCP伪首部、校验和为0的TCP首部、TCP数据段、通信双方约定的密钥(可选)计算出MD5摘要值并附加到该选项中,作为类似对TCP报文的签名。通过 RFC 2385 引入,主要用于增强BGP通信的安全性。
    • 安全摘要,通过 RFC 5925 引入,将“MD5摘要”的散列方法更换为SHA散列算法。

数据包大小

MSS(最大分段大小) 是在单个 TCP 数据包字节数最大值。 MSS 应当足够小以避免 IP 分片,它会导致丢包或过多的重传。在 TCP 连接建立时,双端在 SYN 报文中用 MSS 选项宣布各自的 MSS,这是从双端各自直接相连的数据链路层的 MTU(最大传输单元) 减去固定的 IP 头部(通常为20字节)和 TCP 头部(20字节,如果没有选项字段)。以太网 MTU 为1500字节, MSS 值可达1460字节。 此外,发送方可用传输路径 MTU 发现推导出从发送方到接收方的网络路径上的最小 MTU,以此动态调整 MSS 以避免网络IP分片。

三次握手

TCP 建立连接的过程想必大家都耳熟能详,即三次握手。三次握手可以在两个主机之间建立可靠的连接,并且在此过程中会初始化许多参数,比如 MSS 值。

在三次握手前,服务端执行 Listen 函数进入 Listen 态,此时服务端上会维护两个队列:

  1. SYN 队列,也即是半连接队列,存放完成了二次握手的结果;
  2. ACCEPT队列,也即是全连接队列,存放完成了三次握手的结果。

第一次握手

客户端发送一个 TCP 包,头部 SYN 位设置为 1,SEQ_NUM 设置一个随机数 x,选项会中携带前面提到的 MSS 。 

结果:客户端发起了一个连接请求,进入 SYN_SENT 状态。

 

第二次握手

服务端收到合法的 SYN TCP 包后,应答一个 TCP 包到客户端,头部 SYN/ACK 都设置为 1,SEQ_NUM 设置一个随机数 y,ACK_NUM 设置为序列号加 1(即 x+1),选项中也会携带 MSS。 

结果:服务端将此连接推送到 SYN 队列,服务端进入 SYN_RCVD 状态。

 

第三次握手

客户端收到服务端的 SYN/ACK 包后,向服务端发送一个确认 TCP 包,头部 ACK 位设置为 1,ACK_NUM 设置为 y+1,SEQ_NUM 设置为 x+1。

结果:客户端进入 ESTABLISHED 状态,当服务端接收到这个包时,将此连接从 SYN 队列提到 ACCEPT 队列,也进入 ESTABLISHED 状态。

 

为什么 TCP 被设计成三次握手呢,我们说 TCP 是一种可靠的协议,三次握手的主要目的就是确保可靠。假使不使用三次握手,会发生什么呢?要知道在网络层中, IP 协议不能保证数据包的可靠性,会发生丢包,延迟等现象。

比如客户端发起了一次请求,这个包在更底层的网络中延迟了,造成了客户端超时,于是客户端又发送了一次包,服务端收到这个包后正常应答一个包,之后服务端又收到了之前的延迟包,于是乎,服务端以为是新的连接,又傻乎乎的建立了一个新的连接。。。这还不算完事,如果服务端真的建立了两个连接,但是客户端那边的网络有点问题,只出不进,收不到包!此时会怎么样呢?客户端因为没有收到响应,会不停的发包,服务端会不停的建立新的连接,并且每建立一个连接,就开始等待客户端发送正式数据,但殊不知,客户端压根不知道服务器应答了,还以为是自己的问题,在那突突发包呢。

三次握手在现实中的其他场景也会遇到,比如航空领域的通信。机长:机坪你好,海航2024准备起飞;空管:海港2024你好,18R跑道可以起飞;某机长:收到机坪,18R跑道起飞。

四次挥手

在图中我没有表示 SEQ_NUM 和 ACK_NUM,过程与四次挥手类似,不再赘述。

第一次挥手

客户端发送一个 TCP 包,头部 FIN 位设置为1,表示客户端发送数据结束,但仍然可以接收数据。

结果:客户端进入 FIN-WAIT-1 状态。

 

第二次挥手

服务端接收到合法的 FIN 包后,应答一个 TCP 包到客户端,头部 ACK 设置为 1,确认已接收到客户端的 FIN 请求。

结果:服务端进入 CLOSE-WAIT 状态,客户端收到应答后进入 FIN-WAIT-2 状态。

 

第三次挥手

服务端主动发送一个 TCP 包,头部 FIN 位设置为1,表示服务端也没有数据要发送了。

结果:服务端进入 LAST-ACK 状态。

 

第四次挥手

客户端接收到 FIN 包后,向服务端发送一个 TCP 包,头部 ACK 设置为 1,确认已接收到服务端的 FIN 请求。

结果:客户端进入 TIME-WAIT 状态,等待一段时间后进入 CLOSED 状态。 服务端接收到 ACK 包后进入 CLOSED 状态,将此连接从 ACCEPT 队列中去除

 

使用四次挥手的原因类似三次握手,也是为了可靠。TCP 协议是全双工通信,也就是双方可以互发数据。客户端停止发送数据后通知服务端,服务端收到后应答,停止接收信息,此时就是两次挥手了,反之服务端停止发送数据亦然,一共就是四次挥手。或许你有疑问,为什么不把 FIN 和 ACK 做一个包发送呢,这样不就是三次挥手了吗?这是因为底层网络的不可靠,如果数据没发完就关闭了,会导致数据的丢失。

评论

欢迎您的回复 取消回复

您的邮箱不会显示出来,*必填

本文目录