美文网首页
bind 如何绑定端口

bind 如何绑定端口

作者: 董泽润 | 来源:发表于2018-08-22 12:14 被阅读253次

当套接字创建后,服务端需要调用函数 bind 来绑定监听端口,看似一句话很简单,那他是如何实现的呢?


bind函数使用

先看一下函数声明

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

第一个整形参数是己经创建的 socket 文件描述符,第二个是传入的地址结构体。由于要兼容老系统,函数声明类型是 struct sockaddr *, 但是我们一般使用 sockaddr_in, 后者更易于使用。

struct sockaddr {
    sa_family_t sa_family;  /* address family, AF_xxx   */
    char        sa_data[14];    /* 14 bytes of protocol address */
};

struct sockaddr_in {
  __kernel_sa_family_t  sin_family; /* Address family       */
  __be16        sin_port;   /* Port number          */
  struct in_addr    sin_addr;   /* Internet address     */

  /* Pad to size of `struct sockaddr'. */
  unsigned char     __pad[__SOCK_SIZE__ - sizeof(short int) -
            sizeof(unsigned short int) - sizeof(struct in_addr)];
};

可以看到,实际上两个结构体大小都是 16 字节,不足的部份使用 pad 填充。sockaddr_in 有成员 sin_port, sin_addr 使用更方便。

绑定流程

查看系统调用,bind实现流程有三个步骤

int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
    struct socket *sock;
    struct sockaddr_storage address;
    int err, fput_needed;

    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (sock) {
        err = move_addr_to_kernel(umyaddr, addrlen, &address);
        if (err >= 0) {
            err = security_socket_bind(sock,
                           (struct sockaddr *)&address,
                           addrlen);
            if (!err)
                err = sock->ops->bind(sock,
                              (struct sockaddr *)
                              &address, addrlen);
        }
        fput_light(sock->file, fput_needed);
    }
    return err;
}

1.由于传入的是 fd, 所以要调用 sockfd_lookup_light 查找到 socket 结构,如果失败返回。这块代码很简单,从当前进程 PCB 打开文件表里找到对应 fdfile. 在创建 socket 时,sock_map_fdfilesocket 关联,所以是一一对应关系
2.内核拷贝地址结构,move_addr_to_kernel, 从用户空间将 sockaddr 拷贝到内核空间,涉及到地址空间有效性检查。
3.调用 socket 指定协义的 sock->ops->bind 回调函数,完成绑定。在创建套接字时得知,sock->ops 被赋值为接口 inet_stream_ops, 而调用 bind 最终会调用 inet_bind

inet_bind 实现

查看 af_inet.c 文件,inet_bind 函数除了 raw 类型的套接字都会调用 __inet_bind. 分段来看实现

