TCP(二) 连接的建立和断开

转载自:小林coding 公众号中的图解网络书籍,写的很详细,总结一部分记录下。

TCP 头部格式

tcp头

序列号:在建立连接时由计算机组成的随机数作为其初始值,通过SYN包传给接收端主机,每发送一次数据,就累加一次该数据字节数的大小。用来解决网络包乱序问题。

确认应答号:指下一次期望收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数
据都已经被正常接收。用来解决不丢包的问题。

控制位:

ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。

RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。

SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。

FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。

TCP连接建立

TCP的三次握手

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

img

一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。

img

客户端会随机初始化序号( client_isn ),将此序号置于 TCP 首部的序列号字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。

img

服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号( server_isn ),将此序号填入TCP 首部的序列号字段中,其次把 TCP 首部的确认应答号字段填入 client_isn + 1 , 接着把 SYNACK 标志位置为 1 。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN_RCVD 状态。

img

客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次确认应答号字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态。

服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态。

为什么是三次握手?

TCP 连接:用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket序列号窗口大小

为什么三次握手才可以初始化Socket序列号窗口大小并建立 TCP 连接。

接下来以三个方面分析三次握手的原因:

  • 三次握手才可以阻止重复历史连接的初始化(主要原因);

  • 三次握手才可以同步双方的初始序列号;

  • 三次握手才可以避免资源浪费。

避免历史连接

我们来看看 RFC 793 指出的 TCP 连接使用三次握手的首要原因:

The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing

confusion.

简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。

网络环境是错综复杂的,往往并不是如我们期望的一样,先发送的数据包,就先到达目标主机,可能会由于网络拥堵等乱七八糟的原因,会使得旧的数据包,先到达目标主机,那么这种情况下 TCP 三次握手是如何避免的呢?

image-20210818085950301

客户端连续发送多次 SYN 建立连接的报文,在网络拥堵情况下:

一个旧 SYN 报文最新的 SYN 报文早到达了服务端,那么此时服务端就会回一个 SYN + ACK 报文给客户端。客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送 RST 报文给服务端,表示中止这一次连接。

如果是两次握手连接,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接。

如果是历史连接(序列号过期或超时),则第三次握手发送的报文是 RST 报文,以此中止历史连接。如果不是历史连接,则第三次发送的报文是 ACK 报文,通信双方就会成功建立连接。

所以,TCP 使用三次握手建立连接的最主要原因是防止历史连接初始化了连接。

同步双方初始序列号

TCP 协议的通信双方, 都必须维护一个序列号, 序列号是可靠传输的一个关键因素,它的作用:

  • 接收方可以去除重复的数据;

  • 接收方可以根据数据包的序列号按序接收;

  • 可以标识发送出去的数据包中, 哪些是已经被对方收到的。

可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带初始序列号SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送初始序列号给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。

img

四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了三次握手。而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。

避免资源浪费

如果只有两次握手,当客户端的 SYN 请求连接在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK 确认信号,所以每收到一个 SYN 就只能先主动建立一个连接,这会造成什么情况呢?

如果客户端的 SYN 阻塞了,重复发送多次 SYN 报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。

image-20210818090023410

即两次握手会造成消息滞留情况下,服务器重复接受无用的连接请求 SYN 报文,而造成重复分配资源。

为什么客户端和服务端的初始序列号 ISN 是不相同的?

如果一个已经失效的连接被重用了,但是该旧连接的历史报文还残留在网络中,如果序列号相同,那么就无法分辨 出该报文是不是历史报文,如果历史报文被新的连接接收了,则会产生数据错乱。

初始序列号 ISN 是如何随机产生的?

所以,每次建立连接前重新初始化一个序列号主要是为了通信双方能够根据序号将不属于本连接的报文段丢弃。 另一方面是为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收。

ISN起始是基于时钟的,每 4 毫秒 + 1,转一圈要 4.55 个小时。

RFC1948 中提出了一个较好的初始化序列号 ISN 随机生成算法。

ISN = M + F (localhost, localport, remotehost, remoteport)

M是一个计时器,这个计时器每隔 4 毫秒加 1。

F是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。

既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?

我们先来认识下 MTU 和 MSS

img

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

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

如果在 TCP 的整个报文(头部 + 数据)交给 IP 层进行分片,会有什么异常呢?

