1. 程式人生 > >網絡編程-Java中的Internet查詢

網絡編程-Java中的Internet查詢

平臺 route 存在 最大 基礎概念 net 過度 ipv 十六

前提

在深入理解URL、URI等概念,或者學些Socket相關的知識之,有必要系統理解一下Internet相關的一些基礎知識。

Internet地址

連接到Internet(因特網)的設備稱為節點(node),而任意一個計算機節點稱為主機(host)。每個節點或者主機都由至少一個唯一的數來標識,這稱為Internet地址或者IP地址。

IP和域名

如果使用Java作為開發語言的話,不需要擔心IP或者域名的工作原理,但是我們需要理解IP尋址的一些基礎知識。我們目前常用的網絡都是IPv4網絡,每個計算機節點都是由一個4字節(32bit)的數字標識,這個數字標識的格式是點分四段(dotted quad,形式是:xxx.xxx.xx.xx),其中的每個數字都是一個無符號字節,取值範圍是0到255。當數據通過網絡傳輸的時候,數據包的首部中要包括要發往的機器地址(目的地址)和發送這個數據包的機器地址(源地址)。

可以使用的IPv4類型的IP地址總量大概是40億多一點,因此無法做到地球上每個人都分配一個唯一的IPv4的IP地址,所以IPv6就誕生了,目前網絡由IPv4向IPv6過度(不過這個過度過程相對緩慢,因素很多)。IPv6網絡中的IP地址使用16字節(128bit)的數字標識。IPv6地址的表示形式通常是以英文冒號分隔的8個區域,每個區域都是4個十六進制的數字,舉個例子:FEDC:BA98:7654:3210:FEDC:BA98:7654:3210就是一個合法的IPV6地址。而在IPv4和IPv6的混合網絡中,IPv6地址的最後四個字節有時候表示形式為IPv4地址的點分四段地址。IPv6地址只在Jdk1.4以及之後的版本支持,換言之,Jdk1.3或者之前的版本只能使用IPv4地址。

雖然計算機可以輕松地處理數字,但是人腦的記憶對於數字並不敏感,因此開發了域名系統(Domain Name System,也就是DNS),用於將人腦易於記憶的主機名(如www.baidu.com)轉換為數字Internet地址(如183.232.231.173)。這裏不展開DNS的具體內容,作為開發者,我們可以簡單理解為它就是一個巨型分布式數據庫,用於映射主機名(域名)和IP地址,畫個圖大致如下:

技術分享圖片

端口

因為每臺計算機都不是只做一件事,相當於計算機做的每一種業務邏輯需要從邏輯上隔離,例如FTP請求的處理要和電子郵件的處理分離,FTP請求處理也要和Web業務處理分離,所以每種業務邏輯的處理需要使用一個邏輯分離的標識,這個標識就是端口(port)。一般每臺計算機有成千上萬個邏輯端口(確切來說,每個傳輸層協議都有65535個端口,Windows系統中,1~1023號端口是系統端口,用戶無法修改,1024~65534端口是系統為用戶預留的端口,而65535號端口為系統保留端口),每個端口可以分配給一個特定的服務。例如Web的底層協議Http協議通訊一般使用80端口,使用瀏覽器URL訪問服務器的80端口可以省略URL中的端口號。

Java對網絡的抽象

InetAddress

單詞InetAddress是Internet Address的縮寫合並,代表因特網地址。java.net.InetAddress類是Java對IP地址(包括IPv4和IPv6地址)的高度抽象表示。大多數網絡編程相關的類都會用到InetAddress,如Socket、ServerSocket等,InetAddress兩個最核心的屬性是主機名(host)和IP地址,對應屬性hostName和address。

創建InetAddress實例

創建InetAddress實例主要依賴它的工廠方法(實際上InetAddress的構造函數是包私有的,也就是無法通過new關鍵字創建實例),比較常用的一個靜態工廠方法是:

