根據 LumaQQ 開發者文檔中的純真 IP 數據庫格式詳解,我編寫了一個 PHP 的查詢 IP 所在地區信息的類。在編寫過程中發現純真 IP 數據庫格式詳解中關於記錄區的描述不是很全面,不過出入也不是很大,所以我沒必要再寫一份純真 IP 數據庫的格式說明了,大家感興趣的話,讀一讀下面的代碼應該就能看出來了。代碼中加了很詳細的註釋,應該很容易讀懂的。
    在創建這個類的一個實例後,實例中就保存了打開的文件指針和一些查詢需要的信息,每次查詢時不需要重新打開文件,直到頁面執行結束後,打開的文件才會自動關閉。這樣。在一個頁面內進行多次查詢時,效率是很高的。並且此類不僅可以直接查詢 IP,還可以自動將域名解析為 IP 進行查詢。
下面是程序代碼:
--------------------------------------------------------------------------------
/**
* IP 地理位置查詢類
*
* @author 馬秉堯
* @version 1.5
* @copyright 2005 CoolCode.CN
*/
class IpLocation {
    /**
     * QQWry.Dat文件指針
     *
     * @var resource
     */
    var $fp;
    
    /**
     * 第一條IP記錄的偏移地址
     *
     * @var int
     */
    var $firstip;
    
    /**
     * 最後一條IP記錄的偏移地址
     *
     * @var int
     */
    var $lastip;
    
    /**
     * IP記錄的總條數(不包含版本信息記錄)
     *
     * @var int
     */
    var $totalip;
    
    /**
     * 返回讀取的長整型數
     *
     * @access private
     * @return int
     */
    function getlong() {
        //將讀取的little-endian編碼的4個字節轉化為長整型數
        $result = unpack('Vlong', fread($this->fp, 4));
        return $result['long'];
    }
    
    /**
     * 返回讀取的3個字節的長整型數
     *
     * @access private
     * @return int
     */
    function getlong3() {
        //將讀取的little-endian編碼的3個字節轉化為長整型數
        $result = unpack('Vlong', fread($this->fp, 3).chr(0));
        return $result['long'];
    }
    
    /**
     * 返回壓縮後可進行比較的IP地址
     *
     * @access private
     * @param string $ip
     * @return string
     */
    function packip($ip) {
        // 將IP地址轉化為長整型數,如果在PHP5中,IP地址錯誤,則返回False,
        // 這時intval將Flase轉化為整數-1,之後壓縮成big-endian編碼的字符串
        return pack('N', intval(ip2long($ip)));
       }
    
    /**
     * 返回讀取的字符串
     *
     * @access private
     * @param string $data
     * @return string
     */
    function getstring($data = "") {
        $char = fread($this->fp, 1);
        while (ord($char) > 0) {        // 字符串按照C格式保存,以\0結束
            $data .= $char;                // 將讀取的字符連接到給定字符串之後
            $char = fread($this->fp, 1);
        }
        return $data;
    }
    
    /**
     * 返回地區信息
     *
     * @access private
     * @return string
     */
    function getarea() {
        $byte = fread($this->fp, 1);    // 標誌字節
        switch (ord($byte)) {
            case 0:                        // 沒有區域信息
                $area = "";
                break;
            case 1:
            case 2:                        // 標誌字節為1或2,表示區域信息被重定向
                fseek($this->fp, $this->getlong3());
                $area = $this->getstring();
                break;
            default:                    // 否則,表示區域信息沒有被重定向
                $area = $this->getstring($byte);
                break;
        }
        return $area;
    }
    
