自从有了IP数据库这种东西,QQ外挂的显示IP功能也随之而生,本人见识颇窄,是否还有其他应用不得而知,不过,IP数据库确实是个不错的东西。如今网络上最流行的IP数据库我想应该是纯真版的(说错了也不要扁我),迄今为止其IP记录条数已经接近30000,对于有些IP甚至能精确到楼层,不亦快哉。2004年4、5月间,正逢LumaQQ破土动工,为了加上这个人人都喜欢,但是好像人人都不知道为什么喜欢的显IP功能,我也采用了纯真版IP数据库,它的优点是记录多,查询速度快,它只用一个文件QQWry.dat就包含了所有记录,方便嵌入到其他程序中,也方便升级。

基本结构

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表示地区名:

   1. 有两条记录a(C1, A1), b(C2, A2),如果C1 = C2, A1 = A2,那么我们就可以使用图3显示的结构来实现重用

   2. 有三条记录a(C1, A1), b(C2, A2), c(C3, A3),如果C1 = C2, A2 = A3,现在我们想存储记录b,那么我们可以用图6的结构来实现重用

   3. 有两条记录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,到现在你应该全部清楚了。

Demo

下一步:我给出一个读取IP记录的程序片断,此片断摘录自LumaQQ源文件edu.tsinghua.lumaqq.IPSeeker.java,如果你有兴趣,可以下载源代码详细看看。

代码并不复杂,getIPLocation是主要方法,它检查国家记录格式,并针对字符串形式,模式1,模式2采用不同的代码,readArea则相对简单,因为只有字符串和重定向两种情况需要处理。

总结

纯真IP数据库的结构使得查找IP简单迅速,不过你想要编辑它却是比较麻烦的,我想应该需要专门的工具来生成QQWry.dat文件,由于其文件格式的限制,你要直接添加IP记录就不容易了。不过,能查到IP已经很开心了,希望纯真记录越来越多~。

