IPv 背景介绍
目前我们使用的是第二代互联网 IPv 技术它的最大问题是网络地址资源有限从理论上讲可以编址 万个网络 亿台主机但采用 ABC 三类编址方式后可用的网络地址和主机地址的数目大打折扣以至目前的 IP 地址近乎枯竭网络地址不足严重地制约了全球互联网的应用和发展
一方面是地址资源数量的限制另一方面是随着电子技术及网络技术的发展计算机网络将进入人们的日常生活可能身边的每一样东西都需要连入全球因特网在这种网络空间匮乏的环境下IPv 应运而生它的产生不但解决了网络地址资源数量的问题同时也为除电脑外的设备连入互联网在数量限制上扫清了障碍
如果说 IPv 实现的只是人机对话那么 IPv 则扩展到任意事物之间的对话它不仅可以为人类服务还将服务于众多硬件设备如家用电器传感器远程照相机汽车等它将是无时不在无处不在的深入社会每个角落的真正的宽带网它所带来的经济效益也将非常巨大
当然IPv 并非十全十美一劳永逸不可能解决所有问题IPv 只能在发展中不断完善也不可能在一夜之间发生过渡需要时间和成本但从长远看IPv 有利于互联网的持续和长久发展目前国际互联网组织已经决定成立两个专门工作组制定相应的国际标准
Java 对 IPv 的支持
随着 IPv 越来越受到业界的重视Java 从 版开始支持 Linux 和 Solaris 平台上的 IPv 版起又加入了 Windows 平台上的支持相对于 C++Java 很好得封装了 IPv 和 IPv 的变化部分遗留代码都可以原生支持 IPv而不用随底层具体实现的变化而变化
那么 Java 是如何来支持 IPv 的呢? Java 网络栈会优先检查底层系统是否支持 IPv以及采用的何种 IP 栈系统如果是双栈系统那它直接创建一个 IPv 套接字(如图 )
图 双栈结构
对于分隔栈系统Java 则创建 IPv/v 两个套接字(如图 )如果是 TCP 客户端程序一旦其中某个套接字连接成功另一个套接字就会被关闭这个套接字连接使用的 IP 协议类型也就此被固定下来如果是 TCP 服务器端程序因为无法预期客户端使用的 IP 协议所以 IPv/v 两个套接字会被一直保留对于 UDP 应用程序无论是客户端还是服务器端程序两个套接字都会保留来完成通信
图 分隔栈结构
如何验证 IPv 地址
IPv 地址表示
从 IPv 到 IPv 最显着的变化就是网络地址的长度IPv 地址为 位长度一般采用 个十六进制数但通常写做 组每组 个十六进制的形式例如
:db:a:d::ae:: 是一个合法的 IPv 地址如果四个数字都是零则可以被省略
:db:a:::ae:: 等同于 :db:a:::ae::
遵从这些规则如果因为省略而出现了两个以上的冒号的话可以压缩为一个但这种零压缩在地址中只能出现一次因此
:DB::::::ab
:DB::::::ab
:DB::::::ab
:DB:::::ab
:DB:::ab
都是合法的地址并且他们是等价的但 ::de::cade 是非法的(因为这样会使得搞不清楚每个压缩中有几个全零的分组)同时前导的零可以省略因此:DB:de::e 等于 :DB:de::e
IPv 地址校验
IPv 地址可以很容易的转化为 IPv 格式举例来说如果 IPv 的一个地址为 (十六进制为 xBB)它可以被转化为 ::::::B:B 或者:B:B同时还可以使用混合符号(IPv compatible address)则地址可以为:
在 IPv 的环境下开发 Java 应用或者移植已有的 IPv 环境下开发的 Java 应用到 IPv 环境中来对于 IPv 网络地址的验证是必须的步骤尤其是对那些提供了 UI(用户接口)的 Java 应用
所幸的是从 Java 开始Sun 就增加了对 IPv 网络地址校验的 Java 支持程序员可以通过简单地调用方法 utilIPAddressUtilisIPvLiteralAddress() 来验证一个 String 类型的输入是否是一个合法的 IPv 网络地址
为了更深入一步地了解 IPv 的网络地址规范及其验证算法笔者参阅了一些材料包括上文所述的方法 utilIPAddressUtilisIPvLiteralAddress() 的源代码以及目前网络上流传的一些 IPv 网络地址的正则表达式发现
由于 IPv 协议所允许的网络地址格式较多规范较宽松(例如零压缩地址IPv 映射地址等)所以导致了 IPv 网络地址的格式变化很大
Java 对于 IPv 网络地址的验证是通过对输入字符的循环匹配做到的并没有采取正则表达式的做法其匹配过程中还依赖于其它的 Java 方法
目前网络上流传的 IPv 网络地址验证的正则表达式通常都只能涵盖部分地址格式而且表达式冗长难读非常不易于理解
基于通用性考虑以及为了使验证方法尽量简单易读笔者尝试将 IPv 网络地址的格式简单分类以后使用多个正则表达式进行验证
这种做法兼顾了通用性(基于正则表达式所以方便用各种不同的编程语言进行实现)以及易读性(每个独立的正则表达式相对简短);并且根据测试支持目前所有的 IPv 网络地址格式类型尚未发现例外
以下是笔者用 Java 编写的对于 IPv 网络地址的验证方法此算法可被简单地用其它编程语言仿照重写
清单 验证地址
//IPvaddressvalidatormatchestheseIPvformats
//::ffff::|:db:a:d::ae::
//|::ae:::|:db:a:d::ae:
//|:db::ae:|::|ffff::
//Andsuchaddressesareinvalid
//::ae:::|:idb:::|::a
//|:::
publicstaticbooleanisIPVFormat(Stringip){
ip=iptrim();
//inmanycasessuchasURLsIPvaddressesarewrappedby[]
if(ipsubstring()equals([)&&ipsubstring(iplength())equals(]))
ip=ipsubstring(iplength());
return(<pile(:)split(ip)length)
//avalidIPvaddressshouldcontainsnolessthan
//andnomorethan:asseparators
&&(pile(:)split(ip)length<=)
//theaddresscanbecompressedbut::canappearonlyonce
&&(pile(::)split(ip)length<=)
//ifacompressedaddress
&&(pile(::)split(ip)length==)
//ifstartswith::–leadingzerosarecompressed
?(((ipsubstring()equals(::))
?Patternmatches(^::([\\daf]{}(:)){}(([\\daf]{}(:)[\\daf]{})
|([\\daf]{})|((\\d{}){}\\d{}))ip)
:Patternmatches(^([\\daf]{}(:|::)){}
(([\\daf]{}(:|::)[\\daf]{})|([\\daf]{})
|((\\d{}){}\\d{}))ip)))
//ifendswith::endingzerosarecompressed
:((ipsubstring(iplength())equals(::))
?Patternmatches(^([\\daf]{}(:|::)){}ip)
:Patternmatches(^([\\daf]{}:){}(([\\daf]{}
:[\\daf]{})|((\\d{}){}\\d{}))ip));
}}
如何正规化 IPv 地址
在网络程序开发中经常使用 IP 地址来标识一个主机例如记录终端用户的访问记录等由于 IPv 具有有零压缩地址等多种表示形式因此直接使用 IPv 地址作为标示符可能会带来一些问题为了避免这些问题在使用 IPv 地址之前有必要将其正规化除了通过我们熟知的正则表达式笔者在开发过程中发现使用一个简单的 Java API 也可以达到相同的效果
清单 正规化地址
InetAddressinetAddr=InetAddressgetByName(ipAddr);
ipAddr=inetAddrgetHostAddress();
Systemoutprintln(ipAddr);
InetAddressgetByName(String) 方法接受的参数既可以是一个主机名也可以是一个 IP 地址字符串我们输入任一信息的合法 IPv 地址再通过 getHostAddress() 方法取出主机 IP 时地址字符串 ipAddr 已经被转换为完整形式例如输入 :b:eaa::b:eaa上述代码执行过后零压缩部分将被还原ipAddr 变为 :b:eaa::::b:eaa
如何获取本机 IPv 地址
有时为了能够注册 listener开发人员需要使用本机的 IPv 地址这一地址不能简单得通过 InetAddressgetLocalhost() 获得因为这样有可能获得诸如 ::::::: 这样的特殊地址使用这样的地址其他服务器将无法把通知发送到本机上因此必须先进行过滤选出确实可用的地址以下代码实现了这一功能思路是遍历网络接口的各个地址直至找到符合要求的地址
清单 获取本机 IP 地址
publicstaticStringgetLocalIPvAddress()throwsIOException{
InetAddressinetAddress=null;
EnumerationnetworkInterfaces=NetworkInterface
getNetworkInterfaces();
outer:
while(networkInterfaceshasMoreElements()){
EnumerationinetAds=networkInterfacesnextElement()
getInetAddresses();
while(inetAdshasMoreElements()){
inetAddress=inetAdsnextElement();
//Checkifitsipvaddressandreservedaddress
if(inetAddressinstanceofInetAddress
&&!isReservedAddr(inetAddress)){
breakouter;
}
}
}
StringipAddr=inetAddressgetHostAddress();
//FilternetworkcardNo
intindex=ipAddrindexOf(%);
if(index>){
ipAddr=ipAddrsubstring(index);
}
returnipAddr;
}
/**
*Checkifitslocaladdressorlinklocaladdressor
*loopbackaddress
*
*@paramipaddress
*
*@returnresult
*/
privatestaticbooleanisReservedAddr(InetAddressinetAddr){
if(inetAddrisAnyLocalAddress()||inetAddrisLinkLocalAddress()
||inetAddrisLoopbackAddress()){
returntrue;
}
returnfalse;
}
为了支持 IPvJava 中增加了两个 InetAddress 的子类InetAddress 和 InetAddress一般情况下这两个子类并不会被使用到但是当我们需要分别处理不同的 IP 协议时就非常有用在这我们根据 InetAddress 来筛选地址
isReservedAddr() 方法过滤了本机特殊 IP 地址包括LocalAddressLinkLocalAddress和LoopbackAddress读者可根据自己的需要修改过滤标准
另一个需要注意的地方是在 windows 平台上取得的 IPv 地址后面可能跟了一个百分号加数字这里的数字是本机网络适配器的编号这个后缀并不是 IPv 标准地址的一部分可以去除
IPv/IPv 双环境下网络的选择和测试
我们先看一下笔者所在的 IPv/IPv 开发测试环境及其配置方法
笔者所处的 IPv/IPv 双环境是一个典型的to双栈网络其中存在着一个 IPv 到 IPv 的映射机制即任意一个 IPv 地址 :a:fa::a:b:c:d 在路由时会被默认映射为 IPv 地址 abcd所以路由表只有一套
在此环境内IPv 地址与 IPv 地址的一一对应是人工保证的如果一台客户机使用不匹配的 IPv 和 IPv 双地址或者同时使用 DHCPv 和 DHCPv(可能会导致 IPv 地址和 IPv 地址不匹配)会导致 IPv 的路由寻址失败
正因为如此为了配置双地址环境我们一般使用 DHCPv 来自动获取 IPv 地址然后人工配置相对应的 IPv 地址
Windows 系统
Windows 及以下不支持 IPv
Windows 和 Windows XP使用 Windows 自带的 netsh 命令行方式添加 IPv 地址以及 DNS 例如C:\>netsh interface ipv add address Local Area Connection :a:fa::::: 和 C:\>netsh interface ipv add dns Local Area Connection :a:fa::::
Windows 和 Windows Vista既可以使用 Windows 网络属性页面进行配置也可以使用类似 Windows 和 Windows XP 的 netsh 命令行来配置
Linux 系统 (以下是 IPv 的临时配置方法即不修改配置文件计算机重启后配置失效)
Redhat Linux最简单的方法是使用 ifconfig 命令行添加 IPv 地址例如ifconfig eth inet add :a:fa:::::/
SUSE Linux同上
从实践上讲由于 Java 的面向对象特性以及 包对于 IP 地址的良好封装从而使得将 Java 应用从 IPv 环境移植到 IPv/IPv 双环境或者纯 IPv 环境变得异常简单通常我们需要做的仅是检查代码并移除明码编写的 IPv 地址用主机名来替代则可
除此以外对于一些特殊的需求Java 还提供了 InetAddress 的两个扩展类以供使用InetAddress 和 InetAddress其中封装了对于 IPv 和 IPv 的特殊属性和行为然而由于 Java 的多态特性使得程序员一般只需要使用父类 InetAddressJava 虚拟机可以根据所封装的 IP 地址类型的不同在运行时选择正确的行为逻辑所以在多数情况下程序员并不需要精确控制所使用的类型及其行为一切交给 Java 虚拟机即可
具体的新增类型及其新增方法请具体参阅 Sun 公司的 JavaDoc
另外在 IPv/IPv 双环境中对于使用 Java 开发的网络应用比较值得注意的是以下两个 IPv 相关的 Java 虚拟机系统属性
preferIPvStack=<true|false>
preferIPvAddresses=<true|false>
preferIPvStack(默认 false)表示如果存在 IPv 和 IPv 双栈Java 程序是否优先使用 IPv 套接字默认值是优先使用 IPv 套接字因为 IPv 套接字可以与对应的 IPv 或 IPv 主机进行对话;相反如果优先使用 IPv则只不能与 IPv 主机进行通信
preferIPvAddresses(默认 false)表示在查询本地或远端 IP 地址时如果存在 IPv 和 IPv 双地址Java 程序是否优先返回 IPv 地址Java 默认返回 IPv 地址主要是为了向后兼容以支持旧有的 IPv 验证逻辑以及旧有的仅支持 IPv 地址的服务
总结
从计算机技术的发展因特网的规律和网络的传输速率来看IPV 都已经不适用了其中最主要的问题就是 IPV 的 比特的 IP 地址空间已经无法满足迅速膨胀的因特网规模但是 IPv 的引入为我们解决了 IP 地址近乎枯竭的问题本文对 IPv 地址做了一些基本的介绍着重介绍了如何使用 Java 开发兼容 IPv 的网络应用程序包括如何验证 IPv 地址如何正规化 IPv 地址的表示如何获取本机 IPv 的地址以及在 IPv/IPv 双地址环境下的网络选择和测试同时作者结合在日常工作中使用的 Java 代码片段希望呈现给读者一个全方位的具有较强实用性的文本介绍也希望本文能给读者在以后使用 Java 开发 IPv 兼容程序的过程中带来一些帮助