IPv6网络编程
Contents
翻译
本文另有 英文版本。
垃圾在玉泉三本的有线好歹是有 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_family、sin6_port、sin6_addr 之外,还有两个额外的成员 sin6_flowinfo、sin6_scope_id,所以在使用时要注意先用 memset 把全部成员清零,否则有可能造成连接失败。
sockaddr_storage
这个结构体可以容纳下所有类型的 sockaddr。对于 IPv4 和 IPv6 混合编程,可以使用这个结构来统一存放地址。但是这个结构体使用时,除了一个 ss_family 能直接访问,其它的总要类型转换,有点麻烦。我一般用一个 sockaddr_in、sockaddr_in6、sockaddr_storage 组成的 union 来保存地址,这样调试的时候也方便查看。
connect
connect() 并没有什么特别的地方,注意 addrlen 参数不要填得太短了导致失败,send 和 recv 和 IPv4 的一样。
IN6ADDR_ANY_INIT、IN6ADDR_LOOPBACK_INIT
类似 IPv4 的 INADDR_ANY、INADDR_LOOPBACK,可以直接赋值给 sin6_addr,分别用来 bind 到 [::] 和 [::1]。
地址的转换
可以用 inet_pton() 把字符串格式的地址转换为 sin6_addr 的二进制表示。inet_ntop() 作用与其相反。
连接到链路本地地址
前面提到的手工填入各项参数的方法比较适合 IPv6 单播地址,但对于没有接入到 IPv6 互联网的情况,只有链路本地地址可以使用(当然链路本地地址只能在同一个“链路”内使用,比如同一个交换机内)。链路本地地址的前缀为 fe80::/10,后面跟着 64 位的后缀。
原理
直接把链路本地地址填进去是不能连接的,因为链路本地地址并没有像单播地址那样的前缀机制,没法区分出哪些地址处于一个子网中,路由表中也没有链路本地地址的路由信息。如果电脑有多块网卡,就无法判断该由哪块网卡发送数据。
ipv6(7) 中提到了 sin6_scope_id 的用法:
sin6_scope_idis 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 casesin6_scope_idcontains the interface index (seenetdevice(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 地址,例如:
| |
单播地址或者带有 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 啊……