珊瑚虫IP库浅析

  这不是什么新鲜事情了,很早之前就已经有人做出来了。

  就是使用PHP操作纯真IP库或珊瑚虫IP库,根据来访者的IP得到所在的物理位置。

  我先帖出代码。然后再慢慢一步步浅析出来。希望对想了解这一块的朋友们有帮助。

  Only For PHP5的代码。会继续优化代码的。

  

  

  class IpLocation{

  private $fp;

  private $wrydat;

  private $wrydat_version;

  private $ipnumber;

  private $firstip;

  private $lastip;

  private $ip_range_begin;

  private $ip_range_end;

  private $country;

  private $area;

  const REDIRECT_MODE_0 = 0;

  const REDIRECT_MODE_1 = 1;

  const REDIRECT_MODE_2 = 2;

  function __construct(){

  $args = func_get_args();

  $this->wrydat = func_num_args()>0?$args[0]:'CoralWry.dat';

  $this->initialize();

  }

  function __destruct(){

  fclose($this->fp);

  }

  private function initialize(){

  if(file_exists($this->wrydat))

  $this->fp = fopen($this->wrydat,'rb');

  $this->getipnumber();

  $this->getwryversion();

  }

  public function get($str){

  return $this->$str;

  }

  public function set($str,$val){

  $this->$str = $val;

  }

  private function getbyte($length,$offset=null){

  if(!is_null($offset)){

  fseek($this->fp,$offset,SEEK_SET);

  }

  $b = fread($this->fp,$length);

  return $b;

  }

  /**

  * 把IP地址打包成二进制数据,以big endian(高位在前)格式打包

  * 数据存储格式为 little endian(低位在前) 如:

  * 00 28 C6 DA    218.198.40.0    little endian

  * 3F 28 C6 DA    218.198.40.0    little endian

  * 这样的数据无法作二分搜索查找的比较,所以必须先把获得的IP数据使用strrev转换为big endian

  * @param $ip

  * @return big endian格式的二进制数据

  */

  private function packip($ip){

  return pack( "N", intval( ip2long( $ip)));

  }

  private function getlong($length=4, $offset=null){

  $chr=null;

  for($c=0;$length%4!=0&&$c<(4-$length%4);$c++){

  $chr .= chr(0);

  }

  $var = unpack( "Vlong", $this->getbyte($length, $offset).$chr);

  return $var['long'];

  }

  private function getwryversion(){

  $length = preg_match("/coral/i",$this->wrydat)?26:30;

  $this->wrydat_version = $this->getbyte($length, $this->firstip-$length);

  }

  private function getipnumber(){

  $this->firstip = $this->getlong();

  $this->lastip = $this->getlong();

  $this->ipnumber = ($this->lastip-$this->firstip)/7+1;

  }

  private function getstring($data="",$offset=null){

  $char = $this->getbyte(1,$offset);

  while(ord($char) > 0){

  $data .= $char;

  $char = $this->getbyte(1);

  }

  return $data;

  }

  private function iplocaltion($ip){

  $ip = $this->packip($ip);

  $low = 0;

  $high = $this->ipnumber-1;

  $ipposition = $this->lastip;

  while($low <= $high){

  $t = floor(($low+$high)/2);

  if($ip < strrev($this->getbyte(4,$this->firstip+$t*7))){

  $high = $t - 1;

  } else {

  if($ip > strrev($this->getbyte(4,$this->getlong(3)))){

  $low = $t + 1;

  }else{

  $ipposition = $this->firstip+$t*7;

  break;

  }

  }

  }

  return $ipposition;

  }

  private function getarea(){

  $b = $this->getbyte(1);

  switch(ord($b)){

  case self::REDIRECT_MODE_0 :

  return "未知";

  break;

  case self::REDIRECT_MODE_1:

  case self::REDIRECT_MODE_2:

  return $this->getstring("",$this->getlong(3));

  break;

  default:

  return $this->getstring($b);

  break;

  }

  }

  public function getiplocation($ip){

  $ippos = $this->iplocaltion($ip);

  $this->ip_range_begin = long2ip($this->getlong(4,$ippos));

  $this->ip_range_end = long2ip($this->getlong(4,$this->getlong(3)));

  $b = $this->getbyte(1);

  switch (ord($b)){

  case self::REDIRECT_MODE_1:

  $b = $this->getbyte(1,$this->getlong(3));

  if(ord($b) == REDIRECT_MODE_2){

  $countryoffset = $this->getlong(3);

  $this->area = $this->getarea();

  $this->country = $this->getstring("",$countryoffset);

  }else{

  $this->country = $this->getstring($b);

  $this->area    = $this->getarea();

  }

  break;

  case self::REDIRECT_MODE_2:

  $countryoffset = $this->getlong(3);

  $this->area = $this->getarea();

  $this->country = $this->getstring("",$countryoffset);

  break;

  default:

  $this->country = $this->getstring($b);

  $this->area    = $this->getarea();

  break;

  }

  }

  }

  /* */

  echo microtime();

  echo "\n";

  $iploca = new IpLocation;

  //$iploca = new IpLocation('QQWry.dat');

  echo $iploca->get('wrydat_version');

  echo "\n";

  echo $iploca->get('ipnumber');

  echo "\n";

  $iploca->getiplocation('211.44.32.34');

  /**/

  echo $iploca->get('ip_range_begin');

  echo "\n";

  echo $iploca->get('ip_range_end');

  echo "\n";

  echo $iploca->get('country');

  echo "\n";

  echo $iploca->get('area');

  echo "\n";

  echo $iploca->get('lastip');

  echo "\n";

  echo microtime();

  echo "\n";

  unset($iploca);

  参考资料:LumaQQ的 纯真IP数据库格式详解

  CoralWry.dat文件结构上分为3个区域:

  • 文件头[固定8个字节]
  • 数据区[不固定长度,记录IP的地址信息]
  • 索引区[大小由文件头决定]

  该文件数据的存储方式是:little endian。

  在这里引用了谈谈Unicode编码里的关于little endian 与 big endian的区别

  引用

  

  big endian和little endian是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。还是将49写在前面,就是little endian。

  “endian”这个词出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位。

  我们一般将endian翻译成“字节序”,将big endian和little endian称作“大尾”和“小尾”。

  文件头:

  红色框框里的就是文件头,前4个字节是索引区的开始地址,后4个字节是索引区的结束地址。

  如下图所示:

珊瑚虫IP库浅析

  点击放大

  由于数据库是使用了little endian的字节库,所以我们需要把它倒过来。

  把文件头的0-3的字节读取出来,再使用 unpack 函数把二进制数据转换为big endian格式的无符号整型。

  处理后,索引区的开始地址位置是:00077450 ;索引区的结束地址位置是:000CE17C。

  如果你手头上有UltraEdit的软件,可以打开CoralWry.dat文件,查找地址为:00077450 的位置,那就是IP地址索引区的开始。

  如下图所示:

珊瑚虫IP库浅析

  点击放大

  红色框框住那就是索引区的开始位置。