文章目录
- 1. 前言
- 2. 背景
- 3. TCP连接的建立和断开
- 3.1 TCP协议状态机
- 3.2 TCP的三握四挥
- 3.2.1 TCP 连接建立的三次握手过程分析
- 3.2.1.1 服务端和客户端套接字的创建
- 3.2.1.2 服务端进入 LISTEN 状态
- 3.2.1.3 服务端 应用程序 用 accept() 取出 已建立好 的 客户端连接
- 3.2.1.4 客户端 向 服务端 发送 SYN 连接请求,进入 SYN-SENT 态
- 3.2.1.5 服务端 收取 客户端 SYN,然后回复 客户端 SYN + ACK,进入 SYN-RECEIVED 态
- 3.2.1.6 客户端 收取 服务端 SYN + ACK,回复 服务端 ACK,进入 ESTABLISHED 态
- 3.2.1.7 服务端 收取 客户端 ACK,进入 ESTABLISHED 态,完成 三次握手
- 3.2.1.8 TCP 三次握手 过程小结
- 3.2.2 TCP 连接断开的四次挥手过程分析
- 4. 抓包三握四挥过程示例
- 5. 参考资料
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 背景
本文基于 linux-4.14.132
内核代码进行分析。
3. TCP连接的建立和断开
3.1 TCP协议状态机
3.2 TCP的三握四挥
在后面的分析中,我们将始终参考 3.1,3.2
两小节中的状态图。
3.2.1 TCP 连接建立的三次握手过程分析
3.2.1.1 服务端和客户端套接字的创建
通过 socket()
系统调用创建套接字后,套接字初始状态为 CLOSED 状态(即 TCP_CLOSE)
。来看代码实现细节:
/* 应用层 通过系统调用 sys_socket() 创建套接字 */
server_fd = socket(AF_INET, SOCK_STREAM, 0); // 服务端
remote_fd = socket(AF_INET, SOCK_STREAM, 0); // 客户端
/* 内核空间:初始创建时,套接字为 CLOSED 状态(即 TCP_CLOSE) */
sys_socket(AF_INET, SOCK_STREAM, 0) // net/socket.c
sock_create(family, type, protocol, &sock)
__sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
sock = sock_alloc();
sock->type = type; // sock->type = SOCK_STREAM;
...
pf = rcu_dereference(net_families[family]); // 获取协议簇接口
...
// 进入协议簇(family)的套接字创建过程
pf->create(net, sock, protocol, kern) = inet_create() // net/ipv4/af_inet.c
struct sock *sk;
sock->state = SS_UNCONNECTED; /* socket 初始为[未连接状态 (SS_UNCONNECTED)] */
...
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
if (protocol == answer->protocol) { /* 显式指定了 protocol */
...
} else { /* 非显式指定 protocol */
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) { /* protocol == 0 意味着创建各 @type 下缺省协议的套接字 */
protocol = answer->protocol;
break;
}
...
}
...
}
...
/* 设定套接字对应协议接口 */
sock->ops = answer->ops; /* 设定套接字对应协议接口: &inet_stream_ops */
answer_prot = answer->prot; /* &tcp_prot */
...
/* 创建套接字[网络层管理数据]对象 */
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
struct sock *sk;
sk = sk_prot_alloc(prot, priority | __GFP_ZERO, family);
if (sk) {
sk->sk_family = family; // sk->sk_family = PF_INET;
sk->sk_prot = sk->sk_prot_creator = prot; /* &tcp_prot */
...
}
return sk;
...
/* 初始化套接字[网络层管理数据]: 如 type, 状态等 (TCP_CLOSE) */
sock_init_data(sock, sk);
...
//sk->sk_rcvbuf = sysctl_rmem_default;
//sk->sk_sndbuf = sysctl_wmem_default;
//sk->sk_state = TCP_CLOSE; /* 设定套接字初始状态为 CLOSE */
sk_set_socket(sk, sock); /* 绑定网络层管理数据到 socket */
sk_tx_queue_clear(sk);
sk->sk_socket = sock;
...
if (sock) {
sk->sk_type = sock->type; // sk->sk_type = SOCK_STREAM;
sk->sk_wq = sock->wq;
sock->sk = sk; /* 设定套接字的网络层管理数据对象 */
...
}
...
sk->sk_state_change = sock_def_wakeup;
sk->sk_data_ready = sock_def_readable;
//sk->sk_write_space = sock_def_write_space;
...
sk->sk_destruct = inet_sock_destruct;
sk->sk_protocol = protocol; // IPPROTO_TCP
sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;
...
/*
* 前面完成的是 IPv4 协议簇套接字公共初始化。
* 这里是 IPv4 协议簇下的子类型 @type 和 子协议 @protocol 套接字的特定初始化。
* 套接字的初始化是一个个层层递进的过程,有点类似于 C++ 子类对象的构建过程:
* 先调用父类的构造函数,然后在逐级调用子类的构造函数。
*
* 这里是 TCP 类型(IPPROTO_TCP) 套接字 初始化。
*/
if (sk->sk_prot->init) {
err = sk->sk_prot->init(sk); /* tcp_v4_init_sock() */
...
}
...
/* TCP 类型套接字初始化 */
tcp_v4_init_sock() // net/ipv4/tcp_ipv4.c
struct inet_connection_sock *icsk = inet_csk(sk);
/* TCP 套接字初始化 */
tcp_init_sock(sk);
...
sk->sk_state = TCP_CLOSE; /* TCP 套接字创建时初始状态为 TCP_CLOSE */
sk->sk_write_space = sk_stream_write_space;
...
sk->sk_sndbuf = sysctl_tcp_wmem[1];
sk->sk_rcvbuf = sysctl_tcp_rmem[1];
...
/* 设定 IPv4 TCP 套接字操作接口 */
icsk->icsk_af_ops = &ipv4_specific;
3.2.1.2 服务端进入 LISTEN 状态
调用 listen()
后,服务端监听套接字
由 CLOSED 状态进入 LISTEN :CLOSED => LISTEN
。
// 用户空间
struct sockaddr_in server_addr;
int backlog = 8;
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888); // 服务端 端口号
server_addr.sin_addr.s_addr = inet_addr("192.168.1.123"); // 服务端 IP 地址
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
listen(server_fd, backlog); // 服务端: CLOSED => LISTEN
// 内核空间
sys_listen(fd, backlog) // net/socket.c
struct socket *sock;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
...
err = sock->ops->listen(sock, backlog); /* inet_listen() */
...
}
inet_listen() // net/ipv4/af_inet.c
struct sock *sk = sock->sk;
unsigned char old_state;
err = -EINVAL;
// 处于未连接状态的、 SOCK_STREAM 类型套接字 才能监听
if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
goto out;
old_state = sk->sk_state;
/* 只有对 TCP_CLOSE 或 TCP_LISTEN 态套接字 listen 才是合法的 */
if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
goto out;
if (old_state != TCP_LISTEN) {
...
/*
* . accept 队列初始化
* . backlog 初始化
* . 套接字由 TCP_CLOSE 转为 TCP_LISTEN 态 (CLOSED => LISTEN)
*/
err = inet_csk_listen_start(sk, backlog);
if (err)
goto out;
}
sk->sk_max_ack_backlog = backlog;
err = 0;
out:
return err;
inet_csk_listen_start() // net/ipv4/inet_connection_sock.c
struct inet_connection_sock *icsk = inet_csk(sk);
reqsk_queue_alloc(&icsk->icsk_accept_queue); // 创建和初始化 accept 队列(全连接队列)
sk->sk_max_ack_backlog = backlog;
sk->sk_ack_backlog = 0;
inet_csk_delack_init(sk);
sk_state_store(sk, TCP_LISTEN); // 由 TCP_CLOSE 转为 TCP_LISTEN 态 (CLOSED => LISTEN)
smp_store_release(&sk->sk_state, newstate);
...
3.2.1.3 服务端 应用程序 用 accept() 取出 已建立好 的 客户端连接
服务端 应用程序
用 accept()
取出 位于 服务端监听套接字
accept 队列(全连接队列)
中、已建立好(已完成三次握手
) 的 客户端连接套接字信息。如果服务端监听套接字
accept 队列(全连接队列)
当前没有已建立好的客户端连接,则陷入睡眠等待,直到有新建立好(已完成三次握手
)的客户端连接到来后被唤醒。
client_fd = accept(server_fd, NULL, NULL);
sys_accept(server_fd, NULL, NULL) // net/socket.c
sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
struct socket *sock, *newsock;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
/* 为可能连接的客户端准备一个 sock 对象 */
newsock = sock_alloc(); /* (1) */
...
newsock->type = sock->type; // SOCK_STREAM
newsock->ops = sock->ops; // &inet_stream_ops
...
/*
* 为客户端 sock 分配一个 fd 。
*
* !!!
* 注意:
* @newfd 和 服务端监听套接字 @server_fd 不是同一个,@newfd 是 用来和 新的
* 客户端连接 通信 的 服务端 套接字,也即前面代码中 accept() 返回的 @client_fd 。
*/
newfd = get_unused_fd_flags(flags);
/* 为客户端 sock 分配一个文件对象 (struct file) */
newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
...
// 睡眠等待 完成握手过程、添加到 服务端监听套接字 @sock 的 accept 队列(全连接队列) 的 客户端连接
err = sock->ops->accept(sock, newsock, sock->file->f_flags, false); /* inet_accept() */
/* 将指代 已连接客户端 的 套接字 @newsock 的 fd 放入进程的 fd 表 */
fd_install(newfd, newfile);
err = newfd; /* 返回 服务端 和 客户端通信的套接字 句柄 到用户空间 */
...
out:
return err;
inet_accept(newsock, flags, sock->sk->sk_prot_creator->name) // net/ipv4/af_inet.c
struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err, kern); /* inet_csk_accept() */
inet_csk_accept(sk1, flags, &err, kern) // net/ipv4/inet_connection_sock.c
struct inet_connection_sock *icsk = inet_csk(sk);
struct request_sock_queue *queue = &icsk->icsk_accept_queue;
struct request_sock *req;
struct sock *newsk;
/* Find already established connection */
if (reqsk_queue_empty(queue)) { /* accept 队列(全连接队列) 为空,即 还没有 建立好的 客户端 连接 */
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
// 这里只讨论阻塞方式,不关心非阻塞方式的逻辑
/* If this is a non blocking socket don't sleep */
error = -EAGAIN;
if (!timeo)
goto out_err;
// 等待 直到有 建立好的 客户端连接
error = inet_csk_wait_for_connect(sk, timeo);
if (error)
goto out_err;
}
req = reqsk_queue_remove(queue, sk); // 从 accpet 队列(全连接队列) 中取出/移除一个建立好的连接
newsk = req->sk; // 返回 新建立的、 和 客户端通信的 套接字
...
...
newsock->state = SS_CONNECTED; // 等待到客户端连接后,套接字标记为已连接状态 SS_CONNECTED
// 睡眠等待 新建立好的客户端连接 (已完成三次握手)
inet_csk_wait_for_connect(sk, timeo) // net/ipv4/inet_connection_sock.c
struct inet_connection_sock *icsk = inet_csk(sk);
DEFINE_WAIT(wait);
int err;
for (;;) {
prepare_to_wait_exclusive(sk_sleep(sk), &wait,
TASK_INTERRUPTIBLE);
...
if (reqsk_queue_empty(&icsk->icsk_accept_queue)) // 还没有 建立好的 客户端连接,陷入睡眠等待
timeo = schedule_timeout(timeo);
...
if (!reqsk_queue_empty(&icsk->icsk_accept_queue)) // 有 建立好的 客户端连接了,结束等待
break;
...
if (signal_pending(current)) // 被信号中断
break;
err = -EAGAIN;
if (!timeo)
break;
}
finish_wait(sk_sleep(sk), &wait);
return err;
3.2.1.4 客户端 向 服务端 发送 SYN 连接请求,进入 SYN-SENT 态
客户端
通过 connect()
向 服务端 (监听套接字)
发起了 SYN
连接请求,然后自身进入 SYN-SENT 态睡眠等待服务端的 SYN + ACK。来看这一个过程的代码实现细节:
// 客户端
int remote_fd;
struct sockaddr_in server_addr;
remote_fd = socket(AF_INET, SOCK_STREAM, 0);
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT); // 服务端端口号
server_addr.sin_addr.s_addr = inet_addr("192.168.1.188"); // 假设服务端 IP 为 192.168.1.188
connect(remote_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr))); // 连接服务端
/*
* c1. 客户端 通过 connect() 向 服务端监听套接字 发送 SYN 连接请求。
*/
sys_connect(remote_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr))) // net/socket.c
struct socket *sock;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
...
/* @uservaddr: 客户端 想连接的 目标地址 */
err = move_addr_to_kernel(uservaddr, addrlen, &address);
...
err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, sock->file->f_flags); // inet_stream_connect()
inet_stream_connect() // net/ipv4/af_inet.c
__inet_stream_connect(sock, uaddr, addr_len, flags, 0);
switch (sock->state) {
...
// 发送 SYN 请求包到 服务端
case SS_UNCONNECTED:
...
err = sk->sk_prot->connect(sk, uaddr, addr_len); /* tcp_v4_connect() */
sock->state = SS_CONNECTING; /* 套接字标记为 正在连接状态 */
...
break;
}
tcp_v4_connect(sk, uaddr, addr_len) // net/ipv4/tcp_ipv4.c
// 一些路由等相关的其它处理
...
tcp_set_state(sk, TCP_SYN_SENT); /* 客户端套接字状态由 CLOSED 转为 SYN-SENT: CLOSED => SYN-SENT */
...
err = tcp_connect(sk); /* 发送 SYN 包 */
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *buff;
...
/* 为 SYN 包分配空间 */
buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);
...
tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN); /* 构建 SYN 包 */
...
/* 发送 SYN 包 */
err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
...
/*
* c2. 客户端 套接字 向 服务端监听套接字 发送 SYN 请求,在 SYN-SENT 态睡眠等待 服务端回复 SYN+ACK 。
*/
// 接前面的 inet_stream_connect() 调用 tcp_v4_connect() 之后的流程
inet_stream_connect() // net/ipv4/af_inet.c
...
switch (sock->state) {
// 发送 SYN 请求包到服务端
case SS_UNCONNECTED:
...
err = sk->sk_prot->connect(sk, uaddr, addr_len); /* tcp_v4_connect() */
sock->state = SS_CONNECTING; /* 套接字标记为 已连接状态 */
...
break;
}
...
if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
...
/*
* 已向服务端发送 SYN 包,陷入睡眠等待服务端回复 SYN + ACK.
* 在收到服务端的 SYN + ACK 后, 内核再回复 ACK 给服务端,
* 然后唤醒等待在此处的进程.
*/
if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
goto out;
...
}
...
sock->state = SS_CONNECTED; /* 到此, socket 进入已连接状态 (SS_CONNECTED) */
// 睡眠等待服务端回送 SYN + ACK
inet_wait_for_connect() // net/ipv4/af_inet.c
DEFINE_WAIT_FUNC(wait, woken_wake_function);
// sk_sleep()
// return &rcu_dereference_raw(sk->sk_wq)->wait;
add_wait_queue(sk_sleep(sk), &wait);
...
while ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
...
timeo = wait_woken(&wait, TASK_INTERRUPTIBLE, timeo); // 等待服务端回复: SYN + ACK
...
}
...
3.2.1.5 服务端 收取 客户端 SYN,然后回复 客户端 SYN + ACK,进入 SYN-RECEIVED 态
上一小节说到 客户端套接字
向 服务端监听套接字
发送 SYN
连接请求后,在 SYN-SENT
态睡眠等待 服务端监听套接字
回复 SYN + ACK
。接下来 服务端监听套接字
收取 客户端套接字
SYN
连接请求,为 SYN 连接请求 新建一个 记录客户端连接信息、初始为 SYN-RECEIVED 态 的 轻量套接字 struct request_sock
(见后面代码注释 /* (2) */
处),然后将其插入到 服务端监听套接字
的 SYN 队列(半连接队列)
,最后回复 客户端套接字
SYN + ACK
的细节。注意,服务端监听套接字
仍维持在 LISTEN
态,进入 SYN-RECEIVED
态的是为新连接建立的轻量套接字 struct request_sock
。
/*
* s1. 服务端监听套接字 收取 客户端 SYN,为 SYN 连接请求 建立轻量套接字,
* 并将 轻量套接字 插入 服务端监听套接字 的 SYN 队列(半连接队列),
* 最后 服务端监听套接字 回复 客户端 SYN + ACK.
*/
xxx_nic_interrput() // 从网卡数据接收中断入口开始
napi_gro_receive()
napi_skb_finish()
netif_receive_skb_internal()
__netif_receive_skb()
__netif_receive_skb_core()
pt_prev->func() = ip_rcv()
ip_rcv_finish()
dst_input()
ip_local_deliver()
ip_local_deliver_finish()
// 见后续分析
ipprot->handler() = tcp_v4_rcv()
// 接上面的分析
tcp_v4_rcv()
...
/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字: 服务端监听套接字 */
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
if (sk->sk_state == TCP_LISTEN) { /* 服务端监听套接字 处于 LISTEN 态 */
ret = tcp_v4_do_rcv(sk, skb); // 见后续分析
goto put_and_return;
}
...
// 接上面的分析
tcp_v4_do_rcv(sk, skb)
...
if (tcp_rcv_state_process(sk, skb)) { // 见后续分析
// 出错处理
...
}
return 0;
...
// 接上面的分析
tcp_rcv_state_process(sk, skb)
...
switch (sk->sk_state) {
...
case TCP_LISTEN:
if (th->syn) { /* @skb 为 SYN 报文 */
...
// 见后续分析
acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0; // tcp_v4_conn_request()
...
if (!acceptable) /* @sk 不接收 @skb, 回发给源头 RESET */
return 1;
/* @sk 正常接收 SYN @skb */
consume_skb(skb);
return 0;
}
...
}
// 接上面的分析
acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0; // tcp_v4_conn_request()
tcp_v4_conn_request(sk, skb) // net/ipv4/tcp_ipv4.c
// 见后续分析
return tcp_conn_request(&tcp_request_sock_ops,
&tcp_request_sock_ipv4_ops, sk, skb);
// 接上面的分析
tcp_conn_request(&tcp_request_sock_ops, // net/ipv4/tcp_input.c
&tcp_request_sock_ipv4_ops, sk, skb);
...
struct request_sock *req;
...
if (sk_acceptq_is_full(sk)) { // 套接字的 @sk accept 队列(全连接队列) 已满
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
...
/*
* 为连接请求, 分配 轻量套接字 struct request_sock (指代 server_fd 套接字),
* 将其先放到 服务端监听套机字 @sk 的 SYN 队列(半连接队列),然后在收到 客户端
* 对 服务端 SYN (SYN+ACK) 的 ACK 回复后,再从 服务端监听套机字 @sk 的 SYN 队列(半连接队列)
* 移动到 accept 队列(全连接队列)。
*/
req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie); /* (2) */
struct request_sock *req = reqsk_alloc(ops, sk_listener, attach_listener);
req = kmem_cache_alloc(ops->slab, GFP_ATOMIC | __GFP_NOWARN);
if (attach_listener) {
...
/*
* 设定 记录 客户端连接信息 的 轻量套接字 所属的 服务端监听套接字,
* 即 客户端 的 SYN 请求是向 服务端监听套接字 @sk_listener 发起的。
*/
req->rsk_listener = sk_listener;
}
req->rsk_ops = ops; /* req->rsk_ops = &tcp_request_sock_ops */
req_to_sk(req)->sk_prot = sk_listener->sk_prot;
...
if (req) {
struct inet_request_sock *ireq = inet_rsk(req);
...
// 标记 服务端 记录客户端连接信息的、轻量级套接字(struct request_sock) 为 SYN-RECEIVED 态:
// 即 已经收到 客户端的 SYN 连接请求包。
// 注:TCP_NEW_SYN_RECV 是 高内核版本新引入的套接字状态,TCP_SYN_RECV 状态被 TFO 特性使用。
ireq->ireq_state = TCP_NEW_SYN_RECV;
...
ireq->ireq_family = sk_listener->sk_family;
}
tcp_rsk(req)->af_specific = af_ops; /* tcp_rsk(req)->af_specific = &tcp_request_sock_ipv4_ops */
...
af_ops->init_req(req, sk, skb); /* 设置轻量级套接字的 源、目的 IP: 同 @sk 的 源、目的 IP */
tcp_v4_init_req()
...
if (fastopen_sk) { /* TCP Fast Open(TFO) 特性 */
...
} else {
tcp_rsk(req)->tfo_listener = false;
if (!want_cookie)
/* 添加 记录客户端连接信息的 轻量套接字 @req 到 服务端监听套接字 @sk 的 SYN 队列(半连接队列) */
inet_csk_reqsk_queue_hash_add(sk, req, tcp_timeout_init((struct sock *)req));
reqsk_queue_hash_req(req, timeout); // 将 轻量套接字 插入到 SYN 队列(半连接队列)
inet_csk_reqsk_queue_added(sk);
reqsk_queue_added(&inet_csk(sk)->icsk_accept_queue);
atomic_inc(&queue->young);
atomic_inc(&queue->qlen); // SYN 队列(半连接队列) 长度 加 1
/* 服务端监听套接字 收到 客户端 SYN 后,回送 SYN + ACK */
af_ops->send_synack(sk, dst, &fl, req, &foc, // tcp_v4_send_synack()
!want_cookie ? TCP_SYNACK_NORMAL : TCP_SYNACK_COOKIE);
...
struct sk_buff *skb;
skb = tcp_make_synack(sk, dst, req, foc, synack_type);
if (skb) {
...
err = ip_build_and_send_pkt(skb, sk, ireq->ir_loc_addr,
ireq->ir_rmt_addr,
rcu_dereference(ireq->ireq_opt));
...
}
}
reqsk_put(req);
return 0;
3.2.1.6 客户端 收取 服务端 SYN + ACK,回复 服务端 ACK,进入 ESTABLISHED 态
上一小节中,服务端监听套接字
完成了收取 客户端 SYN
,建立插入连接请求轻量套接字,并回复 客户端套接字
以 SYN + ACK
的过程。接下来 客户端套接字
将收取 服务端
的 SYN + ACK
,并回复 服务端 的 SYN
以 ACK
,进入 ESTABLISHED
态,于是完成了 客户端 => 服务端 单向连接
的建立。来看这一过程代码实现细节:
// 从网卡数据接收中断入口开始
xxx_nic_interrput()
napi_gro_receive()
napi_skb_finish()
netif_receive_skb_internal()
__netif_receive_skb()
__netif_receive_skb_core()
pt_prev->func() = ip_rcv()
ip_rcv_finish()
dst_input()
ip_local_deliver()
ip_local_deliver_finish()
ipprot->handler() = tcp_v4_rcv()
tcp_v4_rcv() // net/ipv4/tcp_ipv4.c
struct sock *sk;
...
lookup:
/* @sk: 客户端套接字 */
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
ret = 0;
if (!sock_owned_by_user(sk)) {
ret = tcp_v4_do_rcv(sk, skb);
} else if (tcp_add_backlog(sk, skb)) {
...
}
...
tcp_v4_do_rcv(sk, skb)
...
if (tcp_rcv_state_process(sk, skb)) {
...
}
tcp_rcv_state_process(sk, skb)
...
switch (sk->sk_state) {
...
case TCP_SYN_SENT: /* 已往服务端发送了 SYN 的客户端处于 TCP_SYN_SENT (connect() 调用) */
...
queued = tcp_rcv_synsent_state_process(sk, skb, th); /* 收取服务端发送的 SYN + ACK */
...
return 0;
...
}
tcp_rcv_synsent_state_process(sk, skb, th)
...
if (th->ack) { // ACK 包标记
...
if (!th->syn) // 不是 (SYN + ACK)
goto discard_and_undo;
...
tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
tcp_ack(sk, skb, FLAG_SLOWPATH); // 处理服务端回复包中的 ACK
// 包序列号的一些处理
tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;
tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;
...
// MTU, MSS 处理
tcp_mtup_init(sk);
tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);
tcp_initialize_rcv_mss(sk);
...
tcp_finish_connect(sk, skb); // 连接建立完成:套接字 @sk 由 SYN-SENT 转为 ESTABLISHED
tcp_set_state(sk, TCP_ESTABLISHED);
...
sk_state_store(sk, state); // sk->state = TCP_ESTABLISHED
...
...
if (!sock_flag(sk, SOCK_DEAD)) {
/* 唤醒等待 server 端 SYN + ACK 的 connect() */
sk->sk_state_change(sk) = sock_def_wakeup(sk)
struct socket_wq *wq;
...
wq = rcu_dereference(sk->sk_wq);
if (skwq_has_sleeper(wq))
wake_up_interruptible_all(&wq->wait); // 唤醒 connect() 调用链中在 inet_wait_for_connect() 中等待连接建立完成的进程
...
sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
}
...
if (sk->sk_write_pending ||
icsk->icsk_accept_queue.rskq_defer_accept ||
icsk->icsk_ack.pingpong) {
...
} else {
tcp_send_ack(sk); /* 客户端 构建 + 发送 ACK 包给服务端 */
}
}
...
3.2.1.7 服务端 收取 客户端 ACK,进入 ESTABLISHED 态,完成 三次握手
上一小节中,客户端 => 服务端 的单向连接
已经建立完成。接下来 服务端
在 3.2.1.6
小节中代码注释 /* (2) */
处、为 客户端 SYN 连接请求
建立的 轻量套接字 struct request_sock
,将收取 客户端套接字
对 服务端 SYN 连接请求
回复的 ACK
,然后将 位于 服务端监听套接字 SYN 队列(半连接队列)
的 连接请求轻量套接字 struct request_sock
取出,用 轻量套接字
的数据信息、在 服务端
为该 轻量套接字
所指代的 客户端连接
建立新的、初始状态为 SYN-RECEIVED
的、完整的 TCP 套接字对象(struct sock)
,然后添加该 TCP 套接字对象
到 服务端监听套接字 的 accept 队列(全连接队列)
,同时将 TCP 套接字对象(struct sock)
的 状态更新为 ESTABLISHED
,此时如果发现有在 accept()
中 睡眠等待 新连接就绪的 进程,则唤醒它们获取代表新连接的 TCP 套接字对象
。进程调用 accept()
进入睡眠的过程参考 3.2.1.3
小节的分析。同时,来看这一过程代码实现细节:
/*
* s2. 服务端 收取 客户端 ACK,将位于 服务端监听套接字 SYN 队列(半连接队列)、代表 客户端连接 的
* 轻量套接字,用 轻量套接字 的数据信息,为 客户端连接 建立完成 TCP 套接字 struct sock,并
* 添加到 服务端监听套接字 的 accept 队列(全连接队列)。此时如果发现有在 accept() 中 睡眠等待
* 新连接就绪的 进程,则唤醒它们获取 代表新连接 TCP 套接字对象。
*/
xxx_nic_interrput() // 从网卡数据接收中断入口开始
...
tcp_v4_rcv()
...
lookup:
// 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字。
// @sk: 3.2.1.6 小节中代码注释 /* (2) */ 处 建立的 轻量套接字 struct request_sock
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
process:
...
/*
* 已经收到了客户端 SYN 并回应了 SYN + ACK 的 服务端 客户端连接轻量级套接字:
* . 服务端 客户端连接 轻量套接字 @sk / @req 处于 TCP_NEW_SYN_RECV 状态
* . 服务端 客户端连接 轻量套接字 @sk / @req 关联 的 服务端监听套接字
* @req->rsk_listener 处于 TCP_LISTEN 状态,即 @req 指代的客户端连接
* 是 向 服务端监听套接字 @req->rsk_listener 发起。
*/
if (sk->sk_state == TCP_NEW_SYN_RECV) {
struct request_sock *req = inet_reqsk(sk); // 服务端轻量级"代理"套接字
struct sock *nsk;
sk = req->rsk_listener;
...
if (unlikely(sk->sk_state != TCP_LISTEN)) {
inet_csk_reqsk_queue_drop_and_put(sk, req);
goto lookup;
}
...
if (!tcp_filter(sk, skb)) { /* 如果数据包没有被 eBPF 过滤掉 */
...
/*
* 通过 轻量套接字 @req 的信息,为 客户端新连接 建立 服务端的完整套接字:
* 建立 TCP 的套接字数据 (struct sock *nsk),并将 添加 到 服务端监听套接字
* @req->rsk_listener 的 accept 队列 (全连接队列)。
*/
nsk = tcp_check_req(sk, skb, req, false); // 见后续 s2.1 分析
}
...
if (nsk == sk) {
...
}
// 新连接初始化(MTU、缓冲等),然后唤醒在 accept() 调用中阻塞等待新连接的进程
// 见后续 s2.2 分析
else if (tcp_child_process(sk, nsk, skb)) {
...
} else {
sock_put(sk);
return 0;
}
...
}
// s2.1
// 为 客户端新连接 建立完整的 TCP 套接字(struct sock),并将其添加到 accept 队列(全连接队列)。
nsk = tcp_check_req(sk, skb, req, false); // net/ipv4/tcp_minisocks.c
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL, req, &own_req);
tcp_v4_syn_recv_sock() // net/ipv4/tcp_ipv4.c
struct sock *newsk;
...
newsk = tcp_create_openreq_child(sk, req, skb);
struct sock *newsk = inet_csk_clone_lock(sk, req, GFP_ATOMIC);
struct sock *newsk = sk_clone_lock(sk, priority);
if (newsk) {
...
newsk->sk_state = TCP_SYN_RECV;
...
}
...
...
...
/*
* . 将 客户端新连接 轻量套接字 从 服务端监听套接字 的 SYN 队列(半连接队列) 移除,
* . 将 新建的 客户端新连接 TCP 套接字(struct sock) 添加到 服务端监听套接字 的 accept 队列(全连接队列)
*/
return inet_csk_complete_hashdance(sk, child, req, own_req);
if (own_req) {
// 将 客户端新连接 轻量套接字 从 服务端监听套接字 的 SYN 队列(半连接队列) 移除
inet_csk_reqsk_queue_drop(sk, req); // 从 ehash 表删除
reqsk_queue_removed(&inet_csk(sk)->icsk_accept_queue, req);
if (req->num_timeout == 0)
atomic_dec(&queue->young);
atomic_dec(&queue->qlen); // SYN 队列(半连接队列) 长度 减 1
// 将 新建的 客户端新连接 TCP 套接字(struct sock),
// 添加到 服务端监听套接字 @sk 的 accept 队列(全连接队列).
if (inet_csk_reqsk_queue_add(sk, req, child))
return child;
}
...
// 接 s2.1 分析:
// 将 新建的 客户端新连接 TCP 套接字(struct sock),
// 添加到 服务端监听套接字 @sk 的 accept 队列(全连接队列).
inet_csk_reqsk_queue_add(sk, req, child)
struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;
...
if (unlikely(sk->sk_state != TCP_LISTEN)) {
...
} else {
req->sk = child;
req->dl_next = NULL;
if (queue->rskq_accept_head == NULL)
queue->rskq_accept_head = req;
else
queue->rskq_accept_tail->dl_next = req;
queue->rskq_accept_tail = req;
sk_acceptq_added(sk);
sk->sk_ack_backlog++; /* accept 队列(全连接队列) 长度 加 1 */
}
...
// s2.2 新连接初始化(MTU、缓冲等),然后唤醒在 accept() 调用中阻塞等待新连接的进程
tcp_child_process(sk, nsk, skb)
int state = child->sk_state;
...
if (!sock_owned_by_user(child)) {
ret = tcp_rcv_state_process(child, skb);
...
switch (sk->sk_state) {
case TCP_SYN_RECV:
...
tcp_set_state(sk, TCP_ESTABLISHED); /* 与客户端的通信的套接字装换为已连接状态 ESTABLISHED */
...
tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
...
tcp_initialize_rcv_mss(sk);
...
break;
}
...
/* Wakeup parent, send SIGIO */
if (state == TCP_SYN_RECV && child->sk_state != state)
/* 唤醒在 accept() 中等待连接的进程 */
parent->sk_data_ready(parent) = sock_def_readable()
...
wq = rcu_dereference(sk->sk_wq);
if (skwq_has_sleeper(wq))
wake_up_interruptible_sync_poll(&wq->wait, POLLIN | POLLPRI |
POLLRDNORM | POLLRDBAND);
sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
...
} else {
...
}
...
return 0;
到此,3.2
小节中图示的 TCP 连接建立的 三次握手 过程
已经全部分析完成。当然,这里分析的只是 TCP 连接建立的几种可能序列当中的一个,对其它的建立过程序列感兴趣的读者可以自行分析源码。另外,对于 SYN Cookies ,TFO(TCP Fast Open),端口重用
等特性,本文也未加讨论。
3.2.1.8 TCP 三次握手 过程小结
调用 listen()
后,服务端 监听套接字
(即前面代码中的 server_fd
指代的套接字) 处于 LISTEN
状态,客户端套接字
处于 CLOSED
状态。客户端通过 connect()
调用,向 服务端 监听套接字
发送 SYN
请求,然后自身进入到 SYN-SENT
状态等待服务端对 SYN 请求
的 SYN + ACK
回复,客户端在收到服务端的 SYN + ACK
回复后,也对服务端的 SYN
回复一个 ACK
,之后自身进入到 ESTABLISHED
状态,并从 connect()
调用返回;而处于 LISTEN
状态的 服务端 监听套接字
收到客户端的 SYN
请求后,先是新建一个记录客户端连接信息的、初始为 SYN-RECEIVED 状态 的 轻量连接请求套接字(struct request_sock)
,接着将该轻量套接字添加到 服务端 监听套接字
的 SYN 队列(半连接队列)
,回复 SYN + ACK
给客户端,然后服务端等待客户端回复 ACK
,当客户端的 ACK
到来后,将前面新建的、记录客户端连接信息的 轻量套接字(struct request_sock)
,从服务端 监听套接字
的 SYN 队列(半连接队列)
移出,并以该 轻量套接字 为模板,为客户端连接建立一个完成 TCP 套接字(struct sock),并将其添加到 服务端监听套接字 的 accept 队列(全连接队列)
,并唤醒可能阻塞在 accept() 等待连接到来的进程
。细心的读者注意到了吧,服务端 监听套接字 server_fd
的状态不会发生变化,仍然处于 LISTEN
状态,这从 3.1,3.2
小节中的状态转换图是看不出来的,而且还会误解是监听套接字 server_fd
的状态发生了变化。服务端 真正变为 ESTABLISHED
状态的套接字是 accept()
调用中注释 /* (1) */
处创建的 newsock
,这个后面再做分析。
TCP 连接的建立是一个双向的过程,不管是服务端还是客户端,都会向对端通过 SYN
发起连接请求,而对方也会在收到 SYN
后回应一个 ACK
,这样一个双向连接就建立好了。以这样的方式建立连接的基本理由,是因为 TCP 是全双工
通信。从后面的章节也可以看到,拆除连接(四次挥手) 也是一个双向拆除的过程。
另外,连接建立的过程,是由内核协议栈完成的。细心的童鞋会发现,在调用 listen()
后,即使服务端不调用 accept()
,客户端照样可以通过 connect()
建立连接,只是这样的连接,无法正常和服务端进行数据通信。这是因为 listen()
已经建立好了 accept 队列
,当客户端发起连接时,内核协议栈会把建立好的连接信息放入 accept 队列
;而 accept() 只是从该队列中取出建立好的连接信息,它本身并不参与连接的建立过程
。用 netstat
观察,可以发现这些 TCP 连接没有进程名信息
,取而代之的是 --
。这样的现象,可能会让初接触 TCP 编程的童鞋大吃一惊。
3.2.2 TCP 连接断开的四次挥手过程分析
当我们的通信完毕,可以通过调用 close()
关闭连接。和连接的建立一样,连接的关闭过程也存在多种可能的序列,本文以 3.2
小节中的连接关闭过程为例来进行分析。
3.2.2.1 客户端通过 close() 向服务端发送 FIN,进入 FIN-WAIT-1 状态
// 客户端发起本端的连接断开请求
close(remote_fd);
sys_close() // fs/open.c
__close_fd(current->files, fd)
filp_close(file, files)
// 中间过程有点小复杂,不是这里关注的重点
...
sock_close()
sock_close() // net/socket.c
__sock_release(SOCKET_I(inode), inode)
if (sock->ops) {
...
sock->ops->release(sock) = inet_release() // net/ipv4/af_inet.c
struct sock *sk = sock->sk;
if (sk) {
...
sk->sk_prot->close(sk, timeout) = tcp_close()
}
return 0;
...
}
tcp_close(sk, timeout) // net/ipv4/tcp.c
...
sk->sk_shutdown = SHUTDOWN_MASK;
...
/*
* 关闭连接时,需处理接收缓冲里还没有被应用层读取的数据,
* 我们假定关闭连接时,客户端应用已经拿走了接收缓冲里的
* 所有数据。
* 对于客户端套接字缓冲还有未读取数据的情形,读者可自行分析。
*/
...
sk_mem_reclaim(sk);
if (unlikely(tcp_sk(sk)->repair)) {
...
} else if (data_was_unread) {
...
} else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {
...
} else if (tcp_close_state(sk)) { // 套接字由 ESTABLISHED 态转为 FIN-WAIT-1
tcp_send_fin(sk); // 向服务端发送 FIN 包
}
...
3.2.2.2 服务端收取客户端 FIN,回以 ACK,进入 CLOSE-WAIT 状态
客户端发送 FIN
包给服务端,服务端收到客户端的 FIN
包后,回应以一个 ACK
:
xxx_nic_interrput()
...
tcp_v4_rcv()
...
lookup:
/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字 */
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
process:
...
if (!sock_owned_by_user(sk)) {
ret = tcp_v4_do_rcv(sk, skb);
...
if (sk->sk_state == TCP_ESTABLISHED) {
...
tcp_rcv_established(sk, skb, tcp_hdr(skb)); // net/ipv4/tcp_input.c
...
tcp_send_ack(sk); // 回应 FIN 一个 ACK
...
tcp_data_queue(sk, skb);
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
...
// 服务端收到客户端的 FIN ,除了回一个 ACK 外,
// 还要针对 FIN 包做一些和客户端连接的套接字的特定处理:
// 状态由 ESTABLISHED 转为 CLOSE-WAIT 等等...
if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
tcp_fin(sk);
sk->sk_shutdown |= RCV_SHUTDOWN;
sock_set_flag(sk, SOCK_DONE);
switch (sk->sk_state) {
case TCP_SYN_RECV:
case TCP_ESTABLISHED:
// ESTABLISHED => CLOSE-WAIT
tcp_set_state(sk, TCP_CLOSE_WAIT);
inet_csk(sk)->icsk_ack.pingpong = 1;
break;
...
}
...
...
}
...
return 0;
}
} else if (...) {
...
}
3.2.2.3 客户端收取服务端对 FIN 的回应 ACK,进入 FIN-WAIT-2 状态
客户端在调用 close()
向服务端发送 FIN
包后,当前处于 FIN-WAIT-1
状态(如果没有设置SOCK_LINGER
,close()
调用也已经返回);服务端对客户端的 FIN
包回应了一个 ACK
,自身进入 CLOSE-WAIT
状态;客户端收到这个 ACK
后,进入 FIN-WAIT-2
状态,这时从服务端向客户端发送数据的通道就已经关闭了。看客户端处理 FIN
的 ACK
包后,进入 FIN-WAIT-2
状态这一过程的代码实现细节:
xxx_nic_interrput()
...
tcp_v4_rcv()
...
lookup:
/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字 */
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
process:
...
if (!sock_owned_by_user(sk)) {
ret = tcp_v4_do_rcv(sk, skb);
...
if (tcp_rcv_state_process(sk, skb)) {
rsk = sk;
goto reset;
}
return 0;
...
} else if (tcp_add_backlog(sk, skb)) {
...
}
...
tcp_rcv_state_process(sk, skb)
...
switch (sk->sk_state) {
...
case TCP_FIN_WAIT1: {
...
tcp_set_state(sk, TCP_FIN_WAIT2); /* 客户端: FIN-WAIT-1 => FIN-WAIT-2 */
sk->sk_shutdown |= SEND_SHUTDOWN;
...
}
...
}
...
3.2.2.4 服务端通过 close() 向客户端发 FIN,进入 LAST-ACK 状态
此时,TCP 连接已经处于半关闭状态(客户端接收数据通道已经关闭)
。同样的,在服务端不再想接收客户端的数据时,调用 close()
向客户端发送 FIN
包,然后服务端套接字由 CLOSE-WAIT
进入 LAST-ACK
状态:
// 关闭服务端和客户端通信的套接字,注意,这里关闭的不是 server_fd
close(client_fd);
sys_close() // fs/open.c
...
sock_close() // net/socket.c
__sock_release(SOCKET_I(inode), inode)
if (sock->ops) {
...
sock->ops->release(sock) = inet_release() // net/ipv4/af_inet.c
struct sock *sk = sock->sk;
if (sk) {
...
sk->sk_prot->close(sk, timeout) = tcp_close()
}
return 0;
...
}
tcp_close(sk, timeout) // net/ipv4/tcp.c
...
sk->sk_shutdown = SHUTDOWN_MASK;
...
sk_mem_reclaim(sk);
...
if (unlikely(tcp_sk(sk)->repair)) {
...
} else if (data_was_unread) {
...
} else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {
...
} else if (tcp_close_state(sk)) { // 套接字由 CLOSE-WAIT 态转为 LAST-ACK
tcp_send_fin(sk); // 向客户端发送 FIN 包
}
...
3.2.2.5 客户端收取服务端 FIN,回以 ACK,进入 TIME-WAIT,超时后进入 CLOSED 终态
客户端当前处于 FIN-WAIT-2
状态,收到服务端的 FIN
包后,回复服务端一个 ACK
,然后自身进入 TIME-WAIT
状态:
xxx_nic_interrput()
...
tcp_v4_rcv()
...
lookup:
/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字 */
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
process:
...
if (!sock_owned_by_user(sk)) {
ret = tcp_v4_do_rcv(sk, skb);
...
if (tcp_rcv_state_process(sk, skb)) {
rsk = sk;
goto reset;
}
return 0;
...
} else if (tcp_add_backlog(sk, skb)) {
...
}
...
tcp_rcv_state_process(sk, skb)
...
/* step 7: process the segment text */
switch (sk->sk_state) {
...
case TCP_FIN_WAIT1:
case TCP_FIN_WAIT2: // 客户端当前处于 FIN-WAIT-2 状态
...
/* Fall through */
case TCP_ESTABLISHED:
tcp_data_queue(sk, skb); // 处理服务端发送的 FIN , 并进入 TIME-WAIT 状态
queued = 1;
break;
}
...
// 处理服务端发送的 FIN , 并进入 TIME-WAIT 状态
tcp_data_queue(sk, skb)
...
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
...
if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
tcp_fin(sk);
switch (sk->sk_state) {
...
case TCP_FIN_WAIT2:
/* Received a FIN -- send ACK and enter TIME_WAIT. */
tcp_send_ack(sk); // 回复给服务端 ACK
tcp_time_wait(sk, TCP_TIME_WAIT, 0); // 进入 TIME-WAIT 态: FIN-WAIT-2 => TIME-WAIT
break;
...
}
...
}
...
// 进入 TIME-WAIT 态: FIN-WAIT-2 => TIME-WAIT
tcp_time_wait(sk, TCP_TIME_WAIT, 0);
...
struct inet_timewait_sock *tw;
...
/* 分配 TIME-WAIT 套接字数据,初始化包括 TIME-WAIT 超时定时器 等 */
tw = inet_twsk_alloc(sk, tcp_death_row, state);
...
tw = kmem_cache_alloc(sk->sk_prot_creator->twsk_prot->twsk_slab, GFP_ATOMIC);
...
if (tw) {
...
tw->tw_state = TCP_TIME_WAIT;
tw->tw_substate = state; // tw->tw_substate = TCP_TIME_WAIT
...
// 初始化 TIME-WAIT 超时定时器
setup_pinned_timer(&tw->tw_timer, tw_timer_handler, (unsigned long)tw);
...
}
if (tw) {
...
// TIME-WATI 超时时间设置
tw->tw_timeout = TCP_TIMEWAIT_LEN;
if (state == TCP_TIME_WAIT)
timeo = TCP_TIMEWAIT_LEN;
...
inet_twsk_schedule(tw, timeo); /* 启动 TIME-WAIT 超时定时器 */
__inet_twsk_schedule(tw, timeo, false);
tw->tw_kill = timeo <= 4*HZ;
if (!rearm) {
BUG_ON(mod_timer(&tw->tw_timer, jiffies + timeo)); // 启动 TIME-WAIT 超时定时器
atomic_inc(&tw->tw_dr->tw_count);
} else {
...
}
...
}
...
tcp_done(sk);
...
tcp_set_state(sk, TCP_CLOSE);
...
// TIME-WAIT 定时器超时,触发 tw_timer_handler(),
// 回收 TIME-WAIT (struct inet_timewait_sock) 套接字资源
tw_timer_handler()
struct inet_timewait_sock *tw = (struct inet_timewait_sock *)data;
...
inet_twsk_kill(tw); // 回收 TIME-WAIT (struct inet_timewait_sock) 套接字资源
3.2.2.6 服务端收取客户端对 FIN 的回应 ACK,进入 CLOSED 终态
上面分析中,客户端收到服务端的 FIN
后,回复服务端一个 ACK
,然后自身进入由 FIN-WAIT-2
进入到 TIME-WAIT
,并在超时时间到达后,进入终态 CLOSED
。服务端收到客户端对 FIN
的 ACK
后,也由 LAST-ACK
转入终态 CLOSED
,到此,整个连接的双向关闭过程终结了。下面看服务端收到客户端对 FIN
的 ACK
,由 LAST-ACK
转入终态 CLOSED
的代码细节:
xxx_nic_interrput()
...
tcp_v4_rcv()
...
lookup:
/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字 */
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
process:
...
if (!sock_owned_by_user(sk)) {
ret = tcp_v4_do_rcv(sk, skb);
...
if (tcp_rcv_state_process(sk, skb)) {
rsk = sk;
goto reset;
}
return 0;
...
} else if (tcp_add_backlog(sk, skb)) {
...
}
...
// 服务端: LAST-ACK => CLOSED
tcp_rcv_state_process(sk, skb)
...
switch (sk->sk_state) {
...
case TCP_LAST_ACK:
if (tp->snd_una == tp->write_seq) {
tcp_update_metrics(sk);
tcp_done(sk);
tcp_set_state(sk, TCP_CLOSE); // 服务端: LAST-ACK => CLOSED
tcp_clear_xmit_timers(sk);
...
goto discard;
}
break;
...
}
...
到此,TCP 连接断开的四次挥手过程已经分析完毕。细心的读者可能会发现,示例代码中的 server_fd
还没有关闭。是的,但本文不打算对此进行分析,相信有了前面的基础,读者自行分析有不会是多困难的事情。
4. 抓包三握四挥过程示例
可通过工具 tcpdump
观察 TCP 三握四挥 的过程,请参考博文 Linux: tcpdump抓包示例 。
当然,Wireshark
可能是更好的选择。
5. 参考资料
rfc793: https://www.rfc-editor.org/rfc/rfc793.html
https://mp.weixin.qq.com/s/tCXH8BTrgYaVmwVx_Ek1qA