int __inet_bind(struct sock *sk, struct sockaddr *uaddr, int addr_len,
        bool force_bind_address_no_port, bool with_lock)
{
    struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
    struct inet_sock *inet = inet_sk(sk);
    struct net *net = sock_net(sk);
    unsigned short snum;
    int chk_addr_ret;
    u32 tb_id = RT_TABLE_LOCAL;
    int err;

    if (addr->sin_family != AF_INET) {
        /* Compatibility games : accept AF_UNSPEC (mapped to AF_INET)
         * only if s_addr is INADDR_ANY.
         */
        err = -EAFNOSUPPORT;
        if (addr->sin_family != AF_UNSPEC ||
            addr->sin_addr.s_addr != htonl(INADDR_ANY))
            goto out;
    }

    tb_id = l3mdev_fib_table_by_index(net, sk->sk_bound_dev_if) ? : tb_id;
    chk_addr_ret = inet_addr_type_table(net, addr->sin_addr.s_addr, tb_id);

两个函数 l3mdev_fib_table_by_index, inet_addr_type_table 涉及路由,功能就是根据传入的地址 s_addr 来判断是多播,广播,还是单播。比如地址是 0.0.0.0 就是广播,192.168.1.34 就是单播,其它广播,多播地址类型由以上两个函数确定。只有 udp 使用,忽略细节。

    /* Not specified by any standard per-se, however it breaks too
     * many applications when removed.  It is unfortunate since
     * allowing applications to make a non-local bind solves
     * several problems with systems using dynamic addressing.
     * (ie. your servers still start up even if your ISDN link
     *  is temporarily down)
     */
    err = -EADDRNOTAVAIL;
    if (!net->ipv4.sysctl_ip_nonlocal_bind &&
        !(inet->freebind || inet->transparent) &&
        addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
        chk_addr_ret != RTN_LOCAL &&
        chk_addr_ret != RTN_MULTICAST &&
        chk_addr_ret != RTN_BROADCAST)
        goto out;

    snum = ntohs(addr->sin_port);
    err = -EACCES;
    if (snum && snum < inet_prot_sock(net) &&
        !ns_capable(net->user_ns, CAP_NET_BIND_SERVICE))
        goto out;

ntohs 将网络大端端口转换成主机测的端口,inet_prot_sock 用来判断申请的端口是否小于在 1024,由于这些都是有限制的, 普通用户无法使用,ns_capable 没有权限就会退出。

    /*      We keep a pair of addresses. rcv_saddr is the one
     *      used by hash lookups, and saddr is used for transmit.
     *
     *      In the BSD API these are the same except where it
     *      would be illegal to use them (multicast/broadcast) in
     *      which case the sending device address is used.
     */
    if (with_lock)
        lock_sock(sk);

    /* Check these errors (active socket, double bind). */
    err = -EINVAL;
    if (sk->sk_state != TCP_CLOSE || inet->inet_num)
        goto out_release_sock;

查看套接字当前的状态,不处于关闭状态,或是己经绑定端口了,肯定是不可以再绑定。

    inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
    if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
        inet->inet_saddr = 0;  /* Use device */

对于多播,广播地址,源地址肯定是不能确认的,需要有对端连接,跟据路由来智能识别。所以 inet_saddr 置 0

    /* Make sure we are allowed to bind here. */
    if (snum || !(inet->bind_address_no_port ||
              force_bind_address_no_port)) {
        if (sk->sk_prot->get_port(sk, snum)) {
            inet->inet_saddr = inet->inet_rcv_saddr = 0;
            err = -EADDRINUSE;
            goto out_release_sock;
        }
        err = BPF_CGROUP_RUN_PROG_INET4_POST_BIND(sk);
        if (err) {
            inet->inet_saddr = inet->inet_rcv_saddr = 0;
            goto out_release_sock;
        }
    }

sk->sk_prot->get_port 核心的设置端口函数,在创建 socket 时可知,sk_prot 指向 tcp_prot, 所以最终 get_port 调用 tcp_prot.inet_csk_get_port,这是和协义相关的最核心代码。


    if (inet->inet_rcv_saddr)
        sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
    if (snum)
        sk->sk_userlocks |= SOCK_BINDPORT_LOCK;
    inet->inet_sport = htons(inet->inet_num);
    inet->inet_daddr = 0;
    inet->inet_dport = 0;
    sk_dst_reset(sk);
    err = 0;
out_release_sock:
    if (with_lock)
        release_sock(sk);
out:
    return err;
}

如果 inet_rcv_saddr 不为 0 ,那么设置锁。如果 snum 不为 0,设置锁。并将目地地址,端口置 0. 另外 sk_dst_reset 涉及路由,暂时也忽略。

inet_csk_get_port 实现

分析代码前先想个问题,bind 端口时有哪些注意点呢?换句话说,如果是我们自己实现,会怎么写呢。

  1. 如果 bind 时不指定端口,那系统会怎么挑选端口?
  2. 如果指定的端口被占用了,系统会不会强制使用?
  3. 内核如何保存所有 socket 连接?怎样做到高效的冲突检测
  4. SO_REUSEPORT 是如何工作的呢?