static InetAddress getByName(String host) throws UnknownHostException

其中參數可以為主機名(域名)或者點分四段地址,前者相當於通過主機名查找一個可連接的IP地址,後者相當於通過IP地址反查主機名,值得註意的是,這個方法調用的使用會建立與本地DNS服務器的連接進行主機名或者數字地址查找,如果DNS服務器找不到主機或者地址,會拋出UnknownHostException異常。

    public static void main(String[] args) throws Exception{
        InetAddress inetAddress = InetAddress.getByName("www.baidu.com");
        System.out.println(inetAddress);
    }

InetAddress覆寫了toString方法,返回結果是hostName/address格式,上面的main方法執行的一個可能的結果是:

www.baidu.com/14.215.177.39

有些時候,我們知道數字IP地址,就可以由數字地址直接創建一個InetAddress實例,這樣就可以不必使用getByName(String host)方法和DNS交互。

static InetAddress getByAddress(byte[] addr)throws UnknownHostException
static InetAddress getByAddress(String host,byte[] addr)throws UnknownHostException

這兩個方法可以創建主機名不存在或者主機名無法解析的InetAddress實例。舉個例子:

    public static void main(String[] args) throws Exception {
        byte[] bytes = {14, (byte) 215, (byte) 177, 39};
        InetAddress inetAddress = InetAddress.getByAddress("www.doge.com",bytes);
        System.out.println(inetAddress);
    }

實際上,域名www.doge.com並不存在,但是這個方法並不會拋出異常。

如果要查詢一個主機名的所有IP地址,可以使用:

static InetAddress[] getAllByName(String host) throws UnknownHostException

如果需要查詢本機的主機名和IP地址,可以使用:

static InetAddress getLocalHost() throws UnknownHostException

註意這個方法會嘗試連接DNS去查詢本地計算機的真正的主機名和IP地址,如果查詢失敗,它就會返回回送地址,也就是主機名是"localhost",IP地址是點分四段地址"127.0.0.1"。

InetAddress緩存

DNS查找的開銷可能相當大(如果請求需要經過多個中間服務器或者嘗試解析一個不可達的主機,可能需要耗費幾秒的時間),所以InetAddress會緩存DNS查詢結果,也就是一旦得到一個給定主機的地址,就不會再次查找,即使為同一個主機創建多個InetAddress實例,也不會再次進行DNS查詢。這樣的緩存機制對於性能來說是有好處的,但是也會帶來負面影響:

  • 程序運行期間連接的主機的IP地址很大可能會發生變化,已緩存的IP有可能不可用。
  • 剛開始嘗試解析一個主機時候是失敗的,但是隨後嘗試解析的時候會成功,但是緩存了首次解析失敗的記錄。
  • 遠程DNS服務器發送的信息還在傳輸,第一次嘗試超時,下一次請求即可成功。

因此,Java對於不成功的DNS查詢結果僅僅緩存10秒,而且可以通過下面兩個系統變量控制緩存的時間:

  • networkaddress.cache.ttl:成功的DNS查詢結果的緩存時間(秒數),-1表示不會超時。
  • networkaddress.cache.negative.ttl:成功的DNS查詢結果的緩存時間(秒數),-1表示不會超時。

InetAddress提供的基本屬性獲取方法

InetAddress提供四個基本屬性獲取方法,用於獲取當前InetAddress表示的主機名和IP地址。

public String getHostName();
public String getCanonicalHostName();
public byte[] getAddress();
public String getHostAddress();