当 IP 层有一个超过MTU大小的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,由目标主机的 IP 层来进行重新组装后, 再交给上一层 TCP 传输层。

这看起来井然有序,但这存在隐患的,那么当如果一个 IP 分片丢失,整个IP报文的所有分片都得重传。因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。

当接收方发现 TCP 报文(头部 + 数据)的某一片丢失后,则不会响应 ACK 给对方,那么发送方的 TCP 在超时后,就会重发整个 TCP 报文(头部 + 数据)。

因此,可以得知由 IP 层进行分片传输,是非常没有效率的。所以,为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的MSS值,当 TCP 层发现数据超过MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。

img

经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以MSS为单位,而不用重传所有的分片,大大增加了重传的效率。

TCP 连接断开

天下没有不散的宴席,对于 TCP 连接也是这样, TCP 断开连接是通过四次挥手方式。双方都可以主动断开连接,断开连接后主机中的资源将被释放。

img

  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入FIN_WAIT_1状态。

  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态。

  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。

  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。

  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态。

  • 服务器收到了 ACK 应答报文后,就进入了 CLOSED 状态,至此服务端已经完成连接的关闭。

  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSED 状态,至此客户端也完成连接的关闭。

你可以看到,每个方向都需要一个FIN和一个ACK,因此通常被称为四次挥手。这里一点需要注意是:主动关闭连接的,才有TIME_WAIT状态。

为什么挥手需要四次?

再来回顾下四次挥手双方发 FIN 包的过程,就能理解为什么需要四次了。

关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,从而比三次握手导致多了一次。

为什么 TIME_WAIT 等待的时间是 2MSL?

MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时
间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。

MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的
时间,以确保报文已被自然消亡。

TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包
被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。

比如如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。 在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。

Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。其定义在 Linux 内核代码里的名称为TCP_TIMEWAIT_LEN

*#define TCP_TIMEWAIT_LEN (60*HZ) /*how long to wait to destroy TIME-WAIT state, about 60 seconds /

如果要修改 TIME_WAIT 的时间长度,只能修改 Linux 内核代码里 TCP_TIMEWAIT_LEN 的值,并重新编译 Linux内核。

为什么需要 TIME_WAIT 状态?

主动发起关闭连接的一方,才会有 TIME-WAIT 状态。

需要 TIME-WAIT 状态,主要是两个原因:

  • 防止具有相同四元组(目的地址,源地址,目的端口,源端口)的旧数据包被收到;

  • 保证被动关闭连接的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭;

原因一:防止旧连接的数据包

假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会发生什么呢?

img

如上图黄色框框服务端在关闭连接之前发送的 SEQ = 301 报文,被网络延迟了。

这时有相同端口的 TCP 连接被复用后,被延迟的 SEQ = 301 抵达了客户端,那么客户端是有可能正常接收这个过期的报文,这就会产生数据错乱等严重的问题。

所以,TCP 就设计出了这么一个机制,经过 2MSL 这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。

原因二:保证连接正确关闭

在 RFC 793 指出 TIME-WAIT 另一个重要的作用是:

TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request.

也就是说,TIME-WAIT 作用是等待足够的时间以确保最后的ACK能让被动关闭方接收,从而帮助其正常关闭。

