0


InetAddress.getByName背后发生了什么

【背景】

在一次问题排查过程中,发现偶现调用"InetAddress.getByName()"无法通过域名解析到IP(实际在容器中都能正确解析到),因此怀疑和容器的DNS解析有问题。但在与容器的开发兄弟沟通过程中,被反问了一句,确定该方法一定触发调用了DNS的域名解析吗?

对此问题一时半会无法准确的答复,因此花了些时间对背后的逻辑原理,相关源码(涉及JDK、glibc源码)进行走读分析,并总结分享。

【准备知识】

1. IP

IP指网络互联协议,即Internet Protocol的缩写,是TCP/IP体系中的网络层协议。设计IP的目的是提高网络的可扩展性:一是解决互联网问题,实现大规模、异构网络的互联互通;二是分割顶层网络应用和底层网络技术之间的耦合关系。

IP规定网络上所有的设备都必须有一个独一无二的地址,即IP地址。

2. 主机名

主机名也就是一个网络设备的别名。是连接到计算机网络中并具有特定IP地址的计算机或任何设备的昵称。

3. 域名

根据百度百科的介绍:

域名(Domain Name),又称网域,是由一串用点分隔的名字组成的Internet上某一台计算机或计算机组的名称,用于在数据传输时对计算机的定位标识。

由于IP地址具有不方便记忆并且不能显示地址组织的名称和性质等缺点,人们设计出了域名,并通过域名名称系统(DNS)来讲域名和IP地址相互映射,使人更方便地访问互联网,而不用去记住能够被机器直接读取的IP地址数串。

域名通常由两部分组成:顶级域名和次级域名,最后一个"."的右边被称为顶级域名(Top-Level Domain),最后一个"."左边部分被称为二级域名,二级域名的左边为三级域名,以此类推。

注:主机名与域名的区别

主机名就是机器本身的名字,而域名是用来解析到IP的。但在局域网中,通过一定配置,主机名也可以解析到IP。

4. FQDN&PQDN

FQDN是"Full Qualified Domain Name"的简称,翻译过来称为完全合规域名 或 完全限定域名。FQDN的组成格式为:

[hostname].[domain].[tld].
# FQDN 由主机名+域名两部分组成, 其中hostname 为主机名; 而域名则是包含了顶级域的全路径
# 注意FQDN以"."结束

与FQDN相对应的就是PQDN(Partially Qualified Domain Name)部分限定域名。通常情况下,仅引用域名的一部分,而没有全部指定的就是PQDN。

5. DNS

域名系统,即Domain Name System的简称,是英特网中作为域名和IP地址互相映射的一个分布式数据库,能够使用用户更方便的访问互联网,而不用记住能够被机器直接读取的IP数串。通过主机名/域名,最终能够得到该主机/域名对应的IP地址的过程称为域名解析(或主机名解析)。

DNS的分布式数据库是以域名为索引的,每个域名实际上就是一颗很大的逆向树中的路径。

【相关的系统配置】

1. /etc/hosts

该配置文件的作用就是配置主机IP以及对应的主机名。一般情况下,该文件的每行为一个主机,且由三部分组成,以空格分隔开。第一部分为IP地址;第二部分为主机名或域名;第三部分为主机名。当然,每行也可以为两部分,即IP地址和主机名。

127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.168.3.21   nn-0-hncscwc.network-hncscwc nn-0

2. /etc/resolv.conf

是DNS客户端的配置文件,用于设置DNS服务器的地址,以及主机的域名搜索顺序。其格式很简单,每行以一个关键字开头,后面接一个或多个由空格分隔的参数。

一个典型的配置文件如下所示:

nameserver 10.254.0.2
search hncscwc-1.svc.cluster.local svc.cluster.local cluster.local kube-system.svc.cluster.local
options ndots:5

其中nameserver指明dns服务器的地址,可以有多行,每行指定一个DNS服务器的地址,查询时按照先后顺序,依次进行查询,但是仅当前面一个nameserver查询失败时才从后面nameserver继续进行查询。

search则指明搜索域的顺序,配合options中的ndots选项,对短域名进行补齐,然后再进行查询。

options为DNS的参数选项,可选的参数较多,如下图所示:

