1

最近有需求需要解析ipv6地址,由于数据量有点大,所以不可能去请求先用的http查询接口,这样子肯定严重影响使用,所以在网上搜索的时候找到了zxinc这个网站,提供离线文件进行查询,然后就下载了最新的ipv6离线数据,里边也有一些编程语言(python,php,c++)的读取实现,却没有我要的JAVA的实现,于是参考python的实现,自己实现了一下读取数据的java版本。在这里分享出来供大家使用,如果代码有什么问题或者可以优化的地方,还请大家指正。

ipv6

首先我们先介绍一下ipv6地址的组成:IPv6 地址是128位的,就是说ipv6地址总共由128个0或1组成,分成8块(block),每块16位。每块可以表示成四个16进制数,用冒号“:”隔开。
ipv6地址的书写有两个规则,以FF01:0000:3EE8:DFE1:0063:0000:0000:F001为例,我们说明这两个规则:

  1. 去掉前导的0
    去掉后的结果FF01:0000:3EE8:DFE1:63:0000:0000:F001
  2. 如果两个以上的块含有连续的0,则省去所有的块并代之以“::” , 如果还有全0的块,它们可以缩写为一个0
    结果为FF01:0:3EE8:DFE1:63::F001

BigInteger

在实现代码以前要先熟悉一下BigInteger这个类,如字面意思大整形,在java中,int类型占4个字节,long类型占8个字节,每个字节8位,所以int所能表达的最大值为2^(48-1) - 1=2147483647,而long类型所能表达的最大值为2^(88-1) - 1 = 9223372036854775807,而我们在进行ipv6解析的时候,经常要对long类型进行左移运算,这个时候就有可能导致位移后的数超出了long类型所能表达的范围,这时候结果会以负数形式展示,这个结果自然就不是我们想要的了,所以在ipv6解析的时候我们要使用BigInteger这个类来进行位运算操作,下边列举一下BigInteger的位运算相关方法,具体的BigInteger请参考这篇文章:Java 大数高精度函数(BigInteger)

方法示例

BigInteger n = new BigInteger( String ); 
BigInteger m = new BigInteger( String );
方法 等同操作 说明
n.shiftLeft(k) n << k 左移
n.shiftRight(k) n >> k 右移
n.and(m) n & m
n.or(m) n l m

ipv6转Long

ipv6转long类型,直接参考zxinc中的python代码编写,替换了其中可能出现运算溢出的情况

public BigInteger ipv6ToNum(String ipStr) {
    // String ipStr = "2400:3200::1";
    // 最多被冒号分隔为8段
    int ipCount = 8;
    List<String> parts = new ArrayList<>(Arrays.asList(ipStr.split(":")));
    // 最少也至少为三段,如:`::1`
    if (parts.size() < 3) {
        System.out.println("error ip address");
    }
    String last = parts.get(parts.size() - 1);
    if (last.contains(".")) {
        long l = ipv4ToNum(last);
        parts.remove(parts.size() - 1);
        // (l >> 16) & 0xFFFF;
        parts.add(new BigInteger(((l >> 16) & 0xFFFF) + "").toString(16));
        parts.add(new BigInteger((l & 0xFFFF) + "").toString(16));
    }
    int emptyIndex = -1;
    for (int i = 0; i < parts.size(); i++) {
        if (StringUtils.isEmpty(parts.get(i))) {
            emptyIndex = i;
        }
    }
    int parts_hi, parts_lo, parts_skipped;
    if (emptyIndex > -1) {
        parts_hi = emptyIndex;
        parts_lo = parts.size() - parts_hi - 1;
        if (StringUtils.isEmpty(parts.get(0))) {
            parts_hi -= 1 ;
            if (parts_hi > 0) {
                System.out.println("error ip address");
            }
        }
        if (StringUtils.isEmpty(parts.get(parts.size() - 1))) {
            parts_lo -= 1;
            if (parts_lo > 0) {
                System.out.println("error ip address");
            }
        }
        parts_skipped = ipCount - parts_hi - parts_lo;
        if (parts_skipped < 1) {
            System.out.println("error ip address");
        }
    } else {
        // 完全地址
        if (parts.size() != ipCount) {
            System.out.println("error ip address");
        }
        parts_hi = parts.size();
        parts_lo = 0;
        parts_skipped = 0;
    }
    BigInteger ipNum = new BigInteger("0");
    if (parts_hi > 0) {
        for (int i = 0; i < parts_hi; i++) {
            ipNum = ipNum.shiftLeft(16);
            String part = parts.get(i);
            if (part.length() > 4) {
                System.out.println("error ip address");
            }
            BigInteger bigInteger = new BigInteger(part, 16);
            int i1 = bigInteger.intValue();
            if (i1 > 0xFFFF) {
                System.out.println("error ip address");
            }
            ipNum = ipNum.or(bigInteger);
        }
    }
    ipNum = ipNum.shiftLeft(16 * parts_skipped);
    for (int i = -parts_lo; i < 0; i++) {
        // ipNum <<= 16;
        ipNum = ipNum.shiftLeft(16);
        String part = parts.get(parts.size() + i);
        if (part.length() > 4) {
            System.out.println("error ip address");
        }
        BigInteger bigInteger = new BigInteger(part, 16);
        int i1 = bigInteger.intValue();
        if (i1 > 0xFFFF) {
            System.out.println("error ip address");
        }
        // ipNum |= i1;
        ipNum = ipNum.or(bigInteger);
    }

    System.out.println(ipNum);
    return ipNum;
}

