Java Socket 地址結構
1. 前言
我們知道計算機網絡中連接的設備有很多,比如 PC、手機、打印機、路由器、交換機、網關等,通常把這些網絡設備叫做節點(Node)。每一個節點都分配有唯一的 IP 地址,用以標識此設備。IP 地址包含 32 位 IPv4 和 128 位 IPv6 兩個版本。由于 IP 地址是一串數字或者是字節序列,對計算機是友好的,但是對我們人類非常不友好,不利于傳播、記憶。為此,計算機科學家又開發了一套 DNS 系統,給每一臺計算機分配了唯一的、對人類友好的主機名字,通常叫做域名。比如,www.xianlaiwan.cn 是慕課網主站的域名。當然,有的主機會分配多個域名。
人們常說生活沒有那么簡單,往往是解決了一個老問題,又引出了新問題。當你開發了 DNS 系統以后,我們人類確實方便了,可是域名對計算機來說不方便,計算機更喜歡 IP 地址。這就又需要解決 IP 地址和域名之間相互解析、映射的問題,當然這些問題在 DNS 系統中都得到了妥善的處理。域名解析系統是一個分布式集群系統,是一個樹形結構。一次域名解析可能需要經過本地緩存、本地域名服務器、遠程域名服務器之間多次交互。
從上面的描述可以看出,IP 地址和域名之間的相互解析是一套非常復雜的機制。好在操作系統將這一套復雜的機制進行了封裝,以 API 的形式提供給網絡程序員,這樣極大的簡化了編程的復雜度。
一般操作系統都提供了 C 語言接口 getaddrinfo 和 getnameinfo,前者的功能是通過域名獲取 IP 地址,后者的功能是通過 IP 地址獲取域名。
在 Java 平臺中,java.net.InetAddress 類實現了完整的 IP 地址和域名之間的相互解析機制。
2. InetAddress 類的體系結構
java.net.InetAddress 類的體系結構如下:
各類的功能說明:
- InetAddress 是 Java IP 地址的包裝類,也是域名解析的核心類。
- Inet4Address 代表了 IPv4 地址格式的封裝,一般程序員不需要關心此類。
- Inet6Address 代表了 IPv6 地址格式的封裝,一般程序員不需要關心此類。
- InetSocketAddress 是 Socket 地址的封裝,它通過私有內部類 InetSocketAddressHolder 間接包裝了 InetAddress 結構和 端口號(Port)。在網絡編程中,通常把 Socket 地址叫做 Endpoint,用 <IP, Port> 的組合來表示。
在網絡編程中,應用最為頻繁的兩個類是 InetSocketAddress 和 InetAddress。其中,InetSocketAddress 類對 InetAddress 和 Port 進行了封裝,形成了完整的 Socket 地址。而 InetAddress 核心實現就是域名解析和緩存。
InetAddress 類沒有 public 構造方法,提供了一組 public static 工廠方法用以創建 InetAddress 實例。接下來,我們重點分析一下 getByName 和 getByAddress 兩類方法。
3. getByName 方法
InetAddress 提供了兩個公有靜態方法 getByName 和 getAllByName 來構造 InetAddress 實例,它們的原型如下:
// 創建單個 InetAddress 實例
public static InetAddress getByName(String host) throws UnknownHostException
// 創建多個 InetAddress 實例
public static InetAddress[] getAllByName(String host) throws UnknownHostException
這兩個方法都會連接域名解析服務器進行域名解析,具體工作原理如下:
- 首先會檢查傳入參數 host,也就是域名。如果傳入參數為 null,那么會返回以 loopback 地址構造的 InetAddress 結構。
- 如果輸入參數 host 是一個 IP 地址,那么根據 IP 地址是 IPv4 還是 IPv6,分別構造 Inet4Address 或 Inet6Address 結構,并且返回。
- 查詢本地 Cache,如果本地 Cache 中已經存在 host 相應的地址,則直接返回。
- 如果本地 Cache 查詢失敗,則遍歷本地注冊的 name services。如果有定制的 name services 注冊,那么會調用此定制的 name services。如果沒有定制的 name services,那么會調用 default name services,最終會調用系統的 getaddrinfo 函數。getaddrinfo 是一個 POSIX 標準函數,一般系統都會實現。
getByName 方法的應用非常簡單,示例如下:
public static void testInetAddressByName(String host){
try {
InetAddress addr = InetAddress.getByName(host);
System.out.println("getByName addr=" + addr.toString());
InetAddress[] addrs = InetAddress.getAllByName(host);
for (InetAddress a: addrs){
System.out.println("getAllByName addr=" + a.toString());
}
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
測試 wwww.xianlaiwan.cn 域名,執行結果如下:
getByName addr=www.xianlaiwan.cn/115.182.41.103
getAllByName addr=www.xianlaiwan.cn/115.182.41.103
getAllByName addr=www.xianlaiwan.cn/117.121.101.144
getAllByName addr=www.xianlaiwan.cn/115.182.41.180
getAllByName addr=www.xianlaiwan.cn/117.121.101.40
getAllByName addr=www.xianlaiwan.cn/117.121.101.134
getAllByName addr=www.xianlaiwan.cn/115.182.41.163
需要注意的是 getByName 方法會拋出 UnknownHostException 異常,需要捕獲。
4. getByAddress 方法
如果你有明確的 IP 地址,并不需要進行域名解析,可以調用 InetAddress 提供的另一組工廠方法 getByAddress,方法原型如下:
public static InetAddress getByAddress(byte[] addr) throws UnknownHostException
public static InetAddress getByAddress(String host, byte[] addr) throws UnknownHostException
這是兩個重載的 public static 方法,功能都類似:
- 第一個重載的 getByAddress 方法提供一個參數,即用 byte [] 類型的數組表示的 IP 地址。
- 第二個重載的 getByAddress 方法提供兩個參數,用 String 類型表示的域名(host),和用 byte [] 類型的數組表示的 IP 地址。
- 二者都不進行域名解析,只是根據輸入參數構造 InetAddress 實例。
- 接收 host 輸入參數的 getByAddress 方法不保證域名和 IP 地址的對應關系,也不保證域名是否可以訪問。
getByAddress 方法應用的示例代碼如下:
public static void testInetAddressByAddr()
{
byte[] ips = new byte[]{ (byte)192, (byte)168,1,101};
try {
InetAddress addr = InetAddress.getByAddress(ips);
System.out.println("getByAddress addr=" + addr.toString());
InetAddress addr2 = InetAddress.getByAddress("www.example.com", ips);
System.out.println("getByAddress with host addr=" + addr2.toString());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
我們輸入 192.168.1.101,執行結果如下:
getByAddress addr=/192.168.1.101
getByAddress with host addr=www.example.com/192.168.1.101
5. InetAddress 的 Cache 策略
由于域名解析需要客戶端和域名服務器經過很多次交互,一般都比較耗費時間,所以 InetAddress 提供了 Cache 機制。這樣,當客戶程序調用 getByName 解析域名的時候,首先是從 Cache 中查找,這樣可以極大提高域名解析的效率。
域名綁定的 IP 地址可能會發生變化,所以 Cache 中存儲的 IP 地址也是有生命周期的。Java 提供了兩個全局參數可以用來配置 Cache 的有效時間。
-
networkaddress.cache.ttl
成功解析的域名在 Cache 中的存活時間。 -
networkaddress.cache.negative.ttl
解析失敗的域名在 Cache 中的存活時間。
實際上除了 Java 本地有 Cache 機制,域名解析服務器也是有 Cache 機制的,目的都是相同的。
6. 小結
InetAddress 類在網絡編程中的應用是非常頻繁的,了解域名解析機制有利于我們更好的應用此類的功能。在實際產品應用中都是通過 getByName 方法構造 InetAddress 實例的,盡量避免通過 getByAddress 方法構造 InetAddress 實例。這樣可以提高程序的維護性。當然,在實驗室的內網環境中進行開發測試,往往采用的是私有 IP 地址,這時可以通過 getByAddress 方法來構造 InetAddress 實例。