註意上面的幾個方法只有Getter,沒有Setter方法,說明這幾個屬性的設置權限是java.net包中的類庫。

  • getHostName:返回當前InetAddress實例的主機名,如果對應的機器沒有主機名或者安全管理器阻止確定主機名,則會返回點分四段數字IP地址。
  • getCanonicalHostName:getCanonicalHostName與getHostName,不過getHostName方法只是在不知道主機名的情況下才連接DNS進行查詢,getCanonicalHostName方法總是連接DNS查詢主機名並且替換緩存值,所以這個方法調用會比較耗時。
  • getAddress:返回當前InetAddress實例的數字IP地址的byte數組,註意因為Java中沒有無符號的byte,因此負數byte值要+256變成int類型才是無符號的byte值。
  • getHostAddress:實際上就返回getAddress方法中的byte數組轉換成的IP地址點分四段表示形式的字符串,也就是IP地址字符串。

上面的getAddress()方法還有一個特殊的判斷使用場景,就是它的返回值byte數組的長度如果是4,那麽InetAddress一定是Inet4Address的實例,如果長度為16,那麽那麽InetAddress一定是Inet6Address的實例,由此可以判斷InetAddress中的IP地址到底是IPv4還是IPv6。

InetAddress提供的地址類型判斷方法

有些IP地址和地址模式有特殊的含義,例如127.0.0.1是本地回送地址,244.0.0.0到239.255.255.255範圍內的IPv4地址是組播地址。InetAddress中提供10個公有實例方法來判斷InetAddress對象是否符合這些地址模式:

  • 1、boolean isAnyLocalAddress():如果地址是通配地址則返回true,所謂通配地址就是可以匹配本地系統中的任何地址,在IPv4中的通配地址是0.0.0.0,在IPv6中的通配地址是0:0:0:0:0:0:0:0(::)。
  • 2、boolean isLoopbackAddress():如果地址是回送地址則返回true,在IPv4中的回送地址是127.0.0.1,在IPv6中的回送地址是0:0:0:0:0:0:0:1(::1)。
  • 3、boolean isLinkLocalAddress():如果地址是一個IPv6本地鏈接地址則返回true。
  • 4、boolean isSiteLocalAddress():如果地址是一個IPv6本地網站地址則返回true。
  • 5、boolean isMulticastAddress():如果地址是一個組播地址則返回true。
  • 6、boolean isMCGlobal():如果地址是一個全球組播地址則返回true。
  • 7、boolean isMCOrgLocal():如果地址是一個組織範圍組播地址則返回true。
  • 8、boolean isMCSiteLocal():如果地址是一個網站範圍組播地址則返回true。
  • 9、boolean isMCLinkLocal():如果地址是一個子網範圍組播地址則返回true。
  • 10、boolean isMCNodeLocal():如果地址是一個本地接口組播地址則返回true。

實際上,我們很少用到這十個方法。

InetAddress的可達性測試

InetAddress提供兩個isReaachable()方法用於測試可達性。其實就是測試一個特定的節點對當前主機是否可達(兩者是否能夠建立一個網絡連接)。因為網絡連接有可能因為多種原因阻塞,列舉一些原因如下:

  • 防火墻攔截。
  • 代理服務器攔截。
  • 不能正常服務的路由器。
  • 斷開的網絡線纜。
  • 嘗試連接的遠程計算機沒有開機。
public boolean isReachable(int timeout) throws IOException
public boolean isReachable(NetworkInterface netif, int ttl, int timeout) throws IOException

這兩個方法會嘗試使用Traceroute查看指定地址是否可達。Traceroute程序使用ICMP報文和IP首部中的TTL字段(一般為64)。TTL字段的目的是防止數據報在選路時候無休止的在網絡中流動(當路由故障的時候,可能在兩個路由循環)。可以理解TTL字段用於控制連接被丟棄之前的網絡最大跳數。第一個方法isReachable(int timeout)只有一個參數控制檢測可達性的超時毫秒數,第二個方法可以控制指定本地的網絡接口、TTL參數和超時時間進行可達性測試。

    public static void main(String[] args) throws Exception{
        InetAddress inetAddress = InetAddress.getByName("www.baidu.com");
        System.out.println(inetAddress.isReachable(2000));
    }

比較不同的InetAddress