byte[]数据转为数字

在处理ipv4的时候,我们通常把byte[]转为long返回,但是在处理ipv6的时候则不能处理为long类型,因为可能存在溢出的情况,所以使用我们前边介绍的BigInteger进行处理。

private BigInteger byteArrayToBigInteger(byte[] b) {
    BigInteger ret = new BigInteger("0");
    // 循环读取每个字节通过移位运算完成long的8个字节拼装
    for(int i = 0; i < b.length; i++){
        // value |=((long)0xff << shift) & ((long)b[i] << shift);
        int shift = i << 3;
        BigInteger shiftY = new BigInteger("ff", 16);
        BigInteger data = new BigInteger(b[i] + "");
        ret = ret.or(shiftY.shiftLeft(shift).and(data.shiftLeft(shift)));
    }
    return ret;
}

就是这里注释的语句把0xff转成long类型,导致数溢出,在这里坑了我好久。最好全部都用BigInteger进行处理。
这里的byte数组转BigInteger的操作参考:java:bytes[]转long的三种方式文章中的第一种方法。
这里的byteArrayToBigInteger等效与下边的代码,写法上简单一点,而下边这个方法用long去作为最后的结果,就会有溢出的风险:

//这是将二进制转化成long类型的方法
    public static long getLongAt(byte[] buffer,int offset) {
    long value = 0;
    //第一个value和上面的long类型转化成二进制对应起来,
    //先将第一个取出来的左移64位与FF000000相与就是这八位,再相或就是原来的前八位
    value |= buffer[offset + 0] << 56 & 0xFF00000000000000L;
    value |= buffer[offset + 1] << 48 & 0x00FF000000000000L;
    value |= buffer[offset + 2] << 40 & 0x0000FF0000000000L;
    value |= buffer[offset + 3] << 32 & 0x000000FF00000000L;
    value |= buffer[offset + 4] << 24 & 0x00000000FF000000L;
    value |= buffer[offset + 5] << 16 & 0x0000000000FF0000L;
    value |= buffer[offset + 6] << 8 & 0x0000000000000FF0L;
    value |= buffer[offset + 7] & 0x00000000000000FFL;
    return value;
}

大概需要注意的就这些地方,下边贴上完整代码:

import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;

