ip存储
IP 地址存储方案:IPv4 与 IPv6 的高效处理方式
在开发中,IP 地址(尤其是用户访问日志、设备管理场景)需要高效存储和查询。直接存储字符串形式的 IP 不仅占用空间大,还不利于范围查询(如判断某个 IP 是否在指定网段内)。本文将详细讲解 IPv4 和 IPv6 地址的数字化存储方案,包括转换原理、代码实现及应用场景。
IPv4 地址的存储:使用 long 类型
IPv4 地址由 4 个 0-255 的整数组成(如 192.168.1.1),总长度为 32 位(4×8 位),刚好可以用一个 64 位的 long 类型存储。
转换原理
将 IPv4 地址的 4 个分段视为 32 位二进制数的 4 个字节,按从左到右的顺序拼接后转换为十进制整数。例如:
- IP 地址
192.168.1.1可拆分为192、168、1、1; - 转换为二进制:
11000000.10101000.00000001.00000001; - 拼接后为 32 位二进制数,转换为十进制即
3232235777。
代码实现
public class IPv4Converter {
/**
* 将 IPv4 字符串转换为 long 类型
* @param ipString IPv4 地址(如 "192.168.1.1")
* @return 对应的 long 值,转换失败返回 -1
*/
public static long ipToLong(String ipString) {
// 拆分 IP 地址为 4 个分段
String[] segments = ipString.split("\\.");
if (segments.length != 4) {
return -1; // 格式错误
}
long result = 0;
try {
// 每个分段左移相应位数后累加(第一段占最高 8 位,第四段占最低 8 位)
result += Long.parseLong(segments[0]) << 24; // 第一段:左移 24 位
result += Long.parseLong(segments[1]) << 16; // 第二段:左移 16 位
result += Long.parseLong(segments[2]) << 8; // 第三段:左移 8 位
result += Long.parseLong(segments[3]); // 第四段:不左移
return result;
} catch (NumberFormatException e) {
return -1; // 分段包含非数字
}
}
/**
* 将 long 类型转换为 IPv4 字符串
* @param ipLong 用 long 存储的 IPv4 地址
* @return 对应的 IPv4 字符串
*/
public static String longToIp(long ipLong) {
// 从 long 中提取 4 个分段(通过右移和与运算)
StringBuilder ip = new StringBuilder();
ip.append((ipLong >>> 24)).append("."); // 提取最高 8 位
ip.append((ipLong >>> 16) & 0xFF).append("."); // 提取次高 8 位(&0xFF 确保只取 8 位)
ip.append((ipLong >>> 8) & 0xFF).append("."); // 提取次低 8 位
ip.append(ipLong & 0xFF); // 提取最低 8 位
return ip.toString();
}
// 测试
public static void main(String[] args) {
String ip = "255.255.255.255";
long ipLong = ipToLong(ip);
System.out.println(ipLong); // 输出:4294967295
System.out.println(longToIp(ipLong)); // 输出:255.255.255.255
}
}
优势与应用
- 节省空间:
long类型仅占 8 字节,比字符串形式(如"192.168.1.1"占 11 字节)更节省存储; - 高效查询:支持范围查询(如判断 IP 是否在
192.168.1.0/24网段内,可通过比较 long 值范围实现); - 数据库优化:在 MySQL 中可直接用
BIGINT类型存储,支持索引加速查询。
IPv6 地址的存储:使用 BigInteger
IPv6 地址长度为 128 位(16 个字节),远超 long 类型的 64 位,因此需要用 BigInteger 存储(支持任意长度整数)。
IPv6 地址的特点
- 格式:由 8 个 16 进制分段组成,中间用冒号分隔(如
2001:0db8:85a3:0000:0000:8a2e:0370:7334); - 压缩规则:连续的零分段可省略为
::(如2001:db8::8a2e:370:7334); - 长度:128 位,可表示的地址数量约为 3.4×10³⁸,远超 IPv4 的 43 亿。
转换原理
将 IPv6 地址的 8 个 16 进制分段转换为 128 位二进制数,再转换为 BigInteger。例如:
- 分段
2001:0db8:85a3:0000:0000:8a2e:0370:7334转换为 16 进制字符串20010db885a3000000008a2e03707334; - 将 16 进制字符串转换为
BigInteger。
代码实现
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class IPv6Converter {
private static final Logger LOGGER = LoggerFactory.getLogger(IPv6Converter.class);
/**
* 将 IPv6 字符串转换为 BigInteger
* @param ipv6Address IPv6 地址(如 "2001:db8::1")
* @return 对应的 BigInteger,转换失败返回 BigInteger.ZERO
*/
public static BigInteger ip6ToNumber(String ipv6Address) {
try {
// 使用 InetAddress 解析 IPv6 地址(自动处理压缩格式)
InetAddress inetAddress = InetAddress.getByName(ipv6Address);
byte[] bytes = inetAddress.getAddress(); // 获取 16 字节的 IP 地址字节数组
// 将字节数组转换为 BigInteger(注意:需处理符号位,避免负数)
return new BigInteger(1, bytes); // 1 表示正数
} catch (UnknownHostException e) {
LOGGER.error("IPv6 转换失败:{}", ipv6Address, e);
return BigInteger.ZERO;
}
}
/**
* 将 BigInteger 转换为 IPv6 字符串
* @param number 用 BigInteger 存储的 IPv6 地址
* @return 对应的 IPv6 字符串
*/
public static String numberToIp6(BigInteger number) {
try {
// 将 BigInteger 转换为 16 字节的数组(不足 16 字节则补前导零)
byte[] bytes = number.toByteArray();
byte[] ipv6Bytes = new byte[16];
// 复制字节(若 bytes 长度超过 16,则截取后 16 字节;否则在前面补零)
int copyLength = Math.min(bytes.length, 16);
System.arraycopy(
bytes, bytes.length - copyLength,
ipv6Bytes, 16 - copyLength,
copyLength
);
// 通过 InetAddress 格式化 IPv6 地址(自动处理压缩)
return InetAddress.getByAddress(ipv6Bytes).getHostAddress();
} catch (UnknownHostException e) {
LOGGER.error("BigInteger 转换为 IPv6 失败:{}", number, e);
return "";
}
}
// 测试
public static void main(String[] args) {
String ipv6 = "2001:db8::1";
BigInteger number = ip6ToNumber(ipv6);
System.out.println(number); // 输出:32322355216131583745068724708956782081
System.out.println(numberToIp6(number)); // 输出:2001:db8:0:0:0:0:0:1
}
}
关键细节
- 处理压缩格式:
InetAddress.getByName()会自动解析::压缩格式,无需手动处理; - 字节数组转换:
BigInteger的toByteArray()可能包含符号位(首位为 0 时),需通过new BigInteger(1, bytes)确保正数; - 性能考量:
BigInteger操作比long稍慢,但对于 IPv6 场景是唯一可行的方案。
存储方案对比与建议
| 类型 | 地址长度 | 存储类型 | 占用空间 | 适用场景 |
|---|---|---|---|---|
| IPv4 | 32 位 | long | 8 字节 | 传统网络设备、多数服务器 |
| IPv6 | 128 位 | BigInteger | 16-20 字节 | 物联网、新网络协议场景 |
最佳实践
区分 IP 类型:存储前先判断是 IPv4 还是 IPv6(可通过是否包含冒号区分),分别使用
long和BigInteger;数据库存储:
- IPv4 用
BIGINT类型; - IPv6 用
VARBINARY(16)(存储 16 字节数组)或VARCHAR(39)(字符串,不推荐);
- IPv4 用
索引优化:对存储 IP 的字段建立索引,加速范围查询(如查询某个网段内的所有 IP)。
ipv4
ipv4使用long类型进行存储即可
public static long ipToLong(String ipString) throws NumberFormatException {
long result = 0;
String[] terms = ipString.split("\\.");
if (terms.length != 4) {
return -1;
}
result += Long.parseLong(terms[0]) << 24;
result += Long.parseLong(terms[1]) << 16;
result += Long.parseLong(terms[2]) << 8;
result += Long.parseLong(terms[3]);
return result;
}
public static String longToIp(long ipLong) {
return (ipLong >>> 24) + "." +
((ipLong >>> 16) & 0xFF) + "." +
((ipLong >>> 8) & 0xFF) + "." +
(ipLong & 0xFF);
}
public static void main(String[] args) {
long result = ipToLong("255.255.255.255");
System.out.println(result);
System.out.println(longToIp(result));
}
ipv6
ipv6使用long类型是存储不了的,需要使用BigInteger
public static BigInteger ip6ToNumber(String ipv6Address) throws NumberFormatException {
try{
InetAddress inetAddress = InetAddress.getByName(ipv6Address);
String hostAddress = inetAddress.getHostAddress();
StringBuilder decimalString = new StringBuilder();
String[] split = hostAddress.split(":");
for(String s : split){
// 十六进制转为十进制
int i = Integer.parseInt(s, 16);
// 不足五位拿0补齐
decimalString.append(String.format("%05d",i));
}
return new BigInteger(decimalString.toString());
} catch (UnknownHostException exception){
LOGGER.error("ipv6转换失败 {}", ipv6Address, exception);
return BigInteger.ZERO;
}
}