TCP 三次握手底层解析
在 TCP 协议中,三次握手(Three-Way Handshake)用于建立一个可靠的连接。在本文中,我们将从用户态 API 到 Linux 内核中的具体实现,详细解析每一步的流程,并对代码关键点进行注释说明。
高层 API 与基本架构
从用户角度来看,TCP 连接的建立非常简单,客户端调用 socket()
创建套接字,然后调用 connect()
建立连接;服务端调用 socket()
后绑定 IP/port,通过 listen()
进入监听状态,并调用 accept()
接收客户端连接。
// 服务端示例
int main() {
// 1. 创建 socket
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. bind 绑定服务端需要监听的 [IP, port]
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 3. 监听
ret = listen(lfd, 8);
// 4. 接受客户端连接,返回用于通信的 socket
int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
}
// 客户端示例
int main() {
// 1. 创建 socket
int fd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 连接服务器
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
}
在内核内部,connect()
、accept()
等系统调用会调用一系列函数完成三次握手过程。下面我们逐步解析这一过程。
第一次握手:客户端 connect() 与 SYN 报文
在客户端调用 connect()
后,会进入 tcp_v4_connect
函数,主要流程如下:
-
设置 socket 状态:将状态设置为
TCP_SYN_SENT
。 -
动态选择端口:调用
inet_hash_connect
为 socket 分配一个本地端口。 -
构造 SYN 包并发送:调用
tcp_connect
分配一个 skb,将其放入发送队列,并启动重传定时器。
下面是核心代码:
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
// 设置 socket 状态为 TCP_SYN_SENT
tcp_set_state(sk, TCP_SYN_SENT);
// 动态选择一个端口
err = inet_hash_connect(tcp_death_row, sk);
// 根据 sk 中的信息构建一个 SYN 报文,并将其发送出去
err = tcp_connect(sk);
}
int tcp_connect(struct sock *sk)
{
// 申请 skb,用于构造 SYN 包
buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);
// 将 skb 添加到发送队列中
tcp_connect_queue_skb(sk, buff);
tcp_ecn_send_syn(sk, buff);
tcp_rbtree_insert(&sk->tcp_rtx_queue, buff);
// 发送 SYN 包,支持 Fast Open 分支
err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
// 启动重传定时器,直到收到响应前重复发送 SYN
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
return 0;
}
补充说明:
-
SYN 报文的作用:客户端随机生成初始序列号,并通过 SYN 报文通知服务器开始建立连接。
-
重传定时器:确保在网络丢包情况下,SYN 能够重传,直到收到服务器的响应。
第二次握手:服务端响应 SYN,发送 SYN+ACK
当服务端处于 TCP_LISTEN
状态时,收到客户端发来的 SYN 包后,会调用 tcp_v4_do_rcv
进行处理。此函数判断当前状态,然后调用 tcp_conn_request
构建半连接请求(request_sock
),并发送 SYN+ACK 报文。
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
// 服务端收到 SYN 或者 ACK(第三步)时都会进入此处
if (sk->sk_state == TCP_LISTEN) {
struct sock *nsk = tcp_v4_cookie_check(sk, skb); // 检查半连接队列
} else {
sock_rps_save_rxhash(sk, skb);
}
// 根据状态执行不同处理
if (tcp_rcv_state_process(sk, skb)) {
rsk = sk;
goto reset;
}
}
// 服务端处理 SYN 连接请求
int tcp_conn_request(struct request_sock_ops *rsk_ops,
const struct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
// 检查半连接队列是否满了(结合 SYN Cookies 机制以防止 SYN Flood 攻击)
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
// 检查全连接队列是否满了,若满则丢弃连接
if (sk_acceptq_is_full(sk)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
// 分配 request_sock 内核对象
req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
if (fastopen_sk) {
// Fast Open 处理分支:直接发送 SYN+ACK 并将子 socket 放入 accept 队列
af_ops->send_synack(fastopen_sk, dst, &fl, req,
&foc, TCP_SYNACK_FASTOPEN);
} else {
tcp_rsk(req)->tfo_listener = false;
if (!want_cookie)
// 添加到半连接队列,并开启定时器
inet_csk_reqsk_queue_hash_add(sk, req,
tcp_timeout_init((struct sock *)req));
// 构造并发送 SYN+ACK 包,支持 SYN Cookies 分支
af_ops->send_synack(sk, dst, &fl, req, &foc,
!want_cookie ? TCP_SYNACK_NORMAL :
TCP_SYNACK_COOKIE);
}
}
补充说明:
-
半连接队列 vs 全连接队列:服务器先将连接放入半连接队列,等待客户端确认后再转移到全连接队列,确保连接真正建立后才暴露给应用层。
-
SYN Cookies:当连接请求过多时(如遭遇 SYN Flood 攻击),服务器可以采用 SYN Cookies 技术,在不分配过多资源的情况下,安全地回应 SYN 请求。
第三次握手:客户端响应 SYN+ACK,发送 ACK
当客户端收到服务端的 SYN+ACK 包后,会调用 tcp_rcv_state_process
处理收到的报文。主要流程包括删除重传队列中的 SYN 包、重置定时器、切换状态为 TCP_ESTABLISHED
,以及最终发送 ACK 确认报文。
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
switch (sk->sk_state) {
case TCP_CLOSE:
goto discard;
// 服务端状态处理(例如处于 LISTEN 状态)直接丢弃
case TCP_LISTEN:
goto discard;
// 客户端处于 SYN_SENT 状态时处理 SYN+ACK 报文
case TCP_SYN_SENT:
// 处理 SYN+ACK 包
queued = tcp_rcv_synsent_state_process(sk, skb, th);
return 0;
}
}
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th)
{
// 确认收到 SYN+ACK,删除发送队列中的 SYN 包,并重置定时器
tcp_ack(sk, skb, FLAG_SLOWPATH);
// 完成 TCP 连接建立的工作
tcp_finish_connect(sk, skb);
// 如果存在数据发送挂起或者特殊状态,则可能延迟发送 ACK
if (sk->sk_write_pending ||
icsk->icsk_accept_queue.rskq_defer_accept ||
inet_csk_in_pingpong_mode(sk)) {
return 0;
} else {
// 否则,单独发送 ACK 确认报文
tcp_send_ack(sk);
}
}
/* 删除发送队列中的已确认报文,并重置定时器 */
static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
{
// 清理重传队列中的对应 skb
flag |= tcp_clean_rtx_queue(sk, prior_fack, prior_snd_una, &sack_state);
// 根据需要重置定时器
if (flag & FLAG_SET_XMIT_TIMER)
tcp_set_xmit_timer(sk);
}
/* 完成连接建立,状态转换为 TCP_ESTABLISHED */
void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
{
// 修改 socket 状态为 ESTABLISHED
tcp_set_state(sk, TCP_ESTABLISHED);
icsk->icsk_ack.lrcvtime = tcp_jiffies32;
// 拥塞控制初始化
tcp_init_transfer(sk, BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB);
tp->lsndtime = tcp_jiffies32;
// 如果启用了保活机制,则重置保活定时器
if (sock_flag(sk, SOCK_KEEPOPEN))
inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tp));
}
/* 申请 skb,构造 ACK 包并发送 */
void __tcp_send_ack(struct sock *sk, u32 rcv_nxt)
{
if (sk->sk_state == TCP_CLOSE)
return;
// 申请内存,构造 ACK 包
buff = alloc_skb(MAX_TCP_HEADER,
sk_gfp_mask(sk, GFP_ATOMIC | __GFP_NOWARN));
// 发送 ACK 包
__tcp_transmit_skb(sk, buff, 0, (__force gfp_t)0, rcv_nxt);
}
补充说明:
-
ACK 的双重作用:不仅确认收到 SYN+ACK,还清除重传队列,防止多余重传。
-
延时 ACK 策略:在某些情况下,为了提高效率(比如减少报文数),ACK 可能被延时或者与数据包合并发送。
服务端收到 ACK,建立连接并进入 accept 队列
当服务端收到客户端的 ACK 后,表明三次握手完成。服务端会根据收到的 ACK 创建一个新的子 socket,并将其从半连接队列转移到全连接队列,等待应用层调用 accept()
提取连接。
// 在 tcp_v4_cookie_check -> tcp_get_cookie_sock 中处理 ACK
struct sock *tcp_get_cookie_sock(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct dst_entry *dst, u32 tsoff)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct sock *child;
bool own_req;
// 调用回调函数,创建子 socket(最终会调用 tcp_v4_syn_recv_sock)
child = icsk->icsk_af_ops->syn_recv_sock(sk, skb, req, dst,
NULL, &own_req);
if (child) {
// 将新创建的子 socket 添加到全连接队列
if (inet_csk_reqsk_queue_add(sk, req, child))
return child;
bh_unlock_sock(child);
sock_put(child);
}
return NULL;
}
// 将子 socket 添加到全连接队列中
struct sock *inet_csk_reqsk_queue_add(struct sock *sk,
struct request_sock *req,
struct sock *child)
{
struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;
req->sk = child;
req->dl_next = NULL;
if (queue->rskq_accept_head == NULL)
WRITE_ONCE(queue->rskq_accept_head, req);
// 此处省略链表插入的后续逻辑
}
// 回调函数,创建子 socket 的具体过程
const struct inet_connection_sock_af_ops ipv4_specific = {
.syn_recv_sock = tcp_v4_syn_recv_sock,
};
// 子 socket 的创建过程
struct sock *tcp_v4_syn_recv_sock(const struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct dst_entry *dst,
struct request_sock *req_unhash,
bool *own_req)
{
// 检查 accept 队列是否已满
if (sk_acceptq_is_full(sk))
goto exit_overflow;
// 创建并初始化子 socket
newsk = tcp_create_openreq_child(sk, req, skb);
if (!newsk)
goto exit_nonewsk;
// 其他初始化流程……
}
最终,当应用层调用 accept()
时,会从全连接队列中取出这个已建立连接的子 socket:
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct request_sock_queue *queue = &icsk->icsk_accept_queue;
// 从全连接队列中移除第一个连接请求
req = reqsk_queue_remove(queue, sk);
newsk = req->sk;
// 返回新创建的、已建立连接的 socket
}
补充说明:
-
半连接队列与全连接队列:半连接队列用于保存未完成三次握手的连接,待收到最终 ACK 后,再将连接转入全连接队列,确保只有完成握手的连接才被应用层处理。
-
错误处理:实际实现中,每一步都会有严格的错误检测与处理,确保资源正确释放和状态一致。
总结
本文详细解析了 TCP 三次握手的底层实现流程:
-
客户端调用
connect()
后,状态切换为TCP_SYN_SENT
,并发送 SYN 包。 -
服务端处于
TCP_LISTEN
状态,收到 SYN 包后,根据半连接队列状态、SYN Cookies 策略等,分配request_sock
并发送 SYN+ACK 包。 -
客户端接收到 SYN+ACK 后,清理发送队列、重置定时器、切换状态为
TCP_ESTABLISHED
并发送 ACK 确认。 -
服务端收到 ACK 后,调用回调函数创建子 socket,将其加入全连接队列;应用层调用
accept()
后,取出这个已建立连接。
此外,我们还讨论了重传定时器、拥塞控制、保活机制以及延时 ACK 等细节,帮助大家更全面地理解 TCP 连接建立的底层实现。
希望这篇博客能帮助你更好地理解 TCP 三次握手的底层实现,欢迎留言讨论、分享你的见解!