代码分段阅读

/* Obtain a reference to a local port for the given sock,
 * if snum is zero it means select any available local port.
 * We try to allocate an odd port (and leave even ports for connect())
 */
int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
    bool reuse = sk->sk_reuse && sk->sk_state != TCP_LISTEN;
    struct inet_hashinfo *hinfo = sk->sk_prot->h.hashinfo;
    int ret = 1, port = snum;
    struct inet_bind_hashbucket *head;
    struct net *net = sock_net(sk);
    struct inet_bind_bucket *tb = NULL;
    kuid_t uid = sock_i_uid(sk);

这里重点是 h.hashinfo,他的类型是 inet_hashinfo, 通过 sk->sk_prot 得知,hashinfo 是一个全局单例,有两个核心成员 inet_ehash_bucketinet_bind_hashbucket 分别保存己建立连接的 socket 集合,和己 bind 的集合。类似哈希桶的实现,根据端口来找到桶,然后桶里的数据用链表来维护。

    if (!port) {
        head = inet_csk_find_open_port(sk, &tb, &port);
        if (!head)
            return ret;
        if (!tb)
            goto tb_not_found;
        goto success;
    }

如果没有指定端口,那么系统会根据 inet_csk_find_open_port 来分配,这个一会回过头再看实现。

    head = &hinfo->bhash[inet_bhashfn(net, port,
                      hinfo->bhash_size)];
    spin_lock_bh(&head->lock);
    inet_bind_bucket_for_each(tb, &head->chain)
        if (net_eq(ib_net(tb), net) && tb->port == port)
            goto tb_found;
tb_not_found:
    tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
                     net, head, port);
    if (!tb)
        goto fail_unlock;

函数 inet_bhashfn 是哈希算法,主要是看端口。得到对应桶,一个链表的 header, 如果桶不为空,说明很可能端口己经被占用。如果桶为空,那一定没有被占用,调用 inet_bind_bucket_create 初始化桶。

inet_bind_bucket_for_each 宏用来遍历链表,如果网络设备相同,并且端口相等,那说明找到了,但是能否使用,还不一定。

tb_found:
    if (!hlist_empty(&tb->owners)) {
        if (sk->sk_reuse == SK_FORCE_REUSE)
            goto success;

        if ((tb->fastreuse > 0 && reuse) ||
            sk_reuseport_match(tb, sk))
            goto success;
        if (inet_csk_bind_conflict(sk, tb, true, true))
            goto fail_unlock;
    }

tb 的类型是 inet_bind_bucket, 看下他的定义得知,tb->owners 也是个链表,因为端口可以被多个 socket 同时绑定。如果使用者链表不为空,那么就是检查当前 sk->sk_reuse 是否强制,sk_reuseport_match 检查是否可重用,inet_csk_bind_conflict 检查是否冲突。

sk_reuseport_match
static inline int sk_reuseport_match(struct inet_bind_bucket *tb,
                     struct sock *sk)
{
    kuid_t uid = sock_i_uid(sk);

    if (tb->fastreuseport <= 0)
        return 0;
    if (!sk->sk_reuseport)
        return 0;
    if (rcu_access_pointer(sk->sk_reuseport_cb))
        return 0;
    if (!uid_eq(tb->fastuid, uid))
        return 0;
    /* We only need to check the rcv_saddr if this tb was once marked
     * without fastreuseport and then was reset, as we can only know that
     * the fast_*rcv_saddr doesn't have any conflicts with the socks on the
     * owners list.
     */
    if (tb->fastreuseport == FASTREUSEPORT_ANY)
        return 1;
#if IS_ENABLED(CONFIG_IPV6)
    if (tb->fast_sk_family == AF_INET6)
        return ipv6_rcv_saddr_equal(&tb->fast_v6_rcv_saddr,
                        inet6_rcv_saddr(sk),
                        tb->fast_rcv_saddr,
                        sk->sk_rcv_saddr,
                        tb->fast_ipv6_only,
                        ipv6_only_sock(sk), true);
#endif
    return ipv4_rcv_saddr_equal(tb->fast_rcv_saddr, sk->sk_rcv_saddr,
                    ipv6_only_sock(sk), true);
}
  1. 如果 tb->fastreuseport 或是 sk-> sk_reuseport 没有打开,那么肯定不可以重用
  2. 检查 uid, 必须是同一个用户下的才可以
  3. 如果当前 tb->fastreuseportFASTREUSEPORT_ANY 那么无条件匹配
  4. 调用 ipv4_rcv_saddr_equal 判断是否匹配,如果最后一个参数是 true, 打开了泛匹配,那么 0.0.0.0可以匹配任意 ip 地址
