面向 Web 开发者的 IPv6
Web 开发者需要了解的 IPv6 知识:带括号的 URL、数据库存储、套接字编程以及要避免的常见错误。
面向 Web 开发者的 IPv6#
如果你一直忽略 IPv6,因为「这将是以后的问题」,以后就是现在。移动网络运行 IPv6 优先。NAT64 后面的一些用户只能通过 IPv6 访问你。即使你的开发机器上的 localhost 也可能在 127.0.0.1 之前解析为 ::1。
本指南涵盖你实际需要知道的内容:在 URL 中处理 IPv6、在数据库中存储地址、套接字编程以及如果你不小心会咬你的错误。
URL 中的 IPv6#
IPv6 地址包含冒号,这与 URL 中的端口分隔符冲突。解决方案是方括号表示法:
http://[2001:db8::1]:8080/api/users
https://[2606:2800:220:1:248:1893:25c8:1946]/没有括号,解析器无法判断地址在哪里结束,端口在哪里开始。这适用于:
- HTTP/HTTPS URL
- WebSocket 连接(
ws://[::1]:3000) - 数据库连接字符串
- 任何 URI 方案
以编程方式生成 URL 时,将 IPv6 地址包装在括号中。解析时,在验证或存储之前去除它们。
数据库存储#
经典错误是使用 VARCHAR(15) 存储 IP 地址。这适合 IPv4(最大 15 个字符:255.255.255.255),但不适合 IPv6。
PostgreSQL 有一个原生 INET 类型,可以处理 IPv4 和 IPv6:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
ip_address INET NOT NULL
);
-- 自动验证和规范化
INSERT INTO users (ip_address) VALUES ('2001:db8::1');
INSERT INTO users (ip_address) VALUES ('192.0.2.1');INET 类型有效地存储地址,在插入时验证它们,并支持使用 << 和 >> 运算符的子网匹配等网络操作。
MySQL 缺乏原生类型,因此使用:
VARBINARY(16)- 存储二进制表示(IPv6 为 16 字节,IPv4 为 4 字节)VARCHAR(45)- 存储字符串表示(扩展 IPv6 的最大长度)
二进制存储更高效,但需要转换函数:
-- 存储
INSERT INTO users (ip_address) VALUES (INET6_ATON('2001:db8::1'));
-- 检索
SELECT INET6_NTOA(ip_address) FROM users;**规范化很重要。**IPv6 地址有多个有效表示:
2001:0db8:0000:0000:0000:0000:0000:00012001:db8::1(压缩)2001:db8:0:0:0:0:0:1(部分压缩)
在比较或索引之前始终规范化。大多数库都有规范形式函数。
套接字编程#
套接字 API 将 IPv4 和 IPv6 视为单独的地址族。现代应用程序应同时支持两者。
Node.js 默认使用双栈套接字:
const http = require('http');
// 绑定到 ::(所有 IPv6 地址)并通过映射接受 IPv4
const server = http.createServer((req, res) => {
res.end(`Your IP: ${req.socket.remoteAddress}`);
});
server.listen(3000, '::');:: 地址是 0.0.0.0 的 IPv6 等效项,接受所有接口上的连接。大多数系统支持 IPv4 映射的 IPv6 地址(::ffff:192.0.2.1),允许单个 IPv6 套接字处理两个协议。
Python 需要显式双栈处理:
import socket
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
sock.bind(('::', 8080))
sock.listen(5)IPV6_V6ONLY 标志控制套接字是仅接受 IPv6 还是同时接受 IPv4 和 IPv6。
Go 默认使双栈:
listener, err := net.Listen("tcp", ":8080")
// 自动在 IPv4 和 IPv6 上监听对于客户端连接,始终使用 getaddrinfo()(或你的语言的等效项)而不是手动解析地址。它正确处理 IPv4、IPv6 和双栈场景:
import socket
# 不要这样做
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('example.com', 80))
# 这样做 - 适用于 IPv4 和 IPv6
addr_info = socket.getaddrinfo('example.com', 80, socket.AF_UNSPEC, socket.SOCK_STREAM)
sock = socket.socket(addr_info[0][0], addr_info[0][1])
sock.connect(addr_info[0][4])本地测试#
大多数系统将 localhost 解析为 ::1(IPv6)和 127.0.0.1(IPv4)。检查你的 /etc/hosts:
127.0.0.1 localhost
::1 localhost如果你的应用程序仅绑定到 127.0.0.1,它将不接受 IPv6 连接。绑定到 :: 以实现双栈或明确绑定到两个地址。
要测试仅 IPv6 行为,请在本地主机接口上禁用 IPv4 或在 HTTP 客户端中明确使用 [::1]:
curl http://[::1]:3000/对于真实测试,使用仅 IPv6 测试网络。许多云提供商提供仅 IPv6 实例,强制你处理边缘情况,如 NAT64/DNS64。
CDN 和源服务器#
大多数 CDN(Cloudflare、Fastly、AWS CloudFront)默认支持 IPv6。它们将通过 IPv6 向客户端提供内容,并在需要时为你的源转换为 IPv4。
问题是:如果你直接公开 API(绕过 CDN),你的源服务器必须支持 IPv6。检查你的 DNS 有 AAAA 记录,你的防火墙允许 IPv6 流量。
常见错误#
**正则表达式验证。**IPv6 正则表达式模式非常复杂,通常是错误的:
// 不要这样做
const ipv6Regex = /^([0-9a-f]{1,4}:){7}[0-9a-f]{1,4}$/i;
// 在 :: 压缩、IPv4 映射地址、区域 ID 上失败...改用适当的解析库。每种主要语言都有一个正确处理 IPv6 的库。对于验证,请使用我们的 IPv6 验证器工具。
**硬编码的 IPv4 地址。**在你的代码库中搜索如下模式:
const API_SERVER = 'http://192.168.1.100:3000'; // 不适用于 IPv6改用主机名,或同时支持两个地址族。
**基于 IP 的速率限制。**由于隐私扩展,IPv6 地址经常更改。按确切地址进行速率限制将失败。改为按 /64 子网限制:
// 不好
const key = `rate:${ipAddress}`;
// 对于 IPv6 更好
const key = `rate:${ipv6ToSubnet64(ipAddress)}`;**没有 IPv6 支持的库。**较旧的 HTTP 客户端、数据库驱动程序和网络库可能不处理 IPv6。使用实际的 IPv6 地址进行测试,而不仅仅是 localhost。如果库失败,请检查更新或替代方案。
**URL 解析边缘情况。**确保你的路由器/框架处理带括号的 IPv6 地址:
GET http://[2001:db8::1]:8080/api/users
Host: [2001:db8::1]:8080一些解析器错误地将括号包含在 Host 头中或无法正确提取地址。
检查清单#
当你的应用程序准备好 IPv6 时:
- 数据库列可以存储 45 个字符的字符串或使用原生 IP 类型
- 服务器绑定到
::或明确监听 IPv4 和 IPv6 - 客户端代码使用
getaddrinfo()或等效项进行名称解析 - IP 验证不依赖正则表达式
- 配置中没有硬编码的 IPv4 地址
- 速率限制和地理位置处理 IPv6 子网
- URL 解析处理带括号的表示法
IPv6 不再是可选的。从一开始就构建它,你将避免以后的痛苦迁移。
相关文章#
- IPv6 基础知识,它是什么以及为何重要 - 了解开发者需要掌握的 IPv6 核心概念。
- IPv6 地址类型:全局、链路本地、多播详解 - 深入了解应用程序需要处理的不同地址类型。