可用类及调用方法:

  1 <?php
  2 
  3 /**
  4 
  5 * IP 地理位置查询类
  6 
  7 *
  8 
  9 * @author 马秉尧
 10 
 11 * @version 1.5
 12 
 13 * @copyright 2005 CoolCode.CN
 14 
 15 */
 16 
 17 class IpLocation {
 18 
 19      /**
 20 
 21       * QQWry.Dat文件指针
 22 
 23       *
 24 
 25       * @var resource
 26 
 27       */
 28 
 29      var $fp;
 30 
 31      /**
 32 
 33       * 第一条IP记录的偏移地址
 34 
 35       *
 36 
 37       * @var int
 38 
 39       */
 40 
 41      var $firstip;
 42 
 43      /**
 44 
 45       * 最后一条IP记录的偏移地址
 46 
 47       *
 48 
 49       * @var int
 50 
 51       */
 52 
 53      var $lastip;
 54 
 55      /**
 56 
 57       * IP记录的总条数(不包含版本信息记录)
 58 
 59       *
 60 
 61       * @var int
 62 
 63       */
 64 
 65      var $totalip;
 66 
 67      /**
 68 
 69       * 返回读取的长整型数
 70 
 71       *
 72 
 73       * @access private
 74 
 75       * @return int
 76 
 77       */
 78 
 79      function getlong() {
 80 
 81          //将读取的little-endian编码的4个字节转化为长整型数
 82 
 83          $result = unpack('Vlong', fread($this->fp, 4));
 84 
 85          return $result['long'];
 86 
 87      }
 88 
 89      /**
 90 
 91       * 返回读取的3个字节的长整型数
 92 
 93       *
 94 
 95       * @access private
 96 
 97       * @return int
 98 
 99       */
100 
101      function getlong3() {
102 
103          //将读取的little-endian编码的3个字节转化为长整型数
104 
105          $result = unpack('Vlong', fread($this->fp, 3).chr(0));
106 
107          return $result['long'];
108 
109      }
110 
111      /**
112 
113       * 返回压缩后可进行比较的IP地址
114 
115       *
116 
117       * @access private
118 
119       * @param string $ip
120 
121       * @return string
122 
123       */
124 
125      function packip($ip) {
126 
127          // 将IP地址转化为长整型数,如果在PHP5中,IP地址错误,则返回False,
128 
129          // 这时intval将Flase转化为整数-1,之后压缩成big-endian编码的字符串
130 
131          return pack('N', intval(ip2long($ip)));
132 
133      }
134 
135      /**
136 
137       * 返回读取的字符串
138 
139       *
140 
141       * @access private
142 
143       * @param string $data
144 
145       * @return string
146 
147       */
148 
149      function getstring($data = "") {
150 
151          $char = fread($this->fp, 1);
152 
153          while (ord($char> 0) {        // 字符串按照C格式保存,以\0结束
154 
155              $data .= $char;             // 将读取的字符连接到给定字符串之后
156 
157              $char = fread($this->fp, 1);
158 
159          }
160 
161          return $data;
162 
163      }
164 
165      /**
166 
167       * 返回地区信息
168 
169       *
170 
171       * @access private
172 
173       * @return string
174 
175       */
176 
177      function getarea() {
178 
179          $byte = fread($this->fp, 1);    // 标志字节
180 
181          switch (ord($byte)) {
182 
183              case 0:                     // 没有区域信息
184 
185                  $area = "";
186 
187                  break;
188 
189              case 1:
190 
191              case 2:                     // 标志字节为1或2,表示区域信息被重定向
192 
193                  fseek($this->fp, $this->getlong3());
194 
195                  $area = $this->getstring();
196 
197                  break;
198 
199              default:                    // 否则,表示区域信息没有被重定向
200 
201                  $area = $this->getstring($byte);
202 
203                  break;
204 
205          }
206 
207          return $area;
208 
209      }
210 
211      /**
212 
213       * 根据所给 IP 地址或域名返回所在地区信息
214 
215       *
216 
217       * @access public
218 
219       * @param string $ip
220 
221       * @return array
222 
223       */
224 
225      function getlocation($ip) {
226 
227          if (!$this->fp) return null;            // 如果数据文件没有被正确打开,则直接返回空
228 
229          $location['ip'= gethostbyname($ip);   // 将输入的域名转化为IP地址
230 
231          $ip = $this->packip($location['ip']);   // 将输入的IP地址转化为可比较的IP地址
232 
233                                                  // 不合法的IP地址会被转化为255.255.255.255
234 
235          // 对分搜索
236 
237          $l = 0;                         // 搜索的下边界
238 
239          $u = $this->totalip;            // 搜索的上边界
240 
241          $findip = $this->lastip;        // 如果没有找到就返回最后一条IP记录(QQWry.Dat的版本信息)
242 
243          while ($l <= $u) {              // 当上边界小于下边界时,查找失败
244 
245              $i = floor(($l + $u/ 2); // 计算近似中间记录
246 
247              fseek($this->fp, $this->firstip + $i * 7);
248 
249             $beginip = strrev(fread($this->fp, 4));     // 获取中间记录的开始IP地址
250 
251              // strrev函数在这里的作用是将little-endian的压缩IP地址转化为big-endian的格式
252 
253              // 以便用于比较,后面相同。
254 
255              if ($ip < $beginip) {       // 用户的IP小于中间记录的开始IP地址时
256 
257                  $u = $i - 1;            // 将搜索的上边界修改为中间记录减一
258 
259              }
260 
261              else {
262 
263                  fseek($this->fp, $this->getlong3());
264 
265                  $endip = strrev(fread($this->fp, 4));   // 获取中间记录的结束IP地址
266 
267                  if ($ip > $endip) {     // 用户的IP大于中间记录的结束IP地址时
268 
269                      $l = $i + 1;        // 将搜索的下边界修改为中间记录加一
270 
271                  }
272 
273                  else {                  // 用户的IP在中间记录的IP范围内时
274 
275                      $findip = $this->firstip + $i * 7;
276 
277                      break;              // 则表示找到结果,退出循环
278 
279                  }
280 
281              }
282 
283          }
284 
285          //获取查找到的IP地理位置信息
286 
287          fseek($this->fp, $findip);
288 
289          $location['beginip'= long2ip($this->getlong());   // 用户IP所在范围的开始地址
290 
291          $offset = $this->getlong3();
292 
293          fseek($this->fp, $offset);
294 
295          $location['endip'= long2ip($this->getlong());     // 用户IP所在范围的结束地址
296 
297          $byte = fread($this->fp, 1);    // 标志字节
298 
299          switch (ord($byte)) {
300 
301              case 1:                     // 标志字节为1,表示国家和区域信息都被同时重定向
302 
303                  $countryOffset = $this->getlong3();         // 重定向地址
304 
305                  fseek($this->fp, $countryOffset);
306 
307                  $byte = fread($this->fp, 1);    // 标志字节
308 
309                  switch (ord($byte)) {
310 
311                      case 2:             // 标志字节为2,表示国家信息又被重定向
312 
313                          fseek($this->fp, $this->getlong3());
314 
315                          $location['country'= $this->getstring();
316 
317                          fseek($this->fp, $countryOffset + 4);
318 
319                          $location['area'= $this->getarea();
320 
321                          break;
322 
323                      default:            // 否则,表示国家信息没有被重定向
324 
325                          $location['country'= $this->getstring($byte);
326 
327                          $location['area'= $this->getarea();
328 
329                          break;
330 
331                  }
332 
333                  break;
334 
335              case 2:                     // 标志字节为2,表示国家信息被重定向
336 
337                  fseek($this->fp, $this->getlong3());
338 
339                  $location['country'= $this->getstring();
340 
341                  fseek($this->fp, $offset + 8);
342 
343                  $location['area'= $this->getarea();
344 
345                  break;
346 
347              default:                    // 否则,表示国家信息没有被重定向
348 
349                  $location['country'= $this->getstring($byte);
350 
351                  $location['area'= $this->getarea();
352 
353                  break;
354 
355          }
356 
357          if ($location['country'== " CZ88.NET") { // CZ88.NET表示没有有效信息
358 
359              $location['country'= "未知";
360 
361          }
362 
363          if ($location['area'== " CZ88.NET") {
364 
365              $location['area'= "";
366 
367          }
368 
369          return $location;
370 
371      }
372 
373      /**
374 
375       * 构造函数,打开 QQWry.Dat 文件并初始化类中的信息
376 
377       *
378 
379       * @param string $filename
380 
381       * @return IpLocation
382 
383       */
384 
385      function IpLocation($filename = "QQWry.Dat") {
386 
387          $this->fp = 0;
388 
389          if (($this->fp = fopen($filename, 'rb')) !== false) {
390 
391              $this->firstip = $this->getlong();
392 
393              $this->lastip = $this->getlong();
394 
395              $this->totalip = ($this->lastip - $this->firstip) / 7;
396 
397              //注册析构函数,使其在程序执行结束时执行
398 
399              register_shutdown_function(array(&$this, '_IpLocation'));
400 
401          }
402 
403      }
404 
405      /**
406 
407       * 析构函数,用于在页面执行结束后自动关闭打开的文件。
408 
409       *
410 
411       */
412 
413      function _IpLocation() {
414 
415          if ($this->fp) {
416 
417              fclose($this->fp);
418 
419          }
420 
421          $this->fp = 0;
422 
423      }
424 
425 }
426 
427 $mtime = explode(' ', microtime());
428 
429 $starttime = $mtime[1+ $mtime[0];
430 
431 echo "<pre>";
432 
433 $ip = "202.101.111.195";
434 
435 $iplocation = new IpLocation();
436 
437 $location = $iplocation->getlocation($ip);
438 
439 print_r($location);
440 
441 $mtime = explode(' ', microtime());
442 
443 $endtime = $mtime[1+ $mtime[0];
444 
445 var_dump($endtime-$starttime);
446 
447 ?>
448 

posted on 2009-06-05 23:00  睿达团队  阅读(1307)  评论(0编辑  收藏  举报