TCP协议connect的端口选择

connect端口选择

在socket编程中, 客户端使用connect向服务端发起请求时,如果不指定本地端口(一般都不指定), 内核会自动为连接分配一个可用的端口. connect是如何进行端口选择的呢? connect端口选择逻辑如下:

  • 如果sock已经指定了端口, 使用指定的端口
  • 如果sock没有指定端口, 分配一个端口
    1. 先获取内核参数设置的本地可用端口范围,默认为low: 32768 - high: 60999
    2. 根据hint和三元组的hash得到一个随机的偏移量offset
    3. 从low+offset开始, 在可用端口范围内遍历判断端口是否可用, 每次端口值+2
    4. 不允许使用用户设置的保留端口
    5. 如果端口已经使用
      1. 不允许使用bind绑定的端口
      2. 检查端口是否可重用
        1. ehash表中没有四元组匹配的sock时端口可重用
        2. 有四元组匹配的sock时进行TIME_WAIT判断 符合以下条件可重用 1. 匹配的连接处于TIME_WAIT状态 2. 满足TIME_WAIT端口复用条件
    6. 如果端口没有被使用,使用此端口

内核允许同一个端口向两个不同的服务端发起连接请求

源码分析:

connect端口选择核心函数inet_hash_connect分析:

/*
 * Bind a port for a connect operation and hash it.
 */
int inet_hash_connect(struct inet_timewait_death_row *death_row,
              struct sock *sk)
{
    u32 port_offset = 0;

    /* 如果sk的本地源端口设置为0 */
    if (!inet_sk(sk)->inet_num)
        /* 根据源IP、目的IP、目的端口,用hash函数计算出一个随机数,作为端口的初始偏移值 */
        port_offset = inet_sk_port_offset(sk);
    return __inet_hash_connect(death_row, sk, port_offset,
                   __inet_check_established);
                   /* __inet_check_established为检查端口是否可用的回调函数 */
}

__inet_hash_connect分析:


int __inet_hash_connect(struct inet_timewait_death_row *death_row,
        struct sock *sk, u32 port_offset,
        int (*check_established)(struct inet_timewait_death_row *,
            struct sock *, __u16, struct inet_timewait_sock **))
{
    struct inet_hashinfo *hinfo = death_row->hashinfo;
    struct inet_timewait_sock *tw = NULL;
    struct inet_bind_hashbucket *head;
    int port = inet_sk(sk)->inet_num;
    struct net *net = sock_net(sk);
    struct inet_bind_bucket *tb;
    u32 remaining, offset;
    int ret, i, low, high;
    static u32 hint;
    int l3mdev;

    /* sock已经设置了端口 */
    if (port) {
        head = &hinfo->bhash[inet_bhashfn(net, port,
                          hinfo->bhash_size)];
        tb = inet_csk(sk)->icsk_bind_hash;
        spin_lock_bh(&head->lock);
        if (sk_head(&tb->owners) == sk && !sk->sk_bind_node.next) {
            inet_ehash_nolisten(sk, NULL);
            spin_unlock_bh(&head->lock);
            return 0;
        }
        spin_unlock(&head->lock);
        /* No definite answer... Walk to established hash table */
        ret = check_established(death_row, sk, port, NULL);
        local_bh_enable();
        return ret;
    }

    /* 
    * sock没有设置端口时会走到这里,以下是内核自动选择端口的过程
    * connect自动选择端口会保证和ip_local_port_range的low的奇偶性保持一致
    * bind自动选择端口会保证和low奇偶性相反 */

    /* 
    * 绑定的VRF(Virtual Routing and Forwarding)设备
    * 内核默认不开启tcp_l3mdev_accept,直接返回0 */
    l3mdev = inet_sk_bound_l3mdev(sk);

    /* 获取本地可用端口范围,ip_local_port_range是可以设置的内核参数,默认是32768-60999 */
    inet_get_local_port_range(net, &low, &high);
    high++; /* [32768, 60999] -> [32768, 61000[ */
    /* 计算端口范围差值 */
    remaining = high - low;
    if (likely(remaining > 1))
        /* 确保remaining为偶数,保证和low的奇偶性保持一致 */
        remaining &= ~1U;

