目前运行主流的IT系统中,用于解决分布式系统内部模块及不同的系统间通信的一种主要的解决方案就是使用套接字Socket来开发应用。由于当前大部分正在运行的IT系统中使用套接字Socket开发环境基本上都是基于IPv4完成的,因此在IT系统由IPv4向IPv6演进方案中如何完成这部分相关应用的演进就显得尤为的关键,下面本文将从技术角度分别从编程API接口的差异性、为实现IPv6重构软件的关键技术及如何支持IPv4及IPv6双栈完成演进等这几个方面进行分析,给出完整的解决方案
1. 基于SOCKET技术的接口协议
通常用BSD Socket API (Windows平台用Win Socket API)作为基础开发应用协议。以下是IT系统常用的接口协议:
基于SOCKET技术的接口协议通常以SOKET API作为为基础开发应用协议,下表是IT系统常用的接口协议:
序号 协议名称 类型
1 TELNET TCP
2 SSH TCP
3 FTP TCP
4 TFTP TCP
5 SNMP UDP
6 SOCKET自定义 TCP/UDP,IT系统或厂商基于SOKET API定义的私有协议
Telnet协议是TCP/IP协议族中的一员,是Internet远程登陆服务的标准协议。应用Telnet协议能够把本地用户所使用的计算机变成远程主机系统的一个终端。它提供了三种基本服务:
(1) Telnet定义一个网络虚拟终端为远的系统提供一个标准接口。客户机程序不必详细了解远的系统,他们只需构造使用标准接口的程序;
(2) Telnet包括一个允许客户机和服务器协商选项的机制,而且它还提供一组标准选项; .
(3) Telnet对称处理连接的两端,即Telnet不强迫客户机从键盘输入,也不强迫客户机在屏幕上显示输出。
SSH的英文全称是Secure Shell。通过使用SSH,你可以把所有传输的数据进行加密,这样“中间人”这种攻击方式就不可能实现了,而且也能够防止DNS和IP欺骗。还有一个额外的好处就是传输的数据是经过压缩的,所以可以加快传输的速度。 SSH有很多功能,它既可以代替telnet,又可以为ftp、pop、甚至ppp提供一个安全的“通道”。
从客户端来看,SSH提供两种级别的安全验证: 第一种级别(基于口令的安全验证)只要你知道自己帐号和口令,就可以登录到远程主机。所有传输的数据都会被加密,但是不能保证你正在连接的服务器就是你想连接的服务器。可能会有别的服务器在冒充真正的服务器, 也就是受到“中间人”这种方式的攻击。第二种级别(基于密匙的安全验证)需要依靠密匙,也就是你必须为自己创建一对密匙,并把公用密匙放在需要访问的服务器上。如果你要连接到SSH服务器上,客户端软件就会向服务器发出请求,请求用你的密匙进行安全验证。服务器收到请求之后,先在你在该服务器的HOME目录下寻找你的公用密匙,然后把它和你发送过来的公用密匙进行比较。如果两个密匙一致,服务器就用公用密匙加密“质询”(challenge)并把它发送给客户端软件。客户端软件收到“质询”(CHAP)之后就可以用你的私人密匙解密再把它发送给服务器。用这种方式,你必须知道自己密匙的口令。但是,与第一种级别相比,第二种级别不需要在网络上传送口令。第二种级别不仅加密所有传送的数据,而且“中间人”这种攻击方式也是不可能的(因为他没有你的私人密匙)。
FTP是TCP/IP协议组中的协议之一,它工作在TCP模型的应用层之上,使用TCP传输,FTP需要两个端口,一个端口是作为控制连接端口,也就是21端口,用于发送指令给服务器端以及等待服务器端的响应;另一个端口是数据传输端口,端口号20(仅PORT模式),是用来建立数据传输通道的。
TFTP全称为Trivial File Transfer Protocol,中文名叫简单文件传输协议。大家可以从它的名称上看出,它适合传送“简单”的文件。与FTP不同的是,它使用的是UDP的69端口,因此它可以穿越许多防火墙。不过它也有缺点,比如传送不可靠、没有密码验证等。虽然如此,它还是非常适合传送小型文件的。
SNMP(Simple Network Management Protocol,简单网络管理协议)的前身是简单网关监控协议(SGMP),用来对通信 <http://baike.baidu.com/view/15007.htm>线路进行管理。随后,人们对SGMP进行了很大的修改,特别是加入了符合Internet < http://baike.baidu.com/view/11165.htm>定义的SMI和MIB <http://baike.baidu.com/view/141513.htm>:体系结构,改进后的协议就是著名的SNMP。SNMP的目标是管理互联网 <http://baike.baidu.com/view/6825.htm>Internet上众多厂家生产的软硬件 <http://baike.baidu.com/view/25278.htm>平台,因此SNMP受Internet标准网络管理框架的影响也很大。现在SNMP已经出到第三个版本的协议 <http://baike.baidu.com/view/36190.htm>,其功能较以前已经大大地加强和改进了。
SNMP的体系结构是围绕着以下四个概念和目标进行设计的:保持管理代理(Agent)的软件成本尽可能低;最大限度地保持远程管理的功能,以便充分利用Internet的网络资源 <http://baike.baidu.com/view/8439.htm>;体系结构 <http://baike.baidu.com/view/1188494.htm>必须有扩充的余地;保持SNMP的独立性 <http://baike.baidu.com/view/562176.htm>,不依赖于具体的计算机 <http://baike.baidu.com/view/3314.htm>、网关 <http://baike.baidu.com/view/807.htm>和网络传输协议 <http://baike.baidu.com/view/16807.htm>。
2. SOCKET API接口的差异性:
基于套接字Socket API所开发的应用中,基本上都使用相同的编程模型,且所有通信的基本操作如connect、accept、listen、send/sendto、read/readfrom等都是通过Socket API函数来完成的。这一点无论在IPv4的网络环境下还是IPv6的网络环境下基本上都是一致的,这就保证了基于套接字Socket开发的应用软件在软件结构上基本没有变化,IPv4向IPv6演进所带来的变化主要集中在那些与地址相关的API函数上(包括地址有关的数据结构体)。RFC2553针对IPv6带来的套接字Socket API函数上的变化有明确的定义,IPv4和IPv6体现在套接字Socket API函数级别的差异可以用下面的表格来表示:
映射项 功能说明 IPv4 IPv6
常量定义 地址族 AF_INET AF_INET6
协议族 PF_INET PF_INET6
IP地址结构体 结构体 sockaddr_in sockaddr_in6
结构体成员:套接口长度 sin_len sin6_len
结构体成员:协议族 sin_family sin6_family
结构体成员:端口号 sin_port sin6_port
地址 通配地址 INADDR_ANY in6addr_any
环回地址 INADDR_LOOPBACK in6addr_loopback
地址-表达式转换函数 字符串地址转为IP地址 inet_aton( ) inet_pton( )
IP地址结构转为字符串 inet_ntoa( ) inet_ntop( )
名字-地址转换函数 根据名字获得IP地址 gethostbyname() getaddrinfo()
根据IP地址获得名字 gethostbyaddr() getnameinfo()
根据名字获得IP地址 gethostbyname2() getaddrinfo()
根据服务名获得全部服务信息 getservbyname() getaddrinfo ()
根据服务端口获得全部服务信息 getservbyport() getaddrinfo()
那些使用C/C++等语言开发的应用软件基本上主要关注这些API函数及结构体的变化就可以了。
JAVA作为主要编程语言的情况:
Java是目前IT系统广泛使用的程序设计语言,在JDK的java.net包中(包括javax.net包)提供了完整的对套接字Socket编程的类定义,使用Java语言开发的应用都是基于这些类来完成的。JDK 从1.4版本开始部分支持IPv6协议,到了JDK1.5、JDK1.6就完全支持IPv6协议栈。现网调研的结果表明大多的IT系统都是可以运行在JDK1.5或以上的版本中。因此我们可以认为目前我们使用的JDK就已经具备了IPv6的能力。在JDK中和IPv4及IPv6相关的类只有两个:java.net.Inet4Address和java.net.Inet6Address, 也就是说如果要区分IPv4和IPv6的话,通过区分这两个类就可以了,而且这两个类均继承自同一个父类java.net.InetAddress,在JDK中,和套接字Socket相关的其他所有的类都仅仅与这个父类java.net.InetAddress相关(都应用的该类),而与IPv4(java.net.Inet4Address)及IPv6(java.net.Inet6Address)没有直接的关系,即这些类对于是IPv4还是IPv6是透明的,因此从套接字Socket API接口这个层面上来看没有差异。
3. 软件重构涉及到的主要技术
通过上面的分析,我们知道了IPv4向IPv6演进所带来的套接字Socket API接口上的差异,在了解及关注这些差异的基础上,我们通过一些关键技术点的分析帮助我们重构软件代码以实现向IPv6的演进。
在使用C/C++语言来开发的Socket软件中,主要关注以下几个技术关键点:
3.1 地址结构的变化
IPv4环境下,通常使用的地址结构体sockaddr_in在头文件中定义如下:
struct sockaddr_in{
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
struct in_addr{
unsigned long s_addr;
};
面向IPv4编程的socket编程中,针对上述地址结构体的赋值example如下:
rcv_udp_addr.sin_family = AF_INET;
rcv_udp_addr.sin_addr.s_addr = htonl(INADDR_ANY);
rcv_udp_addr.sin_port = htons(UDPRCV_PORT);
IPv6环境下,地址结构体的发生对应的变化,定义如下:
struct sockaddr_in6 {
uint8_t sin6_len;
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};
struct in6_addr {
uint8_t s6_addr[16];
};
相对的对于IPv6地址结构体的赋值如下:
rcv_udp_addr.sin6_family = PF_INET;
rcv_udp_addr.sin6_addr.s6_addr =in6addr_any;
rcv_udp_addr.sin6_prot = htons (UDPRCV_PORT);
需要注意的是地址在IPv4中的地址结构体中sin_addr是主机字节顺序(经过htons()函数转换),
而在IPv6中,直接使用的是网络字节顺序。
3.2 socket的创建
建立socket的连接的API函数定义原型如下:
int socket(int domain,int type,int protocol);
对于IPv4而言,domain=AF_INET, 而IPv6,则domain=PF_INET6
例如创建TCP的Socket语句如下:
sock_tcp_ipv4 = socket(AF_INET,SOCK_STREAM,0);
sock_tcp_ipv6 = socket(PF_INET6,SOCK_STREAM,0);
3.3 字符串地址和网络顺序IP地址的相互转换
在IPv4环境中,使用int inet_aton(const char *cp, struct in_addr *np)来完成从字符串地址到IP地址的转换,在IPv6环境中,使用int inet_pton(int Af,const char *src,void *dst),多一个输入参数,使用样例:inet_pton(AF_INET6,hostname,&snd_tcp_addr.sin6_addr);
反之,当由网络顺序的IP地址转换为字符串的时候,IPv4中,使用char *inet_ntoa(struct in_addr_in)而在IPv6环境中,使用const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt)这个函数,参数发生了明显的变化,使用样例:
inet_ntop(PF_INET6, &rcv_udp_addr_sin6_addr,ip,sizeof(ip));
3.4 主机名和地址的转换(域名解析)
在IPv4环境中,使用gethostbyname()或者gethostbyaddr()函数来实现转换;在IPv6环境中,则使用getnameinfo()或者getaddrinfo()函数来完成转换,需要包含以下的三个头文件:
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
3.5 组播地址常量
对于组播的应用,在IPv6环境下增加了一些常量的定义,在调用套接字Socket API相关函数的时候使用。代码重构的时候,关注的常量如下表所示:
常量 IPv6
IPV6_MULTICAST_IF 设置某个网络接口为发送组播数据报的接口
IPV6_MULTICAST_HOPS 对于发送出的组播数据报为其设置hop的范围
IPV6_MULTICAST_LOOP 如果组播源本身也属于所发送出的组播数据的目标组播组,并且该选项设置为1的话,将会存本地发送的时候产生回环。该选项的缺省值为1
IPV6_JOIN_GROUP 加入组播组
IPV6_LEAVE_GROUP 离开组播组
样例代码:
rc=setsockopt(s,IPPROTO_IPV6,IPV6_MULTICAST_HOPS,(char )&gTtl,sizeof(gTt1));
以上的几个技术关键点适合于Unix、Linux平台下的C/C++语言来开发的Socket软件的重构。相对于Windows平台,以上几点基本上也是适合的(毕竟Winsock库还是基本兼容标准的Socket API定义的),但是相对于Windows平台在重构代码是需要多注意下面几点特殊性:
(1) 确保Winsock库的版本在2.2或以上
对于Windows平台而言,只有Winsock库在2.2以上才支持IPv6。
(2) IP地址信息存储
在Winsock2库中,使用新的SOCKADDR_STORGE结构体来存储地址信息,这样可以屏蔽IPv4地址和IPv6地址差异。
(3) 套接字Connect操作
在Winsock2库中,为了兼容IPv4和IPv6,,提供了两个新的Connect函数(WSAConnectByName()和WSAConnectByList())用于建立Socket连接,与标准的connect()函数相比,这两个Connect函数的优势在于IP地址可以是IPv4的地址,也可以是IPv6的地址。
在现场调研的过程中,我们也发现了许多的IT系统在使用使用C/C++语言开发Socket软件的时候,并不是直接使用原始的套接字Socket API函数或在此基础上的封装的库来完成的,而是使用了第三方的库来完成的,这里我们对常用的用于网络编程第三方的库也进行相关的分析。
在Linux、Unix及 Windows平台下,ACE(The Adaptive Comunication Environment)是一个被广泛用来进行网络应用开发的软件包,ACE软件包从5.3版本开始就提供了对IPv6的支持,ACE软件包中提供的有关IP地址封装的类是ACE_INET_Addr,该类屏蔽了IP地址不同版本(IPv4、IPv6)的区别,相应类的成员方法也都兼容了不同地址版本,因此建立在ACE软件包基础上进行的网络应用的开发是不需要做这种区分的(输入的地址是IPv4的地址,则ACE_INET_Addr实例化的是具备IPv4特性的实例,输入地址是IPv6的地址,则ACE_INET_Addr实例化的是具备IPv6特性的实例), 在重构代码过程中重点需要关注的是当前ACE库是否具备了支持IPv6的能力,即检查编译ACE库的时候是否定有#ACE_HAS_IPV6宏定义。
相对于C/C++开发的Socket应用程序而言,使用Java程序设计语言开发的Socket应用程序在从IPv4环境重构到IPv6环境中基本上不需要做什么代码的重构,这主要取决于JDK目前的版本已经提供了对IPv6的支持且有关IP地址的差异(IPv4和IPv6)已经被屏蔽(上一节已经进行了分析JDK类库的情况),例如下面的代码:
Socket echosocket = new Socket(“hostip”,7);
如果”hostip”对应的是IPv4的地址,则echosocket 是IPv4类型的Socket,如果”hostip”对应的是IPv6的地址,则echosocket是IPv6类型,一条语句对于这两种地址类型都适应。
虽然JDK对于我们提供了很多的方便,使得我们的代码基本上不需要重构,但是还是有两个问题需要考虑:
1) Java虚拟机运行的操作系统环境
必须确保JVM所运行的操作系统环境支持IPv6, 以Sun的JVM为例,要向支持IPv6, 对于操作系统的要求如下:
(1) Solaris 8 或更高的版本;
(2) Linux Kernel 2.1.2或更高版本(2.4.0版本以上可以更好的支持);
(3) Windows XP sp1、Windows 2003或更高的版本。
相对于其他的JVM(例如IBM的JVM、Oracle的JRockit等)需要检查相应的对主机操作系统的要求。
2) 如何分是IP4和IPv6的地址
虽然大部分Java开发的应用中都不用关心到底是用的是IPv4的地址还是IPv6的地址,在某些情况下,例如需要输出连接的IP地址信息或者需要获得地址信息的详细内容,这个时候是需要区分的。前面已经分析过java.net包中大部分类的成员函数使用的地址类是java.net.InetAddress,这个类是java.net.Inet4Address和java.net.Inet6Address类的父类,因此可以通过检查java.net.InetAddress的子类类型来确认到底地址使用的是IPv4的地址还是IPv6的地址,样例代码如下:
InetAddress address = Socket.getInetAddress();
if ( address instanceof java.net.Inet4Address ) {
Inet4Address address_ipv4 = (java.net.Inet4Address)address;
address_ipv4.getAddress();
……… //相关的IPv4的地址操作
} else if ( address instanceof java.net.Inet6Address ) {
Inet6Address address_ipv6 = (java.net.Inet6Address)address;
address_ipv6.getScopeId();
……..//相关的IPv6地址操作
} else {
……….
}
由于JDK所提供的有关套接字Socket相关的类非常的完善,基本上有关Java语言开发Socket应用很少使用第三方的包来完成。即使是那些建立在java.net包基础上的应用于更高层协议应用第三方的包,例如Apache Commons中的Net包,该软件包封装了ntp、smtp、ftp、telnet、pop3等上层协议,也都是秉承了java.net包的特色,在软件包中使用java.net.InetAddress作为地址类型,从而屏蔽了IPv4和IPv6的差异,使得功能上都同时支持IPv4环境和IPv6环境。
1.1.1.1. IPv4/IPV6双栈实现方式
本文前面已经分析认为从IPv4向IPv6演进的最佳解决方案是IT系统提供对双栈应用的支撑,下面将对如何使得那些基于套接字Socket技术实现的接口支持双栈进行重点分析。
目前的IT系统大都是建立在操作系统上层的应用软件,要想应用系统支持IPv4及IPv6双栈,操作系统首先要支持双栈,因此在重构应用软件之前,确认或升级操作系统至支持双栈是必要条件。
前面已经分析过基于套接字Socket技术实现的接口都采用相同的编程模式,即典型的客户-服务器模式(Client-Server),针对于服务器端而言,双栈模式下服务器端Socket应该能够同时对绑定的IPv4地址及IPv6地址进行监听,从而完成对不同地址的服务请求。想要达到这一目的可以有以下几种方法:
1) 实施全地址监听
针对服务端而言,如果将服务端监听的地址绑定为”::”(IPv4中的0.0.0.0),将意味着服务器监听系统地址列表中的所有地址,即无论是IPv4的地址还是IPv6的地址都将被服务器监听以提供服务。例如下面的代码:
int port = 1099;
ServerSocket server = new ServerSocket(port);
Socket s ;
while (true) {
S = server.accept();
doClientStuff(s);
}
服务器端将监听地址列表中(无论IPv4地址还是IPv6地址)所有地址的1099端口,客户端无论访问的是IPv4的地址还是IPv6的地址,只要端口是1099,都将获得服务器端的服务。
显然,这种模式是一种最简单的重构模式。
2) 兼容IPv4的IPv6地址
服务器端监听的IPv6地址是兼容地址(地址模式为::w.x.y.z)或者是IPv4映射的地址(地址模式为::ffff:w.x.y.z),那么在双栈模式下,服务器实际上是对两个地址进行监听,例如,服务器端绑定监听的IPv6地址是::ffff:192.158.112.8, 那么实际上服务器对”::ffff:192.158.112.8”这个IPv6的地址进行监听,同时也对192.158.112.8这个地址进行监听,客户端Socket访问的目标地址无论是192.158.112.8这个IPv4地址,还是访问”::ffff:192.158.112.8”这个IPv6的地址,服务端都可以提供服务。
3) IPv4地址与IPv6地址各自独立
服务端需要监听的地址是两个完全独立的IPv4地址及IPv6地址,此时通过分别对两个不同的地址分别监听来完成双栈应用支撑,参见下面的样例代码:
SOCKET ServerSocket[FD_SETSIZE];
ADDRINFO AI0;// 存储的是IPv6的地址
ADDRINFO AI1;// 存储的是IPv4的地址
ServerSocket[0] = socket(AF_INET6, SOCK_STREAM,PF_INET6);
ServerSocket[1] = socket(AF_INET, SOCK_STREAM,PF_INET);
………..
bind(ServerSocket[0], AI0->ai_addr,AI0->ai_addrlen);
bind(ServerSocket[1], AI1->ai_addr,AI1->ai_addrlen);
…………
select (2, &ServerSet, 0, 0, 0); // select()系统调用:异步I/O多工
if ( FD_ISSET(ServerSocket[0], &ServerSet) ) {
//IPv6连接 sockv6 = accept(….)
…….
}
if ( FD_ISSET(ServerSocket[1], &ServerSet) ) {
//IPv4 连接 sockv4 = accept(…..)
…..
}
显然,这种模式是一种比较复杂的重构模式。
与服务器端复杂的重构方案相比,客户端的改造就会相对简单,在一个支持双栈应用的系统中,客户端每一个Socket连接实例,都只会在IPv4模式及IPv6模式间选择一种,这种选择可以通过相应的配置文件来完成,每一次创建客户端Socket实例之前,依据从配置文件中读取出的服务端地址类型(或者自动分析地址格式来获得类型)配置项来决定创建哪一种类型的Socket实例。相对于Java开发的客户端而言,由于JDK对IPv6很好的适应性(前面已经分析过),代码几乎不需要做重构,相对于C/C++开发的客户端而言,需要根据不同服务端IP地址的类型,使用不同的地址结构体及参数等来创建Socket实例(关键技术一节已经分析)。
对于实际运行环境是单栈的系统,分别对IPv4和IPv6开发出不同的版本是不可取的。通常的解决方案(无论是客户端还是服务端)和双栈环境下客户端的解决方案相类似,即通过选择配置文件选择项来确定当前实例的运行环境是IPv4环境还是IPv6环境。