zoukankan      html  css  js  c++  java
  • 纯真IP数据库格式详解 附demo

    纯真版IP数据库,优点是记录多,查询速度快,它只用一个文件QQWry.dat就包含了所有记录,方便嵌入到其他程序中,也方便升级。缺点是你想要编辑它却是比较麻烦的,由于其文件格式的限制,你要直接添加IP记录就不容易了

    基本结构
    QQWry.dat 文件在结构上分为3块:文件头,记录区,索引区。一般我们要查找IP时,先在索引区查找记录偏移,然后再到记录区读出信息。由于记录区的记录是不定长的, 所以直接在记录区中搜索是不可能的。由于记录数比较多,如果我们遍历索引区也会是有点慢的,一般来说,我们可以用二分查找法搜索索引区,其速度比遍历索引 区快若干数量级。图1是QQWry.dat的文件结构图。


    图1. QQWry.dat文件结构
    要注意的是,QQWry.dat里面全部采用了little-endian字节序

    一. 了解文件头
    QQWry.dat的文件头只有8个字节,其结构非常简单,首四个字节是第一条索引的绝对偏移,后四个字节是最后一条索引的绝对偏移。

    二. 了解记录区
    每 条IP记录都由国家和地区名组成,国家地区在这里并不是太确切,因为可能会查出来“清华大学计算机系”之类的,这里清华大学就成了国家名了,所以这个国家 地区名和IP数据库制作的时候有关系。所以记录的格式有点像QName,有一个全局部分和局部部分组成,我们这里还是沿用国家名和地区名的说法。

    于 是我们想象着一条记录的格式应该是: [IP地址][国家名][地区名],当然,这个没有什么问题,但是这只是最简单的情况。很显然,国家名和地区名可能会有很多的重复,如果每条记录都保存一 个完整的名称拷贝是非常不理想的,所以我们就需要重定向以节省空间。所以为了得到一个国家名或者地区名,我们就有了两个可能:第一就是直接的字符串表示的 国家名,第二就是一个4字节的结构,第一个字节表明了重定向的模式,后面3个字节是国家名或者地区名的实际偏移位置。对于国家名来说,情况还可能更复杂 些,因为这样的重定向最多可能有两次。

    那么什么是重定向模式?根据上面所说,一条记录的格式是[IP地址][国家记录][地区记录],如果国家记录是重定向的话,那么地区记录是有可能没有的,于是就有了两种情况,我管他叫做模式1和模式2。我们对这些格式的情况举图说明:


    图2. IP记录的最简单形式
    图2表示了最简单的IP记录格式,我想没有什么可以解释的


    图3. 重定向模式1
    图3演示了重定向模式1的情况。我们看到在模式1的情况下,地区记录也跟着国家记录走了,在IP地址之后只剩下了国家记录的4字节,后面3个字节构成了一个指针,指向了实际的国家名,然后又跟着地址名。模式1的标识字节是0x01。


    图4. 重定向模式2
    图 4演示了重定向模式2的情况。我们看到了在模式2的情况下(其标识字节是0x02),地区记录没有跟着国家记录走,因此在国家记录之后4个字节之后还是有 地区记录。我想你已经明白了模式1和模式2的区别,即:模式1的国家记录后面不会再有地区记录,模式2的国家记录后会有地区记录。下面我们来看一下更复杂 的情况。


    图5. 混和情况1
    图 5演示了当国家记录为模式1的时候可能出现的更复杂情况,在这种情况下,重定向指向的位置仍然是个重定向,不过第二次重定向为模式2。大家不用担心,没有 模式3了,这个重定向也最多只有两次,并且如果发生了第二次重定向,则其一定为模式2,而且这种情况只会发生在国家记录上,对于地区记录,模式1和模式2 是一样的,地区记录也不会发生2次重定向。不过,这个图还可以更复杂,如图7:


    图6. 混和情况2
    图6是模式1下最复杂的混和情况,不过我想应该也很好理解,只不过地区记录也来重定向而已,有一点我要提醒你,如果重定向的地址是0,则表示未知的地区名。

    所 以我们总结如下:一条IP记录由[IP地址][国家记录][地区记录]组成,对于国家记录,可以有三种表示方式:字符串形式,重定向模式1和重定向模式 2。对于地区记录,可以有两种表示方式:字符串形式和重定向,另外有一条规则:重定向模式1的国家记录后不能跟地区记录。按照这个总结,在这些方式中合理 组合,就构成了IP记录的所有可能情况。

    设计的理由
    在我们继续去了解索引区的结构之前,我们先来了解一下为何记录区的结构要如此设 计。我想你可能想到了答案:字符串重用。没错,在这种结构下,对于一个国家名和地区名,我只需要保存其一次就可以了。我们举例说明,为了表示方便,我们用 小写字母代表IP记录,C表示国家名,A表示地区名:

    有两条记录a(C1, A1), b(C2, A2),如果C1 = C2, A1 = A2,那么我们就可以使用图3显示的结构来实现重用
    有三条记录a(C1, A1), b(C2, A2), c(C3, A3),如果C1 = C2, A2 = A3,现在我们想存储记录b,那么我们可以用图6的结构来实现重用
    有两条记录a(C1, A1), b(C2, A2),如果C1 = C2,现在我们想存储记录b,那么我们可以采用模式2表示C2,用字符串表示A2

    你可以举出更多的情况,你也会发现在这种结构下,不同的字符串只需要存储一次。

    了解索引区
    在"了解文件头"部分,我们说明了文件头实际上是两个指针,分别指向了第一条索引和最后一条索引的绝对偏移。如图8所示:


    图8. 文件头指向索引区图示
    实 在是很简单,不是吗?从文件头你就可以定位到索引区,然后你就可以开始搜索IP了!每条索引长度为7个字节,前4个字节是起始IP地址,后三个字节就指向 了IP记录。这里有些概念需要说明一下,什么是起始IP,那么有没有结束IP? 假设有这么一条记录:166.111.0.0 - 166.111.255.255,那么166.111.0.0就是起始IP,166.111.255.255就是结束IP,结束IP就是IP记录中的那头 4个字节,这下你应该就清楚了吧。于是乎,每条索引配合一条记录,构成了一个IP范围,如果你要查找166.111.138.138所在的位置,你就会发 现166.111.138.138落在了166.111.0.0 - 166.111.255.255 这个范围内,那么你就可以顺着这条索引去读取国家和地区名了。那么我们给出一个最详细的图解吧:


    图9. 文件详细结构
    现 在一切都清楚了是不是?也许还有一点你不清楚,QQWry.dat的版本信息存在哪里呢? 答案是:最后一条IP记录实际上就是版本信息,最后一条记录显示出来就是这样:255.255.255.0 255.255.255.255 纯真网络 2004年6月25日IP数据。OK,到现在你应该全部清楚了。

    PHP Demo 不记得demo出处了

    <?php
    class IP {
        var $fh;        //IP数据库文件句柄
        var $first;        //第一条索引
        var $last;        //最后一条索引
        var $total;        //索引总数
    
        //构造函数
        function __construct(){
            $this->fh = fopen('./qqwry.dat', 'rb');                //qqwry.dat文件
            $this->first = $this->getLong4();
            $this->last  = $this->getLong4();
            $this->total = ($this->last - $this->first) / 7;    //每条索引7字节
        }
    
        //检查IP合法性
        function checkIp($ip){
            $arr = explode('.', $ip);
            if( count($arr) !=4 ){
                return false;
            }
            else{
                for( $i=0; $i<4; $i++){
                    if( $arr[$i] <'0' || $arr[$i] > '255' ) {
                        return false;
                    }
                }
            }
            return true;
        }
    
        function getLong4(){
            //读取little-endian编码的4个字节转化为长整型数
            $result = unpack('Vlong', fread($this->fh, 4));
            return $result['long'];
        }
    
        function getLong3() {
            //读取little-endian编码的3个字节转化为长整型数
            $result = unpack('Vlong', fread($this->fh, 3).chr(0));
            return $result['long'];
        }
    
        //查询信息
        function getInfo($data = "") {
            $char = fread($this->fh, 1);
            while( ord($char) != 0) {        //国家地区信息以0结束
                $data .= $char;
                $char = fread($this->fh, 1);
            }
            return $data;
        }
    
        //查询地区信息
        function getArea() {
            $byte = fread($this->fh, 1);    //标志字节
            switch (ord($byte)) {
                case 0: $area = ''; break;    //没有地区信息
                case 1:        //地区被重定向
                    fseek($this->fh, $this->getLong3());
                    $area = $this->getInfo(); break;
                case 2:        //地区被重定向
                fseek($this->fh, $this->getLong3());
                $area = $this->getInfo(); break;
                default: $area = $this->getInfo($byte);  break;        //地区没有被重定向
            }
            return $area;
        }
    
        function ip2addr($ip) {
            if( !$this->checkIp($ip) ){
                return false;
            }
    
            $ip = pack('N', intval(ip2long($ip)) );
    
            //二分查找
            $l = 0;
            $r = $this->total;
    
            while($l <= $r) {
                $m = floor(($l + $r) / 2);                //计算中间索引
                fseek($this->fh, $this->first + $m * 7);
                $beginip = strrev(fread($this->fh, 4)); //中间索引的开始IP地址
                fseek($this->fh, $this->getLong3());
                $endip = strrev(fread($this->fh, 4));    //中间索引的结束IP地址
    
                if( $ip < $beginip) {    //用户的IP小于中间索引的开始IP地址时
                    $r = $m - 1;
                }
                else{
                    if( $ip > $endip) {    //用户的IP大于中间索引的结束IP地址时
                        $l = $m + 1;
                    }
                    else{                //用户IP在中间索引的IP范围内时
                        $findip = $this->first + $m * 7;
                        break;
                    }
                }
            }
    
            //查询国家地区信息
            fseek($this->fh, $findip);
            $location['beginip'] = long2ip($this->getLong4());    //用户IP所在范围的开始地址
            $offset = $this->getlong3();
            fseek($this->fh, $offset);
            $location['endip'] = long2ip($this->getLong4());    //用户IP所在范围的结束地址
            
            $byte = fread($this->fh, 1);                //标志字节
            switch( ord($byte) ){
                case 1:            //国家和区域信息都被重定向
                    $countryOffset = $this->getLong3();    //重定向地址
                    fseek($this->fh, $countryOffset);
                    $byte = fread($this->fh, 1);        //标志字节
                    switch( ord($byte) ){
                        case 2: //国家信息被二次重定向
                            fseek($this->fh, $this->getLong3());
                            $location['country'] = $this->getInfo();
                            fseek($this->fh, $countryOffset + 4);
                            $location['area'] = $this->getArea();
                            break;
                        default: //国家信息没有被二次重定向
                            $location['country'] = $this->getInfo($byte);
                            $location['area'] = $this->getArea();
                            break;
                    }
                    break;
    
                case 2:            //国家信息被重定向
                    fseek($this->fh, $this->getLong3());
                    $location['country'] = $this->getInfo();
                    fseek($this->fh, $offset + 8);
                    $location['area'] = $this->getArea();
                    break;
    
                default:        //国家信息没有被重定向
                    $location['country'] = $this->getInfo($byte);
                    $location['area'] = $this->getArea();
                    break;
            }
    
            //gb2312 to utf-8(去除无信息时显示的CZ88.NET)
            foreach ($location as $k => $v) {
                $location[$k] = str_replace('CZ88.NET', '', iconv('gb2312', 'utf-8', $v));
            }
    
            return $location;
        }
    
        //析构函数
        function __destruct() {
            fclose($this->fh);
        }
        
    }
    
    $ip = new IP();
    $addr = $ip->ip2addr('113.247.15.120');
    print_r($addr);
  • 相关阅读:
    delphi不同版本字符串类型的演化(要支持基于firemonkey的app调用,字符串最好使用olevariant类型)
    IdHttpServer实现webservice(130篇DataSnap文章)
    hdu 1809 求SG函数
    delphi中无类型文件读写
    delpi中的RTTI初试
    后台调用外部程序的完美实现(使用CreateDesktop建立隐藏桌面)
    delphi之完美Splash方案(在TfrmMain.FormCreate里不断调用TfrmSplash显示加载进度文字,并且及时Update显示)
    查看内存数据的函数(ByteToHex和ByteToBin,最终都变成String)
    SQLsever2008 远程连接错误 linq
    delphi 利用HTTP的POST方法做个在线翻译的小工具 good
  • 原文地址:https://www.cnblogs.com/kjcy8/p/5787723.html
Copyright © 2011-2022 走看看