    /* 
    * 根据hint和port_offset计算出一个remaining范围内的偏移量
    * hint是一个静态变量,每次+(i+2),i为上次的可用端口-初始选择端口
    * hint尽量使每次选择的端口递增,提高端口命中率
    * port_offset是之前根据源地址,目的地址,目的端口hash出来的一个随机数 */
    offset = (hint + port_offset) % remaining;
    /* In first pass we try ports of @low parity.
     * inet_csk_get_port() does the opposite choice.
     */
    /* 确保offset为偶数,保证和low的奇偶性保持一致 */
    offset &= ~1U;
other_parity_scan:
    /* 选择第一个port的值 */
    port = low + offset;
    /* 从第一个port开始判断端口是否可用,每次port+2,保证和low的奇偶性保持一致 */
    for (i = 0; i < remaining; i += 2, port += 2) {
        /* port超范围则返回low继续查找 */
        if (unlikely(port >= high))
            port -= remaining;
        /* 
        * 排除ip_local_reserved_ports内核参数中设置的保留端口
        * 参数默认为空,可以自己配置想要为某些服务保留的端口 */
        if (inet_is_local_reserved_port(net, port))
            continue;
        
        /* 根据端口号和命名空间的哈希得到哈希表头 */
        head = &hinfo->bhash[inet_bhashfn(net, port,
                          hinfo->bhash_size)];

        /* 锁住此表头 */
        spin_lock_bh(&head->lock);

        /* Does not bother with rcv_saddr checks, because
         * the established check is already unique enough.
         */
        /* 从哈希得到的链表中查找对应的命名空间和端口号的bind_bucket */
        inet_bind_bucket_for_each(tb, &head->chain) {
            /* 对比命名空间,端口号和VRF设备(默认不开启) */
            if (net_eq(ib_net(tb), net) && tb->l3mdev == l3mdev &&
                tb->port == port) {
                /* 不允许使用bind创建或者使用的端口
                 * bind创建结构体时,会使得fastreuse和fastreuseport>=0
                 * connect创建结构体时,两个值为-1 */
                if (tb->fastreuse >= 0 ||
                    tb->fastreuseport >= 0)
                    goto next_port;
                WARN_ON(hlist_empty(&tb->owners));
                /* 检查端口是否可重用
                 * 1.ehash表中没有四元组,命名空间匹配的sock时可重用
                 * 2.有四元组命名空间匹配的连接时进行TIME_WAIT判断
                 *     符合以下条件可重用
                 *     - 匹配的连接处于TIME_WAIT状态
                 *     - 满足TIME_WAIT端口复用条件 */
                if (!check_established(death_row, sk,
                               port, &tw))
                    goto ok;
                goto next_port;
            }
        }

        /* 没有找到对应的bind_bucket时会走到这里
         * 说明还没有创建端口的inet_bind_bucket结构,端口一定可用 */

        /* 为端口创建inet_bind_bucket结构 */
        tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
                         net, head, port, l3mdev);
        if (!tb) {
            spin_unlock_bh(&head->lock);
            return -ENOMEM;
        }
        /* fastreuse和fastreuseport设置为-1 */
        tb->fastreuse = -1;
        tb->fastreuseport = -1;
        goto ok;
next_port:
        spin_unlock_bh(&head->lock);
        cond_resched();
    }

    /* 走到这里说明没有合适端口,改变奇偶性再选一次 */

    offset++;
    if ((offset & 1) && remaining > 1)
        goto other_parity_scan;

    /* 改变奇偶性依然没有合适端口,返回错误Cannot assign requested address */
    return -EADDRNOTAVAIL;