/**
 * ip库来源:http://ip.zxinc.org/
 * 以下所有数据类型(short/int/int64/IP地址/偏移地址等)均为小端序
 *
 * 文件头
 * 0~3        字符串    "IPDB"
 * 4-5        short    版本号,现在是2。版本号0x01到0xFF之间保证互相兼容。
 * 6        byte    偏移地址长度(2~8)
 * 7        byte    IP地址长度(4或8或12或16, 现在只支持4(ipv4)和8(ipv6))
 * 8~15        int64    记录数
 * 16-23    int64    索引区第一条记录的偏移
 * 24        byte    地址字段数(1~255)[版本咕咕咕新增,现阶段默认为2]
 * 25-31    reserve    保留,用00填充
 * 32~39    int64    数据库版本字符串的偏移[版本2新增,版本1没有]
 *
 * 记录区
 * array 字符串[地址字段数]
 *     与qqwry.dat大致相同,但是没有结束IP地址
 *     01开头的废弃不用
 *     02+偏移地址[偏移长度]表示重定向
 *     20~FF开头的为正常的字符串,采用UTF-8编码,以NULL结尾
 *
 * 索引区
 * struct{
 *     IP[IP地址长度]    开始IP地址
 *     偏移[偏移长度]    记录偏移地址
 * }索引[记录数];
 *
 */
public class Ipv6Service {

    private String file = "D:\\project\\idea\\spring-boot-demo\\ipv6wry.db";

    //单一模式实例
    private static Ipv6Service instance = new Ipv6Service();

    // private RandomAccessFile randomAccessFile = null;
    private byte[] v6Data;
    // 偏移地址长度
    private int offsetLen;
    // 索引区第一条记录的偏移
    private long firstIndex;
    // 总记录数
    private long indexCount;

    public long getIndexCount() {
        return indexCount;
    }

    private Ipv6Service() {
        try(RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
            v6Data = new byte[(int) randomAccessFile.length()];
            randomAccessFile.readFully(v6Data, 0, v6Data.length);
        } catch (IOException e) {
            System.out.println("读取文件失败!");
        }
        // 获取偏移地址长度
        byte[] bytes = readBytes(6, 1);
        offsetLen = bytes[0];
        // 索引区第一条记录的偏移
        bytes = readBytes(16, 8);
        BigInteger firstIndexBig = byteArrayToBigInteger(bytes);
        firstIndex = firstIndexBig.longValue();
        // 570063
        System.out.println("索引区第一条记录的偏移:" + firstIndex);
        // 总记录数
        bytes = readBytes(8, 8);
        BigInteger indexCountBig = byteArrayToBigInteger(bytes);
        indexCount = indexCountBig.longValue();
    }

    /**
     * @return 单一实例
     */
    public static Ipv6Service getInstance() {
        return instance;
    }

    private byte[] readBytes(long offset, int num) {
        byte[] ret = new byte[num];
        for(int i=0; i < num; i++) {
            ret[i] = v6Data[(int) (offset + i)];
        }
        return ret;
    }

    /**
     * 对little-endian字节序进行了转换
     * byte[]转换为long
     * @param b
     * @return ret
     */
    private BigInteger byteArrayToBigInteger(byte[] b) {
        BigInteger ret = new BigInteger("0");
        // 循环读取每个字节通过移位运算完成long的8个字节拼装
        for(int i = 0; i < b.length; i++){
            // value |=((long)0xff << shift) & ((long)b[i] << shift);
            int shift = i << 3;
            BigInteger shiftY = new BigInteger("ff", 16);
            BigInteger data = new BigInteger(b[i] + "");
            ret = ret.or(shiftY.shiftLeft(shift).and(data.shiftLeft(shift)));
        }
        return ret;
    }

    /**
     * 二分查找
     * @param ip
     * @param l
     * @param r
     * @return
     */
    public long find (BigInteger ip, long l, long r){
        if (r - l <= 1)
            return l;
        long m = (l + r) >>> 1;
        long o = firstIndex + m * (8 + offsetLen);
        byte[] bytes = readBytes(o,  8);
        BigInteger new_ip = byteArrayToBigInteger(bytes);
        if (ip.compareTo(new_ip) == -1) {
            return find(ip, l, m);
        } else {
            return find(ip, m, r);
        }
    }

    public static long ipv4ToNum(String ipStr) {
        long result = 0;
        String[] split = ipStr.split("\\.");
        if (split.length != 4) {
            System.out.println("error ip address");
        }
        for (int i = 0; i < split.length; i++) {
            int s = Integer.valueOf(split[i]);
            if (s > 255) {
                System.out.println("error ip address");
            }
            result = (result << 8) | s;
        }
        return result;
    }

