0%

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 可拆分为 19216811
  • 转换为二进制:11000000.10101000.00000001.00000001
  • 拼接后为 32 位二进制数,转换为十进制即 3232235777

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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() 会自动解析 :: 压缩格式,无需手动处理;
  • 字节数组转换BigIntegertoByteArray() 可能包含符号位(首位为 0 时),需通过 new BigInteger(1, bytes) 确保正数;
  • 性能考量BigInteger 操作比 long 稍慢,但对于 IPv6 场景是唯一可行的方案。

存储方案对比与建议

类型 地址长度 存储类型 占用空间 适用场景
IPv4 32 位 long 8 字节 传统网络设备、多数服务器
IPv6 128 位 BigInteger 16-20 字节 物联网、新网络协议场景

最佳实践

  • 区分 IP 类型:存储前先判断是 IPv4 还是 IPv6(可通过是否包含冒号区分),分别使用 longBigInteger

  • 数据库存储

    • IPv4 用 BIGINT 类型;
    • IPv6 用 VARBINARY(16)(存储 16 字节数组)或 VARCHAR(39)(字符串,不推荐);
  • 索引优化:对存储 IP 的字段建立索引,加速范围查询(如查询某个网段内的所有 IP)。

ipv4

ipv4使用long类型进行存储即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}

}

欢迎关注我的其它发布渠道