Java 统计文件ip_基于zxinc网站ipv6静态数据文件的JAVA查询实现

最近有需求需要解析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 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类型溢出的坑里,一步一步走过来,最终也算是实现了查找功能,收获也颇丰。代码中如果有什么不对的地方欢迎大家指出,共同交流,或者其中的方法有什么可以优化的,也可以提出,谢谢大家。

参考文章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值