ok:
    /* 保存静态变量的值,下个相同三元组会使用新的hint,减少重复判断 */
    hint += i + 2;

    /* Head lock still held and bh's disabled */
    /* 将sock添加到inet_bind_bucket结构的owner链表中 */
    inet_bind_hash(sk, tb, port);
    /* 如果sokc没有添加到ehash表,将sock添加到ehash表中 */
    if (sk_unhashed(sk)) {
        inet_sk(sk)->inet_sport = htons(port);
        inet_ehash_nolisten(sk, (struct sock *)tw);
    }
    /* 提前结束time_wait状态 */
    if (tw)
        inet_twsk_bind_unhash(tw, hinfo);
    spin_unlock(&head->lock);
    if (tw)
        inet_twsk_deschedule_put(tw);
    local_bh_enable();
    return 0;
}

__inet_check_established分析:

/* called with local bh disabled */
static int __inet_check_established(struct inet_timewait_death_row *death_row,
                    struct sock *sk, __u16 lport,
                    struct inet_timewait_sock **twp)
{
    struct inet_hashinfo *hinfo = death_row->hashinfo;
    struct inet_sock *inet = inet_sk(sk);
    __be32 daddr = inet->inet_rcv_saddr;
    __be32 saddr = inet->inet_daddr;
    int dif = sk->sk_bound_dev_if;
    struct net *net = sock_net(sk);
    int sdif = l3mdev_master_ifindex_by_index(net, dif);
    INET_ADDR_COOKIE(acookie, saddr, daddr);
    const __portpair ports = INET_COMBINED_PORTS(inet->inet_dport, lport);
    /* 根据四元组和命名空间得到ehash表的哈希值 */
    unsigned int hash = inet_ehashfn(net, daddr, lport,
                     saddr, inet->inet_dport);
    /* 获取指定哈希值的哈希桶 */
    struct inet_ehash_bucket *head = inet_ehash_bucket(hinfo, hash);
    /* 指定哈希桶的锁 */
    spinlock_t *lock = inet_ehash_lockp(hinfo, hash);
    struct sock *sk2;
    const struct hlist_nulls_node *node;
    struct inet_timewait_sock *tw = NULL;

    /* 锁住哈希桶 */
    spin_lock(lock);

    /* 遍历哈希桶匹配四元组,命名空间,绑定设备相同的sock */
    sk_nulls_for_each(sk2, node, &head->chain) {
        /* 比较hash值 */
        if (sk2->sk_hash != hash)
            continue;

        /* 有四元组,命名空间,绑定设备完全匹配的连接 */
        if (likely(INET_MATCH(sk2, net, acookie,
                     saddr, daddr, ports, dif, sdif))) {
            /* 连接处于TIME_WAIT状态 */
            if (sk2->sk_state == TCP_TIME_WAIT) {
                tw = inet_twsk(sk2);
                /* 判断是否满足TIME_WAIT端口复用条件 */
                if (twsk_unique(sk, sk2, twp))
                    break;
            }
            /* 如果有完全匹配的连接,且不可TIME_WAIT复用,会走到这里,返回不可用 */
            goto not_unique;
        }
    }

    /* 没有匹配到相同的连接,或者time_wait重用会走到这里 */

    /* Must record num and sport now. Otherwise we will see
     * in hash table socket with a funny identity.
     */
    inet->inet_num = lport;
    inet->inet_sport = htons(lport);
    sk->sk_hash = hash;
    WARN_ON(!sk_unhashed(sk));
    /* 添加sock到ehash哈希桶中 */
    __sk_nulls_add_node_rcu(sk, &head->chain);
    /* 从ehash哈希桶中删除TIME_WAIT连接 */
    if (tw) {
        sk_nulls_del_node_init_rcu((struct sock *)tw);
        __NET_INC_STATS(net, LINUX_MIB_TIMEWAITRECYCLED);
    }
    spin_unlock(lock);

    /* 增加端口使用计数 */
    sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1);

    /* 提前终止time_wait */
    if (twp) {
        *twp = tw;
    } else if (tw) {
        /* Silly. Should hash-dance instead... */
        inet_twsk_deschedule_put(tw);
    }
    /* 返回可用 */
    return 0;

not_unique:
    spin_unlock(lock);
    return -EADDRNOTAVAIL;
}