inet_csk_bind_conflict
static int inet_csk_bind_conflict(const struct sock *sk,
                  const struct inet_bind_bucket *tb,
                  bool relax, bool reuseport_ok)
{
    struct sock *sk2;
    bool reuse = sk->sk_reuse;
    bool reuseport = !!sk->sk_reuseport && reuseport_ok;
    kuid_t uid = sock_i_uid((struct sock *)sk);
    /*
     * Unlike other sk lookup places we do not check
     * for sk_net here, since _all_ the socks listed
     * in tb->owners list belong to the same net - the
     * one this bucket belongs to.
     */
    sk_for_each_bound(sk2, &tb->owners) {
        if (sk != sk2 &&
            (!sk->sk_bound_dev_if ||
             !sk2->sk_bound_dev_if ||
             sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) {
            if ((!reuse || !sk2->sk_reuse ||
                sk2->sk_state == TCP_LISTEN) &&
                (!reuseport || !sk2->sk_reuseport ||
                 rcu_access_pointer(sk->sk_reuseport_cb) ||
                 (sk2->sk_state != TCP_TIME_WAIT &&
                 !uid_eq(uid, sock_i_uid(sk2))))) {
                if (inet_rcv_saddr_equal(sk, sk2, true))
                    break;
            }
            if (!relax && reuse && sk2->sk_reuse &&
                sk2->sk_state != TCP_LISTEN) {
                if (inet_rcv_saddr_equal(sk, sk2, true))
                    break;
            }
        }
    }
    return sk2 != NULL;
}

这个函数用来检查是否冲突,刚才说了 tb->owners 是一个链表,保存的是共用这个 inet_bind_bucketsocket 集合,也就是说如果想重用这个端口,还要遍历。

  1. sk != sk2 如果是自己肯定跳过
  2. 任一一个没有 bindtodeivce ,或是 bound 了同一个需要继续检查,如果分别 bound 不同设备,那肯定不冲突
  3. tb 连接处于 TCP_LISTEN 状态,并且设置了 sk_reuseport_cb 不可以重用
  4. tb 连接如果不处于 TCP_TIME_WAIT状态,并且不是同一个用户,不可重用

回头看 inet_csk_get_port 主流程

success:
    if (hlist_empty(&tb->owners)) {
        tb->fastreuse = reuse;
        if (sk->sk_reuseport) {
            tb->fastreuseport = FASTREUSEPORT_ANY;
            tb->fastuid = uid;
            tb->fast_rcv_saddr = sk->sk_rcv_saddr;
            tb->fast_ipv6_only = ipv6_only_sock(sk);
            tb->fast_sk_family = sk->sk_family;
#if IS_ENABLED(CONFIG_IPV6)
            tb->fast_v6_rcv_saddr = sk->sk_v6_rcv_saddr;
#endif
        } else {
            tb->fastreuseport = 0;
        }

此时如果 tb->owners 为空,说明是第一次分配,被始化 tb 即可

    } else {
        if (!reuse)
            tb->fastreuse = 0;
        if (sk->sk_reuseport) {
            /* We didn't match or we don't have fastreuseport set on
             * the tb, but we have sk_reuseport set on this socket
             * and we know that there are no bind conflicts with
             * this socket in this tb, so reset our tb's reuseport
             * settings so that any subsequent sockets that match
             * our current socket will be put on the fast path.
             *
             * If we reset we need to set FASTREUSEPORT_STRICT so we
             * do extra checking for all subsequent sk_reuseport
             * socks.
             */
            if (!sk_reuseport_match(tb, sk)) {
                tb->fastreuseport = FASTREUSEPORT_STRICT;
                tb->fastuid = uid;
                tb->fast_rcv_saddr = sk->sk_rcv_saddr;
                tb->fast_ipv6_only = ipv6_only_sock(sk);
                tb->fast_sk_family = sk->sk_family;
#if IS_ENABLED(CONFIG_IPV6)
                tb->fast_v6_rcv_saddr = sk->sk_v6_rcv_saddr;
#endif
            }
        } else {
            tb->fastreuseport = 0;
        }
    }
    if (!inet_csk(sk)->icsk_bind_hash)
        inet_bind_hash(sk, tb, port);
    WARN_ON(inet_csk(sk)->icsk_bind_hash != tb);
    ret = 0;

fail_unlock:
    spin_unlock_bh(&head->lock);
    return ret;
}
EXPORT_SYMBOL_GPL(inet_csk_get_port);

