2025-05-23

 

  個人網站上有個功能,記錄訪問者的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;

    }

 

}

 

 

 

本文出自 “木又寸的技術博客” 博客

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *