【计算机网络】TCP 三次握手底层解析

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 函数,主要流程如下:

  1. 设置 socket 状态:将状态设置为 TCP_SYN_SENT

  2. 动态选择端口:调用 inet_hash_connect 为 socket 分配一个本地端口。

  3. 构造 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 三次握手的底层实现流程:

  1. 客户端调用 connect() 后,状态切换为 TCP_SYN_SENT,并发送 SYN 包。

  2. 服务端处于 TCP_LISTEN 状态,收到 SYN 包后,根据半连接队列状态、SYN Cookies 策略等,分配 request_sock 并发送 SYN+ACK 包。

  3. 客户端接收到 SYN+ACK 后,清理发送队列、重置定时器、切换状态为 TCP_ESTABLISHED 并发送 ACK 确认。

  4. 服务端收到 ACK 后,调用回调函数创建子 socket,将其加入全连接队列;应用层调用 accept() 后,取出这个已建立连接。

在这里插入图片描述

此外,我们还讨论了重传定时器、拥塞控制、保活机制以及延时 ACK 等细节,帮助大家更全面地理解 TCP 连接建立的底层实现。

希望这篇博客能帮助你更好地理解 TCP 三次握手的底层实现,欢迎留言讨论、分享你的见解!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值