IPv6的普及速度比很多人预想的要快,国内运营商分配的宽带地址已经大量切换为IPv6,云服务器默认也带IPv6地址。如果用Ruby写的网络服务只支持IPv4,部署到这类环境时就可能出现无法监听、连接失败的问题。好在Ruby标准库自带的Socket模块对IPv6支持相当完善,本文就从地址格式、套接字创建、地址绑定到作用域ID处理,完整讲解如何在Ruby中做好IPv6网络编程。

一、IPv6地址格式与Ruby中的表示方法
IPv6地址长度为128位,通常写成8组冒号分隔的十六进制数,例如2001:db8:1234:5678:9abc:def0:1234:5678。书写时有两条压缩规则:每组前导零可以省略,连续的全零组可以用双冒号::替代,但双冒号在同一个地址中只能出现一次。比如回环地址0:0:0:0:0:0:0:1压缩后就是::1。
在Ruby中,处理IPv6地址的核心类是IPAddr。它可以完成解析、合法性校验、判断地址类型等操作,是网络编程中非常实用的工具。
require 'ipaddr'
addr = IPAddr.new('2001:db8::1')
puts addr.to_s # 输出 2001:db8::1
puts addr.family # 输出 Socket::AF_INET6
# 判断地址类型
addr6 = IPAddr.new('fe80::1%eth0')
puts addr6.to_s
loopback = IPAddr.new('::1')
puts loopback.loopback? # true
link_local = IPAddr.new('fe80::1')
puts link_local.link_local? # true,链路本地地址
# 注意:非法地址会抛出 ArgumentError
begin
IPAddr.new('2001::db8::1') # 双冒号出现两次,非法
rescue ArgumentError => e
puts "地址非法: #{e.message}"
end需要特别注意的是以fe80开头的链路本地地址。这类地址只在同一链路(同一个网段或同一交换机下)内有效,且每块网卡都可以有自己独立的链路本地地址,所以单独一个地址无法唯一标识目标。这就引出了后面要讲的scope_id(作用域ID)的概念。
二、创建IPv6套接字并绑定地址
Ruby中创建IPv6套接字最直接的方式是指定地址族为Socket::AF_INET6。下面是一个标准的TCP服务端写法,创建套接字、绑定地址、监听端口、接受连接一步到位。
require 'socket'
# 创建IPv6的TCP套接字
server = Socket.new(Socket::AF_INET6, Socket::SOCK_STREAM, 0)
# 设置地址复用,避免程序重启时报端口被占用
server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
# 构造IPv6套接字地址:端口9999,绑定到所有IPv6地址
sockaddr = Socket.pack_sockaddr_in(9999, '::')
server.bind(sockaddr)
server.listen(5)
puts 'IPv6服务端监听中...'
loop do
client, client_addr = server.accept
puts "客户端接入: #{client_addr.inspect}"
client.puts "hello from ipv6 server"
client.close
end这里绑定地址用了::,表示监听本机所有IPv6地址。如果只想监听某个具体地址,把它换成具体值即可,例如Socket.pack_sockaddr_in(9999, '2001:db8::1')。
另一个更简洁的方案是利用双栈套接字。在Linux和Windows上,绑定::的IPv6套接字默认还能接收IPv4连接,IPv4客户端的地址会被映射成::ffff:192.168.1.10这种IPv4映射格式。如果不希望接收IPv4连接,可以设置IPV6_V6ONLY选项:
# 仅接受IPv6连接,关闭双栈 server.setsockopt(Socket::IPPROTO_IPV6, Socket::IPV6_V6ONLY, 1)
如果觉得手动管理套接字太繁琐,Ruby也提供了更高层的封装。例如TCPServer.new('::', 9999)就能创建一个IPv6监听服务,内部帮你处理了创建、绑定、监听的全过程。而Socket.tcp_server_sockets(9999)更贴心,它会同时绑定IPv4和IPv6两套地址,返回一个套接字数组,兼容性最好。
require 'socket'
# 一次性绑定IPv4和IPv6
sockets = Socket.tcp_server_sockets(9999)
sockets.each { |s| puts "监听地址族: #{s.local_address.family}" }
# 客户端连接示例
Socket.tcp('::1', 9999) do |sock|
puts sock.read
end三、作用域ID:链路本地地址通信的关键
很多Ruby开发者第一次用IPv6时都会遇到同一个坑:尝试连接fe80::1这样的链路本地地址时,无论怎么写都报错Invalid argument或者连接超时。原因在于链路本地地址必须配合scope_id使用,用来告诉系统这个地址属于哪块网卡。没有scope_id,系统根本不知道该从哪个网络接口发包。
在命令行中,scope_id用百分号加接口名表示,比如fe80::1%eth0。在Ruby的Socket库中,Addrinfo对象专门有scope_id属性来处理它。下面的例子演示了完整的获取流程:
require 'socket'
# 获取网卡的IPv6地址信息,包括scope_id
Addrinfo.getaddrinfo('fe80::1', nil, Socket::AF_INET6, Socket::SOCK_STREAM).each do |ai|
puts "地址: #{ai.ip_address}"
puts "scope_id: #{ai.scope_id}"
end
# 手动构造带scope_id的套接字地址
iface_index = Socket.getifaddrs.find do |ifa|
ifa.name == 'eth0' && ifa.addr && ifa.addr.ipv6_linklocal?
end.addr.ifindex
addrinfo = Addrinfo.udp('fe80::1', 9999)
addrinfo.scope_id = iface_index
# 使用该地址发送UDP数据报
sock = Socket.open(Socket::AF_INET6, Socket::SOCK_DGRAM, Socket::IPPROTO_UDP)
sock.sendto('hello', 0, addrinfo)
sock.close获取网卡索引还有更直接的办法,就是遍历Socket.getifaddrs。它返回系统中所有网络接口的信息,每个条目都包含接口名和对应的地址对象,通过ipv6_linklocal?可以筛出链路本地地址,通过ifindex拿到接口索引。跨平台开发时要注意,Windows上的接口名通常是类似eth0之外的形式(如以太网或数字索引),所以推荐用接口索引而不是接口名字符串来设置scope_id,兼容性更可靠。
四、实战:一个兼容IPv6的回显服务
把前面的知识串起来,写一个同时支持IPv6的TCP回显服务,服务端把收到的数据原样返回,并打印客户端的地址信息。代码中使用了Socket.tcp_server_sockets实现双栈监听,用线程处理并发连接。
require 'socket'
sockets = Socket.tcp_server_sockets(9999)
threads = []
loop do
ready = IO.select(sockets)
readable, = ready
readable.each do |srv|
client, addr = srv.accept
threads << Thread.new(client) do |c|
peer = c.remote_address
puts "接入: #{peer.ip_address}:#{peer.ip_port} (#{peer.family})"
while (line = c.gets)
c.puts line # 原样回显
end
c.close
end
end
end测试时可以在本机用telnet ::1 9999验证IPv6回环,再用局域网内另一台机器的IPv6地址连接验证。如果服务部署在云服务器上,记得在安全组里放行对应端口,同时确认系统防火墙对IPv6流量放行,这两点是部署阶段最常被忽略的环节。
总的来说,Ruby的IPv6网络编程并不复杂,核心就三点:用AF_INET6或直接绑定::创建套接字,用Socket.tcp_server_sockets获得最好的双栈兼容性,处理fe80链路本地地址时别忘了带上scope_id。掌握这些,写出的网络程序就能在纯IPv6环境下稳定运行了。