IPv6套接字编程
1.概述
由于互联网用户的日益增加,网络需求日益扩大,IPv4地址也日益紧张。人们为了解决地址日趋耗尽的问题,采用了CIDR、NAT等技术来延缓地址耗尽的速度,但这并不能从根本上解决IPv4目前存在的问题,IPv4地址耗尽只是一个时间问题。随着互联网的发展,Internet骨干路由器的路由表也日益扩大,这使得路由器必须维护大量路由表。
由于IPv6可以解决传统的IP技术的瓶颈问题,因此,它会推动整个信息产业的发展。目前,第三代移动技术的基本协议就采用IPv6,这意味着下一代互联网具有移动性,将来手机或其他个人移动终端都将具有全球唯一的IPv6地址,因而IPv6技术将会变得越来越重要。
1.1套接字与通信
1.1.1套接字的概念
套接字Socket,是指从应用程序中接受计算机网络通信服务时的应用程序接口,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。套接字是个抽象编程概念,它把用户代码与TCP/IP协议堆栈的底层实现隔离开了,TCP套接字可以使用户快速地开发出自定义协议的客户/服务器应用程序。
套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。可以将套接字看作不同主机间的进程进行双向通信的端点,它构成了单个主机内及整个网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中的套接字交换数据(数据交换也可能穿越域的界限,但这时一定要执行某种解释程序)。各种进程使用这个相同的域互相之间用Internet协议簇来进行通信。
1.1.2套接字应用程序编程接口
套接字应用程序编程接口是网络应用程序通过网络协议栈进行通信时所使用的接口,即应用程序与协议栈软件之间的接口,简称套接字编程接口(Socket API)。它定义了应用程序与协议栈软件进行交互时可以使用的一组操作,决定了应用程序使用协议栈的方式、应用程序所能实现的功能、以及开发具有这些功能的程序的难度。图1-1为应用进程通过套接字接入到网络的示意图。
套接口是对网络中不同主机上应用进程之间进行双向通信的端点的抽象,一个套接口就是网络上进程通信的一端,提供了应用层进程利用网络协议栈交换数据的机制。要想实现套接字编程接口,可以采用两种实现方式:
一种是在操作系统的内核中增加相应的软件来实现;
一种是通过开发操作系统之外的函数库来实现。
在这里我们采用的是第一种实现方式。
1.1.3套接字的分类
套接字具有三种类型:
(1)数据报套接字(Datagram SOCKET)
数据报套接字提供无连接的不保证可靠的独立的数据报传输服务。在Internet通信域中,数据报套接字使用UDP数据报协议形成的进程间通路,具有UDP协议为上层所提供的服务的所有特点。图1-2揭示了基于UDP协议的数据报套接字的工作模型。
(2)流式套接字(Stream SOCKET)
流式套接字提供双向的、有序的、无重复的、无记录边界的可靠的数据流传输服务。在Internet通信域中,流式套接字使用TCP协议形成的进程间通路,具有TCP协议为上层所提供的服务的所有特点,在使用流式套接字传输数据之前,必须在数据的发送端和接收端之间建立连接。 图1-3揭示了基于TCP协议的流式套接字的工作模型
(3)原始式套接字(RAW SOCKET)
原始式套接字允许对较低层次的协议,如IP、ICMP直接访问,用于检验新的协议的实现。
每一个正被使用的套接字都有它确定的类型,只有相同类型的套接字才能相互通信。
1.1.4套接字的应用场合
(1)不管是采用对等模式或者客户机/服务器模式,通信双方的应用程序都需要开发。
(2)双方所交换数据的结构和交换数据的顺序有特定的要求,不符合现在成熟的应用层协议,甚至需要自己去开发应用层协议,自己设计最适合的数据结构和信息交换规程。
1.1.5套接字的一般通信过程
网络通信一般为Client/Server模式
Server:运行一个特定的程序,它申请一个Socket,该Socket在某一个Port监听客户机的连接。
Client:申请一个Socket,将该Socket与服务器端的Port相联,服务器在接受该Client的连接后,新生成一个Port,在该新Port上与Client通信;原Port继续监听,准备接受新的Client的连接。
图1-5 server通过另一个端口与client建立连接
套接字是网络上与另一个应用程序建立连接并通信的一个句柄。
1.2 IPv6
1.2.1 IPv6协议
IPv6是因特网协议第六版(Internet Protocol Version Six)的缩写。目前,在Internet中广泛使用的IP协议是被人们称为IP第四版的IPv4协议。IPv4协议只使用了32位的IP地址,在迅速发展的Internet中,发生了地址的绝度数严重不足的问题。为了解决这个问题,人们对IP的第六版本进行了标准化,并且目前已经有一些操作系统对它进行支持。在IPv6协议中,IP地址的长度变为128位,在Internet中能够连接巨大数目的主机。
IPv6协议对IPv4的改进表现在:
(1)扩展地址空间。IP地址长度由32位增加到128位。
(2)简化的首部格式,优化路由选择。IPv4首部的某些字段被取消或改为选项,以减少报文分组处理过程中常用情况的处理开销,并使得IPv6首部的带宽开销尽可能低。
(3)支持扩展首部和选项。IPv6的选项放在单独的扩展首部中,位于报文分组中IPv6基本首部和传送层首部之间。因为大多数IPv6选项首部不会被报文分组投递路径上的任何路由器检查和处理,直至其到达最终目的地,这种组织方式有利于改进路由器在处理包含选项的报文分组时的性能。IPv6的另一改进,是其选项与IPv4不同,可具有任意长度,不限于40字节。
(4)支持认证和加密机制。IPv6定义了一种扩展,可支持权限验证和数据完整性并支持保密性要求。
(5)支持自动配置。IPv6支持多种形式的自动配置,从孤立网络结点地址的“即插即用”自动配置,到DHCP提供的全功能的设施。
(6)服务质量能力。IPv6增加了一种新的能力,如果某些报文分组属于特定的工作流,发送者要求对其给予特殊处理,则可对这些报文分组加标号,例如非缺省服务质量通信业务或“实时”服务。
1.2.2 IPv6数据报
图1-6 为IPv6数据报报头
Ipv6协议的结构体定义如下:
Struct ip6_hdr{
Union{
Struct ip6_hdrctl{
u_int32_t ip6_unl_flow;/*4位的版本,8位的传输与分类,20位的流标识符*/
u_int16_t ip6_unl_plen;/*报头长度*/
u_int8_t ip6_unl_nxt;/*下一个报头*/
u_int8_t ip6_unl_hlim;/*跨度限制*/
}ip6_unl ;
u_int8_t ip6_un2_vfc;/*4位的版本号,跨度为4位的传输分类*/
}ip6_ctlun ;
struct in6_addr ip6_src;/*发送端地址*/
struct in6_addr ip6_dst;/*接收端地址*/
};
#define ip6_vfc ip6_ctlun.ip6_un2_vfc
#define ip6_flow ip6_ctlun.ip6_unl.ip6_unl_flow
#define ip6_plen ip6_ctlun.ip6_unl.ip6_unl_plen
#define ip6_nxt ip6_ctlun.ip6_unl.ip6_unl_nxt
#define ip6_hlim ip6_ctlun.ip6_unl.ip6_unl_hlim
#define ip6_hops ip6_ctlun.ip6_unl.ip6_unl_hops
1.3 IPv6与IPv4的兼容性问题
由于IPv6与IPv4在地址长度,数据报格式等方面存在许多不同点,因此在IPv6套接字编程的时候,如何使程序既能适应Ipv6的特点,又能消除不同地址间的差异,使程序既能处理IPv4的地址,又能处理IPv6地址,实现IPv6与IPv4的兼容,对程序编程者来说显得非常重要。IPv6套接字编程将综合考虑各种情况,解决IPv6与IPv4、IPv6与Ipv6节点间的通信问题。
2.从IPv4网络向IPv6网络过渡
目前,Internet中的绝大部分节点用的都是IPv4地址。为了解决IPv4地址的缺陷,Internet将逐步向IPv6过渡,在一段时间后,使Internet中所有的节点都具备处理IPv6地址的能力。
为了达到这个目标,因特网工程任务组(IETF)已经设计出两种解决方案。通过这两种方案,可以使IPv6无缝地移植到IPv4中。这两种方法就是:双栈协议和隧道技术。
双栈协议
双协议栈(dual stack)是指在完全过渡到 IPv6 之前,使一部分主机(或路由器)装有两个协议栈,一个 IPv4和一个 IPv6。
双栈网络的建设有两种模式:
(1)完全双栈网络,即所有网络设备、用户终端都支持IPv4、IPv6双协议栈,用户通信既可使用IPv4协议栈也可使用IPv6协议栈。
(2)有限双栈网络,网络中部分网络设备、用户终端采用双协议栈,这些用户可使用IPv4或IPv6与其它用户互联互通,但新增的网络设备和用户终端则仅使用IPv6协议栈,应用基于IPv6协议栈。
双协议栈的具体工作方式如下:
(1)若应用程序使用的目的地址为IPv4地址,则使用IPv4协议;
(2)若应用程序使用的目的地址为IPv4兼容的IPv6地址,则同样使用IPv4协议,区别仅在于此时的IPv6封装在IPv4中;
(3)若应用程序使用的目的地址是一个非IPv4兼容的IPv6地址,则使用IPv6协议,而且很可能要采用隧道等机制来进行路由传送;
(4)若应用程序使用域名作为目标地址,则先从DNS服务器得到相应的IPv4/IPv6地址,然后根据地址情况进行相应的处理。
2.2隧道技术
所谓隧道,就是在一方将IPv6的包封装在IPv4包里,然后在目的地对其解析,得到IPv6包。通过隧道,IPv6分组被作为无结构无意义的数据,封装在IPv4数据报中,被IPv4网络传输。由于IPv4网络把IPv6数据当作无结构无意义数据传输,因此不提供帧自标示能力,所以只有在IPv4连接双方都同意时才能交换IPv6分组,否则收方会将IPv6分组当成IPv4分组而造成混乱。
在IPv6协议中,为了存储通信所需要的IP地址和端口号,定义了一个sockaddr_in6的结构体。sockaddr_in6的结构如下:
struct sockaddr_in6{
u_int8_t sin6_len;
u_int8_ sin6_family;
u_int16_t sin6_port;
u_int32_t sin6_flowinfo;
struct in6_addr sin6_addr;
u_int32_t sin6_scope_id;
};
数据类型u_int8_t为8位无符号整数,typedef unsigned char u_int8_t。u_int16_t 与u_int32_t亦类似。
在sin6_len域中,存储有sockaddr_in6结构体的长度。在 sin6_family域中,存储有表示IPv6地址系列的AF_INET6.在sin6_port域中,存储有一个传输层所使用的端口号。在sin6_flowinfo域中,存储有一个在QoS中所使用的流标识符。在sin6_addr域中,存储有IPv6协议的地址。在sin6_scope_id域中,存储有表示范围的ID.
IPv6协议的地址是由下面的int6_addr结构体加以定义的:
struct int6_addr{
u_int8_t s6_addr[16];
};
in6_addr与IPv6协议的地址相同为16Byte,但为了在操作系统内部处理方便起见,实际上是由联合体来定义的。
在IPv4协议中,相对于sockaddr_in6结构体的为sockaddr_in结构体,其中缺少域sin6_flowinfo与sin6_scope_id。
3.2 addrinfo
addrinfo结构体是为了消除IPv6协议与IPv4协议之间的差异,编制统一的程序而追加的。在各台主机中,考虑能够赋予多个IPv4地址或IPv6地址,将addrinfo结构体设计为具有下面的列表结构:
Struct addrinfo{
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
size_t ai_addrlen;
char *ai_canonname;
struct sockaddr *ai_addr;
struct addrinfo *ai_next;
};
在ai_flags域中,能够设定3位的标志位。它们分别是AI_PASSIVE、AI_CANONNAME、AI_NUMERICHOST。AI_PASSIVE在IPv4协议中指定INADDR_ANY的时候,不需要指定具体的主机,而是利用任意的主机。AI_CANONNAME是在最初的列表结构中存储正式名称的时候所设定的值。AI_NUMERICHOST不使用DNS进行检索,只使用IP地址,它在不想使用DNS查询处理、需要等待一定时间的时候等情况下才使用。
ai_family域表示了地址系列。在地址系列中,具有表示IPv4协议的AF_INET、IPv6协议的AF_INET6等。
ai_socktype域表示了套接字的类型。在一个套接字的类型域中,具有下面的三种类型:表示流型的SOCK_STREAM,表示数据报型的SOCK_DGRAM,表示raw IP的SOCK_RAW。
ai_protocol域表示了传输层所使用的协议。在使用TCP协议时,它为IPPROTO_TCP;在使用UDP协议时,它为IPPROTO_UDP;在不使用传输层时,在该域中存储0.
ai_addrlen域表示ai_addr的长度。ai_canonname域表示ai_addr的别名。
ai_addr域表示了访问sockaddr_in或sockaddr_in6的指针。ai_next域表示列表的下一个地址。在列表结束时,在该域中存储NULL。
4.IPv6套接字编程中用到的函数
4.1 socket()函数
在使用套接字的时候,利用socket系统调用来打开一个套接字。socket系统调用的语法如下所示:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,int type,int protocol);
在domain域中,指定地址系列(协议系列)。地址系列表示所使用的地址体系。在TCP/IP协议中,将IP地址和端口号所形成的地址体系指定为AF_INET或AF_INET6,将表示TCP/IP协议的地址体系指定为PF_INET。
在type变量中,指定所使用的协议的类型,其中可以指定下面的值:
#define sock_STREAM /*流式套接字*/
#define sock_DGRAM /*数据报套接字*/
#define sock_RAW /*原始式套接字*/
#define sock_RDM /*可靠传输报文*/
#define sock_SEQPACKET /*序列包流*/
在使用TCP协议时,指定为SOCK_STREAM;在使用UDP协议时,指定为SOCK_DGRAM;在使用原始IP协议时,指定为SOCK_RAW。
在protocol域中,指定所使用的协议类型。在使用TCP协议或UDP协议时,由于指定type就可以确定方法,所以在protocol域中,缺省值为0。
如果成功地调用了socket系统调用,则打开一个套接字,并且返回一个可以利用该套接字的描述符。在发生错误时,则返回值为-1。
4.2 bind()函数
在利用自己的主机指定所使用的IP地址和端口号时,一般都使用bind系统调用。关于bind系统调用语法,如下所示:
#include <sys/types.h>
#include <sys/socket.h>
int bind(int s,struct sockaddr *my_addr,socklen_t addrlen);
在变量s中,指定的是利用socket系统调用所打开的套接字的描述符。在my_addr指针中,指定自己的IP地址和端口号。在addrlen域中,指定结构体my_addr的大小。
在利用bind系统调用将一个IP地址设置为INADDR_ANY的时候,该主机或路由器的所有IP地址都能够接收到一个包。在主机中,除了NIC的IP地址之外,都带有一个循环测试(loopback)的IP地址(127.0.0.1)。并且,在使用路由器等时,由于准备了多个接口,所以带有多个IP地址。这时,如果使用bind系统调用来指定一个IP地址,那么处理IP数据报的接收端地址和指定的IP地址之外,不能够接收到通信。但是,在指定为INADDR_ANY时,无论接收端的IP地址是什么,都能够接收到包。
在服务器中,无论是TCP协议还是UDP协议,都必须使用bind系统调用来指定自己的端口号。在客户机中,可以有操作系统来指定一个端口号。如果使用bind系统调用来指定的端口号时,能够自动地分配一个端口号。另外,在TCP协议的客户机中,可以省略执行bind系统调用。
4.3 close()函数
在结束对套接字的使用的时候,使用close系统调用。关于close系统调用的语法,如下面的语句所示:
#include <unistd.h>
int close(int s);
在变量s中,存储着利用socket系统调用所打开的套接字的描述符,或存储着accept系统调用的返回值。
4.4 sendto()函数
套接字函数可以分为两种:一种为无连接型的函数,另一种为面向连接型的函数。
#include <sys/types.h>
#include <sys/socket.h>
int sendto(int s,const void *msg,size_t len,int flags,const struct sockaddr *to,socklen_t tolen);
在raw IP协议中,必须使用无连接型的函数。在UDP协议中,通常都是使用无连接型的函数,但是,也可以使用面向连接型的函数。在TCP协议中,必须使用面向连接型的函数。
在UDP协议中,如果没有使用connect系统调用,则可以使用一个无连接型的函数。如果使用connect系统调用,也可以使用一个面向连接型的函数。
对于报文的发送与接收,可以利用sendto系统调用或recvfrom系统调用。在发送报文的时候,使用sendto系统调用;而在接收报文的时候,则使用recvfrom系统调用。无论是哪一种系统调用,都必须在实际参数中指定访问sockaddr结构体的指针。
在变量s中,指定的是一个利用socket系统调用所打开的端口号描述符。在msg结构体中,存储着所发送报文的存储器的初始地址,在len变量中,指定的是所发送报文的字节数;在to结构体中,指定的是接收端的IP地址和接收端的端口号;在tolen变量中,指定结构体to的大小;
在flags变量中,通常指定为0。
sendto系统调用的返回值是已经发送报文的字节数。严格地来讲,该返回值并不是在计算机网络上所传输的字节数,而是从应用程序传递给套接字模块的字节数。在发生错误时,返回值为-1。
4.5 recvfrom()函数
#include <sys/types.h>
#include <sys/socket.h>
int recvfrom(int s,void *hbuf,size_t len,int flags,struct sockaddr *from,socklen_t *fromlen);
在变量s中,指定的是利用socket系统调用所打开的一个套接字描述符。在buf指针中,存储的是所接收到报文的缓冲区的起始地址;len变量中指定的是buf中所能够存储的最大字节数。在from结构体中,存储着所接收到的包的发送端IP地址和发送端端口号;在fromlen变量中,存储这结构体from的大小;在flags变量中,通常指定为0。
recvfrom系统调用的返回值是所接收到的报文的字节数。在发生错误时,返回值为-1。
4.6 connect()函数
在指定通信对方的IP地址的时候,通常采用connect系统调用。在TCP协议中,需要传输建立连接请求的包。在UDP协议中,并不使用sendto系统调用或recvfrom系统调用,而是通过send系统调用或recv系统调用进行通信。connect系统调用的语法格式如下:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int s,const struct sockaddr *addr,socklen_t addrlen);
在变量s中,指定的是利用socket系统调用所打开的一个套接字描述符。在addr结构体中,指定的是通信对方的IP地址和端口号。在addrlen变量中,指定的是addr结构体的大小。
4.7 listen()函数
当服务器接收到TCP协议连接的时候,执行一个listen系统调用。关于listen系统调用的语法格式如下:
#include <sys/socket.h>
int listen(int s,int backlog);
在变量s中,存储着利用socket系统调用所打开的一个套接字描述符。在backlog变量中,指定的是队列的长度。如果listen系统调用正常,则返回值为0;否则发生错误时,返回值为-1。
4.8 getaddrinfo()函数
IPv4中使用gethostbyname()函数完成主机名到地址解析,但是该API不允许调用者指定所需地址类型的任何信息,返回的结构只包含了用于存储IPv4地址的空间。为了解决该问题,IPv6中引入了getaddrinfo()的新API,它是协议无关的,既可用于IPv4也可用于IPv6。getaddrinfo函数能够处理名字到地址以及服务到端口这两种转换,调用该函数会获得一个addrinfo结构的列表,调用的返回值是addrinfo的结构(列表)指针。
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char* nodename,const char* servname,
const struct addrinfo* hints,struct addrinfo** res);
nodename域指定了一个域名或IP地址。在这个域中,既能够指定IPv6协议的地址,又能够指定IPv4协议的地址。servname域能够指定表示端口号的服务器名或表示端口号的数字。在指定一个域名或服务器名的时候,与使用C语言处理字符串的规则是相同的,在字符串的最后要追加“/0”。
在hints域中,指定想要获得的信息。例如,在只想获得与IPv6协议有关的信息时,在addrinfo结构体的ai_family中设定AF_INET6之后,再指定hints。在不特别指定时,将hints域设置为NULL。
在res域中,使用一个addrinfo结构体来保存;列表结构的开始地址。
在IPv6协议中,在一个端口能够指定多个IP地址。并且在过渡期中,也有将IPv6地址和IPv4地址这两种地址都赋予一个NIC(网络适配器、网卡)的情况存在。调用一次getaddrinfo函数,即可检索到所以的IP地址,并使用一个addrinfo结构体存储到该列表结构中。
4.9 getnameinfo()函数
getnameinfo()是getaddrinfo()的互补函数。它把一个套接字地址转换为对应的主机名和服务。它是一个“协议无关”的函数,既能处理IPv4地址,又能处理IPv6地址。它集合了gethostbyaddrin()函数和getservbyport()函数的功能,但getnameinfo()消除了地址族依靠的特性。
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *serv, size_t servlen, int flags);
sa域是一个指向sockaddr结构体的指针,其中指定了IP地址和端口号。
host域是存放主机名的字符型指针,而serv域是存放服务的指针,即端口号。
flag域用于控制getnameinfo()的操作,它允许的值如下面所列:
NI_DGRAM
当知道处理的是数据报套接口的时候,调用者应该设置NI_DGRAM标志,因为在套接口地址结构中给出的仅仅是IP地址和端口号,getnameinfo无法就此确定所用协议是TCP还是UDP。比如端口514,在TCP端口上提供rsh服务,而在UDP端口上则提供syslog服务。
NI_NOFQDN
该标志导致返回的主机名称被截去第一个点号之后的内容。比如假设套接口结构中的IP地址为91.168.42.2,那么不设置该标志返回的主机名为sina.aiwen.com,那么如果设置了该标志后返回的主机名则为sina。
NI_NUMERICHOST,NI_NUMERICSERV,NI_NUMERICSCOPE
NI_NUMERICHOST标志通知getnameinfo不要调用DNS,而是以数值表达格式作为字符串返回IP地址;类似的,NI_NUMERICSERV标志指定以十进制数格式作为字符串返回端口号,以代替查找服务名;NI_NUMERICSCOPE则指定以数值格式作为字符串返回范围标识,以代替其名字。
NI_NAMEREQD
该标志通知getnameinfo函数如果无法适用DNS反向解析出主机名,则直接返回一个错误。需要把客户的IP地址映射成主机名的那些服务器可以使用该特性。
4.10 inet_pton()函数和inet_ntop()函数
inet_pton函数是一个将域名或ASCII码表示的IP地址变换为使用字节来表示的IP地址的函数。inet_ntop函数则是inet_pton函数的逆,即把使用字节来表示的IP地址转为使用字符串所表示的IP地址。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_pton(int af,const char *src,void *dst);
const char *inet_ntop(int af,const void *src,char *dst,size_t size);
在af域中,指定地址系列;在src、dst域中,分别指定变换前所存储的地址信息以及返回后所存储的地址信息。在inet_ntop的size域中,指定了从dst开始的缓冲区的大小。如果不指定充分大的缓冲区,则不能够进行变换处理。
4.11 memset()函数
void *memset(void *s,int c,size_t n)
memset函数用来对一段内存空间全部设置为某个字符,常用于内存空间初始化。将已开辟内存空间s的首n个字节的值设为值c。
4.12 memcpy()函数
extern void *memcpy(void *dest, void *src, unsigned int count);
memcpy函数把src所指内存区域复制count个字节到dest所指内存区域,但src和dest所指内存区域不能重叠,函数返回指向dest的指针。memcpy用来做内存拷贝,你可以拿它拷贝任何数据类型的对象,而strcpy就只能拷贝字符串了,它遇到'/0'就结束拷贝。
5.程序的地址族无关性
5.1 地址族无关的概念
在套接字编程中,我们经常说到程序必须实现地址族无关(address-family independent)。什么是地址族无关呢?所谓的地址族无关,就是要求程序在处理IP地址时,能消除不同IP地址间的差异性,可对不同的IP地址进行统一的无差别的处理。不需修改程序,即可对不同的IP地址进行预定的处理,实现相应功能。
5.2 为什么程序需要地址族无关
程序具备地址族无关性可以使程序消除一系列不足,使程序更具灵活性:
(1)为了支持IPv4/v6双栈环境,网络程序必须能够同时正确处理IPv4与IPv6。如果在程序中规定了地址族为AF_INET或AF_INET6,那么程序将无法在IPv4/v6双栈环境中正确运行。
(2)当一个新的协议投入使用后,我们总是希望以前的网络程序能够适应新协议,而不需为了适应新协议而对程序进行重写。这包括在IP层,虽然现在还没有开发IPv7的计划,但谁也不能肯定未来会不会开发。在传输层亦如此。
(3)目前,已经有足够的工具支持网络程序的地址族无关,比如sockaddr_storage,getaddrinfo和getnameinfo。
(4)在一些操作系统中可能不支持地址族。如果在编程中引入了地址族,可能会导致程序不能正确执行。而程序的地址族无关可以解决此类问题。
(5)程序的地址族无关可以使程序更简洁,提高程序的移植性。
(6)有些应用程序接口(API)不支持IPv6,比如gethostbyname()。
5.3 以AF_INET6替代AF_INET,sockaddr_in6替代sockaddr_in的不足
在重写IPv4依靠(IPv4 dependent)程序的时候,能不能只是简单地把AF_INET代替为AF_INET6,sockaddr_in替代为sockaddr_in6,而使程序地址族无关呢?
这样做有几个缺点:
首先,用gethostbyname2(3),程序只能连接IPv6目的地址,而不能连接IPv4目的地址。在一个IPv4/v6双栈环境中,FQDN(Fully Qualified Domain Name,完全合格域名/全称域名,是指主机名加上全路径,全路径中列出了序列中所有域成员)可以被分解为多个IPv4地址和多个IPv6地址。客户端应该尽可能地连接分解出来的IP地址,而不只是连接IPv6地址。
第二,IPv6支持范围IPv6地址(scoped IPv6 addresses),用gethostbyname2(3)并不能处理范围IPv6地址,因为gethostbyname2(3)不返回范围标识符(scope identification)。
第三,在程序中指定地址族为AF_INET6将使程序只能在支持IPv6的内核中运行,因为一个不支持IPv6的内核通常没有AF_INET6套接字的支持。如果想让一个单一双态程序(既能处理IPv4地址,又能处理IPv6地址)能正确运行在IPv4-only内核、IPv6内核和IPv4/v6双栈内核中,地址族无关是必须的。
第四,这样的程序并不能适应未来的需要。如果一些新协议投入使用,这样的程序将不可避免重写。IPv4到IPv6的过渡的花费是巨大的,在此过渡过程中,把其他问题一起解决未尝不是一件好事。
第五,用地址族无关方法进行编程,可以使程序获得更高的移植性和稳定性。
5.4 套接字编程的地址族无关指南
(1)使用sockaddrs用于地址表示
为了处理IPv4和IPv6地址,建议使用sockaddrs,如sockaddr_in或sockaddr_in6。用sockaddrs,可以使数据包含地址族的标识,这样的话我们就可以在传递地址数据时知道其地址族。
当需要一个预留空间给一个sockaddr时,可以使用结构体sockaddr_storage。结构体sockaddr_storage有足够大的空间来存储任何类型的sockaddr。
使用sockaddr的另一个重要原因是一个IPv6地址并不能唯一地确定一个端点,还必须加上一个一个范围标识符,指定出口端(outgoing interface)。
(2)把文本表示转换为sockaddrs。利用getaddrinfo(3)可以实现。
(3)把二进制地址表示转换为文本。可以利用getnameinfo(3)实现。
6.IPv6套接字编程
6.1 编写能处理IPv6地址的程序
为了使程序能够处理IPv6地址,我们知道可以用基于socket的应用程序接口,通过使用getaddrinfo和getnameinfo来使程序具备地址族无关的能力。
getaddrinfo()应用举例:
const struct sockaddr * foo(hostname,servname)
const char *hostname;
const char *servname;
{
struct addrinfo hints,*res;
static struct sockaddr_storage ss;
int error;
memset(&hints,0,sizeof(hints));
hints.ai_socktype=SOCK_STREAM;
error=getaddrinfo(hostname,servname,&hints,&res);
if(error){
fprintf(stderr,”%s/%s:%s/n”,hostname,servname,gai_strerror(error));
exit(1);
}
if(res->ai_addrlen sizeof(ss)){
fprintf(stderr,”sockaddr too large/n”);
exit(1);
}
memcpy(&ss,res->aiaddr,res-ai_addrlen);
freeaddrinfo(res);
return(const struct sockaddr *)&ss;
}
getaddrinfo(3)非常灵活,具有许多状态操作。比如,如果你想避免DNS lookup,你可以在hints.ai_flags域中指定为AI_NUMERICHOST。通过AI_NUMERICHOST,getaddrinfo(3)将只接收数字表示的地址。
getaddrinfo(3)用范围识别(scope identification)处理IPv6字符串地址,所以程序不需要对范围识别做任何的特殊处理。
getnameinfo(3)也非常灵活,既支持数字的地址,也支持FQDN(Fully Qualified Domain Name:完全合格域名/全称域名,是指主机名加上全路径,全路径中列出了序列中所有域成员)表示的地址。getnameinfo(3)同时也可以把端口号转换成字符串。所以getnameinfo(3)能同时支持IPv4和IPv6,并不需要区分是支持IPv4还是支持IPv6。最后一个参数可以控制getnameinfo(3)的行为。
struct sockaddr *sa;
char hbuf[NI_MAXHOST];
sbuf[NI_MAXSERV] ;
int error ;
error=getnameinfo(sa,salen,hbuf,sizeof(hbuf),
NI_NUMERICHOST|NI_NUMERICSERV);
if(error){
fprintf(stderr,”error:%s/n”,gai_strerror(error));
exit(1);
}
fprintf(“addr:%s port:%s/n”,hbuf,sbuf);
error=getnameinfo(sa,salen,hbuf,sizeof(hbuf),0);
if(error){
fprintf(stderr,”error:%s/n”,gai_strerror(error));
exit(1);
}
fprintf(“addr:%s port:%s/n”,hbuf,sbuf);
error=getnameinfo(sa,salen,hbuf,sizeof(hbuf),NULL,0,NI_NAMEREQD);
if(error){
fprintf(stderr,”error:%s/n”,gai_strerror(error));
exit(1);
}
printf(“FQDN:%s/n”,hbuf);
getnameinfo(3)需要生成IPv6地址字符串的范围标识,但也没必要担心在sin6_scope_id域中的范围标识。
6.2移植可用的程序来支持IPv6
为了找到需要重写的部分,我们需要找出IPv4依赖的功能调用和IPv4依赖的数据类型:
%grep gethostby *.c *.h
%grep inet_aton *.c *.h
%grep sockaddr_in *.c *.h
%grep in_addr *.c *.h
然而,如果程序编写不正确并且在int或u_int32_t中传递32位的二进制表示的IPv4地址的话,对in_addr将毫无用处,还需识别在哪个变量中存储了IPv4地址。
如果套接字编程借口(socket API)是由单个*.c文件生成,那么将容易移植。否则,我们必须弄清IPv4依靠的数据是怎样传送的,然后把它们重写成协议族无关的程序。有时候,IPv4依靠的数据类型用在结构体定义和功能原型中。在这种情况下,我们需要辨别出地址族无关的代码。
下面的例子是IPv4依赖的部分程序代码:
struct foo{
struct sockaddr_in dst;
}
struct foo *
setaddr(in)
struct in_addr in;
{
struct foo *foo;
foo=malloc(sizeof(*foo));
if(!foo)
return NULL;
memset(foo,0,sizeof(*foo));
foo->dst.sin_family=AF_INET;
foo->dst.sin_addr=in;
return foo;
}
改变结构体的定义是比较简单的,要么把结构体改为struct sockaddr_storage,要么定义一个struct addrinfo*。而改为功能原型则要难得多。有时候,传递struct sockaddr *会容易些,但如果你要处理多个地址的话,用struct addrinfo *会显得更明智些。
下面是重写后的程序,实现了地址族无关,但不支持多地址:
struct foo{
struct sockaddr_storage dst;
};
struct foo *
setaddr(sa,salen)
struct sockaddr *sin;
socklen_t salen;
{
struct foo *foo;
if(salen>sizeof(foo->dst))
return NULL;
foo=malloc(sizeof(*foo));
if(!foo)
return NULL;
memset(foo,0,sizeof(*foo));
memcpy(&foo->dst,sa,salen);
return foo;
}
如果由于一些限制不能使用select(2)和poll(2),可以运行两个应用程序的实例,一个用于AF_INET socket,另一个用于AF_INET6,这样就可以同时处理IPv4节点和IPv6节点。
结论及尚存在的问题
IPv6的主要优势体现在以下几方面:扩大地址空间、提高网络的整体吞吐量、改善服务质量(QoS)、安全性有更好的保证、支持即插即用和移动性、更好实现多播功能。显然,IPv6的优势能够直接或间接地解决IPv4存在的诸多问题。其中最突出的是IPv6大大地扩大了地址空间,恢复了原来因地址受限而失去的端到端连接功能,为互联网的普及与深化发展提供了基本条件。当然,IPv6并非十全十美、一劳永逸,不可能解决所有问题。IPv6只能在发展中不断完善,也不可能在一夜之间发生,过渡需要时间和成本,但从长远看,IPv6有利于互联网的持续和长久发展。
由于IPv6相关技术目前还不是很成熟,其应用范围还不是很广,很多方面还只是停留在研究与实验阶段,因此本毕业论文的讨论范围也涉及不深,只作浅层次的探索讨论;在程序移植性方面也不是做的很好,本设计的代码是在Linux平台下调试运行的,因此在跨平台方面还有待提高。
在这次毕业设计的过程中,我查阅了大量相关的书籍与相关知识,通过自学与老师的指导,我对毕业设计的要求与内容有了深刻的了解,并通过自己的理解与分析思考,圆满完成了毕业论文。在完成毕业设计的过程中,我对“一份耕耘,一份收获”有了深刻的认识,只要你付出了,你就一定会有收获。在毕业设计期间,我认真查阅资料,虚心请教老师与同学,并仔细弄清相关理论知识,理清思路,详细构思规划,很快就有了一个大致的框架,在后期通过修改与完善,我的论文完成了。在此过程中,我付出了努力,而我也取得了收获。我学到了很多以前课堂上没学到的知识,在IPv6相关知识方面有了更深的理解,并在此过程中,我的查阅资料能力、整体构思规划能力、解决问题的能力也得到了提高,我从中受益甚大。
致 谢
大学生活即将画上记号,而于我的人生却只是一个逗号,我将面对又一次征程的开始。四年的求学生涯在师长、亲友的大力支持下,走得辛苦却也收获满囊,在论文即将付梓之际,思绪万千,心情久久不能平静。伟人、名人为我所崇拜,可是我更急切地要把我的敬意和赞美献给我的大学老师们,是你们的无私教导让我从稚嫩走向成熟,而我也从中学到各种知识与智慧,让我可以更容易地面对社会生活。在这里,我要特别感谢我的指导老师罗海天老师,他给了我很大的帮助,帮我解决了毕业设计中遇到的很多问题。从论文题目的选定到论文写作的指导,经由他悉心的点拨,再经思考后的领悟,常常让我有“山重水复疑无路,柳暗花明又一村”的感觉。授人以鱼不如授人以渔,置身其间,耳濡目染,潜移默化,使我不仅接受了全新的思想观念,树立了宏伟的学术目标,领会了基本的思考方式,从罗老师的身上,我受益良多。
感谢我的爸爸妈妈,焉得谖草,言树之背,养育之恩,无以回报,你们永远健康快乐是我最大的心愿。在论文即将完成之际,我的心情无法平静,从开始进入课题到论文的顺利完成,有多少可敬的师长、同学、朋友给了我无言的帮助,在这里请接受我诚挚谢意!同时也感谢学院为我提供良好的做毕业设计的环境。最后再一次感谢所有在毕业设计中曾经帮助过我的良师益友和同学,以及在设计中被我引用或参考的论著的作者。
本毕业设计是我学习生涯的最后一份答卷,也是我作为学生交给母校最后的一份答卷,尽管倾注了我数月来的心血和汗水,却由于自己的基础知识不够扎实,能力确实有限,多多少少存在着这样那样的缺陷。然而,毕竟已经尽力,已无憾矣!
附录
程序1 client-gethostby.c:TCP客户端实例——通过host/port与服务端通信,并从服务端接收信息。该程序不支持IPv6。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
int main_P((int,char **));
int main(argc,argv)
int agrc;
char **argv;
{
struct hostent *hp;
struct servent *sp;
unsigned long lport;
u_int16_t port;
char *ep;
struct sockaddr_in dst ;
int dstlen ;ssize_t l ;
int s;
char hbuf[INET_ADDRSTRLEN];
char buf[1024];
/*检查参数个数*/
if(argc!=3){
fprintf(stderr,”usage:test host port/n”);
exit(1);
}
/*把主机名解释为IP*/
hp=gethostbyname(argv[1]);
if(!hp){
fprintf(stderr,”%s:%s/n”,argv[1],hstrerror(h_errno));
exit(1);
}
if(hp->h_length!=sizeof(dst.sin_addr)){
fprintf(stderr,”%s:unexpected address length/n”,argv[1]);
exit(1);
}
/*解析端口号*/
sp=getservbyname(argv[2],”tcp”);
if(sp){
port=sp-s_port& 0xffff;
}else{
ep=NULL;errno=0;
lport=strtoul(argv[2],&ep,10);
if(!*argv[2] || errno || !ep || *ep){
fprintf(stderr,”%s:no such service/n”,agrv[2]);
exit(1);
}
if(lport & ~0xffff){
fprintf(stderr,”%s:out of range/n”,argv[2]);
exit(1);
}
port=htons(lport & 0xffff);/*将主机的无符号短整数型数转换为网络字节顺序,如12 34—>34 12*/
}
endservent() ;
/*只尝试第一个地址*/
memset(&dst,0,sizeof(dst)) ;
dst.sin_family=AF_INET ;
/*Linux/Solaris系统不需要下面的一行*/
dst.sin_len=sizeof(struct sockaddr_in) ;
memcpy(&dst.sin_addr,hp->h_addr,sizeof(dst.sin_addr)) ;
dst.sin_port=port ;
dstlen=sizeof(struct sockaddr_in) ;
s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(s<0){
perror(“socket”);
exit(1);
}
inet_ntop(AF_INET,hp->h_addr,hbuf,sizeof(hbuf));
fprintf(stderr,”trying %s port %u/n”,,hbuf,ntohs(port));
if(connect(s,(struct sockaddr *)&dst,dstlen)<0){
perror(“connect”);
exit(1);
}
while((l=read(s,buf,sizeof(buf))>0)
write(STDOUT_FILENO,buf,l);close(s);
exit(0);
}
程序2 client-getaddrinfo.c:在程序6-1的基础上使程序实现地址族无关。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet.h>
#include <netdb.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
int main_P((int,char **));
int main(argc,argv)
int agrc;
char **argv;
{
struct addrinfo hints,*res,*res0;
ssize_t l;
int s;
char hbuf[NI_MAXHOST],sbuf[NI_MAXSERV];
char buf[1024] ;
int error ;
/*检查参数个数*/
if(argc!=3){
fprintf(stderr,”usage:test host port/n”);
exit(1);
}
/*把地址/端口号转换为sockaddr,基于getaddrinfo(3)返回的结果,程序代码运行于数据驱动模式*/
memset(&hints,0,sizeof(hints)) ;
hints.ai_socktype=SOCK_STREAM;
error=getaddinfo(argv[1],argv[2],&hints,&res0);
if(error){
fprintf(stderr,”%s %s/n”,argv[1],argv[1],gai_strerror(error));continue;
exit(1);
/*尝试所有的sockaddr直到通信成功*/
for(res=res0;res;res=res->ai_next){
error=getnameinfo(res->ai_aiaddr,res->ai_addrlen,hbuf,sizeof(hbuf),sbuf,
sizeof(sbuf),NI_NUMERICHOST | NI_NUMERICSERV);
if(error){
fprintf(stderr,”%s%s:%s/n”,arg[1],argv[1],gai_sterror(error));
continue;
}
fprintf(stderror,”trying %s port %s/n”,hbuf,sbuf);
s=socket(res->ai_family,res->ai_socktype,res->ai_protocol);
if(s<0)
continue;
if(connect(s,res-ai_addr,res-ai_addrlen)>0){
close(s);
s=-1;
continue;
}
while((l=read(s,buf,sizeof(buf)))<0)
write(STDOUT_FILENO,buf,l);close(s);
exit(0);
}
fprintf(stderr,”test:no destination to connect to/n”);
exit(1);
}
程序3 server-single.c 一个独立的TCP服务器侦听一个IPv4端口
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
int main_P((int,char **));
int main(argc,argv)
int agrc;
char **argv;
{
struct servent *sp;
unsigned long lport;
u_int16_t port;
char *ep;
struct sockaddr_in serv ;
int servlen ;struct sockaddr_in from;
socklen_t fromlen;
int s;
int ls;
char hbuf[INET_ADDRSTRLEN];
if(argc!=2){
fprintf(stderr,”usage:test port/n”);
exit(1);
}
sp=getservbyname(argv[1],”tcp”);
if(sp)
port=sp->s_port & 0xffff;
else{
ep=NULL;errno=0;
lport=strtoul(argv[1],&ep,10) ;
if(!*argv[1] || errno || !ep || *ep){
frpintf(stderr,”%s: no such service/n”,argv[1]);
exit(1);
}
if(lport & ~0xffff){
fprintf(stderr,”%s: out of range/n”,argv[1]);
exit(1);
}
port=htons(lport &0xffff);
}
endservent() ;
memset(&serv,0,sizeof(serv)) ;
serv.sin_family=AF_INET ;
/*Linux/Solaris系统不需要下面的一行*/
serv.sin_len=sizeof(struct sockaddr_in) ;
serv.sin_port=port;
servlen=sizeof(struct sockaddr_in);
s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(s<0){
perror(“socket”);
exit(1);
}
if(bind(s,(struct sockaddr *)&serv,servlen)<0){
perror(“bind”);
exit(1);
}
if(listen(s,5)<0{
perror(“listen
exit(1);
}
while(l){
fromlen=sizeof(from);
ls=accept(s,(struct sockaddr *)&from,&fromlen);
if(ls<0) continue;
if(from.sin_family!=AF_INET || fromlen!=sizeof(struct sockaddr_in)){
exit(1);
}
if(inet_ntop(AF_INET, &from.sin_addr,hbuf,sizeof(hbuf))= =NULL){
exit(1);
}
write(ls,”hello”,6);
write(ls,hbuf,strlen(hbuf));
write(ls,”/n”,l);
close(ls);
}
}
程序4 server-getaddrinfo.c 在程序6-3的基础上使程序地址族无关
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#define MAXSOCK 20
int main_P((int,char **));
int main(argc,argv)
int agrc;
char **argv;
{
struct addrinfo hints,*res,*res0;
int error;
struct sockaddr_storage from;
socklen_t fromlen;
int ls;
int s[MAXSOCK];
int smax;
int sockmax;
fd_set rfd,rfd0;
int n;
int i;
char hbuf[NI_MAXHOST],sbuf[NI_MAXSERV];
#ifdef IPV6_V6ONLY
const int on=1 ;
#endif
if(argc!=2){
fprintf(stderr,”usage:test port/n”);
exit(1);
}
memset(&hints,0,sizeof(hints)) ;
hints.ai_socktype=SOCK_STREAM;
hints.ai_flags=AI_PASSIVE;
error=getaddinfo(NULL,argv[1],&hints,&res0);
if(error){
fprintf(stderr,”%s %s/n”,argv[1], gai_strerror(error));
exit(1);
}
smax=0;
sockmax=-1;
for(res=res0;res &&smax rMAXSOCK; res=res->ai_next){
s[smax]=socket(res-ai_family,res-ai_socktype,res->ai_protocol);
if(s[smax]<0 continue;
/*避免FD_SET溢出*/
if(s[smax]=FD_SETSIZE){
close(s[smax]);
s[smax]=-1;
continue;
}
#ifdef IPV6_V6ONLY
if(res->ai_family= =AF_INET6 &&
setsockopt(s[smax],IPPROTO_IPV6,IPV6_V6ONLY,&on,
sizeof(on))<0){
perror(“bind”);
s[smax]=-1;
continue;
}
#endif
if(bind(s[smax],res-ai_addr,res-ai_addrlen) 0){
close(s[smax]);
s[smax]=-1;
continue;
}
if(listen(s[smax],5) 0){
close(s[smax]);
s[smax]=-1;
continue;
}
error=getnameinfo(res-ai_addr,res-ai_addrlen,hbuf,sizeof(hbuf),sbuf,sizeof(sbuf),
NI_NUMERICHOST | NI_NUMERICSERV);
if(error){
fprintf(stderr,”test:%s/n”,gai_strerror(error));
exit(1);
}
fprintf(stderr,”listen to %s %s /n”,hbuf,sbuf);
if(s[smax]>sockmax)
sockmax=s[smax];
samx++;
}
if(smax= =0){
fprintf(stderr,”test:no socket to listen to/n”);
exit(1);
}
FD_ZERO(&rfd0);
for(i=0;i<smax;i++)
FD_SET(s[i],&rfd0);
while(l){
rfd=rfd0;
n=select(sockmax+1,&frd,NULL,NULL,NULL);
if(n<0){
perror(“select”);
exit(1);
}
fro(i=0;i<smax;i++){
if(FD_ISSET(s[i],&rfd)){
fromlen=sizeof(from);
ls=accept(s[i],(struct sockaddr *)&from &fromlen);
if(ls<0) continue;
write(ls,”hello/n”,6);
close(ls);
}
}
}
}