传输控制协议(Transmission Control Protocol,TCP)是互联网协议族中最核心的协议之一,由Vint Cerf和Bob Kahn在1974年设计并于1981年在RFC 793中正式定义。TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,为应用程序提供了可靠的数据传输服务,确保数据能够无错、按序地从源主机传输到目标主机。
TCP的诞生源于美国国防部高级研究计划局(DARPA)对可靠军事通信网络的需求。在早期的ARPANET中,网络通信主要依赖于网络控制协议(NCP),但随着网络规模的扩大和异构网络的出现,需要一种更加健壮和灵活的协议来处理复杂的网络环境。
1973年,Vint Cerf和Bob Kahn开始设计一种能够在不同网络之间传输数据的协议,这就是TCP/IP的前身。最初,TCP和IP是一个统一的协议,后来为了模块化和灵活性,在1978年被分离为两个独立的协议。
1981年,TCP协议在RFC 793中被正式定义,并逐渐成为互联网的核心协议。随着互联网的发展,TCP也经历了多次改进和优化,如拥塞控制算法的引入(1988年)、选择性确认(SACK)的支持(1996年)等。
TCP的设计目标主要包括:
可靠传输:确保数据能够无错、完整地从源主机传输到目标主机,即使底层网络不可靠。
有序交付:确保数据按照发送顺序交付给接收方,即使底层网络可能导致数据包乱序到达。
流量控制:防止发送方发送数据的速率超过接收方处理能力,避免接收方缓冲区溢出。
拥塞控制:防止过多数据注入到网络中,导致网络拥塞和性能下降。
面向连接:在数据传输前建立连接,确保双方都准备好进行通信,并在传输完成后正常关闭连接。
全双工通信:支持数据在两个方向上同时传输,提高通信效率。
可靠性与效率的平衡:在提供可靠传输的同时,尽量减少额外开销,提高网络利用率。
TCP具有以下基本特性:
面向连接:TCP在传输数据前需要先建立连接(三次握手),传输完成后需要释放连接(四次挥手)。
可靠传输:TCP通过确认、重传、校验和等机制确保数据的可靠传输。
面向字节流:TCP将应用层交付的数据视为无结构的字节流,不保留应用层的消息边界。
流量控制:TCP使用滑动窗口机制进行流量控制,防止发送方发送速率过快导致接收方缓冲区溢出。
拥塞控制:TCP通过慢启动、拥塞避免、快速重传和快速恢复等算法进行拥塞控制,适应网络状况。
全双工通信:TCP连接的两端都可以同时发送和接收数据。
面向报文段:TCP将字节流分割成报文段进行传输,每个报文段都有序列号,用于重组和确认。
与其他传输协议相比,TCP有其独特的优势和局限性:
与UDP的比较:
与SCTP的比较:
与QUIC的比较:
TCP广泛应用于各种需要可靠数据传输的场景,主要包括:
Web浏览:HTTP/HTTPS协议通常基于TCP,确保网页内容的完整传输。
电子邮件:SMTP、POP3、IMAP等电子邮件协议使用TCP,确保邮件内容不丢失。
文件传输:FTP、SFTP等文件传输协议使用TCP,确保文件的完整性。
远程登录:SSH、Telnet等远程登录协议使用TCP,确保命令和响应的可靠传输。
数据库访问:大多数数据库客户端与服务器之间的通信使用TCP,确保数据的一致性。
流媒体传输:某些流媒体应用使用TCP,特别是在带宽充足且对可靠性要求高的场景。
即时通讯:许多即时通讯应用使用TCP,确保消息的可靠传递。
TCP的这些特性和应用场景使其成为互联网的基石,支撑着各种各样的网络应用和服务。
TCP头部是TCP协议的核心组成部分,包含了控制信息和元数据,用于实现TCP的各种功能。理解TCP头部结构对于深入理解TCP协议的工作原理至关重要。
TCP头部的标准长度为20字节(不包含选项),最大可达60字节(包含选项)。下面是TCP头部的详细结构:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 源端口号 | 目的端口号 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 序列号 (Sequence Number) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 确认号 (Acknowledgment Number) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据偏移 | 保留 |U|A|P|R|S|F| 窗口大小 (Window) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 校验和 (Checksum) | 紧急指针 (Urgent Pointer)|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 选项 (Options) [可变长度] |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
在Linux内核中,TCP头部结构定义如下:
// file: include/uapi/linux/tcp.h
struct tcphdr {
__u16 source; // 源端口
__u16 dest; // 目的端口
__u32 seq; // 序列号
__u32 ack_seq; // 确认号
__u16 doff:4, // 数据偏移(头部长度)
res1:4, // 保留
cwr:1, // 拥塞窗口减少标志
ece:1, // ECN回显标志
urg:1, // 紧急标志
ack:1, // 确认标志
psh:1, // 推送标志
rst:1, // 复位标志
syn:1, // 同步标志
fin:1; // 结束标志
__u16 window; // 窗口大小
__sum16 check; // 校验和
__u16 urg_ptr; // 紧急指针
};
下面详细解释TCP头部的各个字段:
这两个字段共同构成了传输层的寻址机制,使得数据能够准确地交付给正确的应用程序。
序列号用于标识从TCP发送端向TCP接收端发送的数据字节流的第一个字节的编号。在建立连接时,通过SYN包交换随机的初始序列号(ISN)。
序列号的主要作用是:
确认号表示接收方期望从发送方收到的下一个字节的序列号。换句话说,确认号等于已成功接收的数据的最高序列号加1。
确认号的主要作用是:
数据偏移字段指示TCP头部的长度,以32位(4字节)为单位。由于TCP头部可能包含可变长度的选项,因此需要这个字段来指示数据的起始位置。
数据偏移的最小值为5(表示20字节的标准头部),最大值为15(表示60字节的头部,包含40字节的选项)。
这些位保留供将来使用,目前应该设置为0。
TCP头部包含6个标志位,用于控制TCP连接的状态和行为:
此外,还有两个与显式拥塞通知(ECN)相关的标志位:
窗口大小字段指示发送方当前可以接收的数据量,以字节为单位。它是TCP流量控制的关键机制,用于防止发送方发送过多数据导致接收方缓冲区溢出。
由于16位字段的限制,标准窗口大小最大为65535字节。为了支持更大的窗口,TCP引入了窗口缩放选项,允许将窗口大小乘以一个缩放因子(最大为14),从而支持最大约1GB的窗口。
校验和字段用于检测TCP头部和数据在传输过程中是否被损坏。它是对TCP头部、数据以及一个伪头部(包含源IP地址、目的IP地址、协议号和TCP长度)进行计算得出的。
校验和的计算方法是:将所有16位字进行反码求和,然后取反码。接收方使用相同的算法计算校验和,如果结果为0,则认为数据完整;否则,认为数据已损坏,将丢弃该段。
当URG标志位设置时,紧急指针字段有效,它指示紧急数据的末尾相对于当前序列号的偏移量。紧急数据应该优先处理,不受流量控制的限制。
紧急指针机制在现代网络中使用较少,但它提供了一种在正常数据流之外传输紧急信息的方法。
TCP头部可以包含多种选项,用于扩展TCP的功能。常见的选项包括:
选项的格式通常为:选项类型(1字节)、选项长度(1字节,对于某些选项可能省略)和选项数据(可变长度)。
在Linux内核中,TCP头部的处理涉及多个函数和数据结构。以下是一些关键的实现细节:
当发送TCP段时,内核需要构建TCP头部。这通常在tcp_transmit_skb
函数中完成:
// 简化版的TCP头部构建过程
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
gfp_t gfp_mask)
{
// ...
th = tcp_hdr(skb);
th->source = inet->inet_sport;
th->dest = inet->inet_dport;
th->seq = htonl(tcb->seq);
th->ack_seq = htonl(tp->rcv_nxt);
*(((__u16 *)th) + 6) = htons(((tp->tcp_header_len >> 2) << 12) |
tcb->tcp_flags);
// ...
// 设置窗口大小
if (likely(!tp->rx_opt.wscale_ok)) {
th->window = htons(min(tp->rcv_wnd, 65535U));
} else {
th->window = htons(min(tp->rcv_wnd >> tp->rx_opt.rcv_wscale, 65535U));
}
// ...
// 计算校验和
if (likely(skb->ip_summed == CHECKSUM_PARTIAL)) {
th->check = ~tcp_v4_check(skb->len, saddr, daddr, 0);
skb->csum_start = skb_transport_header(skb) - skb->head;
skb->csum_offset = offsetof(struct tcphdr, check);
} else {
th->check = tcp_v4_check(skb->len, saddr, daddr,
csum_partial(th, thlen, skb->csum));
}
// ...
}
当接收TCP段时,内核需要解析TCP头部。这通常在tcp_v4_rcv
或类似函数中完成:
// 简化版的TCP头部解析过程
int tcp_v4_rcv(struct sk_buff *skb)
{
// ...
th = tcp_hdr(skb);
// 检查校验和
if (!pskb_may_pull(skb, sizeof(struct tcphdr)) ||
(skb_checksum_complete(skb))) {
goto discard_it;
}
// 解析TCP头部字段
source = th->source;
dest = th->dest;
seq = ntohl(th->seq);
ack_seq = ntohl(th->ack_seq);
// ...
// 根据TCP状态和头部字段进行处理
// ...
}
TCP选项的处理相对复杂,通常在tcp_parse_options
函数中完成:
// 简化版的TCP选项解析过程
void tcp_parse_options(struct sk_buff *skb, struct tcp_options_received *opt_rx,
int estab, struct tcp_fastopen_cookie *foc)
{
// ...
ptr = (unsigned char *)(th + 1);
while (ptr < end) {
switch (*ptr) {
case TCPOPT_EOL:
// 选项列表结束
return;
case TCPOPT_NOP:
// 无操作,用于填充
ptr++;
continue;
case TCPOPT_MSS:
// 最大段大小选项
// ...
break;
case TCPOPT_WINDOW:
// 窗口缩放选项
// ...
break;
case TCPOPT_TIMESTAMP:
// 时间戳选项
// ...
break;
case TCPOPT_SACK_PERM:
// 选择性确认许可选项
// ...
break;
// 其他选项的处理
// ...
}
// 移动到下一个选项
ptr += optlen;
}
}
理解TCP头部结构对于网络编程、协议分析和故障排除都非常重要。以下是一些实际应用场景:
在使用套接字API进行网络编程时,虽然TCP头部的细节通常由操作系统处理,但了解TCP头部结构有助于理解TCP的行为和优化应用程序的网络性能。例如:
使用Wireshark等网络分析工具时,了解TCP头部结构有助于解读捕获的数据包,分析网络通信过程:
在排查网络问题时,了解TCP头部结构有助于定位和解决问题:
TCP头部结构看似简单,但其设计精巧,每个字段都有其特定的用途,共同支撑了TCP的各种功能。深入理解TCP头部结构是掌握TCP协议的基础。
TCP三次握手(Three-Way Handshake)是TCP连接建立过程中的关键机制,它确保了通信双方都具备发送和接收数据的能力,并协商了初始序列号等连接参数。三次握手的过程精确而高效,是TCP可靠连接的基础。
TCP三次握手的基本过程如下:
第一次握手(SYN):客户端发送一个SYN(同步)包,其中包含随机生成的初始序列号(ISN_c),并将连接状态设置为SYN_SENT。
第二次握手(SYN+ACK):服务器收到SYN包后,回复一个SYN+ACK(同步+确认)包,其中包含服务器生成的初始序列号(ISN_s)和对客户端序列号的确认(ACK=ISN_c+1),并将连接状态设置为SYN_RCVD。
第三次握手(ACK):客户端收到SYN+ACK包后,发送一个ACK(确认)包,确认服务器的序列号(ACK=ISN_s+1),并将连接状态设置为ESTABLISHED。服务器收到ACK包后,也将连接状态设置为ESTABLISHED。
这个过程可以用下图表示:
客户端 服务器
| |
| SYN |
|------------------------------------->|
| seq=ISN_c, ack=0 |
| |
| SYN+ACK |
|<-------------------------------------|
| seq=ISN_s, ack=ISN_c+1 |
| |
| ACK |
|------------------------------------->|
| seq=ISN_c+1, ack=ISN_s+1 |
| |
连接建立| |连接建立
客户端通过调用connect()
函数发起连接请求,这会触发TCP协议栈发送SYN包。SYN包的特点是:
客户端发送SYN包后,进入SYN_SENT状态,等待服务器的响应。如果在一定时间内没有收到响应,客户端会重传SYN包,重传次数和间隔由系统参数控制。
服务器通过listen()
函数进入监听状态,等待客户端的连接请求。当服务器收到SYN包后,会执行以下操作:
服务器发送SYN+ACK包后,进入SYN_RCVD状态,等待客户端的确认。此时连接处于"半开"状态,服务器已分配资源但连接尚未完全建立。
客户端收到服务器的SYN+ACK包后,会执行以下操作:
客户端发送ACK包后,进入ESTABLISHED状态,可以开始发送数据。服务器收到ACK包后,也进入ESTABLISHED状态,连接完全建立。
下面我们通过Linux内核源码来分析TCP三次握手的实现。
当应用程序调用connect()
函数时,最终会调用到tcp_v4_connect()
函数,该函数负责发送SYN包:
// 简化版的tcp_v4_connect函数
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
// ...
// 生成初始序列号
tp->write_seq = secure_tcp_seq(saddr, daddr, sport, dport);
// 设置SYN_SENT状态
tcp_set_state(sk, TCP_SYN_SENT);
// 设置连接参数
tp->ts_recent = 0;
tp->rcv_wnd = 0;
tp->rcv_wup = 0;
tp->snd_wl1 = 0;
// 发送SYN包
err = tcp_transmit_skb(sk, skb, 1, sk->sk_allocation);
// 启动重传定时器
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
// ...
return 0;
}
在tcp_transmit_skb
函数中,会构建TCP头部并设置SYN标志位:
// 简化版的TCP头部构建过程
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
gfp_t gfp_mask)
{
// ...
// 构建TCP头部
th = tcp_hdr(skb);
th->source = inet->inet_sport;
th->dest = inet->inet_dport;
th->seq = htonl(tcb->seq);
th->ack_seq = htonl(tp->rcv_nxt);
// 设置SYN标志位
tcp_header_size = tcp_options_size + sizeof(struct tcphdr);
th->doff = (tcp_header_size >> 2);
TCP_SKB_CB(skb)->tcp_flags = TCPHDR_SYN;
th->syn = 1;
// ...
// 发送数据包
err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
// ...
return err;
}
服务器通过listen()
函数进入监听状态,当收到SYN包时,会调用tcp_v4_do_rcv()
函数处理:
// 简化版的tcp_v4_do_rcv函数
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
// ...
// 根据套接字状态处理
if (sk->sk_state == TCP_LISTEN) {
// 处理监听状态下收到的包
struct sock *nsk = tcp_v4_hnd_req(sk, skb);
if (!nsk)
goto discard;
// 如果是新连接,处理SYN包
if (nsk != sk) {
if (tcp_child_process(sk, nsk, skb)) {
// 子套接字处理成功,释放skb
__kfree_skb(skb);
}
return 0;
}
}
// ...
// 处理已建立连接的数据
return tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len);
}
在tcp_v4_hnd_req
函数中,会检查是否有匹配的请求套接字,如果没有,则创建一个新的请求套接字:
// 简化版的tcp_v4_hnd_req函数
static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
// ...
// 查找匹配的请求
struct request_sock *req = inet_csk_search_req(sk, &prev, th->source,
iph->saddr, iph->daddr);
if (req)
return tcp_check_req(sk, skb, req, prev, false);
// 没有匹配的请求,处理新的SYN
if (th->syn) {
// 处理SYN包,创建新的请求套接字
return tcp_v4_cookie_check(sk, skb);
}
// ...
}
当收到SYN包并创建新的请求套接字后,会调用tcp_v4_send_synack
函数发送SYN+ACK包:
// 简化版的tcp_v4_send_synack函数
static int tcp_v4_send_synack(struct sock *sk, struct dst_entry *dst,
struct request_sock *req,
struct tcp_fastopen_cookie *foc)
{
// ...
// 构建SYN+ACK包
skb = tcp_make_synack(sk, dst, req, foc);
if (!skb)
return -ENOMEM;
// 发送SYN+ACK包
err = ip_build_and_send_pkt(skb, sk, req->rsk_ops->saddr(req),
req->rsk_ops->daddr(req), NULL);
// ...
return err;
}
在tcp_make_synack
函数中,会构建SYN+ACK包的TCP头部:
// 简化版的tcp_make_synack函数
struct sk_buff *tcp_make_synack(struct sock *sk, struct dst_entry *dst,
struct request_sock *req,
struct tcp_fastopen_cookie *foc)
{
// ...
// 构建TCP头部
th = tcp_hdr(skb);
th->source = htons(ireq->ir_num);
th->dest = ireq->ir_rmt_port;
th->seq = htonl(tcp_rsk(req)->snt_isn);
th->ack_seq = htonl(tcp_rsk(req)->rcv_nxt);
// 设置SYN和ACK标志位
th->syn = 1;
th->ack = 1;
// ...
return skb;
}
客户端收到服务器的SYN+ACK包后,会调用tcp_rcv_state_process
函数处理:
// 简化版的tcp_rcv_state_process函数
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
// ...
// 根据套接字状态处理
switch (sk->sk_state) {
case TCP_SYN_SENT:
// 处理SYN_SENT状态下收到的包
queued = tcp_rcv_synsent_state_process(sk, skb, th, len);
// ...
break;
// 其他状态的处理
// ...
}
// ...
}
在tcp_rcv_synsent_state_process
函数中,会验证SYN+ACK包并发送ACK包:
// 简化版的tcp_rcv_synsent_state_process函数
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
// ...
// 验证SYN+ACK包
if (th->ack) {
// 检查确认号
if (!tcp_ack(sk, skb, FLAG_SLOWPATH))
return 1;
}
// 检查SYN标志位
if (th->syn) {
// 处理SYN+ACK
tcp_rcv_synrecv_state_fastpath(sk, skb, th);
// 发送ACK包
tcp_send_ack(sk);
// 设置ESTABLISHED状态
tcp_set_state(sk, TCP_ESTABLISHED);
// ...
}
// ...
}
在tcp_send_ack
函数中,会构建并发送ACK包:
// 简化版的tcp_send_ack函数
void tcp_send_ack(struct sock *sk)
{
// ...
// 构建ACK包
buff = alloc_skb(MAX_TCP_HEADER, sk_gfp_atomic(sk, GFP_ATOMIC));
if (!buff)
return;
// 设置TCP头部
skb_reserve(buff, MAX_TCP_HEADER);
tcp_init_nondata_skb(buff, tp->snd_una, TCPHDR_ACK);
// 发送ACK包
tcp_transmit_skb(sk, buff, 0, sk_gfp_atomic(sk, GFP_ATOMIC));
// ...
}
TCP三次握手的设计有其深刻的考虑,主要解决以下问题:
三次握手使得通信双方能够同步各自的初始序列号(ISN),这是TCP可靠传输的基础。序列号的同步确保了:
如果网络中存在延迟的、过期的连接请求,三次握手可以有效防止这些请求干扰新的连接。假设一个场景:
在这种情况下,如果只有两次握手,服务器会认为这是一个新的连接请求,并分配资源。但由于客户端已经不再期望这个连接,这会导致服务器资源的浪费。
有了三次握手,服务器发送SYN+ACK后,客户端不会回应最后的ACK,服务器会在超时后释放资源,从而避免了资源浪费。
三次握手确保了通信双方都具备发送和接收数据的能力:
只有在双方都确认了对方的收发能力后,连接才被视为完全建立。
三次握手过程中,双方可以协商多种连接参数,如:
这些参数的协商使得TCP连接能够适应不同的网络环境和应用需求。
SYN洪泛攻击是一种常见的拒绝服务攻击,攻击者发送大量的SYN包但不完成三次握手,导致服务器的半连接队列被填满,无法处理正常的连接请求。
防御措施包括:
传统的三次握手需要1.5个往返时间(RTT)才能完成,在高延迟网络中可能导致明显的延迟。为了减少这种延迟,TCP引入了一些优化技术:
当服务器收到大量的SYN包时,半连接队列可能会溢出,导致新的连接请求被丢弃。解决方法包括:
net.ipv4.tcp_max_syn_backlog
参数net.ipv4.tcp_syncookies=1
net.ipv4.tcp_synack_retries
控制重传次数当服务器完成三次握手但应用程序未及时接受连接时,全连接队列可能会溢出。解决方法包括:
net.core.somaxconn
和listen()
函数的backlog参数TCP三次握手是TCP协议的基础机制之一,它通过精心设计的交互过程,确保了连接的可靠建立。理解三次握手的原理和实现,对于网络编程、协议优化和故障排除都有重要意义。
TCP四次挥手(Four-Way Handshake)是TCP连接终止过程中的关键机制,它确保了连接的优雅关闭,使得双方都能够完成数据传输并释放资源。四次挥手的过程精确而可靠,是TCP连接生命周期的最后阶段。
TCP四次挥手的基本过程如下:
第一次挥手(FIN):主动关闭方发送一个FIN(结束)包,表示已经没有数据要发送了,但仍然可以接收数据,并将连接状态设置为FIN_WAIT_1。
第二次挥手(ACK):被动关闭方收到FIN包后,发送一个ACK(确认)包,确认收到了FIN包,并将连接状态设置为CLOSE_WAIT。主动关闭方收到ACK后,将连接状态设置为FIN_WAIT_2。
第三次挥手(FIN):被动关闭方完成所有数据发送后,发送一个FIN包,表示也没有数据要发送了,并将连接状态设置为LAST_ACK。
第四次挥手(ACK):主动关闭方收到FIN包后,发送一个ACK包,确认收到了FIN包,并将连接状态设置为TIME_WAIT。被动关闭方收到ACK后,将连接状态设置为CLOSED。主动关闭方在TIME_WAIT状态等待2MSL(最大报文生存时间)后,也将连接状态设置为CLOSED。
这个过程可以用下图表示:
主动关闭方 被动关闭方
| |
| FIN |
|------------------------------------->|
| seq=u, ack=v |
FIN_WAIT_1| |CLOSE_WAIT
| ACK |
|<-------------------------------------|
| seq=v, ack=u+1 |
FIN_WAIT_2| |
| |
| FIN |
|<-------------------------------------|
| seq=w, ack=u+1 |
TIME_WAIT | |LAST_ACK
| ACK |
|------------------------------------->|
| seq=u+1, ack=w+1 |
| |CLOSED
| |
| (等待2MSL) |
| |
CLOSED | |
当应用程序调用close()
或shutdown()
函数时,会触发TCP协议栈发送FIN包。FIN包的特点是:
主动关闭方发送FIN包后,进入FIN_WAIT_1状态,表示自己已经完成了数据发送,但仍然可以接收数据。
被动关闭方收到FIN包后,会执行以下操作:
被动关闭方发送ACK包后,进入CLOSE_WAIT状态,表示自己已经知道对方不再发送数据,但自己还可能有数据要发送。主动关闭方收到ACK包后,进入FIN_WAIT_2状态,等待被动关闭方的FIN包。
被动关闭方在完成所有数据发送后(可能是立即的,也可能是经过一段时间的处理),会发送FIN包,其特点是:
被动关闭方发送FIN包后,进入LAST_ACK状态,等待最后的确认。
主动关闭方收到FIN包后,会执行以下操作:
主动关闭方发送ACK包后,进入TIME_WAIT状态,等待2MSL(最大报文生存时间)后才会完全关闭连接。被动关闭方收到ACK包后,立即进入CLOSED状态,释放所有资源。
下面我们通过Linux内核源码来分析TCP四次挥手的实现。
当应用程序调用close()
函数时,最终会调用到tcp_close()
函数,该函数负责发送FIN包:
// 简化版的tcp_close函数
void tcp_close(struct sock *sk, long timeout)
{
// ...
// 检查是否有未发送的数据
if (sk->sk_send_head) {
// 还有数据未发送,设置LINGER定时器
if (timeout) {
// ...
} else {
// 没有设置超时,立即关闭
tcp_send_active_reset(sk, sk->sk_allocation);
goto adjudge_to_death;
}
}
// 没有未发送的数据,发送FIN包
if (tcp_close_state(sk)) {
tcp_send_fin(sk);
}
// ...
}
在tcp_send_fin
函数中,会构建并发送FIN包:
// 简化版的tcp_send_fin函数
void tcp_send_fin(struct sock *sk)
{
// ...
// 构建FIN包
buff = tcp_write_queue_tail(sk);
if (buff && skb_queue_len(&sk->sk_write_queue) == 1) {
// 如果写队列中只有一个包,直接在这个包上设置FIN标志
TCP_SKB_CB(buff)->tcp_flags |= TCPHDR_FIN;
TCP_SKB_CB(buff)->end_seq++;
tp->write_seq++;
} else {
// 创建一个新的FIN包
buff = alloc_skb_fclone(MAX_TCP_HEADER, sk->sk_allocation);
if (!buff)
return;
// 设置FIN标志
tcp_init_nondata_skb(buff, tp->write_seq, TCPHDR_ACK | TCPHDR_FIN);
tcp_queue_skb(sk, buff);
}
// 发送FIN包
tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
// ...
}
被动关闭方收到FIN包后,会调用tcp_rcv_state_process
函数处理:
// 简化版的tcp_rcv_state_process函数
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
// ...
// 根据套接字状态处理
switch (sk->sk_state) {
// 其他状态的处理
// ...
case TCP_ESTABLISHED:
// 处理ESTABLISHED状态下收到的包
if (th->fin) {
// 收到FIN包,进入CLOSE_WAIT状态
tcp_set_state(sk, TCP_CLOSE_WAIT);
// 通知应用程序
sk->sk_shutdown |= RCV_SHUTDOWN;
sock_set_flag(sk, SOCK_DONE);
// 发送ACK确认FIN
tcp_send_ack(sk);
// ...
}
break;
// 其他状态的处理
// ...
}
// ...
}
被动关闭方在应用程序调用close()
函数后,会发送FIN包,过程与主动关闭方类似:
// 简化版的tcp_close函数
void tcp_close(struct sock *sk, long timeout)
{
// ...
// 检查是否有未发送的数据
if (sk->sk_send_head) {
// 还有数据未发送,设置LINGER定时器
if (timeout) {
// ...
} else {
// 没有设置超时,立即关闭
tcp_send_active_reset(sk, sk->sk_allocation);
goto adjudge_to_death;
}
}
// 没有未发送的数据,发送FIN包
if (tcp_close_state(sk)) {
tcp_send_fin(sk);
}
// ...
}
不同的是,此时套接字的状态是CLOSE_WAIT,调用tcp_close_state
会将状态设置为LAST_ACK:
// 简化版的tcp_close_state函数
int tcp_close_state(struct sock *sk)
{
// ...
switch (sk->sk_state) {
case TCP_CLOSE_WAIT:
// 从CLOSE_WAIT状态转换为LAST_ACK状态
tcp_set_state(sk, TCP_LAST_ACK);
break;
// 其他状态的处理
// ...
}
// ...
}
主动关闭方收到FIN包后,会调用tcp_rcv_state_process
函数处理:
// 简化版的tcp_rcv_state_process函数
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
// ...
// 根据套接字状态处理
switch (sk->sk_state) {
// 其他状态的处理
// ...
case TCP_FIN_WAIT2:
// 处理FIN_WAIT2状态下收到的包
if (th->fin) {
// 收到FIN包,进入TIME_WAIT状态
tcp_time_wait(sk, TCP_TIME_WAIT, 0);
// 发送ACK确认FIN
tcp_send_ack(sk);
// ...
}
break;
// 其他状态的处理
// ...
}
// ...
}
在tcp_time_wait
函数中,会将套接字状态设置为TIME_WAIT,并启动2MSL定时器:
// 简化版的tcp_time_wait函数
void tcp_time_wait(struct sock *sk, int state, int timeo)
{
// ...
// 设置TIME_WAIT状态
tcp_set_state(sk, state);
// 启动2MSL定时器
inet_csk_reset_keepalive_timer(sk, TCP_TIMEWAIT_LEN);
// ...
}
TCP四次挥手的设计有其深刻的考虑,主要解决以下问题:
四次挥手确保了双方都能够完成数据的发送,避免了数据丢失。具体来说:
这种设计使得即使一方决定关闭连接,另一方仍然有机会发送剩余的数据,确保了数据的完整传输。
TCP支持半关闭(Half-Close)状态,即一方关闭了发送通道,但仍然保持接收通道开放。四次挥手的设计使得半关闭状态能够被正确处理:
shutdown()
函数实现半关闭,只关闭发送通道或接收通道半关闭状态在某些应用场景中非常有用,如文件传输完成后,客户端可以关闭发送通道,但仍然保持接收通道开放,以接收服务器的确认或错误信息。
四次挥手中,主动关闭方在发送最后的ACK后会进入TIME_WAIT状态,并等待2MSL(最大报文生存时间)才会完全关闭连接。这个设计有两个重要作用:
确保最后的ACK能够到达:如果最后的ACK丢失,被动关闭方会重传FIN包,主动关闭方可以再次发送ACK。如果没有TIME_WAIT状态,主动关闭方可能已经关闭连接,无法响应重传的FIN包,导致被动关闭方无法正常关闭连接。
防止历史连接干扰:如果没有TIME_WAIT状态,可能会出现这样的情况:旧连接的延迟数据包在新连接建立后到达,被误认为是新连接的数据包。TIME_WAIT状态确保了旧连接的所有数据包都已经从网络中消失,不会干扰新的连接。
四次挥手确保了连接资源的正确释放,避免了资源泄漏:
这种设计确保了连接资源的有序释放,避免了资源泄漏和不必要的资源占用。
在高并发服务器中,可能会出现大量的TIME_WAIT状态的连接,占用系统资源。解决方法包括:
net.ipv4.tcp_tw_reuse=1
,允许新连接复用TIME_WAIT状态的端口net.ipv4.tcp_fin_timeout
参数(注意,这不会改变2MSL的时间,但会影响FIN_WAIT2状态的超时时间)close()
函数立即发送RST包而不是FIN包,跳过四次挥手过程(注意,这可能导致数据丢失)如果主动关闭方在发送FIN包并收到ACK后,由于某种原因(如被动关闭方崩溃)没有收到被动关闭方的FIN包,就会一直停留在FIN_WAIT2状态。解决方法包括:
net.ipv4.tcp_fin_timeout
参数,控制FIN_WAIT2状态的最长时间SO_KEEPALIVE
选项,定期检测连接是否仍然有效如果被动关闭方收到FIN包后,应用程序没有及时调用close()
函数,就会一直停留在CLOSE_WAIT状态。这通常是应用程序的问题,解决方法包括:
在某些情况下,TCP连接可能会被重置,而不是通过四次挥手正常关闭。常见的原因包括:
close()
函数发送RST包连接重置会导致缓冲区中的数据丢失,应该尽量避免。正常情况下,应该通过四次挥手正常关闭连接,确保数据的完整传输。
TCP四次挥手是TCP连接生命周期的最后阶段,它通过精心设计的交互过程,确保了连接的优雅关闭。理解四次挥手的原理和实现,对于网络编程、协议优化和故障排除都有重要意义。
TCP状态转换图是理解TCP连接生命周期的重要工具,它描述了TCP连接在不同事件下的状态变化。TCP状态转换图不仅是理论上的概念模型,也是TCP协议实现的基础。
TCP连接在其生命周期中会经历多个状态,每个状态都有其特定的含义和行为。以下是TCP的11个标准状态:
除了这些标准状态外,还有一些特殊状态或子状态,如:
TCP状态转换图描述了TCP连接在不同事件下的状态变化。以下是一个简化的TCP状态转换图:
+---------+
| CLOSED |
+---------+
|
passive open | active open
------------- | -------------
create TCB | create TCB
------------- | -------------
| | |
V V V
+---------+ +---------+
| LISTEN | | SYN_SENT|
+---------+ +---------+
| |
rcv SYN | | rcv SYN+ACK
--------- | | -----------
send SYN+ACK| | send ACK
--------- | | -----------
V V
+---------+ +---------+
| SYN_RCVD| |ESTABLISHED|
+---------+ +---------+
| |
rcv ACK | | close
-------- | | -------
| | send FIN
| | -------
V V
+---------+ +---------+
|ESTABLISHED| |FIN_WAIT_1|
+---------+ +---------+
| |
close | | rcv ACK
------- | | -------
send FIN | |
------- | |
V V
+---------+ +---------+
|CLOSE_WAIT| |FIN_WAIT_2|
+---------+ +---------+
| |
close | | rcv FIN
------- | | -------
send FIN | | send ACK
------- | | -------
V V
+---------+ +---------+
| LAST_ACK| | TIME_WAIT|
+---------+ +---------+
| |
rcv ACK | | 2MSL timeout
-------- | | ------------
| |
V V
+---------+ +---------+
| CLOSED | | CLOSED |
+---------+ +---------+
这个图展示了TCP连接的主要状态转换路径,包括连接的建立、数据传输和连接的关闭。实际的TCP实现可能更复杂,包含更多的状态和转换路径。
在Linux内核中,TCP状态是通过枚举类型定义的:
// file: include/net/tcp_states.h
enum {
TCP_ESTABLISHED = 1,
TCP_SYN_SENT,
TCP_SYN_RECV,
TCP_FIN_WAIT1,
TCP_FIN_WAIT2,
TCP_TIME_WAIT,
TCP_CLOSE,
TCP_CLOSE_WAIT,
TCP_LAST_ACK,
TCP_LISTEN,
TCP_CLOSING,
TCP_NEW_SYN_RECV,
TCP_MAX_STATES /* 最大状态数 */
};
TCP状态的转换是通过tcp_set_state
函数实现的:
// 简化版的tcp_set_state函数
void tcp_set_state(struct sock *sk, int state)
{
int oldstate = sk->sk_state;
// 更新状态
sk->sk_state = state;
// 处理状态变化的副作用
if (state == TCP_ESTABLISHED) {
// 连接建立,可能需要启动保活定时器
if (oldstate != TCP_ESTABLISHED)
tcp_init_xmit_timers(sk);
} else if (state == TCP_CLOSE) {
// 连接关闭,需要清理资源
if (oldstate == TCP_CLOSE_WAIT || oldstate == TCP_ESTABLISHED)
tcp_send_fin(sk);
} else if (state == TCP_CLOSE_WAIT) {
// 进入CLOSE_WAIT状态,需要通知应用程序
sk->sk_shutdown |= RCV_SHUTDOWN;
sock_set_flag(sk, SOCK_DONE);
}
// 更新统计信息
if (state == TCP_CLOSE)
inet_csk_destroy_sock(sk);
}
TCP状态的转换是由各种事件触发的,如收到特定类型的TCP段、应用程序调用特定的函数等。以下是一些关键的状态转换函数:
// 客户端调用connect()函数,进入SYN_SENT状态
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
// ...
tcp_set_state(sk, TCP_SYN_SENT);
// ...
}
// 服务器收到SYN包,进入SYN_RCVD状态
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
// ...
if (sk->sk_state == TCP_LISTEN) {
// ...
tcp_set_state(nsk, TCP_SYN_RECV);
// ...
}
// ...
}
// 客户端收到SYN+ACK包,进入ESTABLISHED状态
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
// ...
tcp_set_state(sk, TCP_ESTABLISHED);
// ...
}
// 服务器收到ACK包,进入ESTABLISHED状态
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
// ...
if (sk->sk_state == TCP_SYN_RECV) {
// ...
tcp_set_state(sk, TCP_ESTABLISHED);
// ...
}
// ...
}
// 主动关闭方调用close()函数,进入FIN_WAIT_1状态
int tcp_close_state(struct sock *sk)
{
// ...
switch (sk->sk_state) {
case TCP_ESTABLISHED:
// ...
tcp_set_state(sk, TCP_FIN_WAIT1);
break;
// ...
}
// ...
}
// 被动关闭方收到FIN包,进入CLOSE_WAIT状态
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
// ...
if (sk->sk_state == TCP_ESTABLISHED) {
if (th->fin) {
// ...
tcp_set_state(sk, TCP_CLOSE_WAIT);
// ...
}
}
// ...
}
// 主动关闭方收到ACK包,进入FIN_WAIT_2状态
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
// ...
if (sk->sk_state == TCP_FIN_WAIT1) {
if (th->ack) {
// ...
tcp_set_state(sk, TCP_FIN_WAIT2);
// ...
}
}
// ...
}
// 被动关闭方调用close()函数,进入LAST_ACK状态
int tcp_close_state(struct sock *sk)
{
// ...
switch (sk->sk_state) {
case TCP_CLOSE_WAIT:
// ...
tcp_set_state(sk, TCP_LAST_ACK);
break;
// ...
}
// ...
}
// 主动关闭方收到FIN包,进入TIME_WAIT状态
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
// ...
if (sk->sk_state == TCP_FIN_WAIT2) {
if (th->fin) {
// ...
tcp_time_wait(sk, TCP_TIME_WAIT, 0);
// ...
}
}
// ...
}
// 被动关闭方收到ACK包,进入CLOSED状态
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
// ...
if (sk->sk_state == TCP_LAST_ACK) {
if (th->ack) {
// ...
tcp_set_state(sk, TCP_CLOSE);
// ...
}
}
// ...
}
理解TCP状态转换图对于网络编程、协议分析和故障排除都非常重要。以下是一些实际应用场景:
在使用套接字API进行网络编程时,了解TCP状态转换有助于理解和处理各种网络事件:
connect()
、listen()
和accept()
函数如何影响TCP状态close()
和shutdown()
函数如何影响TCP状态,以及如何处理半关闭状态使用netstat
、ss
等工具或Wireshark等网络分析工具时,了解TCP状态转换有助于分析网络通信过程:
netstat -ant
或ss -ant
命令查看当前系统中各TCP连接的状态在排查网络问题时,了解TCP状态转换有助于定位和解决问题:
在高并发服务器中,可能会出现大量的TIME_WAIT状态的连接,占用系统资源。解决方法包括:
net.ipv4.tcp_tw_reuse=1
,允许新连接复用TIME_WAIT状态的端口net.ipv4.tcp_tw_recycle=1
,加速TIME_WAIT状态的回收(注意,这个选项在Linux 4.12后被移除,因为它可能导致NAT环境下的连接问题)net.ipv4.tcp_fin_timeout
参数(注意,这不会改变2MSL的时间,但会影响FIN_WAIT2状态的超时时间)大量CLOSE_WAIT状态通常表示应用程序没有正确关闭连接。解决方法包括:
SO_LINGER
选项:在特定场景下,可以通过设置SO_LINGER
选项为0,使close()
函数立即发送RST包而不是FIN包,跳过四次挥手过程(注意,这可能导致数据丢失)大量SYN_RCVD状态可能表示SYN洪泛攻击或网络连接问题。解决方法包括:
net.ipv4.tcp_syncookies=1
,在收到SYN包时不立即分配资源,防止SYN洪泛攻击net.ipv4.tcp_max_syn_backlog
参数,增加半连接队列的容量net.ipv4.tcp_synack_retries
参数,减少SYN+ACK的重传次数大量FIN_WAIT2状态可能表示网络连接问题或应用程序行为异常。解决方法包括:
net.ipv4.tcp_fin_timeout
参数,控制FIN_WAIT2状态的最长时间SO_KEEPALIVE
选项,定期检测连接是否仍然有效TCP状态转换图是理解TCP协议行为的重要工具,它描述了TCP连接在不同事件下的状态变化。深入理解TCP状态转换图对于网络编程、协议分析和故障排除都有重要意义。