    // ipv6ToNum方法在文章上边已贴出代码,这里为了减少代码,去掉
    public BigInteger ipv6ToNum(String ipStr) {
        BigInteger ipNum = new BigInteger("0");
        return ipNum;
    }

    public long getIpOff(long findIp) {
        return firstIndex + findIp * (8 + offsetLen);
    }

    public long getIpRecOff(long ip_off) {
        byte[] bytes = readBytes(ip_off + 8, offsetLen);
        BigInteger ip_rec_off_big = byteArrayToBigInteger(bytes);
        return ip_rec_off_big.longValue();
    }

    public String getAddr(long offset) {
        byte[] bytes = readBytes(offset, 1);
        int num = bytes[0];
        if (num == 1) {
            // 重定向模式1
            // [IP][0x01][国家和地区信息的绝对偏移地址]
            // 使用接下来的3字节作为偏移量调用字节取得信息
            bytes = readBytes(offset + 1, offsetLen);
            BigInteger l = byteArrayToBigInteger(bytes);
            return getAddr(l.longValue());
        } else {
            // 重定向模式2 + 正常模式
            // [IP][0x02][信息的绝对偏移][...]
            String cArea = getAreaAddr(offset);
            if (num == 2) {
                offset += 1 + offsetLen;
            } else {
                offset = findEnd(offset) + 1;
            }
            String aArea = getAreaAddr(offset);
            return cArea + "|" + aArea;
        }
    }

    private String getAreaAddr(long offset){
        // 通过给出偏移值,取得区域信息字符串
        byte[] bytes = readBytes(offset, 1);
        int num = bytes[0];
        if (num == 1 || num == 2) {
            bytes = readBytes(offset + 1, offsetLen);
            BigInteger p = byteArrayToBigInteger(bytes);
            return getAreaAddr(p.longValue());
        } else {
            return getString(offset);
        }
    }

    private String getString(long offset) {
        long o2 = findEnd(offset);
        // 有可能只有国家信息没有地区信息,
        byte[] bytes = readBytes(offset, Long.valueOf(o2 - offset).intValue());
        try {
            return new String(bytes, "utf8");
        } catch (UnsupportedEncodingException e) {
            return "未知数据";
        }
    }

    private long findEnd(long offset) {
        int i = Long.valueOf(offset).intValue();
        for (; i < v6Data.length; i++) {
            byte[] bytes = readBytes(i, 1);
            if ("\0".equals(new String(bytes))) {
                break;
            }
        }
        return i;
    }

    public static void main(String[] args) {
        Ipv6Service ipv6Service = Ipv6Service.getInstance();
        BigInteger ipNum = ipv6Service.ipv6ToNum("2409:8754:2:1::d24c:4b55");
        BigInteger ip = ipNum.shiftRight(64).and(new BigInteger("FFFFFFFFFFFFFFFF", 16));
        // 查找ip的索引偏移
        long findIp = ipv6Service.find(ip, 0, ipv6Service.getIndexCount());
        // 得到索引记录
        long ip_off = ipv6Service.getIpOff(findIp);
        long ip_rec_off = ipv6Service.getIpRecOff(ip_off);
        String addr = ipv6Service.getAddr(ip_rec_off);
        System.out.println(addr);
    }

}

上边是完整的代码,整个代码参考zxinc网站给出的python示例代码编写。

最后

由于对二进制的位运算不熟悉,写这个java代码也算花费了不少心血。了解ipv6地址的组成形式;熟悉二进制的位运算;然后到掉进long类型溢出的坑里,一步一步走过来,最终也算是实现了查找功能,收获也颇丰。代码中如果有什么不对的地方欢迎大家指出,共同交流,或者其中的方法有什么可以优化的,也可以提出,谢谢大家。

参考文章

  1. Java 大数高精度函数(BigInteger)
  2. java:bytes[]转long的三种方式
  3. Java中的二进制
  4. 关于Java实现的进制转化(位运算)
  5. java中的二进制以及基本位运算
  6. IPv6 地址类型

要名俗气
104 声望59 粉丝