bf25322f47609f5c1ea7fea8a144682d.jpeg

这里重点讲解下 ndots。ndots指定的值,表示请求查询的域名中,如果点的个数小于指定的值,则按照search配置的内容,依次添加对应的后缀,然后再进行域名解析,直到获取到解析后的地址。而请求查询的域名中,如果点的个数大于等于指定的值,则不会进行补齐动作。

domain指明本地域名。注:domain与search不能同时并存。

3. /etc/host.conf

该配置文件的作用为指明如何解析主机域名(作用于libresolv.so)。

可选的配置项包括:

  • multi:有效值为on/off,当配置为on时,会返回/etc/hosts中出现的主机的所有有效地址,否则仅返回第一个。
  • nospoof:表示是否允许服务器对IP地址进行欺骗 on表示不允许 off表示不允许
  • reorder:表示是否对查询结果进行重新排序 on表示重新排序 off表示不重新排序
  • trim:这个关键字可以多次多次出现,每次出现其后应该跟随单个的以"."开头的域名,如果设置了它,libresolv.so会自动截断通过DNS解析出来的主机名后面的域名。
  • spoofalert:有效值为on/off,仅在nospoof配置为on时有效,即两者均配置为on,当发生IP地址欺骗时记录告警或错误日志

注:老的版本里可能还会有order配置项,指明解析顺序,但从man中已经无该配置项的说明,同时从glibc的代码中可以看到,仅解析了该字段但不做任何处理。

4. /etc/nsswitch.conf

名称服务开关(Name Service Switch)配置文件,主要用于指定glibc以及某些应用程序对名称解析的顺序。和主机、域名解析相关的配置项包括:

hosts: files dns
# 用于 gethostbyname 等相关函数
# files表示先读取 /etc/hosts
# dns 表示查询 dns
# 其他可选值还包括 db, nisplus, nis等
networks: files
# 用于 getnetent 等相关函数
# files 表示先读取 /etc/networks
# 其他可选值包括 nisplus

【常用操作】

在我们常见的操作中,就是将一个主机名/域名解析成IP地址,或者是知道IP地址,反查对应的域名。

对于主机名/域名解析成IP地址,最简单的办法就是用ping命令,例如:

[root@nn-0 /]# ping nn-0-hncscwc
PING nn-0-hncscwc (172.168.3.21) 56(84) bytes of data.
64 bytes from 172.168.3.21 (172.168.3.21): icmp_seq=1 ttl=64 time=0.061 ms

而对于IP反查域名,则可以使用nslookup命令,例如:

[root@nn-0 /]# nslookup 172.168.3.21
21.3.168.172.in-addr.arpa name = nn-0-hncscwc.network-hncscwc.

对于ping内部,先通过gethostbyname的系统调用,将非IP地址的主机/域名转换为IP地址,然后发送ICMP报文。

对于"gethostbyname"、"gethostbyaddr"(通过IP地址获取主机/域名)的系统调用,简单示例代码如下所示:

#include <stdio.h>
#include <netdb.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>

int main(int argc, char * argv[])
{
    if( argc < 2) {
        printf("the argc need more two\n");
        return 1;
    }
    struct hostent *host;
    const char * addr = argv[1];
    char p[30];
    // 对于IPv4类型IP地址 通过IP地址获取域名
    if(inet_pton(AF_INET,addr, p) == 1) {
        host = gethostbyaddr(p, strlen(p), AF_INET);
    } else {
        // 对于非IPv4地址, 通过主机名/域名获取IP
        host = gethostbyname(addr);
    }
    if(NULL != host) {
        printf("hostname: %s\n", host->h_name);
        struct sockaddr_in ipaddr;
        memcpy((char*)&ipaddr.sin_addr, host->h_addr_list[0], host->h_length);
        printf("ipaddr: %s\n", inet_ntoa(ipaddr.sin_addr));
    } else {
       herror("gethostbyname");
    }
    return 0;
}

编译的执行效果为:

ad451a0ecc896b7e9203470762e68329.jpeg

另外,从man中可以知道gethostbyname,gethostbyaddr已经是过时的方法,正确的方式应该是调用getaddrinfo和getnameinfo

