基於zxinc網站ipv6靜態數據文件的JAVA查詢實現

最近有需求須要解析ipv6地址,因爲數據量有點大,因此不可能去請求先用的http查詢接口,這樣子確定嚴重影響使用,因此在網上搜索的時候找到了zxinc這個網站,提供離線文件進行查詢,而後就下載了最新的ipv6離線數據,裏邊也有一些編程語言(python,php,c++)的讀取實現,卻沒有我要的JAVA的實現,因而參考python的實現,本身實現了一下讀取數據的java版本。在這裏分享出來供你們使用,若是代碼有什麼問題或者能夠優化的地方,還請你們指正。php

ipv6

首先咱們先介紹一下ipv6地址的組成:IPv6 地址是128位的,就是說ipv6地址總共由128個0或1組成,分紅8塊(block),每塊16位。每塊能夠表示成四個16進制數,用冒號「:」隔開。
ipv6地址的書寫有兩個規則,以FF01:0000:3EE8:DFE1:0063:0000:0000:F001爲例,咱們說明這兩個規則:html

  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)java

方法示例

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代碼編寫,替換了其中可能出現運算溢出的狀況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進行處理。c++

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去做爲最後的結果,就會有溢出的風險:spring

//這是將二進制轉化成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 地址類型
相關文章
相關標籤/搜索