使用 Java 开发兼容 IPv6 的网络应用程序
根據(jù)現(xiàn)有 IPv4 地址的部署速度,剩余的地址將在 10 到 20 年被使用殆盡。因此網(wǎng)絡(luò)逐漸從 IPv4 向 IPv6 轉(zhuǎn)換是不可避免的,相應的各種網(wǎng)絡(luò)應用程序都將支持 IPv6。對于 Java,從其 1.4 版開始對 IPv6 提供了較好的支持,對程序員基本屏蔽了 IPv4 和 IPv6 的差異,但其中仍有一些 IPv6 引起的變化需要我們小心處理。針對這一情況,本文介紹了如何運用現(xiàn)有的 Java 技術(shù)應對這些變化。
IPv6 背景介紹
目前我們使用的是第二代互聯(lián)網(wǎng) IPv4 技術(shù),它的最大問題是網(wǎng)絡(luò)地址資源有限,從理論上講,可以編址 1600 萬個網(wǎng)絡(luò)、40 億臺主機。但采用 A、B、C 三類編址方式后,可用的網(wǎng)絡(luò)地址和主機地址的數(shù)目大打折扣,以至目前的 IP 地址近乎枯竭。網(wǎng)絡(luò)地址不足,嚴重地制約了全球互聯(lián)網(wǎng)的應用和發(fā)展。
一 方面是地址資源數(shù)量的限制,另一方面是隨著電子技術(shù)及網(wǎng)絡(luò)技術(shù)的發(fā)展,計算機網(wǎng)絡(luò)將進入人們的日常生活,可能身邊的每一樣東西都需要連入全球因特網(wǎng)。在這 種網(wǎng)絡(luò)空間匱乏的環(huán)境下,IPv6 應運而生。它的產(chǎn)生不但解決了網(wǎng)絡(luò)地址資源數(shù)量的問題,同時也為除電腦外的設(shè)備連入互聯(lián)網(wǎng)在數(shù)量限制上掃清了障礙。
如 果說 IPv4 實現(xiàn)的只是人機對話,那么 IPv6 則擴展到任意事物之間的對話,它不僅可以為人類服務(wù),還將服務(wù)于眾多硬件設(shè)備,如家用電器、傳感器、遠程照相機、汽車等,它將是無時不在,無處不在的深入 社會每個角落的真正的寬帶網(wǎng),它所帶來的經(jīng)濟效益也將非常巨大。
當然,IPv6 并非十全十美、一勞永逸,不可能解決所有問題。IPv6 只能在發(fā)展中不斷完善,也不可能在一夜之間發(fā)生,過渡需要時間和成本,但從長遠看,IPv6 有利于互聯(lián)網(wǎng)的持續(xù)和長久發(fā)展。目前,國際互聯(lián)網(wǎng)組織已經(jīng)決定成立兩個專門工作組,制定相應的國際標準。
Java 對 IPv6 的支持
隨 著 IPv6 越來越受到業(yè)界的重視,Java 從 1.4 版開始支持 Linux 和 Solaris 平臺上的 IPv6。1.5 版起又加入了 Windows 平臺上的支持。相對于 C++,Java 很好得封裝了 IPv4 和 IPv6 的變化部分,遺留代碼都可以原生支持 IPv6,而不用隨底層具體實現(xiàn)的變化而變化。
那么 Java 是如何來支持 IPv6 的呢? Java 網(wǎng)絡(luò)棧會優(yōu)先檢查底層系統(tǒng)是否支持 IPv6,以及采用的何種 IP 棧系統(tǒng)。如果是雙棧系統(tǒng),那它直接創(chuàng)建一個 IPv6 套接字(如圖 1)。
圖 1. 雙棧結(jié)構(gòu)
對 于分隔棧系統(tǒng),Java 則創(chuàng)建 IPv4/v6 兩個套接字(如圖 2)。如果是 TCP 客戶端程序,一旦其中某個套接字連接成功,另一個套接字就會被關(guān)閉,這個套接字連接使用的 IP 協(xié)議類型也就此被固定下來。如果是 TCP 服務(wù)器端程序,因為無法預期客戶端使用的 IP 協(xié)議,所以 IPv4/v6 兩個套接字會被一直保留。對于 UDP 應用程序,無論是客戶端還是服務(wù)器端程序,兩個套接字都會保留來完成通信。
圖 2. 分隔棧結(jié)構(gòu)
如何驗證 IPv6 地址
IPv6 地址表示
從 IPv4 到 IPv6 最顯著的變化就是網(wǎng)絡(luò)地址的長度,IPv6 地址為 128 位長度,一般采用 32 個十六進制數(shù),但通常寫做 8 組每組 4 個十六進制的形式。例如:
2001:0db8:85a3:08d3:1319:8a2e:0370:7344 是一個合法的 IPv6 地址。如果四個數(shù)字都是零,則可以被省略。
2001:0db8:85a3:0000:1319:8a2e:0370:7344 等同于 2001:0db8:85a3::1319:8a2e:0370:7344。
遵從這些規(guī)則,如果因為省略而出現(xiàn)了兩個以上的冒號的話,可以壓縮為一個,但這種零壓縮在地址中只能出現(xiàn)一次。因此:
2001:0DB8:0000:0000:0000:0000:1428:57ab 2001:0DB8:0000:0000:0000::1428:57ab 2001:0DB8:0:0:0:0:1428:57ab 2001:0DB8:0::0:1428:57ab 2001:0DB8::1428:57ab
都是合法的地址,并且他們是等價的。但 2001::25de::cade 是非法的。(因為這樣會使得搞不清楚每個壓縮中有幾個全零的分組)。同時前導的零可以省略,因此:2001:0DB8:02de::0e13 等于 2001:DB8:2de::e13。
IPv6 地址校驗
IPv4 地址可以很容易的轉(zhuǎn)化為 IPv6 格式。舉例來說,如果 IPv4 的一個地址為 135.75.43.52(十六進制為 0x874B2B34),它可以被轉(zhuǎn)化為 0000:0000:0000:0000:0000:0000:874B:2B34 或者::874B:2B34。同時,還可以使用混合符號(IPv4- compatible address),則地址可以為::135.75.43.52。
在 IPv6 的環(huán)境下開發(fā) Java 應用,或者移植已有的 IPv4 環(huán)境下開發(fā)的 Java 應用到 IPv6 環(huán)境中來,對于 IPv6 網(wǎng)絡(luò)地址的驗證是必須的步驟,尤其是對那些提供了 UI(用戶接口)的 Java 應用。
所 幸的是,從 Java 1.5 開始,Sun 就增加了對 IPv6 網(wǎng)絡(luò)地址校驗的 Java 支持。程序員可以通過簡單地調(diào)用方法 sun.net.util.IPAddressUtil.isIPv6LiteralAddress() 來驗證一個 String 類型的輸入是否是一個合法的 IPv6 網(wǎng)絡(luò)地址。
為了更深入一步地了解 IPv6 的網(wǎng)絡(luò)地址規(guī)范,及其驗證算法,筆者參閱了一些材料,包括上文所述的方法 sun.net.util.IPAddressUtil.isIPv6LiteralAddress() 的源代碼,以及目前網(wǎng)絡(luò)上流傳的一些 IPv6 網(wǎng)絡(luò)地址的正則表達式,發(fā)現(xiàn):
由于 IPv6 協(xié)議所允許的網(wǎng)絡(luò)地址格式較多,規(guī)范較寬松(例如零壓縮地址,IPv4 映射地址等),所以導致了 IPv6 網(wǎng)絡(luò)地址的格式變化很大。
Java 對于 IPv6 網(wǎng)絡(luò)地址的驗證是通過對輸入字符的循環(huán)匹配做到的,并沒有采取正則表達式的做法。其匹配過程中還依賴于其它的 Java 方法。
目前網(wǎng)絡(luò)上流傳的 IPv6 網(wǎng)絡(luò)地址驗證的正則表達式通常都只能涵蓋部分地址格式,而且表達式冗長難讀,非常不易于理解。
基于通用性考慮,以及為了使驗證方法盡量簡單易讀,筆者嘗試將 IPv6 網(wǎng)絡(luò)地址的格式簡單分類以后,使用多個正則表達式進行驗證。
這種做法兼顧了通用性(基于正則表達式,所以方便用各種不同的編程語言進行實現(xiàn)),以及易讀性(每個獨立的正則表達式相對簡短);并且根據(jù)測試,支持目前所有的 IPv6 網(wǎng)絡(luò)地址格式類型,尚未發(fā)現(xiàn)例外。
以下是筆者用 Java 編寫的對于 IPv6 網(wǎng)絡(luò)地址的驗證方法。此算法可被簡單地用其它編程語言仿照重寫。
清單 1. 驗證地址
//IPv6 address validator matches these IPv6 formats
//::ffff:21:7.8.9.221 | 2001:0db8:85a3:08d3:1319:8a2e:0370:7344
//| ::8a2e:0:0370:7344 | 2001:0db8:85a3:08d3:1319:8a2e:100.22.44.55
//| 2001:0db8::8a2e:100.22.44.55 | ::100.22.44.55 | ffff::
//And such addresses are invalid
//::8a2e:0:0370:7344.4 | 2001:idb8::111:7.8.9.111 | 2001::100.a2.44.55
//| :2001::100.22.44.55
public static boolean isIPV6Format(String ip) {
ip = ip.trim();
//in many cases such as URLs, IPv6 addresses are wrapped by []
if(ip.substring(0, 1).equals("[") && ip.substring(ip.length()-1).equals("]"))
ip = ip.substring(1, ip.length()-1);
return (1 < Pattern.compile(":").split(ip).length)
//a valid IPv6 address should contains no less than 1,
//and no more than 7 “:” as separators
&& (Pattern.compile(":").split(ip).length <= 8)
//the address can be compressed, but “::” can appear only once
&& (Pattern.compile("::").split(ip).length <= 2)
//if a compressed address
&& (Pattern.compile("::").split(ip).length == 2)
//if starts with “::” – leading zeros are compressed
? (((ip.substring(0, 2).equals("::"))
? Pattern.matches("^::([\da-f]{1,4}(:)){0,4}(([\da-f]{1,4}(:)[\da-f]{1,4})
|([\da-f]{1,4})|((\d{1,3}.){3}\d{1,3}))", ip)
: Pattern.matches("^([\da-f]{1,4}(:|::)){1,5}
(([\da-f]{1,4}(:|::)[\da-f]{1,4})|([\da-f]{1,4})
|((\d{1,3}.){3}\d{1,3}))", ip)))
//if ends with "::" - ending zeros are compressed
: ((ip.substring(ip.length()-2).equals("::"))
? Pattern.matches("^([\da-f]{1,4}(:|::)){1,7}", ip)
: Pattern.matches("^([\da-f]{1,4}:){6}(([\da-f]{1,4}
:[\da-f]{1,4})|((\d{1,3}.){3}\d{1,3}))", ip));
}}
如何正規(guī)化 IPv6 地址
在 網(wǎng)絡(luò)程序開發(fā)中,經(jīng)常使用 IP 地址來標識一個主機,例如記錄終端用戶的訪問記錄等。由于 IPv6 具有有零壓縮地址等多種表示形式,因此直接使用 IPv6 地址作為標示符,可能會帶來一些問題。為了避免這些問題,在使用 IPv6 地址之前,有必要將其正規(guī)化。除了通過我們熟知的正則表達式,筆者在開發(fā)過程中發(fā)現(xiàn)使用一個簡單的 Java API 也可以達到相同的效果。
清單 2. 正規(guī)化地址
InetAddress inetAddr = InetAddress.getByName(ipAddr); ipAddr = inetAddr.getHostAddress(); System.out.println(ipAddr);
InetAddress.getByName(String) 方法接受的參數(shù)既可以是一個主機名,也可以是一個 IP 地址字符串。我們輸入任一信息的合法 IPv6 地址,再通過 getHostAddress() 方法取出主機 IP 時,地址字符串 ipAddr 已經(jīng)被轉(zhuǎn)換為完整形式。例如輸入 2002:97b:e7aa::97b:e7aa,上述代碼執(zhí)行過后,零壓縮部分將被還原,ipAddr 變?yōu)?2002:97b:e7aa:0:0:0:97b:e7aa。
回頁首
如何獲取本機 IPv6 地址
有 時為了能夠注冊 listener,開發(fā)人員需要使用本機的 IPv6 地址,這一地址不能簡單得通過 InetAddress.getLocalhost() 獲得。因為這樣有可能獲得諸如 0:0:0:0:0:0:0:1 這樣的特殊地址。使用這樣的地址,其他服務(wù)器將無法把通知發(fā)送到本機上,因此必須先進行過濾,選出確實可用的地址。以下代碼實現(xiàn)了這一功能,思路是遍歷網(wǎng) 絡(luò)接口的各個地址,直至找到符合要求的地址。
清單 3. 獲取本機 IP 地址
public static String getLocalIPv6Address() throws IOException {
InetAddress inetAddress = null;
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface
.getNetworkInterfaces();
outer:
while (networkInterfaces.hasMoreElements()) {
Enumeration<InetAddress> inetAds = networkInterfaces.nextElement()
.getInetAddresses();
while (inetAds.hasMoreElements()) {
inetAddress = inetAds.nextElement();
//Check if it's ipv6 address and reserved address
if (inetAddress instanceof Inet6Address
&& !isReservedAddr(inetAddress)) {
break outer;
}
}
}
String ipAddr = inetAddress.getHostAddress();
// Filter network card No
int index = ipAddr.indexOf('%');
if (index > 0) {
ipAddr = ipAddr.substring(0, index);
}
return ipAddr;
}
/**
* Check if it's "local address" or "link local address" or
* "loopbackaddress"
*
* @param ip address
*
* @return result
*/
private static boolean isReservedAddr(InetAddress inetAddr) {
if (inetAddr.isAnyLocalAddress() || inetAddr.isLinkLocalAddress()
|| inetAddr.isLoopbackAddress()) {
return true;
}
return false;
}
為了支持 IPv6,Java 中增加了兩個 InetAddress 的子類:Inet4Address 和 Inet6Address。一般情況下這兩個子類并不會被使用到,但是當我們需要分別處理不同的 IP 協(xié)議時就非常有用,在這我們根據(jù) Inet6Address 來篩選地址。
isReservedAddr() 方法過濾了本機特殊 IP 地址,包括“LocalAddress”,“LinkLocalAddress”和“LoopbackAddress”。讀者可根據(jù)自己的需要修改過濾標準。
另一個需要注意的地方是:在 windows 平臺上,取得的 IPv6 地址后面可能跟了一個百分號加數(shù)字。這里的數(shù)字是本機網(wǎng)絡(luò)適配器的編號。這個后綴并不是 IPv6 標準地址的一部分,可以去除。
回頁首
IPv4/IPv6 雙環(huán)境下,網(wǎng)絡(luò)的選擇和測試
我們先看一下筆者所在的 IPv4/IPv6 開發(fā)測試環(huán)境及其配置方法。
筆 者所處的 IPv4/IPv6 雙環(huán)境是一個典型的“6to4”雙棧網(wǎng)絡(luò),其中存在著一個 IPv6 到 IPv4 的映射機制,即任意一個 IPv6 地址 2002:92a:8f7a:100:a:b:c:d 在路由時會被默認映射為 IPv4 地址 a.b.c.d,所以路由表只有一套。
在 此環(huán)境內(nèi),IPv4 地址與 IPv6 地址的一一對應是人工保證的。如果一臺客戶機使用不匹配的 IPv4 和 IPv6 雙地址,或者同時使用 DHCPv4 和 DHCPv6(可能會導致 IPv4 地址和 IPv6 地址不匹配),會導致 IPv6 的路由尋址失敗。
正因為如此,為了配置雙地址環(huán)境,我們一般使用 DHCPv4 來自動獲取 IPv4 地址,然后人工配置相對應的 IPv6 地址。
Windows 系統(tǒng)
Windows 2000 及以下:不支持 IPv6
Windows 2003 和 Windows XP:使用 Windows 自帶的 netsh 命令行方式添加 IPv6 地址以及 DNS, 例如:C:>netsh interface ipv6 add address “Local Area Connection” 2002:92a:8f7a:100:10:13:1:2 和 C:>netsh interface ipv6 add dns “Local Area Connection” 2002:92a:8f7a:100:10::250
Windows 2008 和 Windows Vista:既可以使用 Windows 網(wǎng)絡(luò)屬性頁面進行配置,也可以使用類似 Windows 2003 和 Windows XP 的 netsh 命令行來配置
Linux 系統(tǒng) (以下是 IPv6 的臨時配置方法,即不修改配置文件,計算機重啟后配置失效)
Redhat Linux:最簡單的方法是使用 ifconfig 命令行添加 IPv6 地址,例如:ifconfig eth0 inet6 add 2002:92a:8f7a:100:10:14:24:106/96。
SUSE Linux:同上。
從 實踐上講,由于 Java 的面向?qū)ο筇匦裕约?java.net 包對于 IP 地址的良好封裝,從而使得將 Java 應用從 IPv4 環(huán)境移植到 IPv4/IPv6 雙環(huán)境,或者純 IPv6 環(huán)境變得異常簡單。通常我們需要做的僅是檢查代碼并移除明碼編寫的 IPv4 地址,用主機名來替代則可。
除此以外,對于一些特殊的需求,Java 還提供了 InetAddress 的兩個擴展類以供使用:Inet4Address 和 Inet6Address,其中封裝了對于 IPv4 和 IPv6 的特殊屬性和行為。然而由于 Java 的多態(tài)特性,使得程序員一般只需要使用父類 InetAddress,Java 虛擬機可以根據(jù)所封裝的 IP 地址類型的不同,在運行時選擇正確的行為邏輯。所以在多數(shù)情況下,程序員并不需要精確控制所使用的類型及其行為,一切交給 Java 虛擬機即可。
具體的新增類型及其新增方法,請具體參閱 Sun 公司的 JavaDoc。
另外,在 IPv4/IPv6 雙環(huán)境中,對于使用 Java 開發(fā)的網(wǎng)絡(luò)應用,比較值得注意的是以下兩個 IPv6 相關(guān)的 Java 虛擬機系統(tǒng)屬性。
java.net.preferIPv4Stack=<true|false> java.net.preferIPv6Addresses=<true|false>
preferIPv4Stack(默 認 false)表示如果存在 IPv4 和 IPv6 雙棧,Java 程序是否優(yōu)先使用 IPv4 套接字。默認值是優(yōu)先使用 IPv6 套接字,因為 IPv6 套接字可以與對應的 IPv4 或 IPv6 主機進行對話;相反如果優(yōu)先使用 IPv4,則只不能與 IPv6 主機進行通信。
preferIPv6Addresses(默認 false)表示在查詢本地或遠端 IP 地址時,如果存在 IPv4 和 IPv6 雙地址,Java 程序是否優(yōu)先返回 IPv6 地址。Java 默認返回 IPv4 地址主要是為了向后兼容,以支持舊有的 IPv4 驗證邏輯,以及舊有的僅支持 IPv4 地址的服務(wù)。
?
總結(jié)
從 計算機技術(shù)的發(fā)展、因特網(wǎng)的規(guī)律和網(wǎng)絡(luò)的傳輸速率來看,IPV4 都已經(jīng)不適用了。其中最主要的問題就是 IPV4 的 32 比特的 IP 地址空間已經(jīng)無法滿足迅速膨脹的因特網(wǎng)規(guī)模,但是 IPv6 的引入為我們解決了 IP 地址近乎枯竭的問題。本文對 IPv6 地址做了一些基本的介紹,著重介紹了如何使用 Java 開發(fā)兼容 IPv6 的網(wǎng)絡(luò)應用程序,包括如何驗證 IPv6 地址,如何正規(guī)化 IPv6 地址的表示,如何獲取本機 IPv6 的地址,以及在 IPv4/IPv6 雙地址環(huán)境下的網(wǎng)絡(luò)選擇和測試,同時作者結(jié)合在日常工作中使用的 Java 代碼片段,希望呈現(xiàn)給讀者一個全方位的、具有較強實用性的文本介紹,也希望本文能給讀者在以后使用 Java 開發(fā) IPv6 兼容程序的過程中帶來一些幫助。
總結(jié)
以上是生活随笔為你收集整理的使用 Java 开发兼容 IPv6 的网络应用程序的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Docker 环境下部署 redash
- 下一篇: 一夜走红!《羊了个羊》为何出圈:地区排名