最后最重要的是调用 inet_bind_hashsocket, tb, port 关联。看下函数代码

void inet_bind_hash(struct sock *sk, struct inet_bind_bucket *tb,
            const unsigned short snum)
{
    inet_sk(sk)->inet_num = snum;
    sk_add_bind_node(sk, &tb->owners);
    inet_csk(sk)->icsk_bind_hash = tb;
}

snum 即端口赋值给 inet_num, 在 tb 的拥有者链表里把自己注册进去,将 tb 赋值给自己的成员 icsk_bind_hash, 说白了就是互相引用,你中有我,我中有你,方便访问。

inet_csk_find_open_port 自动分配端口

前面挖了一个坑,如果没有指定端口,系统会自动分配。我们都知道端口分配是有范围的,由参数 net.ipv4.ip_local_port_range 控制,我的测试机是从 2048 到 65000. 这里不贴源码了,bind 优先从高半部分端口开始分配,connect 系统调用会产生新的连接,优先从低半部分分配。其它的 reuse 冲突检测一个也不能少。

小结

没想到一个 bind 这么复杂,后面还有 listen, connect, write 等等。另外还是有疑问,如果设置了 SO_REUSEPORT, 并且多进程监听同一个端口,流量如何分配呢?以后再看吧。

相关文章

  • bind 如何绑定端口

    当套接字创建后,服务端需要调用函数 bind 来绑定监听端口,看似一句话很简单,那他是如何实现的呢? bind函数...

  • 2018-11-26 metasploit 框架 payload

    use payload/windows/shell_bind_tcp (攻击绑定端口的脚本) generate...

  • Socket------基于TCP的编程实例

    流程: 服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept...

  • 2018-04-21

    socket中服务端中的bind和listen都是绑定和听自己本地的端口

  • Mysql的my.ini文件配置

    [mysqld] character-set-server=utf8 绑定IPv4和3306端口 bind-add...

  • 二. 网络应用-Socket编程基础

    常见端口号: Socket API 创建套接字:socket() 绑定套接字的本地端点地址: bind() 设置监...

  • 网络编程-socket

    TCP TCP服务端 创建套接字socket 绑定端口bind 侦听客户请求listen 接受客户连接accept...

  • socket原理

    先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用a...

  • socket 编程基础

    TCP 服务器的工作流程: 服务器调用 socket() 创建 socket; 服务器调用 bind() 绑定端口...

  • vue03

    v-bind动态绑定class(对象语法) v-bind动态绑定class(数组语法) v-bind动态绑定sty...

网友评论

      本文标题:bind 如何绑定端口

      本文链接:https://www.haomeiwen.com/subject/udsjiftx.html