TCP抓包
之前用 Java 实现过简单的 socket 编程,于是顺便也对三次握手的过程重新认识一下。
为了不停留在理论表面,更直观的观察连接建立的过程,采用了 Wireshark 抓包软件进行状态跟踪。
因为 socket 通信我用的是本机回路,不经过网卡,因此 Wireshark 无法获取。需要安装 npcap。安装完成后打开 Wireshark,在 device 列表中会多出一个 Npcap Loopback Adapter,即本地环回地址,选择此接口。
设置过滤条件 tcp.port == 2017(2017为我设置的socket通信的端口),启动客户端后显示如下:
![三次握手](https://i-blog.csdnimg.cn/blog_migrate/60f333511f60e77cb4904aa0efeb0853.png)
三次握手
- 第一次握手: 客户端主动打开连接,向服务器发送 SYN = 1,表示要建立连接,同时初始化序号 Sequence Number(简称为Seq)为 0,然后进入 SYN_SEND 状态,等待服务器回应。
- 第二次握手: 服务器收到 SYN ,向客户端发送 ACK = 1、SYN = 1、Ack Number = Seq(客户端) + 1,同时初始化序号 Seq = 0,然后服务器进入 SYN_RCVD 状态。
- 第三次握手:客户端收到服务器发过来的确认报文,检查 Ack Number 是否正确,向服务器发送 ACK = 1、Ack Number = Seq(服务端) + 1,同时 Seq = 0 + 1。进入 ESTABLISHED 状态。此时 SYN 不再置为 1,服务器收到报文检查正确后,进入 ESTABLISHED 状态,完成三次握手,TCP连接建立。
对于建立连接的三次握手,主要目的是初始化序号 Seq,这个序号作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输问题而乱序,因为TCP会用这个序号来拼接数据。
下面是TCP首部格式(摘自计算机与网络(第七版))
![TCP首部](https://i-blog.csdnimg.cn/blog_migrate/73aba28ca353990a60d53819cc012061.png)
四次挥手
数据传输结束后,通信的双方都可释放连接。
下图是电脑(10.255.151.111)与百度(180.97.33.108)的通信过程:
![四次挥手](https://i-blog.csdnimg.cn/blog_migrate/2bab5ca9d575d2edb3f2b3067fc278f0.png)
最后四条记录是四次挥手的过程。
- 初始状态下A和B都处于 ESTABLISHED 状态。当A需要关闭TCP连接的时候,向B发送 FIN = 1。此时A进入 FIN_WAIT_1 状态
- B 确认A的报文后,向A发送 ACK = 1,表明自己接收到了关闭连接的请求,但还没有准备好关闭连接(可能自己还有数据要发送)。发送完毕后,B进入 CLOSE_WAIT 状态,A接收到确认包后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。
- 若 B 已经没有要向 A 发送的数据,向A发送 FIN = 1。发送完毕后,B进入 LAST_ACK 状态。
- A 收到B的报文后,向B发送 ACK = 1。 发送完毕后,A进入 TIME_WAIT 状态。在 TIME_WAIT 状态下,A经过 2MSL(最长报文段寿命)时间后就进入关闭状态。B接收到A的确认包后,立即进入关闭状态。A和B都进入关闭状态后整个TCP连接释放。
TCP状态转换(状态机)
![状态机](https://i-blog.csdnimg.cn/blog_migrate/a841757a09a04a862e7430b3aac9bfa1.png)
- CLOSED — 初始状态,超时或连接关闭后进入此状态
- LISTEN — 服务器端处于监听状态,可以接受连接
- SYN_SENT — 客户端向服务器端发送 SYN 报文,此时客户端进入 SYN_SENT 状态
- SYN_RECV — 服务器端接收到到 SYN 报文后进入此状态
- ESTABLISHED — 表示连接建立,可以开始传输数据
- FIN_WAIT_1 — 请求终止连接,向对方发送 FIN 报文后进入此状态
- FIN_WAIT_2 — 收到 对 FIN 的 ACK 后进入该状态
- TIME_WAIT — 最常见的一种状态。主动关闭连接的一方收到对方发送的 FIN 并返回 ACK 后进入此状态,等待 2MSL 时间后进入 CLOSED 状态。
- CLOSING:表示同时关闭。发生在发送 FIN 报文之后,本应先收到 ACK 报文,却先收到对方的 FIN 报文,从 FIN_WAIT_1 的状态进入 CLOSING 状态
- CLOSE_WAIT — 接收到 FIN 后进入此状态,并向对方发送 ACK
- LAST_ACK — 被动关闭一方发送 FIN 报文后所处的状态
- CLOSED — 收到 ACK 报文后,被动关闭一方进入 CLOSED 状态
关于TIME_WAIT
通信双方建立TCP连接后,主动关闭连接的一方会进入 TIME_WAIT 状态。
TIME_WAIT 状态存在的原因:
- 保证TCP全双工连接的可靠终止。在四次挥手过程中,最终的 ACK 是由主动关闭连接的一端发出的,如果 ACK 丢失,另一方将重发最终的 FIN,因此主动关闭一方必须维护状态信息允许它重发最终的 ACK。如果主动关闭一方不维持 TIME_WAIT 状态,而是处于 CLOSED 状态,那么将会对发来的 FIN 回应 RST ,另一方收到 RST 后认为连接出现异常。
- 如果主动关闭的一方不进入 TIME_WAIT 直接 CLOSED,然后发起一个新连接。原有的数据包因某些原因在新连接建立后才到达目的端,造成新旧数据包混淆,发生错误。因为 TIME_WAIT 状态持续 2MSL,可以保证当成功建立一个新的TCP连接时,来自旧连接的数据包已经在网络中消失。
在Linux中,可以通过以下命令查看 TIME_WAIT 参数:
sysctl -a | grep time | grep wait
输出结果:
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 60
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 120
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120
可以看到,TIME_WAIT 时间为120秒。对于服务器来说,如果出现 TIME_WAIT 数过多,就需要对相关的TCP参数进行修改,比如允许 TIME-WAIT Socket 的复用。
关于RST
在TCP协议中 RST 表示复位,用来异常的关闭连接。发送端会丢弃缓存区的包直接发送 RST 包;接收端收到 RST 包后,不必发送 ACK 包确认。
RST 出现的原因主要有以下两种:
- 端口未打开
- 向一个已关闭的 Socket 发送数据。例如客户端向服务器发送数据,服务器接收到数据,但是发现 Socket 已经关闭,会返回 RST 给客户端。此时客户端就会提示 Connection reset 或类似的信息。
OSI模型与TCP/IP参考模型
![OSI模型与TCP/IP参考模型](https://i-blog.csdnimg.cn/blog_migrate/e03189acf2a809e12d31757216912657.png)