最近有需求須要解析ipv6地址,因爲數據量有點大,因此不可能去請求先用的http查詢接口,這樣子確定嚴重影響使用,因此在網上搜索的時候找到了zxinc
這個網站,提供離線文件進行查詢,而後就下載了最新的ipv6
離線數據,裏邊也有一些編程語言(python,php,c++)的讀取實現,卻沒有我要的JAVA的實現,因而參考python的實現,本身實現了一下讀取數據的java版本。在這裏分享出來供你們使用,若是代碼有什麼問題或者能夠優化的地方,還請你們指正。php
首先咱們先介紹一下ipv6地址的組成:IPv6 地址是128位的,就是說ipv6地址總共由128個0或1組成,分紅8塊(block),每塊16位。每塊能夠表示成四個16進制數,用冒號「:」隔開。
ipv6地址的書寫有兩個規則,以FF01:0000:3EE8:DFE1:0063:0000:0000:F001
爲例,咱們說明這兩個規則:html
FF01:0000:3EE8:DFE1:63:0000:0000:F001
FF01:0:3EE8:DFE1:63::F001
在實現代碼之前要先熟悉一下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類型,直接參考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; }
在處理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類型溢出的坑裏,一步一步走過來,最終也算是實現了查找功能,收穫也頗豐。代碼中若是有什麼不對的地方歡迎你們指出,共同交流,或者其中的方法有什麼能夠優化的,也能夠提出,謝謝你們。數組