InetAddress中覆蓋了equalshashCode方法,比較的時候,實際上比較的是address屬性,也就是IP地址,換言之,只要兩個InetAddress的IP地址一致,這兩個InetAddress對象就是相等的,並不要求兩個InetAddress對象的主機名一致。舉個例子:

    public static void main(String[] args) throws Exception {
        while (true){
            Thread.sleep(500);
            InetAddress inetAddress = InetAddress.getByName("www.baidu.com");
            InetAddress other = InetAddress.getByName("14.215.177.39");
            System.out.println(inetAddress.equals(other));
        }
    }

上面的main方法執行之後,基本上打印true,取決於DNS的處理。

Inet4Address和Inet6Address

Inet4Address和Inet6Address兩個類都繼承自InetAddress,它們分別是IPv4和IPv6的IP地址的高度抽象表示。但是,一般情況下,開發者使無須感知使用或者連接的IP地址到底是IPv4和IPv6的IP地址,因為通常我們都是通過主機名(host)去訪問。

本地網絡接口

NetworkInterface是本地網絡接口,實際上它可以表示一個物理接口,如以太網卡,它也可以表示一個虛擬接口,與機器的其他IP地址綁定到同一個物理硬件,最常見的就是現在的虛擬化容器如Docker提供的網卡。NetworkInterface類提供的一些方法可以枚舉所有的本地地址,通過這些本地地址創建的InetAddress對象,創建出來的這些InetAddress對象就可以使用在客戶端Socket或者服務端Socket。

NetworkInterface的創建方法

static NetworkInterface getByName(String name) throws SocketException 

getByName(String name)方法可以指定網絡接口名字獲取對應的網絡接口實例NetworkInterface,如果沒有這樣名字的網絡接口則返回null。網絡接口的名字格式和平臺相關,例如典型的Unix系統上,以太網接口名字的形式為eth0、eth1等等,在Windows系統中名字類似於"CE31"、"ELX100"等字符串,"lo"一般是本地回送地址的網絡接口名字。

另外,可以通過InetAddress返回指定IP綁定的網絡接口(或者說返回的網絡接口處理指定的IP地址),如果本地主機沒有網絡接口和傳入的IP地址綁定則返回null,如果發生錯誤則拋出一個SocketException。

static NetworkInterface getByInetAddress(InetAddress addr) throws SocketException 

最後,可以使用下面的方法枚舉本地主機上的所有網絡接口。

static Enumeration<NetworkInterface> getNetworkInterfaces() throws SocketException

筆者用的是Windows10系統,嘗試枚舉一下所有的網絡接口:

    public static void main(String[] args) throws Exception{
        Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
        while (networkInterfaces.hasMoreElements()){
            NetworkInterface networkInterface = networkInterfaces.nextElement();
            System.out.println(networkInterface);
        }
    }

執行結果部分如下:

name:lo (Software Loopback Interface 1)
name:ppp0 (WAN Miniport (PPPOE))
name:net0 (Microsoft ISATAP Adapter #2)
name:net1 (Microsoft ISATAP Adapter)
name:net2 (WAN Miniport (L2TP))
name:net3 (WAN Miniport (IKEv2))
...

NetworkInterface提供的屬性獲取方法

NetworkInterface提供一個實例方法public Enumeration<InetAddress> getInetAddresses()用於獲取綁定在一個網絡接口上面的所有IP地址,雖然這種情況不常見,但是確實存在。

實例方法public String getName()返回NetworkInterface實例的對象名稱,例如eth0、lo等。

實例方法public String getDsiplayName()也是返回NetworkInterface實例的對象名稱,不過表示的形式更加友好,例如"Ethernet Card 0"(eth0),但是在Unix系統中和getName()相同。

小結

只有理解網絡編程中的一些基礎概念,才能鋪好學習URI(URL)、TCP協議、HTTP協議和套接字(Socket)的路。

(本文完 c-2-d)

網絡編程-Java中的Internet查詢