最近有需求需要解析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
为例,我们说明这两个规则:
- 去掉前导的0
去掉后的结果FF01:0000:3EE8:DFE1:63:0000:0000:F001
- 如果两个以上的块含有连续的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类型溢出的坑里,一步一步走过来,最终也算是实现了查找功能,收获也颇丰。代码中如果有什么不对的地方欢迎大家指出,共同交流,或者其中的方法有什么可以优化的,也可以提出,谢谢大家。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。