假设 TIME-WAIT 没有等待时间或时间过短,断开连接会造成什么问题呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iimGzt24-1630025850227)(https://cdn.jsdelivr.net/gh/jianningwu/picture/imgwps261.png)]

如上图红色框框客户端四次挥手的最后一个 ACK 报文如果在网络中被丢失了,此时如果客户端 TIME-WAIT 过短或没有,则就直接进入了 CLOSED 状态了,那么服务端则会一直处在 LASE_ACK 状态。

当客户端发起建立连接的 SYN 请求报文后,服务端会发送 RST 报文给客户端,连接建立的过程就会被终止。

如果 TIME-WAIT 等待足够长的情况就会遇到两种情况:

  • 服务端正常收到四次挥手的最后一个 ACK 报文,则服务端正常关闭连接。
  • 服务端没有收到四次挥手的最后一个 ACK 报文时,则会重发 FIN 关闭连接报文并等待新的 ACK 报文。

所以客户端在 TIME-WAIT 状态等待 2MSL 时间后,就可以保证双方的连接都可以正常的关闭。

如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP 有一个机制是保活机制。这个机制的原理是这样的:

定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。

在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:

net.ipv4.tcp_keepalive_time=7200 
net.ipv4.tcp_keepalive_intvl=75 
net.ipv4.tcp_keepalive_probes=9

tcp_keepalive_time=7200:表示保活时间是 7200 秒(2⼩时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制;

tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;

tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。

也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个死亡连接。

img

这个时间是有点长的,我们也可以根据实际的需求,对以上的保活相关的参数进行设置。如果开启了 TCP 保活,需要考虑以下几种情况:

第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP保活时间会被重置,等待下一个 TCP 保活时间的到来。

第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个RST报文,这样很快就会发现 TCP 连接已经被重置。

第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP TCP连接已经死亡。

### Cisco环境下TCP连接建立断开过程分析 #### TCP连接建立(三次握手) 在Cisco环境中,TCP连接建立遵循标准的三次握手流程。以下是具体的过程描述: 1. **第一次握手**:客户端向服务器发送一个带有SYN标志位的TCP包,表示希望发起一个新的连接,并随机生成初始序列号`X`[^1]。 ```plaintext SYN=1, ACK=0, Seq=X ``` 2. **第次握手**:服务器收到客户端的SYN包后,会回复一个同时带有SYNACK标志位的TCP包。该包确认了客户端的SYN请求,并返回自己的初始序列号`Y`,同时也包含了对客户端序列号`X`的确认值`X+1`[^1]。 ```plaintext SYN=1, ACK=1, Seq=Y, Ack=X+1 ``` 3. **第三次握手**:客户端再次发送一个TCP包给服务器,这次仅设置ACK标志位,用于确认服务器的SYN请求。此包中的确认序号为`Y+1`,表明已成功接收并处理了来自服务器的信息[^1]。 ```plaintext SYN=0, ACK=1, Seq=X+1, Ack=Y+1 ``` 完成以上三个步骤之后,TCP连接即被正式建立,双方可以开始正常的数据交换操作。 --- #### TCP连接断开(四次挥手) 对于TCP连接的释放,在Cisco设备中同样按照标准化的四次挥手机制执行。这一机制确保了双向数据流能够安全有序地中止。 1. **第一次挥手**:主动关闭方(通常是客户端)发送一个带FIN标志位的TCP段至被动关闭方(通常为服务器),以此宣告自己不再有更多数据需要传送;此时还附带当前序列号`Z`作为标记[^2]。 ```plaintext FIN=1, ACK=0, Seq=Z ``` 2. **第次挥手**:被动关闭方接获前述FIN信号后进入`CLOSE_WAIT`状态,并且回应一条包含ACK标志的TCP消息来确认对方结束意图,其中携带的是针对先前FIN所对应的序列号加一后的数值`(Z+1)`[^2]。 ```plaintext FIN=0, ACK=1, Seq=W, Ack=Z+1 ``` 3. **第三次挥手**:随后,被动关闭方也会准备停止其自身的输出活动,于是再额外发出另一条具有FIN属性的新TCP分组通知主动关闭方它也打算退出通讯环节[^2]。 ```plaintext FIN=1, ACK=1, Seq=W, Ack=Z+1 ``` 4. **第四次挥手**:最后一步由最初提出中断的一侧负责完成——通过回应最后一个ACK帧告知源端已经知晓整个对话即将彻底终结的事实,至此整个拆除动作才算圆满落幕[^2]。 ```plaintext FIN=0, ACK=1, Seq=Z+1, Ack=W+1 ``` 值得注意的是,在某些优化场景下,第步与第三步可能会合并简化成单一的动作以减少网络交互次数[^2]。 --- #### 总结 无论是TCP连接的创建还是销毁阶段,均严格依据既定协议规定行事,从而保障了跨平台间稳定可靠的通信质量。特别是在像思科这样的企业级解决方案提供商所提供的产品线当中,这些基础功能往往会被进一步封装增强以便更好地服务于实际业务需求。 ```python # Python代码示例展示简单的TCP连接模拟逻辑 import socket def establish_tcp_connection(host='localhost', port=8080): client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: client_socket.connect((host, port)) # 执行三次握手 print(f"Connected to {host}:{port}") except Exception as e: print(e) establish_tcp_connection() ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值