上一篇:腾讯QQ2005最新的17种版本 >>
纯真IP数据库格式详解
luma, 清华大学
修改日期: 2005/01/14
note: 在此感谢纯真ip数据库作者金狐和那唯一一份文档的作者。修改历史:
2005-01-14 修改了原来一些表达不清和错误的地方
自从有了ip数据库这种东西,qq外挂的显示ip功能也随之而生,本人见识颇窄,是否还有其他应用不得而知,不过,ip数据库确实是个不错的东西。如今网络上最流行的ip数据库我想应该是纯真版的(说错了也不要扁我),迄今为止其ip记录条数已经接近30000,对于有些ip甚至能精确到楼层,不亦快哉。 2004年4、5月间,正逢lumaqq破土动工,为了加上这个人人都喜欢,但是好像人人都不知道为什么喜欢的显ip功能,我也采用了纯真版ip数据库,它的优点是记录多,查询速度快,它只用一个文件qqwry.dat就包含了所有记录,方便嵌入到其他程序中,也方便升级。
基本结构
qqwry.dat文件在结构上分为3块:文件头,记录区,索引区。一般我们要查找ip时,先在索引区查找记录偏移,然后再到记录区读出信息。由于记录区的记录是不定长的,所以直接在记录区中搜索是不可能的。由于记录数比较多,如果我们遍历索引区也会是有点慢的,一般来说,我们可以用二分查找法搜索索引区,其速度比遍历索引区快若干数量级。图1是qqwry.dat的文件结构图。
screen.width-300)this.width=screen.width-300 border=0 alt=click to open in new window>
图1. qqwry.dat文件结构
要注意的是,qqwry.dat里面全部采用了little-endian字节序
一. 了解文件头
qqwry.dat的文件头只有8个字节,其结构非常简单,首四个字节是第一条索引的绝对偏移,后四个字节是最后一条索引的绝对偏移。
二. 了解记录区
每条ip记录都由国家和地区名组成,国家地区在这里并不是太确切,因为可能会查出来“清华大学计算机系”之类的,这里清华大学就成了国家名了,所以这个国家地区名和ip数据库制作的时候有关系。所以记录的格式有点像qname,有一个全局部分和局部部分组成,我们这里还是沿用国家名和地区名的说法。
于是我们想象着一条记录的格式应该是: [ip地址][国家名][地区名],当然,这个没有什么问题,但是这只是最简单的情况。很显然,国家名和地区名可能会有很多的重复,如果每条记录都保存一个完整的名称拷贝是非常不理想的,所以我们就需要重定向以节省空间。所以为了得到一个国家名或者地区名,我们就有了两个可能:第一就是直接的字符串表示的国家名,第二就是一个4字节的结构,第一个字节表明了重定向的模式,后面3个字节是国家名或者地区名的实际偏移位置。对于国家名来说,情况还可能更复杂些,因为这样的重定向最多可能有两次。
那么什么是重定向模式?根据上面所说,一条记录的格式是[ip地址][国家记录][地区记录],如果国家记录是重定向的话,那么地区记录是有可能没有的,于是就有了两种情况,我管他叫做模式1和模式2。我们对这些格式的情况举图说明:
screen.width-300)this.width=screen.width-300 border=0 alt=click to open in new window>
图2. ip记录的最简单形式
图2表示了最简单的ip记录格式,我想没有什么可以解释的
screen.width-300)this.width=screen.width-300 border=0 alt=click to open in new window>
图3. 重定向模式1
图3演示了重定向模式1的情况。我们看到在模式1的情况下,地区记录也跟着国家记录走了,在ip地址之后只剩下了国家记录的4字节,后面3个字节构成了一个指针,指向了实际的国家名,然后又跟着地址名。模式1的标识字节是0x01。
screen.width-300)this.width=screen.width-300 border=0 alt=click to open in new window>
图4. 重定向模式2
图4演示了重定向模式2的情况。我们看到了在模式2的情况下(其标识字节是0x02),地区记录没有跟着国家记录走,因此在国家记录之后4个字节之后还是有地区记录。我想你已经明白了模式1和模式2的区别,即:模式1的国家记录后面不会再有地区记录,模式2的国家记录后会有地区记录。下面我们来看一下更复杂的情况。
screen.width-300)this.width=screen.width-300 border=0 alt=click to open in new window>
图5. 混和情况1
图5演示了当国家记录为模式1的时候可能出现的更复杂情况,在这种情况下,重定向指向的位置仍然是个重定向,不过第二次重定向为模式2。大家不用担心,没有模式3了,这个重定向也最多只有两次,并且如果发生了第二次重定向,则其一定为模式2,而且这种情况只会发生在国家记录上,对于地区记录,模式1和模式 2是一样的,地区记录也不会发生2次重定向。不过,这个图还可以更复杂,如图7:
screen.width-300)this.width=screen.width-300 border=0 alt=click to open in new window>
图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,到现在你应该全部清楚了。
demo
下一步:我给出一个读取ip记录的程序片断,此片断摘录自lumaqq源文件edu.tsinghua.lumaqq.ipseeker.java,如果你有兴趣,可以下载源代码详细看看。
| * 给定一个ip国家地区记录的偏移,返回一个iplocation结构 * @param offset 国家记录的起始偏移 * @return iplocation对象 */ private iplocation getiplocation(long offset) { try { // 跳过4字节ip ipfile.seek(offset + 4); // 读取第一个字节判断是否标志字节 byte b = ipfile.readbyte(); if(b == redirect_mode_1) { // 读取国家偏移 long countryoffset = readlong3(); // 跳转至偏移处 ipfile.seek(countryoffset); // 再检查一次标志字节,因为这个时候这个地方仍然可能是个重定向 b = ipfile.readbyte(); if(b == redirect_mode_2) { loc.country = readstring(readlong3()); ipfile.seek(countryoffset + 4); } else loc.country = readstring(countryoffset); // 读取地区标志 loc.area = readarea(ipfile.getfilepointer()); } else if(b == redirect_mode_2) { loc.country = readstring(readlong3()); loc.area = readarea(offset + 8); } else { loc.country = readstring(ipfile.getfilepointer() - 1); loc.area = readarea(ipfile.getfilepointer()); } return loc; } catch (ioexception e) { return null; } } /** * 从offset偏移开始解析后面的字节,读出一个地区名 * @param offset 地区记录的起始偏移 * @return 地区名字符串 * @throws ioexception 地区名字符串 */ private string readarea(long offset) throws ioexception { ipfile.seek(offset); byte b = ipfile.readbyte(); if(b == redirect_mode_1 b == redirect_mode_2) { long areaoffset = readlong3(offset + 1); if(areaoffset == 0) return lumaqq.getstring("unknown.area"); else return readstring(areaoffset); } else return readstring(offset); } /** * 从offset位置读取3个字节为一个long,因为java为big-endian格式,所以没办法 * 用了这么一个函数来做转换 * @param offset 整数的起始偏移 * @return 读取的long值,返回-1表示读取文件失败 */ private long readlong3(long offset) { long ret = 0; try { ipfile.seek(offset); ipfile.readfully(b3); ret = (b3[0] & 0xff); ret = ((b3[1] 下一篇:QQ安装目录文件及子目录作用詳解 >> 相关文章:
© 2006 www.java-asp.net
|
