diff --git a/binding/php/ReadMe.md b/binding/php/ReadMe.md index faaf5d2..bc49246 100644 --- a/binding/php/ReadMe.md +++ b/binding/php/ReadMe.md @@ -1,4 +1,4 @@ -# ip2region php 查询客户端实现 +# ip2region xdb php 查询客户端实现 # 使用方式 diff --git a/binding/php/XdbSearcher.class.php b/binding/php/XdbSearcher.class.php new file mode 100644 index 0000000..569b191 --- /dev/null +++ b/binding/php/XdbSearcher.class.php @@ -0,0 +1,217 @@ + +// @Date 2022/06/21 + +class XdbSearcher +{ + const HeaderInfoLength = 256; + const VectorIndexRows = 256; + const VectorIndexCols = 256; + const VectorIndexSize = 8; + const SegmentIndexSize = 14; + + // xdb file handle + private $handle = null; + + // header info + private $header = null; + private $ioCount = 0; + + // vector index in binary string. + // string decode will be faster than the map based Array. + private $vectorIndex = null; + + // xdb content buffer + private $contentBuff = null; + + /** + * initialize the xdb searcher + * @throws Exception + */ + function __construct($dbFile, $vectorIndex=null, $cBuff=null) { + // check the content buffer first + if ($cBuff != null) { + // check and autoload the vector index + if ($vectorIndex != null) { + // load the vector index + $this->vectorIndex = null; + } + + $this->contentBuff = $cBuff; + } else { + // open the xdb binary file + $this->handle = fopen($dbFile, "r"); + if ($this->handle === false) { + throw new Exception("failed to open xdb file '%s'", $dbFile); + } + + $this->vectorIndex = $vectorIndex; + } + } + + function close() { + if ($this->handle != null) { + fclose($this->handle); + } + } + + function getIOCount() { + return $this->ioCount; + } + + /** + * find the region info for the specified ip address + * @throws Exception + */ + function search($ip) { + // check and convert the sting ip to a 4-bytes long + if (is_string($ip)) { + $t = self::ip2long($ip); + if ($t === false) { + throw new Exception("invalid ip address `$ip`"); + } + $ip = $t; + } + + // reset the global counter + $this->ioCount = 0; + + // locate the segment index block based on the vector index + $il0 = ($ip >> 24) & 0xFF; + $il1 = ($ip >> 16) & 0xFF; + $idx = $il0 * self::VectorIndexRows * self::VectorIndexSize + $il1 * self::VectorIndexSize; + printf("il0: %d, il1: %d, idx: %d\n", $il0, $il1, $idx); + if ($this->vectorIndex != null) { + $sPtr = self::getLong($this->vectorIndex, $idx); + $ePtr = self::getLong($this->vectorIndex, $idx + 4); + } else { + // read the vector index block + $buff = $this->read(self::HeaderInfoLength + $idx, 8); + if ($buff === null) { + throw new Exception("failed to read vector index at ${idx}"); + } + + $sPtr = self::getLong($buff, 0); + $ePtr = self::getLong($buff, 4); + } + + printf("sPtr: %d, ePtr: %d\n", $sPtr, $ePtr); + + // binary search the segment index to get the region info + $dataLen = 0; + $dataPtr = null; + $l = 0; + $h = ($ePtr - $sPtr) / self::SegmentIndexSize; + while ($l <= $h) { + $m = ($l + $h) >> 1; + $p = $sPtr + $m * self::SegmentIndexSize; + + // read the segment index + $buff = $this->read($p, self::SegmentIndexSize); + if ($buff == null) { + throw new Exception("failed to read segment index at ${$p}"); + } + + $sip = self::getLong($buff, 0); + if ($ip < $sip) { + $h = $m - 1; + } else { + $eip = self::getLong($buff, 4); + if ($ip > $eip) { + $l = $m + 1; + } else { + $dataLen = self::getShort($buff, 8); + $dataPtr = self::getShort($buff, 10); + break; + } + } + } + + // match nothing interception. + // @TODO: could this even be a case ? + printf("dataLen: %d, dataPtr: %d\n", $dataLen, $dataPtr); + if ($dataPtr == null) { + return null; + } + + // load and return the region data + $buff = $this->read($dataPtr, $dataLen); + if ($buff == null) { + return null; + } + + $str = []; + foreach (unpack("C*", $buff) as $chr) { + $str[] = chr($chr); + } + + return implode($str); + } + + // read specified bytes from the specified index + private function read($offset, $len) { + // check the in-memory buffer first + if ($this->contentBuff != null) { + return substr($this->contentBuff, $offset, $len); + } + + // read from the file + $r = fseek($this->handle, $offset); + if ($r == -1) { + return null; + } + + $this->ioCount++; + $buff = fread($this->handle, $len); + if ($buff === false) { + return null; + } + + if (strlen($buff) != $len) { + return null; + } + + return $buff; + } + + // convert a string ip to long + public static function ip2long($ip) + { + $ip = ip2long($ip); + if ($ip === false) { + return false; + } + + // convert signed int to unsigned int if on 32 bit operating system + if ($ip < 0 && PHP_INT_SIZE == 4) { + $ip = sprintf("%u", $ip); + } + + return $ip; + } + + // read a 4bytes long from a byte buffer + public static function getLong($b, $idx) + { + $val = (ord($b[$idx])) | (ord($b[$idx+1]) << 8) + | (ord($b[$idx+2]) << 16) | (ord($b[$idx+3]) << 24); + + // convert signed int to unsigned int if on 32 bit operating system + if ($val < 0 && PHP_INT_SIZE == 4) { + $val = sprintf("%u", $val); + } + + return $val; + } + + // read a 2bytes short from a byte buffer + public static function getShort($b, $idx) + { + return ((ord($b[$idx])) | (ord($b[$idx+1]) << 8)); + } + +} \ No newline at end of file diff --git a/binding/php/search_test.php b/binding/php/search_test.php new file mode 100644 index 0000000..883d23d --- /dev/null +++ b/binding/php/search_test.php @@ -0,0 +1,104 @@ + +// @Date 2022/06/21 + +require dirname(__FILE__) . '/XdbSearcher.class.php'; + +if($argc < 2) { + printf("php %s [command options]\n", $argv[0]); + printf("options: \n"); + printf(" --db string ip2region binary xdb file path\n"); + printf(" --cache-policy string cache policy: file/vectorIndex/content\n"); + return; +} + +$dbFile = ""; +$cachePolicy = isset($argv[2]) ? $argv[2] : 'vectorIndex'; +array_shift($argv); +foreach ($argv as $r) { + if (strlen($r) < 5) { + continue; + } + + if (strpos($r, '--') != 0) { + continue; + } + + $sIdx = strpos($r, "="); + if ($sIdx < 0) { + printf("missing = for args pair %s\n", $r); + return; + } + + $key = substr($r, 2, $sIdx - 2); + $val = substr($r, $sIdx + 1); + if ($key == 'db') { + $dbFile = $val; + } else if ($key == 'cache-policy') { + $cachePolicy = $val; + } else { + printf("undefined option `%s`\n", $r); + return; + } +} + +printf("debug: dbFile: %s, cachePolicy: %s\n", $dbFile, $cachePolicy); + +// create the xdb searcher by the cache-policy +$searcher = null; +switch ( $cachePolicy ) { +case 'file': + try { + $searcher = new XdbSearcher($dbFile); + } catch (Exception $e) { + printf("failed to create searcher with '%s': %s\n", $dbFile, $e); + return; + } + break; +case 'vectorIndex': + break; +case 'content': + break; +default: + printf("undefined cache-policy `%s`\n", $cachePolicy); + return; +} + +while ( true ) { + echo "ip2region>> "; + $line = trim(fgets(STDIN)); + if (strlen($line) < 2) { + continue; + } + + if ($line == 'quit') { + break; + } + + if (XdbSearcher::ip2long($line) === false) { + echo "Error: invalid ip address\n"; + continue; + } + + $sTime = getTime(); + try { + $region = $searcher->search($line); + } catch (Exception $e) { + printf("search call failed: %s\n", $e); + continue; + } + + printf("{region: %s, took: %.5f ms}\n", $region, getTime() - $sTime); +} + +// close the searcher at last +$searcher->close(); + +function getTime() +{ + return (microtime(true) * 1000); +} diff --git a/maker/golang/main.go b/maker/golang/main.go index f2cf866..b0b9823 100644 --- a/maker/golang/main.go +++ b/maker/golang/main.go @@ -37,23 +37,26 @@ func genDb() { continue } - var eIdx = strings.Index(r, "=") - if eIdx < 0 { + var sIdx = strings.Index(r, "=") + if sIdx < 0 { fmt.Printf("missing = for args pair '%s'\n", r) return } - switch r[2:eIdx] { + switch r[2:sIdx] { case "src": - srcFile = r[eIdx+1:] + srcFile = r[sIdx+1:] case "dst": - dstFile = r[eIdx+1:] + dstFile = r[sIdx+1:] case "index": - indexPolicy, err = xdb.IndexPolicyFromString(r[eIdx+1:]) + indexPolicy, err = xdb.IndexPolicyFromString(r[sIdx+1:]) if err != nil { fmt.Printf("parse policy: %s", err.Error()) return } + default: + fmt.Printf("undefine option `%s`\n", r) + return } } @@ -115,6 +118,9 @@ func testSearch() { switch r[2:eIdx] { case "db": dbFile = r[eIdx+1:] + default: + fmt.Printf("undefined option '%s'\n", r) + return } } @@ -198,19 +204,19 @@ func testBench() { continue } - var eIdx = strings.Index(r, "=") - if eIdx < 0 { + var sIdx = strings.Index(r, "=") + if sIdx < 0 { fmt.Printf("missing = for args pair '%s'\n", r) return } - switch r[2:eIdx] { + switch r[2:sIdx] { case "db": - dbFile = r[eIdx+1:] + dbFile = r[sIdx+1:] case "src": - srcFile = r[eIdx+1:] + srcFile = r[sIdx+1:] case "ignore-error": - v := r[eIdx+1:] + v := r[sIdx+1:] if v == "true" || v == "1" { ignoreError = true } else if v == "false" || v == "0" { @@ -219,6 +225,9 @@ func testBench() { fmt.Printf("invalid value for ignore-error option, could be false/0 or true/1\n") return } + default: + fmt.Printf("undefined option '%s'\n", r) + return } }