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

代码实现

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() 会自动解析 :: 压缩格式,无需手动处理;
  • 字节数组转换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类型进行存储即可

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;
    }

}