    /**
     * 根據所給 IP 地址或域名返回所在地區信息
     *
     * @access public
     * @param string $ip
     * @return array
     */
    function getlocation($ip) {
        if (!$this->fp) return null;            // 如果數據文件沒有被正確打開,則直接返回空
        $location['ip'] = gethostbyname($ip);     // 將輸入的域名轉化為IP地址
        $ip = $this->packip($location['ip']);    // 將輸入的IP地址轉化為可比較的IP地址
                                                // 不合法的IP地址會被轉化為255.255.255.255
        // 對分搜索
        $l = 0;                            // 搜索的下邊界
        $u = $this->totalip;            // 搜索的上邊界
        $findip = $this->lastip;        // 如果沒有找到就返回最後一條IP記錄(QQWry.Dat的版本信息)
        while ($l <= $u) {                // 當上邊界小於下邊界時,查找失敗
            $i = floor(($l + $u) / 2);    // 計算近似中間記錄
            fseek($this->fp, $this->firstip + $i * 7);
            $beginip = strrev(fread($this->fp, 4));        // 獲取中間記錄的開始IP地址
            // strrev函數在這裡的作用是將little-endian的壓縮IP地址轉化為big-endian的格式
            // 以便用於比較,後面相同。
            if ($ip < $beginip) {        // 用戶的IP小於中間記錄的開始IP地址時
                $u = $i - 1;            // 將搜索的上邊界修改為中間記錄減一
            }
            else {
                fseek($this->fp, $this->getlong3());
                $endip = strrev(fread($this->fp, 4));    // 獲取中間記錄的結束IP地址
                if ($ip > $endip) {        // 用戶的IP大於中間記錄的結束IP地址時
                    $l = $i + 1;        // 將搜索的下邊界修改為中間記錄加一
                }
                else {                    // 用戶的IP在中間記錄的IP範圍內時
                    $findip = $this->firstip + $i * 7;
                    break;                // 則表示找到結果,退出循環
                }
            }
        }
    
        //獲取查找到的IP地理位置信息
        fseek($this->fp, $findip);
        $location['beginip'] = long2ip($this->getlong());    // 用戶IP所在範圍的開始地址
        $offset = $this->getlong3();
        fseek($this->fp, $offset);
        $location['endip'] = long2ip($this->getlong());        // 用戶IP所在範圍的結束地址
        $byte = fread($this->fp, 1);    // 標誌字節
        switch (ord($byte)) {
            case 1:                        // 標誌字節為1,表示國家和區域信息都被同時重定向
                $countryOffset = $this->getlong3();            // 重定向地址
                fseek($this->fp, $countryOffset);
                $byte = fread($this->fp, 1);    // 標誌字節
                switch (ord($byte)) {
                    case 2:                // 標誌字節為2,表示國家信息又被重定向
                        fseek($this->fp, $this->getlong3());
                        $location['country'] = $this->getstring();
                        fseek($this->fp, $countryOffset + 4);
                        $location['area'] = $this->getarea();
                        break;
                    default:            // 否則,表示國家信息沒有被重定向
                        $location['country'] = $this->getstring($byte);
                        $location['area'] = $this->getarea();
                        break;
                }
                break;
            case 2:                        // 標誌字節為2,表示國家信息被重定向
                fseek($this->fp, $this->getlong3());
                $location['country'] = $this->getstring();
                fseek($this->fp, $offset + 8);
                $location['area'] = $this->getarea();
                break;
            default:                    // 否則,表示國家信息沒有被重定向
                $location['country'] = $this->getstring($byte);
                $location['area'] = $this->getarea();
                break;
        }
        if ($location['country'] == " CZ88.NET") {    // CZ88.NET表示沒有有效信息
            $location['country'] = "未知";
        }
        if ($location['area'] == " CZ88.NET") {
            $location['area'] = "";
        }
        return $location;
    }
    
    /**
     * 構造函數,打開 QQWry.Dat 文件並初始化類中的信息
     *
     * @param string $filename
     * @return IpLocation
     */
    function IpLocation($filename = "QQWry.Dat") {
        if (($this->fp = @fopen($filename, 'rb')) !== false) {
            $this->firstip = $this->getlong();
            $this->lastip = $this->getlong();
            $this->totalip = ($this->lastip - $this->firstip) / 7;
            //註冊析構函數,使其在程序執行結束時執行
            register_shutdown_function(array(&$this, '_IpLocation'));
        }
    }
    
    /**
     * 析構函數,用於在頁面執行結束後自動關閉打開的文件。
     *
     */
    function _IpLocation() {
        fclose($this->fp);
    }
}
?>
 
沒有留言:
張貼留言