個人網站上有個功能,記錄訪問者的IP及其歸屬地。最初我偷懶通過一個WebService來查詢IP歸屬地,後來覺得通過這種方法響應時間長,資源耗費大,而且對那個WebSerice的依賴度太高,如果它掛瞭或者網絡原因,經常要到超時才返回。所以,我打算直接從本地的純真IP庫裡查詢。
純真庫的數據結構在https://lumaqq.linuxsir.org/article/qqwry_format_detail.html上講的很詳細瞭。簡單地講數據文件分成三個區域:
1、文件頭(8個字節,前4字節是指向索引區第一條記錄,後4字節指向索引區最後一條記錄)
2、記錄區(一個記錄包含IP地址,國傢記錄,地區記錄,後兩個記錄有可能是字符串,也可能是重定向,有多種重定向模式)
3、索引區(一個索引定長7個字節,前4字節是IP地址(little-endian),後3字節指向對應記錄區的位置,這裡的位置指從文件頭開始計算的偏移字節)
雖然這個庫結構工作的很好,效率也沒有問題,但是我覺得設計的有點小復雜瞭。而且,如果記錄區中有條記錄A,是重定向到記錄B中的,假如我刪除瞭記錄B,查詢記錄A的時候就會有問題。當然,可以在刪除記錄B的時候進行相應處理,隻是有些麻煩。如果把文件結構改成如下,應該處理起來會更方便一些:
1、文件頭(與原庫一樣)
2、字符串區
3、索引區(4字節的IP地址,4字節的偏移值,4字節的偏移值)
所有字符串放在字符串區中,統一管理。索引區中放IP地址,國傢記錄的“指針”和區域記錄的“指針”,所謂的“指針”是對應到字符串區中某條的字符串偏移值。
不過既然純真IP庫是這麼設計的,我隻好根據它的結構來進行相應的查詢。
索引區的記錄是從小到大排列的,可以用二分法來查詢。
IP庫中索引的IP地址,並不是連續的,舉個例子,192.168.0.0的後一條記錄並不是192.168.0.1,可能是192.169.0.0,也就是說,它存儲的一個是IP段。所以要做一個類似於“四舍五入”的處理。好在大部分情況下,我們都隻要舍掉就可以瞭,比如查詢192.168.1.1應該匹配192.168.0.0而不是192.169.0.0。
import java.io.*;
public class IPSeeker
{
protected RandomAccessFile ipDataFile;
protected final int RECORD_LEN = 7;
protected final int MODE_1 = 0x01; //重定向國傢記錄,地區記錄
protected final int MODE_2 = 0x02; //重定向國傢記錄,有地區記錄
protected final int MODE_3 = 0x03; //default
protected long indexBegin;
protected long indexEnd;
public IPSeeker() throws Exception
{
//打開純真IP庫數據文件
ipDataFile = new RandomAccessFile("qqwry.dat", "r");
indexBegin = readLong(4, 0);
indexEnd = readLong(4, 4);
}
public static void main(String[] args) throws Exception
{
IPSeeker seeker = new IPSeeker();//may throw Exception
String result = seeker.search("111.2.13.4");//輸入查詢的IP地址
System.out.println(result);
seeker.close();//關閉,若不調用close,將在finalize關閉
seeker = null;
}
@Override
protected void finalize() throws Throwable
{
try
{
ipDataFile.close();
}
catch (IOException e)
{
}
super.finalize();
}
public void close()
{
try
{
ipDataFile.close();
}
catch (IOException e)
{
}
}
public String search(String ipStr) throws Exception
{
//采用二分法查詢
long recordCount = (indexEnd – indexBegin) / 7 + 1;
long itemStart = 0;
long itemEnd = recordCount – 1;
long ip = IPSeeker.stringIP2Long(ipStr);
long middle = 0;
long midIP = 0;
while(itemStart <= itemEnd)
{
middle = (itemStart + itemEnd) / 2;
midIP = readLong(4, indexBegin + middle * 7);
//String temp = IPSeeker.long2StringIP(midIP);
if(midIP == ip)
{
break;
}
else if(midIP < ip)
{
itemStart = middle + 1;
}
else//midIP > ip
{
itemEnd = middle – 1;
}
}
//若無完全匹配結果,則向前匹配
if(ip < midIP && middle > 0)
{
middle–;
}
long item = readLong(3, indexBegin + middle * 7 + 4);
String[] result = getInfo(item + 4);//取出信息
return long2StringIP(readLong(4, indexBegin + middle * 7))+ ","//匹配到的IP地址(段)
+ result[0] + "," //國傢
+ result[1];//地區
}
//32位整型格式的IP地址(little-endian)轉化到字符串格式的IP地址
public static String long2StringIP(long ip)
{
long ip4 = ip >> 0 & 0x000000FF;
long ip3 = ip >> 8 & 0x000000FF;
long ip2 = ip >> 16 & 0x000000FF;
long ip1 = ip >> 24 & 0x000000FF;
return String.valueOf(ip1) + "." + String.valueOf(ip2) + "." +
String.valueOf(ip3) + "." + String.valueOf(ip4);
}
//字符串格式的IP地址轉化到32位整型格式的IP地址(little-endian)
public static Long stringIP2Long(String ipStr) throws Exception
{
String[] list = ipStr.split("\\.");
if(list.length != 4)
{
throw new Exception("IP地址格式錯誤");
}
long ip = Long.parseLong(list[0]) << 24 & 0xFF000000;
ip += Long.parseLong(list[1]) << 16 & 0x00FF0000;
ip += Long.parseLong(list[2]) << 8 & 0x0000FF00;
ip += Long.parseLong(list[3]) << 0 & 0x000000FF;
return ip;
}
//讀取一個n位的
private long readLong(int nByte, long offset) throws Exception
{
ipDataFile.seek(offset);
long result = 0;
if(nByte > 4 || nByte < 0)
throw new Exception("nBit should be 0-4");
for(int i = 0; i < nByte; i++)
{
result |= ((long)ipDataFile.readByte() << 8 * i) & (0xFFL << 8 * i);
}
return result;
}
private String[] getInfo(long itemStartPos) throws Exception
{
//result[0]放國傢,result[1]放地區
String[] result = new String[2];
ipDataFile.seek(itemStartPos);
int mode = (int)ipDataFile.readByte();
switch (mode)
{
case MODE_1:
{
long offset = itemStartPos + 1;
long redirPos = readLong(3, offset);
result = getInfo(redirPos);
}
break;
case MODE_2:
{
long offset = itemStartPos + 1;
long redirPos = readLong(3, offset);
result = getInfo(redirPos);
result[1] = getArea(offset + 3);
}
break;
default://MODE_3
{
long offset = itemStartPos;
int countryLen = getStrLength(offset);
result[0] = getString(offset, countryLen);
offset = itemStartPos + countryLen + 1;
result[1] = getArea(offset);
}
break;
}
return result;
}
private String getArea(long offset) throws Exception
{
ipDataFile.seek(offset);
int cityMode = (int)ipDataFile.readByte();
if(cityMode == MODE_2 || cityMode == MODE_1)
{
offset = readLong(3, offset + 1);
}
int cityLen = getStrLength(offset);
return getString(offset, cityLen);
}
private int getStrLength(long pos) throws IOException
{
ipDataFile.seek(pos);
long strEnd = pos – 1;
while(ipDataFile.readByte() != (byte)0)
{
strEnd++;
}
return (int) (strEnd – pos + 1);
}
private String getString(long pos, int len) throws IOException
{
byte buf[] = new byte[len];
ipDataFile.seek(pos);
ipDataFile.read(buf);
String s = new String(buf, "gbk");
return s;
}
}
本文出自 “木又寸的技術博客” 博客