文章目录
- 前言
- TCP报文格式
- TCP连接管理
- 连接建立与中止
- 三次握手
- 三次握手的状态变化
- 为什么是三次握手
- 四次挥手
- 四次挥手的状态变化
- FIN_WAIT_2 状态可能导致连接长时间不释放的问题
- TIME_WAIT状态作用
- 复位报文段
- 非法连接请求
- 其他异常情况
- 半打开连接
- 同时握手
- 同时关闭
- 参考资料
前言
TCP(Transmission Control Protocol,传输控制协议)是互联网最重要的协议之一,为应用层提供可靠的、面向连接的数据传输服务。它广泛应用于 HTTP、FTP、SMTP 等协议中,保障数据能够准确、有序地到达目标设备。本文将主要介绍TCP连接管理机制
TCP报文格式
首先我们来看TCP的报文格式
- 16位源端口/目的端口 : 用于寻找发端和收端应用进程。这两个值加 上 IP 首部中的源端 IP 地址和目的端 IP 地址唯一确定一个 TCP 连接。
- 序号 : 序号用来标识从 TCP 发端向 TCP 收端发送的数据字节流,它表示在这个报文段中的的第一 个数据字节。
- 确认序号 : 确认这个序号以前的报文都收到了, ACK标记为 1 生效
- 4位首部长度 : 表示该TCP头部有多少个32位bit(有多少个4字节), 首部长度范围[5,15], 所以TCP报头长度[20,60]字节
- 6个标注位
- URG : 紧急指针有效
- ACK : 确认序号有效
- PSH : 提示接收方应该尽快将这个报文交付到应用层
- RST : 直接终止当前连接或拒绝新的连接请求, 我们把携带 RST 标识的称为复位报文段
- SYN : 请求建立连接; 我们把携带 SYN 标识的称为同步报文段
- FIN : 通知断开连接, 我们称携带 FIN 标识的为结束报文段
- 16位窗口大小 : 自己的接收缓冲区的剩余空间大小
- 16位校验和 : CRC校验, 接收端校验不通过, 则认为数据有问题
- 16位紧急指针 : 标识哪部分数据是紧急数据
TCP连接管理
TCP 是一个面向连接的协议。无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接
连接建立与中止
我们使用tcpdump工具来查看TCP在连接建立和中止时发生了什么, 这里使用了telnet向一个服务端发起连接
localhost.57692 > localhost.http-alt:
Flags [S], cksum 0xfe30 (incorrect -> 0xe677), seq 2011543754, win 65495, options [mss 65495,sackOK,TS val 3010364542 ecr 0,nop,wscale 7], length 0
localhost.http-alt > localhost.57692:
Flags [S.], cksum 0xfe30 (incorrect -> 0xaee7), seq 2821871467, ack 2011543755, win 65483, options [mss 65495,sackOK,TS val 3010364542 ecr 3010364542,nop,wscale 7], length 0
localhost.57692 > localhost.http-alt:
Flags [.], cksum 0xfe28 (incorrect -> 0xd5a3), seq 1, ack 1, win 512, options [nop,nop,TS val 3010364542 ecr 3010364542], length 0
localhost.57692 > localhost.http-alt:
Flags [F], cksum 0xfe28 (incorrect -> 0xc7a7), seq 1, ack 1, win 512, options [nop,nop,TS val 3010368121 ecr 3010364542], length 0
localhost.http-alt > localhost.57692:
lags [.], cksum 0xfe28 (incorrect -> 0xb9a9), seq 1, ack 2, win 512, options [nop,nop,TS val 3010368124 ecr 3010368121], length 0
localhost.http-alt > localhost.57692:
Flags [F], cksum 0xfe28 (incorrect -> 0xae2d), seq 1, ack 2, win 512, options [nop,nop,TS val 3010371063 ecr 3010368121], length 0
localhost.57692 > localhost.http-alt:
Flags [.], cksum 0xa2af (correct), seq 2, ack 2, win 512, options [nop,nop,TS val 3010371063 ecr 3010371063], length 0
主要观察这些标记位变化, 可以得出
- 建立连接
- 客户端 --> 服务端发送带有SYN标识的报文
- 服务端 --> 客户端发送SYN+ACK
- 客户端 --> 服务端发送ACK
- 关闭连接
- 客户端 --> 服务端发送带有FIN标识的报文
- 服务端 --> 客户端发送ACK
- 服务端 --> 客户端发送FIN
- 客户端 --> 服务端发送ACK
有时我们也可以看到在服务端 --> 客户端 合并了FIN+ACK报文
localhost.http-alt > localhost.57692:
Flags [F.] ......
三次握手
从上面的例子来看, 为了建立一TCP条连接
- 请求端(通常称为客户端)发送一个 SYN 段指明打算连接的服务器的端口,
- 服务器发回 SYN 报文段作为应答。同时,对客户的 SYN 报文段进行确认。
- 客户将对服务器的 SYN 报文段进行确认。
这三个报文段完成连接的建立。这个过程也称为三次握手
三次握手的状态变化
- 一开始, 客户端和服务端都处于CLOSED状态, 然后服务端开始监听进入LISTEN状态
- 客户端发起请求建立连接, 发送一个带有SYN标识的报文, 客户端进入SYN_SEND状态
- 服务端收到客户端发送的SYN报文, 回应一个SYN+ACK, 服务端进入SYN_RCVD状态
- 客户端收到服务端的SYN+ACK, 回应一个ACK, 客户端三次握手完成, 进入ESTABLISHED状态
- 服务端收到客户端的ACK后, 服务端三次握手完成, 也进入ESTABLISHED状态
为什么是三次握手
TCP 是一个全双工协议,这意味着连接的双方可以同时发送和接收数据。为了确保这种能力,在连接建立时,必须确认双方都能够发送和接收数据, 三次握手可以保证双方可读可写
如果是二次握手, 客户端在接收到服务端发送的ACK+SYN就已经结束了, 服务端无法得知客户端是否收到此报文, 也就是不能保证客户端是否可以接收数据, 如果服务端发送的ACK+SYN发送丢包, 此时服务端认为连接建立完成, 但实际并没有, 客户端和服务端各自等待对方的响应,最终会导致超时和连接失败
三从握手是可以满足这些条件的最小次数, 三此握手可以确定双方的全双工, 如果任意时刻发生丢包, 也能通过超时重传来机制来保证
四次挥手
建立一个连接需要三次握手,而终止一个连接要经过 4次握手, 从之前的例子来看
- 客户端发送一个FIN标识的报文表示要和服务端关闭连接
- 服务端应答一个ACK
- 服务端发送FIN表示与客户端关闭连接
- 客户端应答一个ACK
四次挥手的状态变化
- 客户端打算关闭连接, 发送一个FIN报文, 客户端进入FIN_WAIT_1状态
- 服务端应答一个ACK, 服务端进入CLOSE_WAIT状态
- 客户端收到服务端的ACK, 客户端进入FIN_WAIT_2状态
- 服务端打算关闭连接, 发送一个FIN报文, 服务端进入LAST_WAIT状态
- 客户端应答一个ACK, 客户端进入TIME_WAIT状态
- 服务端收到应答, 服务端进入CLOSED状态
- 客户端会在2MSL(Maximum Segment Lifetime 最大报文生命周期)后自动关闭, 进入CLOSED状态
FIN_WAIT_2 状态可能导致连接长时间不释放的问题
在 FIN_WAIT_2 状态我方已经发出了 FIN ,并且另一端也已对它进行确认。除非我们在实行半关闭,否则将一直等待另一端的应用层意识到它已收到一个文件结束符说明,并向我们发一 个 FIN 来关闭另一方向的连接。只有当另一端的进程完成这个关闭,我们这端才会从 FIN_WAIT_2 状态进入 TIME_WAIT 状态。 这意味着我们这端可能永远保持这个状态。另一端也将处于 CLOSE_WAIT状态,并一直保持这个状态直到对方关闭连接
某些操作系统(如 Linux)提供了超时机制,当连接在 FIN_WAIT_2状态超过一定时间后,内核会强制关闭连接,释放资源。在 Linux 中,可以通过修改 TCP 超时参数 来限制 FIN_WAIT_2 的最大存活时间
TIME_WAIT状态作用
TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个 MSL(maximum segment lifetime)的时间后才能回到CLOSED状态
查看MSL时间
$ cat /proc/sys/net/ipv4/tcp_fin_timeout
60
MSL 是任何报文段被丢弃前在网络内的最长时间, 等待2MSL时间可以保证一些陈旧报文已经消散, 同时TIME_WAIT状态依然可以接收对方的重传报文,并进行必要的应答
如果没有这个状态的等待时间, 假设我们主动关闭的一方关闭后立即重启, 端口不变, 那么就有可能将一些还在网络中的陈旧报文接收到, 造成曲解, 如果客户端最后应答的ACK报文丢包, 在2MSL时间内也可以进行重传
复位报文段
TCP 首部中的 RST 比特是用于“复位”的。一般说来,无论何时一个报文段发往基准的连接(referenced connection)出现错误,TCP 都会发出一个复位报文段 ------《TCP/IP详解 卷1: 协议》
非法连接请求
当一个数据报到达目的端口时,该端口没在使用,这是一个非法连接请求, TCP 使用复位。我们来查看Linux2.6内核源码方便理解这点
TCP对数据包的处理在tcp_v4_rcv中
/* 处理接收到的TCP数据包 */
int tcp_v4_rcv(struct sk_buff *skb)
{
......
no_tcp_socket: /* 没有找到对应的socket,发送RST */
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) /* 如果不符合安全策略,丢弃 */
goto discard_it;
if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) { /* 如果数据包长度或校验和有问题,丢弃 */
bad_packet:
TCP_INC_STATS_BH(TCP_MIB_INERRS);
} else {
tcp_v4_send_reset(skb); /* 发送RST */
}
discard_it:
/* 丢弃数据包 */
kfree_skb(skb);
return 0;
......
}
然后调用tcp_v4_send_reset发送复位报文
static void tcp_v4_send_reset(struct sk_buff *skb)
{
struct tcphdr *th = skb->h.th;
struct tcphdr rth;
struct ip_reply_arg arg;
......
memset(&rth, 0, sizeof(struct tcphdr));
rth.dest = th->source;
rth.source = th->dest;
rth.doff = sizeof(struct tcphdr) / 4;
rth.rst = 1;//设置RST标志
if (th->ack) {
rth.seq = th->ack_seq;
} else {
rth.ack = 1;
rth.ack_seq = htonl(ntohl(th->seq) + th->syn + th->fin +
skb->len - (th->doff << 2));
}
memset(&arg, 0, sizeof arg);
arg.iov[0].iov_base = (unsigned char *)&rth;
arg.iov[0].iov_len = sizeof rth;
arg.csum = csum_tcpudp_nofold(skb->nh.iph->daddr,
skb->nh.iph->saddr, /*XXX*/
sizeof(struct tcphdr), IPPROTO_TCP, 0);
arg.csumoffset = offsetof(struct tcphdr, check) / 2;
ip_send_reply(tcp_socket->sk, skb, &arg, sizeof rth);
TCP_INC_STATS_BH(TCP_MIB_OUTSEGS);
TCP_INC_STATS_BH(TCP_MIB_OUTRSTS);
}
我们可以看到 RST 标注位被设置
其他异常情况
-
连接中断:当连接的另一端在未正常关闭的情况下强制终止连接(比如程序崩溃或网络故障),接收方可能会发送 RST 报文来终止连接。
-
协议错误或违规行为:当发现协议中的违规行为(例如,接收到错误的序列号、非法的报文等),接收方可以使用 RST 报文来重置连接
半打开连接
如果一方已经关闭或异常终止连接而另一方却还不知道,我们将这样的 TCP 连接称为半打开连接, 半打开连接的常见原因是当客户主机突然掉电或者结束客户应用程序异常中止后再关机, 只有这条连接上没有数据传递, 服务端就不会知道客户端已经关闭, 当客户端再次打开后又会重新建立连接, 长期以往就会出现连接堆积问题, 后面我们介绍TCP的保活机制可以解决这个问题
同时握手
当两端同时向对方发送 SYN 请求以建立 TCP 连接时,这种情况被称为 SYN Flood 的一种特殊形式,或者更正式地叫做 双向 SYN 同时发起。这种情形会引起连接的同时握手(Simultaneous Open)
TCP标准支持这种同时握手, 对于同时打开仅建立一条连接而不是两条连接, 状态转移更像双向的三次握手, 一共要交换四个报文
- 两端同时发起请求建立连接, 发送一个带有SYN标识的报文, 都进入SYN_SEND状态
- 两端收到对方发送发送的SYN报文, 回应一个SYN+ACK, 都进入SYN_RCVD状态
- 两端收到对方的SYN+ACK, 握手完成, 进入ESTABLISHED状态
同时关闭
既然同时握手可能存在, 那么双方都执行主动关闭也是可能的, TCP 协议也允许这样的同时关闭(simultaneous close)
CLOSING状态就是为了处理这种特殊情况, 两端都已经发送了 FIN 包,但还未完全收到对方的确认包时
- 两端同时发送FIN报文表示关闭连接, 都进入FIN_WAIT_1状态
- 两端收到对方发送的FIN报文, 回应一个ACK, 两端进入CLOSING状态
- 两端收到对方的ACK后, 进入TIME_WAIT状态
参考资料
- 《TCP/IP详解 卷1: 协议》
- Linux2.6内核源码