垃圾在玉泉三本的有线好歹是有IPv6的,做计网作业的时候顺便研究了一下IPv6的网络编程。

基本上是我按照自己的经验写出来的,如果有不对的地方还请指正。

生成socket、连接server、绑定地址

socket和AF_INET6

调用socket()的时候,把第一个参数设成AF_INET6即可,例如生成一个TCP的socket:socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP)。有趣的是Windows和Linux上这个宏的实际值并不一样。

sockaddr_in6

sockaddr_in类似,用来存放IPv6的地址信息,除sin6_familysin6_portsin6_addr之外,还有两个额外的成员sin6_flowinfosin6_scope_id,所以在使用时要注意先用memset把全部成员清零,否则有可能造成连接失败。

sockaddr_storage

这个结构体可以容纳下所有类型的sockaddr,对于IPv4和IPv6混合编程,可以使用这个结构来统一存放地址。但是这个结构体使用时,除了一个ss_family能直接访问,其它的总要类型转换,有点麻烦,我一般用一个sockaddr_insockaddr_in6sockaddr_storage组成的union来保存地址,这样调试的时候也方便查看。

connect

connect()并没有什么特别的地方,注意addrlen参数不要填得太短了导致失败,send和recv和IPv4的一样。

IN6ADDR_ANY_INIT、IN6ADDR_LOOPBACK_INIT

类似IPv4的INADDR_ANYINADDR_LOOPBACK,可以直接赋值给sin6_addr,分别用来bind到[::][::1]

地址的转换

可以用inet_pton()把字符串格式的地址转换为sin6_addr的二进制表示。inet_ntop()作用与其相反。

连接到链路本地地址

前面提到的手工填入各项参数的方法比较适合IPv6单播地址,但对于没有接入到IPv6互联网的情况,只有链路本地地址可以使用(当然链路本地地址只能在同一个“链路”内使用,比如同一个交换机内),链路本地地址的前缀为fe80::/10,后面跟着64位的后缀。

原理

直接把链路本地地址填进去是不能连接的,因为链路本地地址并没有像单播地址那样的前缀机制,没法区分出哪些地址处于一个子网中,路由表中也没有链路本地地址的路由信息,如果电脑有多块网卡,就无法判断该由哪块网卡发送数据。

ipv6(7)中提到了sin6_scope_id的用法:

sin6_scope_id is an ID depending on the scope of the address. It is new in Linux 2.4. Linux supports it only for link-local addresses, in that case sin6_scope_id contains the interface index (see netdevice(7))

也就是说,使用链路本地地址时,需要在sin6_scope_id里填上网卡的序号,运行ip a(Linux)或者ipconfig /all(Windows)可以查看当前所有的地址:

lo、ens33前面的1、2就是index。在地址后面加上%x可以表示scope id,例如直接ping6 fe80::20c:29ff:fe09:31c6会提示参数无效,改成ping6 fe80::20c:29ff:fe09:31c6%2就能使用,ping6 fe80::20c:29ff:fe09:31c6%ens33也能运行。

getaddrinfo

可以直接把index填入sin6_scope_id中,但是对于后面给出网卡名称的形式,就需要调用getaddrinfo(),这个函数经常用于解析IPv4地址,但也能用来填写IPv6地址,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#define addr "fe80::20c:29ff:fe09:31c6%ens33"
#define port "80"

struct addrinfo hints, *results;
struct sockaddr_storage addr_svr;

memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_INET6;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
if (getaddrinfo(addr, port, &hint, &results) != 0) {
    puts("error in getaddrinfo");
}
else {
    memcpy(&addr_svr, results[0].ai_addr, results[0].ai_addrlen);
}

单播地址或者带有scope id的链路本地地址都可以用getaddrinfo()来获取sockaddr。

getnameinfo

这个函数作用和getaddrinfo()相反,如果已知某个sockaddr_in6结构体,可以用这个函数来转换成字符串,如果是链路本地地址,后面也会有scope id。

同时监听IPv6和IPv4

想写一个服务器的话肯定需要同时支持两种网络层协议,然而accept()是阻塞的,在IPv4上accept的时候IPv6就没法连接,所以必须使用多线程同时监听,或者采用I/O复用机制。

select()应该算是最原始的I/O复用机制了,把IPv6和IPv4的socket都加入到同一个fd_set里面,传给select()的readfds,当有连接进入的时候,用于accept的socket会变成可读,在可读的socket上调用accept()就可以避免被阻塞了。

最后

IPv6是大势所趋,三本的无线网啥时候也加上IPv6啊…