1. TCP连接:
TCP是基于连接的传输协议,TCP的可靠传输需要建立在一对一的连接的基础之上。
TCP通过三次握手建立连接,通过四次挥手断开连接。
三次握手不仅完成连接的建立,同时完成了交换初始序列号(Initial Sequence Number,ISN)。
通常关闭连接是由应用程序发起(调用close),然而一些服务器(例如Web服务器)在对请求作出响应之后也会发起关闭操作。
TCP使用一个四元组来唯一的表示一条TCP连接(source_ip、dest_ip、source_port、dest_port)。
2. TCP头部格式:
3. TCP连接的11种状态转换:
4. TCP三次握手过程中的状态转换:
第一种情况:正常打开
第二种情况:同时打开
第三种情况:连接建立超时 或 RST复位
4.1 第一种情况:正常打开:
握手过程:
对应的TCP状态转换:
4.2 第二种情况:同时打开:
说明:
关键点:
在 SYN_SENT 状态下收到 SYN报文段,而不是 SYN+ACK 报文段
“同时打开”与“正常连接”过程的区别在于:
① 正常过程中,主动打开一方发送SYN后进入SYN_SENT状态,此时等待被动打开一方回复SYN+ACK,主动打开一方在收到回复后转入ESTABLISHED:
SYN_SENT --(SYN+ACK)--> ESTABLISHED
② “同时打开”过程中,主动打开一方发送SYN后进入SYN_SENT状态,在SYN_SENT状态下并未等来被动打开一方的SYN+ACK,而是只有一个单独的SYN,由此判断对方也调用了主动打开,主动打开一方此时进入SYN_RCVD状态(相当于在LISTEN状态下收到SYN):
SYN_SENT --(SYN)--> SYN_RCVD
TIPS:
“同时打开”是一种非常特殊的场景,正常环境下,服务器是不会主动向客户端发起连接的。
“同时打开”是TCP协议栈支持的情况,如果应用程序要支持“同时打开”,则必须在代码中既调用connect(发起SYN),又调用accept(连接建立后创建关联连接的fd),既是一个“客户端”,也是一个“服务器”。
握手过程:
对应的TCP状态转换图:
4.3 第三种情况:连接建立超时 或 RST复位:
说明:
主动打开方(客户端)发出的 SYN 报文段石沉大海,无人响应,当SYN达到最大重传次数(net.ipv4.tcp_syn_retries)后,则会连接超时,TCP状态从 SYN_SENT 状态重新传入 CLOSED 状态;
或者,客户端调用 connect 发起连接后又迅速调用了 close,TCP状态也会重新转入 CLOSED状态。
(注意:这种情况是SYN报文段根本无人接收,导致客户端收不到任何关于SYN的响应。例如服务器已经关闭。)
另外一种情况,是服务器收到了客户端的 SYN 报文段,但连接信息有误,例如申请连接的端口号无人监听,此时会回复 RST 复位报文段,用于快速重置连接。
//client:
CLOSED --> SYN_SENT --(SYN timeout || close || RST)--> CLOSED
同样的,服务端发出的 SYN+ACK 报文段同样有可能会因达到最大重传次数而超时(net.ipv4.tcp_synack_retries),此时服务器状态会由 SYN_RCVD 重新转回 LISTEN 状态。
//server:
CLOSED --> LISTEN --> SYN_RCVD --(SYN+ACK timeout || close || RST)--> LISTEN
对应的TCP状态转换图:
5. TCP四次挥手过程中的状态转换:
第一种情况:正常关闭
第二种情况:同时关闭
第三种情况:四次挥手合并成三次挥手
5.1 第一种情况:正常关闭:
挥手过程:
对应的TCP状态转换图:
5.2 第二种情况:同时关闭:
说明:
关键点:
在 FIN_WAIT_1 状态下收到 FIN报文段,而不是ACK报文段
“同时关闭”与“正常关闭”过程的区别在于:
① 在 正常关闭 的过程中,主动发起关闭的一方在发送 FIN报文段后,由 ESTABLISHED 状态进入到 FIN_WAIT_1 状态,并等待对方回复ACK,在收到ACK后随即转入 FIN_WAIT_2 状态:
//client:
ESTABLISHED --(FIN)--> FIN_WAIT_1 --(rcv ACK)--> FIN_WAIT_2
② 在 同时关闭 的过程中,主动发起关闭的一方 FIN_WAIT_1 状态下并未收到 ACK报文段,而是收到了对方的FIN,说明对方也主动调用了close关闭连接,此时则由 FIN_WAIT_1 状态转入 CLOSING 状态,并回复ACK:
//client:
ESTABLISHED --(FIN)--> FIN_WAIT_1 --(rcv FIN)--> CLOSING
TIPS:
在“同时关闭”的过程中引入了一个独有的TCP状态:CLOSING。
在“同时打开”的场景下并未引入新的TCP状态,而是直接复用了 SYN_RCVD(由 SYN_SENT 转入 SYN_RCVD,而非转入ESTABLISHED),这是因为SYN_RCVD有“回复ACK并等待对方回复ACK”的功能,而在连接“正常关闭”的流程中并没有这样的一个状态,因此引入“CLOSING”状态,用于“回复ACK并等待ACK”。
另外需要注意的是:在“同时关闭”的场景下同样需要 TIME_WAIT 状态。
挥手过程:
对应的TCP状态转换图:
5.3 第三种情况:四次挥手合并成三次挥手:
说明:
四次挥手中的FIN和ACK可能会合并发送,此时主动关闭的一方会直接从 FIN_WAIT_1 状态转入 TIME_WAIT 状态:
FIN_WAIT_1 --(rcv FIN+ACK)--> TIME_WAIT
注意:
在服务器端(被动关闭一方),应用程序层面并不会合并发送 FIN + ACK,应用程序需要处理客户端的FIN并首先回复ACK,然后才会检测本端无数据需要发送后才会发送FIN。
但是在内核协议栈层面,可能会有ACK延迟确认,导致ACK等到FIN后合并发送。
挥手过程:
对应的TCP状态转换图:
6. TCP连接状态下的异常处理:
6.1 TCP处于连接状态下的两种异常:
- 应用进程异常退出;
- 主机系统异常退出。
- 对于第一类 应用进程异常退出(例如进程被kill掉 或者 访问空指针等运行异常退出),Linux内核可以检测到进程终止,此时进程占用的系统资源(fd、内存等)都会被回收,同时内核会完成TCP的四次挥手流程(TCP协议栈是在内核实现的),但与进程正常调用close关闭连接不同的是,进程异常退出时系统不会等待TCP发送缓冲区的数据全部发完,而是会直接发送FIN报文段,通知对端关闭TCP连接。
- 对于第二类情况 主机系统异常退出(例如主机宕机 或者 网线被拔),此时内核一并挂掉,来不及进入TCP四次挥手流程,所以对端是无法感知到发送端的情况的,对端只能等到TCP保活机制超时后断开连接(默认配置下2小时11分钟)。如果主机迅速恢复或者重新插上网线,连接会恢复到异常之前的状态,对端同样无法感知。
6.2 进程异常退出与TIME_WAIT处理:
当TCP连接中的服务端进程异常退出时,Linux内核可以检测到进程崩溃,此时内核会自动进入四次挥手流程关闭与客户端之间的连接。
由于此时服务端扮演的是主动发起四次挥手的角色,所以服务端最后会进入到 TIME_WAIT
状态,此时就引入了一个新的异常边界问题:
当崩溃的 服务器进程 快速完成重启,并尝试绑定LISTEN监听端口时,由于此时端口、fd等资源还在被之前的连接占用(TIME_WAIT状态中),服务器将无法完成bind绑定。
为了让服务器进程可以快速完成服务恢复,对 监控端口的TIME_WAIT状态 的处理方法是:
在socket之后、bind之前,设置套接字选项 SO_REUSEADDR,该选项表示:如果bind要绑定的端口正被占用且处于TIME_WAIT 状态,则可以重用端口。此时即可绑定成功。
SO_REUSEADDR的使用方法:
//listenfd = socket(AF_INET, SOCK_STREAM, 0);
int optval = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
//bind(listenfd, &servaddr);