开发内功修炼@张彦飞开发内功修炼@张彦飞

talk is cheap,
show me the code!

绑定特殊 IP 之 0.0.0.0 的内部工作原理

大家好,我是飞哥!

前段时间有位读者提了个问题,:“服务器端监听 0.0.0.0 的内部是咋样的?”

大家可能也在 nginx、redis 等 server 的配置文件中见过 bind 的时候不用真实的 IP,而使用 0.0.0.0 的情况。

我觉得这个问题提的很不错,弄懂这个实现过程很有利于大家理解 Linux 服务器在多网卡情况下的监听过程。所以专门来一篇文章解答一下。

这个 0.0.0.0 和 127.0.0.1 都是特殊 IP。为了方便本文展开叙述,咱们先列一段绑定 0.0.0.0 的 c 语言 server 代码(只为了展示,不可运行)。

void main(){
    int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    struct sockaddr_in addr;

    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sinport = ...;

    //绑定 ip 和端口
    bind(fd, addr, ...);

    //监听
    listen(fd, ...);
}

其中 INADDR_ANY 是定义在 include/uapi/linux/in.h 文件下的,就是 0 IP 地址。

#define    INADDR_ANY        ((unsigned long int) 0x00000000)

一、bind 过程

我们来看一下 bind 的相关内部过程,它的核心是 inet_bind, 其源码位于 net/ipv4/af_inet.c 中。我们只看和今天问题相关的部分。

//file: net/ipv4/af_inet.c
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
    struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
    struct sock *sk = sock->sk;
    struct inet_sock *inet = inet_sk(sk);

    ...

    //bind时 将 inet_rcv_saddr 和 inet_saddr 都设置为地址
    inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;

    //bind 时设置要使用的端口
    inet->inet_sport = htons(inet->inet_num);
    ...
}

这个函数有两个重点参数,分别是 sock 和 uaddr。 其中 sock 是我们刚创建出来的 socket 对象,uaddr 的值就是我们在自己的代码里传入的 addr 值。函数接下来的 inet 是获取了 socket 内核对象中的一部分。

在 inet_bind 的函数体中,将要绑定的 IP 地址 addr->sin_addr.s_addr( 0 ) 设置到了 socket 的 inet->inet_rcv_saddr 成员中,将要绑定的端口设置到了 inet->inet_sport 成员上。

接下来服务器在 listen 的时候会把当前 socket 添加到一个 listen 状态的 hash 表中,了解就行了。接下来咱们看当用户握手包到达的时候的处理过程。

二、响应握手请求

在收到来自客户端数据包的时候(包括握手请求),会进入到 tcp_v4_rcv 这个核心函数中。在这里会读取数据包的 tcp 头和 ip 头。其中在 tcp 头中有 ip 和端口的四元组。

//file: net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
    th = tcp_hdr(skb);
    iph = ip_hdr(skb);

    //在这里查找正在监听的 socket 
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
    ......
}

在 \_\_inet_lookup_skb 这个函数内部会寻找服务器上处理该数据包的 socket。先查看是否有已经建立的连接,如果没有就寻找合适的 listen 状态的 socket,以进行握手。我们直接查看查找 listen socket 的 \_\_inet_lookup_listener。

//file: net/ipv4/inet_hashtables.c
struct sock *__inet_lookup_listener(struct net *net,
                    struct inet_hashinfo *hashinfo,
                    const __be32 saddr, __be16 sport,
                    const __be32 daddr, const unsigned short hnum,
                    const int dif)
{
    //根据端口计算 hash 值
    unsigned int hash = inet_lhashfn(net, hnum);

    //所有的 listen 都是存这个 hash 中
    //根据 hash,并把所有可能的 listen 的 socket 链表找出来
    struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash];

begin:
    result = NULL;
    hiscore = 0;
    sk_nulls_for_each_rcu(sk, node, &ilb->head) {
        score = compute_score(sk, net, hnum, daddr, dif);
        if (score > hiscore) {
            result = sk;
        }
        ...
    }
    ...
    return result;
}

在 \_\_inet_lookup_listener 中遍历同 hash 值的所有在监听的 socket。挨个计算匹配分数,并把匹配分最高的挑选出来,就是要握手的 socket 对象了。

我们重点来看下 compute_score,我们今天问题的答案就藏在它里面。在看源码之前先回忆一下,上面我们在 bind 地址为 INADDR_ANY 的时候,内核会把 listen socket 的 inet_rcv_saddr 设置为 0。来看源码:

//file: net/ipv4/inet_hashtables.c
static inline int compute_score(struct sock *sk, struct net *net,
                const unsigned short hnum, const __be32 daddr,
                const int dif)
{
    //默认分数是负数
    int score = -1;
    struct inet_sock *inet = inet_sk(sk);

    //只有网络命名空间和端口等都匹配才真正计算匹配分
    if (net_eq(sock_net(sk), net) && inet->inet_num == hnum &&
            !ipv6_only_sock(sk)) {

        //inet socket 优先级高
        score = sk->sk_family == PF_INET ? 2 : 1;

        //注意!!!
        //这里有真正的算分逻辑
        __be32 rcv_saddr = inet->inet_rcv_saddr;
        if (rcv_saddr) {
            if (rcv_saddr != daddr)
                return -1;
            score += 4;
        }
        ...    
    }
    return score;
}

在计算匹配分的时候会判断 listen 状态的 socket 中 bind 时记录的 inet_rcv_saddr。如果它不为 0(bind 时指定了 IP),则数据包中的目的地址必须和它匹配才行。而如果为 0(bind 时设置 IP 是 INADDR_ANY, 亦即 0.0.0.0),则不会进行 IP 地址的比对就能计算出正的匹配分

四、结论

可以用一句话来总结 0.0.0.0。如果一个服务是绑定到 0.0.0.0 ,那么外部机器访问该机器上所有 IP 都可以访问该服务。如果服务绑定到的是特定的 ip,则只有访问该 ip 才能访问到服务。

实现的原理也很简单,如果 bind 时绑定的是 0.0.0.0(INADDR_ANY),则内核在查找 listen 状态的 socket 的时候不进行目的地址匹配。反之,则必须要网络包中的目的地址和该 socket 上的 IP 匹配才能访问!

写在最后,由于我的这些知识在公众号里文章比较分散,很多人似乎没有理解到我对知识组织的体系结构。而且图文也不像视频那样理解起来更直接。所以我在知识星球上规划了视频系列课程,包括硬件原理、内存管理、进程管理、文件系统、网络管理、Golang语言、容器原理、性能观测、性能优化九大部分大约 120 节内容,每周更新。加入方式参见我要开始搞知识星球啦如何才能高效地学习技术,我投“融汇贯通”一票

Github:https://github.com/yanfeizhang/coder-kung-fu
关注公众号:微信扫描下方二维码
qrcode2_640.png

本原创文章未经允许不得转载 | 当前页面:开发内功修炼@张彦飞 » 绑定特殊 IP 之 0.0.0.0 的内部工作原理