0
点赞
收藏
分享

微信扫一扫

TCP 源码浅读(4)三次握手-被动方接收SYN

胡桑_b06e 2022-05-04 阅读 80

版本:linux 4.18.1

作为学习笔记,只讨论常规的三次握手过程。

服务端调用 listen 函数后一直处于 TCP_LISTEN 状态,等待客户端的连接请求;

上一节我们分析了第一握手,客户端发送了 SYN 包,现在我们来看下服务端接收 SYN 包后的处理过程。

// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
    struct sock *sk;

    // 从 tcp_hashinfo 中的各种 hashtable 中尝试找到对应的 socket
    // th->source 是发送方的本地端口
    // th->dest 是接收方的本地端口
    sk = __inet_lookup_skb(&tcp_hashinfo, ...);
    
    // 找到的 sock 此时处于 TCP_LISTEN 状态
    if (sk->sk_state == TCP_LISTEN) {
		ret = tcp_v4_do_rcv(sk, skb);
		goto put_and_return;
	}
}

内核收到任何 TCP 消息,都会进入该函数。首先在全局的 tcp_hashinfo 中查找 sock,然后进行相应的处理。

看下查找过程:

// /net/ipv4/inet_hashtables.c
// __inet_lookup_skb --> __inet_lookup 
static inline struct sock *__inet_lookup(...)
{
    // 先在 ehash 中查找,刚接收 SYN 包,这里找不到
	sk = __inet_lookup_established(net, hashinfo, saddr, sport,
				       daddr, hnum, dif, sdif);
	if (sk)
		return sk;
    
    // 继续在 listening_hash || lhash2 中查找
	return __inet_lookup_listener(net, hashinfo, skb, doff, saddr,
				      sport, daddr, hnum, dif, sdif);
}

// 在 listening_hash || lhash2 中查找 sock
// 服务端在调用 listen 的时候已经将 sock 加入到以上两个 hashtable
struct sock *__inet_lookup_listener(...)
{
    // 根据端口号计算 hash 值
	unsigned int hash = inet_lhashfn(net, hnum);
    // 根据 hash 值得到 listening_hash 中的 slot
	struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash];
    
    // 如果 slot 指向链表包含的节点小于 10 或者不存在 lhash2,就只能通过 listening_hash 查找
	if (ilb->count <= 10 || !hashinfo->lhash2)
		goto port_lookup;

	/* Too many sk in the ilb bucket (which is hashed by port alone).
	 * Try lhash2 (which is hashed by port and addr) instead.
	 */
    // 如果 listening_hash 中对应 slot 的节点太多, 尝试用 lhash2 查找加速查找过程
	// 通过 本地端口 + 本地地址 计算 hash 值
    hash2 = ipv4_portaddr_hash(net, daddr, hnum);
    // 根据 hash 值得到 lhash2 中的 slot
	struct inet_listen_hashbucket ilb2 = inet_lhash2_bucket(hashinfo, hash2);
	if (ilb2->count > ilb->count)
		goto port_lookup;
    // 在该 slot 中查找 sock 信息
	result = inet_lhash2_lookup(...);
	if (result)
		return result;
    ...

port_lookup:
	sk_for_each_rcu(sk, &ilb->head) {
		...
        // 在 listening_hash 中查找 sock
	}
	return result;
}

服务端在某个 inet_listen_hashbucket 中找到与请求包目的端口对应的 sock,它处于 TCP_LISTEN 状态。

此后,服务端创建一个 request_sock 对象来表示这个半连接,并添加到ehash 中管理;然后构建 SYN+ACK 响应包,向客户端发送 第二次握手

// 调用栈
tcp_v4_do_rcv -- > tcp_rcv_state_process

// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
    case TCP_LISTEN:
        icsk->icsk_af_ops->conn_request(sk, skb) // tcp_v4_conn_request
    ...
}

// tcp_v4_conn_request --> tcp_conn_request
// net/ipv4/tcp_input.c
int tcp_conn_request(...)
{
    ... 
    // 分配一个 request_sock 对象来表示这个半连接 (状态: TCP_NEW_SYN_RECV)
	req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
     
    // 生成初始序列号 
   	af_ops->init_seq(req, sk, skb); // tcp_v4_init_sequence()
        
    // 加入 ehash,启动请求重传定时器 
    inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
    
    // 发送 SYN + ACK
    af_ops->send_synack(...)    
}

// net/ipv4/inet_connection_sock.c
void inet_csk_reqsk_queue_hash_add(...)
{
	reqsk_queue_hash_req(req, timeout); // --> inet_ehash_insert
	inet_csk_reqsk_queue_added(sk);
}

// 记录半连接数
static inline void inet_csk_reqsk_queue_added(struct sock *sk)
{
	reqsk_queue_added(&inet_csk(sk)->icsk_accept_queue);
}

服务端发送第二次握手后,内核中保存的 tcp 相关信息如下所示:

 

举报

相关推荐

0 条评论