06888cbe97e5cdaac0557c525f77d6ab.jpeg

但这两个方法的内部流程和gethostbyname基本雷同。

而java中InetAddress类的getByName、getByAddress、getAllByName等方法,本质上是调用了系统函数getaddrinfo或gethostbyname来进行主机名/域名到IP之间的转换。相关代码如下所示:

// InetAddress.java
public static InetAddress getByName(String host)
    throws UnknownHostException {
    return InetAddress.getAllByName(host)[0];
}

public static InetAddress[] getAllByName(String host)
    throws UnknownHostException {
    return getAllByName(host, null);
}

private static InetAddress[] getAllByName(String host, InetAddress reqAddr)
    throws UnknownHostException {
    ...
    return getAllByName0(host, reqAddr, true);
}

private static InetAddress[] getAllByName0 (String host, InetAddress reqAddr, boolean check)
    throws UnknownHostException {
    ...
    if (addresses == null) {
        addresses = getAddressesFromNameService(host, reqAddr);
    }
    ...
}

// Inet4AddressImpl.c
private static InetAddress[] getAddressesFromNameService(String host, InetAddress reqAddr)
    throws UnknownHostException {
        ...
        addresses = nameService.lookupAllHostAddr(host);
        ...
}

JNIEXPORT jobjectArray JNICALL
Java_java_net_Inet4AddressImpl_lookupAllHostAddr(JNIEnv *env, jobject this,
                                                jstring host) {
    if (!initializeInetClasses(env)) {
        return NULL;
    }

#if defined(__GLIBC__)
    if (glibc_major_version >= 2 && glibc_minor_version >= 12) {
        return lookupAllHostAddrs_getaddrinfo(env, this, host);
    } else {
        return lookupAllHostAddrs_gethostbyname(env, this, host);
    }
#else
    return lookupAllHostAddrs_getaddrinfo(env, this, host);
#endif
}

static jobjectArray
lookupAllHostAddrs_getaddrinfo(JNIEnv *env, jobject this, jstring host) {
    ...
    error = getaddrinfo(hostname, NULL, &hints, &res);
    ...
}

【调用背后发生了什么】

对于"gethostbyname"的系统调用,背后具体又发生了什么呢?从glibc的源码角度来看,总体分为这么两个步骤:

  • 初始化这里包括打开/etc/host.conf、/etc/resolv.conf,从配置文件中解析对应的内容。相关配置的值后续需要用到。
  • 地址解析打开/etc/nsswitch.conf,读取hosts的内容,并根据其顺序,依次加载对应的动态库(libnss_xxx.so),并调用动态库中的方法完成地址的解析。如果通过某一项能正确进行地址解析,则不进行后续动作。从系统动态库中可以看到,每个配置项都有一个对应的动态库。

0fe1e17b2c3d02ad270734de50c661c6.jpeg

对于配置项files而言(libnss_files.so)就是读取/etc/hosts中的内容。而对于dns(libnss_dns.so)自然就是向dns服务器进行查询。

例如在下面配置中执行"gethostbyname":

[root@hdp-hadoop-hdp-namenode-0 opt]# cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
fe00::0 ip6-mcastprefix
fe00::1 ip6-allnodes
fe00::2 ip6-allrouters

[root@hdp-hadoop-hdp-namenode-0 opt]# cat /etc/nsswitch.conf | grep hosts
#hosts:     db files nisplus nis dns
hosts:      files dns myhostname

[root@hdp-hadoop-hdp-namenode-0 opt]# ./main hdp-hadoop-hncscwc-namenode-0
hostname: hdp-hadoop-hncscwc-namenode-0
ipaddr: 172.16.21.104

其strace的分析过程如下所示:

501a7f8c3050fa6afc17fa6556c0e823.jpeg

有兴趣的朋友可以通过strace去分析下其调用流程。

好了,这就是本文的全部内容,如果觉得本文对您有帮助,请点赞+转发,也欢迎加我微信交流~

标签: 网络 linux java

本文转载自: https://blog.csdn.net/hncscwc/article/details/127438055
版权归原作者 陈猿解码 所有, 如有侵权,请联系我们删除。

“InetAddress.getByName背后发生了什么”的评论:

还没有评论