• 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的组成格式为:

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

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

    5. DNS

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

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

    【相关的系统配置】

    1. /etc/hosts

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

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

    2. /etc/resolv.conf

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

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

    1. nameserver 10.254.0.2
    2. search hncscwc-1.svc.cluster.local svc.cluster.local cluster.local kube-system.svc.cluster.local
    3. 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以及某些应用程序对名称解析的顺序。和主机、域名解析相关的配置项包括:

    1. hosts: files dns
    2. # 用于 gethostbyname 等相关函数
    3. # files表示先读取 /etc/hosts
    4. # dns 表示查询 dns
    5. # 其他可选值还包括 db, nisplus, nis等
    6. networks: files
    7. # 用于 getnetent 等相关函数
    8. # files 表示先读取 /etc/networks
    9. # 其他可选值包括 nisplus

    【常用操作】

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

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

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

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

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

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

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

    1. #include 
    2. #include 
    3. #include 
    4. #include 
    5. #include <string.h>
    6. int main(int argc, char * argv[])
    7. {
    8.     if( argc < 2) {
    9.         printf("the argc need more two\n");
    10.         return 1;
    11.     }
    12.     struct hostent *host;
    13.     const char * addr = argv[1];
    14.     char p[30];
    15.     // 对于IPv4类型IP地址 通过IP地址获取域名
    16.     if(inet_pton(AF_INET,addr, p) == 1) {
    17.         host = gethostbyaddr(p, strlen(p), AF_INET);
    18.     } else {
    19.         // 对于非IPv4地址, 通过主机名/域名获取IP
    20.         host = gethostbyname(addr);
    21.     }
    22.     if(NULL != host) {
    23.         printf("hostname: %s\n", host->h_name);
    24.         struct sockaddr_in ipaddr;
    25.         memcpy((char*)&ipaddr.sin_addr, host->h_addr_list[0], host->h_length);
    26.         printf("ipaddr: %s\n", inet_ntoa(ipaddr.sin_addr));
    27.     } else {
    28.        herror("gethostbyname");
    29.     }
    30.     return 0;
    31. }

    编译的执行效果为:

    ad451a0ecc896b7e9203470762e68329.jpeg

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

    06888cbe97e5cdaac0557c525f77d6ab.jpeg

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

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

    1. // InetAddress.java
    2. public static InetAddress getByName(String host)
    3.     throws UnknownHostException {
    4.     return InetAddress.getAllByName(host)[0];
    5. }
    6. public static InetAddress[] getAllByName(String host)
    7.     throws UnknownHostException {
    8.     return getAllByName(host, null);
    9. }
    10. private static InetAddress[] getAllByName(String host, InetAddress reqAddr)
    11.     throws UnknownHostException {
    12.     ...
    13.     return getAllByName0(host, reqAddr, true);
    14. }
    15. private static InetAddress[] getAllByName0 (String host, InetAddress reqAddr, boolean check)
    16.     throws UnknownHostException {
    17.     ...
    18.     if (addresses == null) {
    19.         addresses = getAddressesFromNameService(host, reqAddr);
    20.     }
    21.     ...
    22. }
    23. // Inet4AddressImpl.c
    24. private static InetAddress[] getAddressesFromNameService(String host, InetAddress reqAddr)
    25.     throws UnknownHostException {
    26.         ...
    27.         addresses = nameService.lookupAllHostAddr(host);
    28.         ...
    29. }
    30. JNIEXPORT jobjectArray JNICALL
    31. Java_java_net_Inet4AddressImpl_lookupAllHostAddr(JNIEnv *env, jobject this,
    32.                                                 jstring host) {
    33.     if (!initializeInetClasses(env)) {
    34.         return NULL;
    35.     }
    36. #if defined(__GLIBC__)
    37.     if (glibc_major_version >= 2 && glibc_minor_version >= 12) {
    38.         return lookupAllHostAddrs_getaddrinfo(env, this, host);
    39.     } else {
    40.         return lookupAllHostAddrs_gethostbyname(env, this, host);
    41.     }
    42. #else
    43.     return lookupAllHostAddrs_getaddrinfo(env, this, host);
    44. #endif
    45. }
    46. static jobjectArray
    47. lookupAllHostAddrs_getaddrinfo(JNIEnv *env, jobject this, jstring host) {
    48.     ...
    49.     error = getaddrinfo(hostname, NULL, &hints, &res);
    50.     ...
    51. }

    【调用背后发生了什么】

    对于"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":

    1. [root@hdp-hadoop-hdp-namenode-0 opt]# cat /etc/hosts
    2. # Kubernetes-managed hosts file.
    3. 127.0.0.1       localhost
    4. ::1     localhost ip6-localhost ip6-loopback
    5. fe00::0 ip6-localnet
    6. fe00::0 ip6-mcastprefix
    7. fe00::1 ip6-allnodes
    8. fe00::2 ip6-allrouters
    9. [root@hdp-hadoop-hdp-namenode-0 opt]# cat /etc/nsswitch.conf | grep hosts
    10. #hosts:     db files nisplus nis dns
    11. hosts:      files dns myhostname
    12. [root@hdp-hadoop-hdp-namenode-0 opt]# ./main hdp-hadoop-hncscwc-namenode-0
    13. hostname: hdp-hadoop-hncscwc-namenode-0
    14. ipaddr: 172.16.21.104

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

    501a7f8c3050fa6afc17fa6556c0e823.jpeg

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

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

  • 相关阅读:
    甲骨文、SUSE 和 CIQ (Rocky Linux )提供Open Enterprise Linux Association (OpenELA)
    裸辞半年,靠着这套Java面试宝典,拿下了腾讯T3
    高比例风电电力系统储能运行及配置分析
    高性价比的大带宽机器选哪里?泉州移动大带宽了解下
    34. 在排序数组中查找元素的第一个和最后一个位置
    破解视频会员(你我都懂)
    GoLand软件编码区中出现英文输入异常解决方法
    【仿牛客网笔记】 Redis,一站式高性能存储方案——Redis入门
    Intellij插件之ExtensionPoints
    Swing
  • 原文地址:https://blog.csdn.net/hncscwc/article/details/127438055