├── .gitignore ├── LICENSE ├── README.md ├── download ├── main.go └── qqwry-2021.04.14.dat ├── go.mod ├── go.sum ├── parser.go └── parser_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # .idea 2 | .idea/ 3 | 4 | .DS_Store 5 | 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | # vendor/ 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 漫漫 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ip归属地查询库(基于纯真ip库) 2 | ----- 3 | 4 | ## About 5 | 6 | 纯真ip库Golang解析程序。[社区版](https://www.cz88.net/geo-public) 可关注公众号免费下载(2022年05月)。 7 | 8 | ## Installation 9 | 10 | ```shell 11 | go get -u github.com/yzchan/iploc 12 | ``` 13 | 14 | ## Quickstart 15 | 16 | ```go 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "github.com/yzchan/iploc" 22 | ) 23 | 24 | func main() { 25 | q, err := iploc.NewQQWryParser("/path/to/file/qqwry.dat") 26 | if err != nil { 27 | panic(err) 28 | } 29 | textA, textB := q.Find("127.0.0.1") 30 | fmt.Println(textA, textB) 31 | } 32 | ``` 33 | 34 | ## Benchmarks 35 | 36 | ```shell 37 | go test -v -run="none" -bench=. -benchmem -benchtime=3s 38 | ``` 39 | 40 | ``` 41 | // 测试环境 2017款13寸MacBookPro 42 | goos: darwin 43 | goarch: amd64 44 | pkg: github.com/yzchan/iploc 45 | cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz 46 | BenchmarkFind 47 | BenchmarkFind-4 6399915 552.2 ns/op 568 B/op 6 allocs/op 48 | BenchmarkFindParallel 49 | BenchmarkFindParallel-4 12724434 335.8 ns/op 568 B/op 6 allocs/op 50 | PASS 51 | ok github.com/yzchan/iploc 8.769s 52 | ``` 53 | 54 | ## Links 55 | 56 | - [纯真(CZ88.net)](https://www.cz88.net/) 57 | - [kayon/iploc](https://github.com/kayon/iploc) 58 | - [freshcn/qqwry](https://github.com/freshcn/qqwry) 59 | - [Dnomd343](https://zhuanlan.zhihu.com/p/360624952) 60 | 61 | ## 纯真ip库的存储格式与解析方式浅析 62 | 63 | ### 存储结构分析 64 | 65 | [纯真IP库](https://www.cz88.net/) 是一个打包了起始IP、终止IP、记录A和记录B的二进制文件。分为三个区域:[头部区]、[记录区]和[索引区]。 66 | 67 | [头部区]共8个字节,前4个字节指示[索引区]的开始地址,后4个字节指示最后一条索引的地址偏移量。 68 | 69 | [记录区]存放了终止IP数据、记录A和记录B。记录区每条记录前4个字节为终止ip,后面跟着记录A和记录B的信息,为了节省空间使用了重定向机制。记录A和记录B使用GBK编码。 70 | 71 | [索引区]存放的是起始IP和指向记录区地址偏移量。每条索引占用7个字节,前4个字节为起始IP,后3个字节为偏移。 72 | 73 | 用hexdump来查看一下文件(以download目录下的的2021.04.14日的ip库为例) 74 | 75 | ```shell 76 | hexdump -n 32 qqwry-2021.04.14.dat 77 | ``` 78 | 79 | 结果为 80 | 81 | ``` 82 | [hexdump结果1] 83 | 0000000 7b 9e 66 00 92 1f 9f 00 - ff ff ff 00 49 41 4e 41 84 | 0000010 00 b1 a3 c1 f4 b5 d8 d6 - b7 00 00 00 00 01 c3 c0 85 | 0000020 86 | ``` 87 | 88 | 开始的头8个字节"7b 9e 66 00 92 1f 9f 00"就是索引区的偏移量。这边使用的是小端存储,可以得知第一条和最后一条记录记录的偏移量 89 | 分别为0x00669e7b、0x009f1f92。 90 | 91 | 顺便看下[索引区]后面跟着是记录区,记录区的头4个字节是一个IP段终止IP的地址,也就是说后面跟着的"ff ff ff 00"应该指示的是一个IP地址(这个后面分析)。 再后面的的"49 41 4e 41 00" 92 | 应该就是这条记录的记录A的内容了(记录区的内容都是遇0x00表示结束)。 用 [GBK解码网站](https://www.qqxiuzi.cn/bianma/zifuji.php) 查一下,"49 41 4e 41"就是" 93 | IANA"的ASCII码表示(GBK兼容ASCII码,对于ASCII码依然使用一位存储,中文使用两位存储)。 94 | 95 | "49 41 4e 41 00"后面跟着的"b1 a3 c1 f4 b5 d8 d6 b7 00"就是记录B的内容。这里就不像是ASCII码了,我们把每两位合并起来得到"b1a3 c1f4 b5d8 d6b7" 96 | ,再使用上述GBK网站查询得到结果"保留地址"。 可见这边GBK也是采用大端存储。 97 | 98 | > 关于大端和小端 99 | > 其实就是当使用超过1个字节表示数据时在内存中是从左到右存放还是从右到左存放,大端符合我们的阅读习惯,而计算机内部小端用的更多 100 | 101 | 到这里就分析出了[头部区]和[记录区]的格式,刚好第一条记录区的数据是没有重定向的,比较简单,终止ip后面直接跟着记录A和记录B的数据。 102 | 103 | 接着分析[索引区],使用上面分析得出的索引区偏移量继续hexdump 104 | 105 | ```shell 106 | hexdump -n 32 -s 0x00669e7b qqwry-2021.04.14.dat # -s 表示设置偏移 107 | hexdump -n 32 -s 0x009f1f92 qqwry-2021.04.14.dat 108 | 109 | ``` 110 | 111 | 结果分别为: 112 | 113 | ``` 114 | [hexdump结果2] 115 | 0669e7b 00 00 00 00 08 00 00 00 - 00 00 01 1a 00 00 01 00 116 | 0669e8b 00 01 48 00 00 02 00 00 - 01 6e 00 00 00 01 00 01 117 | 0669e9b 118 | 119 | [hexdump结果3] 120 | 09f1f92 00 ff ff ff 59 9e 66 121 | 09f1f99 122 | 123 | ``` 124 | 125 | 结果3很好理解:"00 ff ff ff"就是最后一条ip记录,后面的"59 9e 66"是一个指向记录区的偏移量。虽然打印了32个字节,这里只显示了7个字节,是因为文件到此就结束了。 126 | 127 | ipv4地址本质上是一个数值,使用小端方式存储占用4个字节,"00 ff ff ff"的ipv4文本形式其实就是255.255.255.0。 后面跟着的"59 9e 66" 128 | 是一个偏移量只有3个字节,是因为纯真ip库尺寸不大,而且记录区实在中间的(偏移数值比较小),所以3个字节完全存的下。 这里的偏移量是0x00669e59。 129 | 130 | 再看结果2就更好理解了,32个字节包含了32/7=4个索引,我们按长度排列一下得到: 131 | 132 | ``` 133 | [分析结果] 134 | 00 00 00 00 - 08 00 00 => ip 0.0.0.0 offset 0x08 135 | 00 00 00 01 - 1a 00 00 => ip 1.0.0.0 offset 0x1a 136 | 01 00 00 01 - 48 00 00 => ip 1.0.0.1 offset 0x48 137 | 02 00 00 01 - 6e 00 00 => ip 1.0.0.2 offset 0x6e 138 | ``` 139 | 140 | 看分析结果的第一条记录起始ip="0.0.0.0",偏移量8正好指向[hexdump结果1]分析的第一条记录区"ff ff ff 00 49 41 4e 41"。 所以这条记录的完整信息为: 141 | 142 | ``` 143 | 起始ip 0.0.0.0 144 | 终止ip 0.255.255.255 145 | 记录A IANA 146 | 记录B 保留地址 147 | ``` 148 | 149 | 用上述方法再分析出最后一条ip记录 150 | 151 | ``` 152 | 起始ip 255.255.255.0 153 | 终止ip 255.255.255.255 154 | 记录A 纯真网络 155 | 记录B 2021年04月14日IP数据 156 | ``` 157 | 158 | 这条记录没有实际意义,提示了纯真ip库的版本号 159 | 160 | ### 索引的查找 161 | 162 | 可见,纯真ip库是将所有ipv4区间进行分区存放,任何有效的ipv4地址都会被匹配到一个区间中。索引区每条索引占用7字节,所以需要查找所属区间也很简单,使用折半查找(二分查找)即可取得非常好的查询效率。 163 | 2021年04月14日IP数据 共有数据:529010 条,折半查找最多20次,其时间复杂度为O(log n)。然后根据偏移量继续查询记录信息。 164 | 165 | ### 记录区的重定向 166 | 167 | 分析到这里,纯真ip库的数据存放结构大体就清楚了,但是记录区中很多数据是重复的,所以为了压缩ip库的文件大小,纯真ip库使用了内容重定向机制。 168 | 简单来说就是终止ip后跟着的第一个字节的数据,如果是0x01或者0x02就表示该条记录的文本区域重定向走了(正常的字符不可能是0x01或者0x02,以此做区分),0x01或者0x02后面跟的就是重定向地址。 169 | 170 | 0x01和0x02这两种重定向有所不同,0x01是整体重定向,0x02是局部重定向。 171 | 172 | 0x02局部重定向表示当前的记录A或者记录B被重定向到了另一个地方,0x02后面跟着4个字节的就是偏移量,根据偏移量重新去查。 这里就有多种情况: 173 | 174 | - 记录A和记录B都没有重定向 (就是上面分析的第一条ip记录的情况) 175 | - 只有记录A重定向了,记录B未重定向 176 | - 只有记录B重定向了,记录A未重定向 177 | - 记录A和记录B都重定向了 178 | 179 | 关于0x02的重定向可以使用一次递归处理,如果是0x02就递归调用当前函数。这样只需要封装一个读取记录A/记录B的递归函数,而无需考虑局部重定向的处理。递归最多调用1次,也不会造成过多栈上资源的浪费。 180 | 181 | 如果读到了0x01整体重定向,表示记录A和记录B都在另一个地方,直接去新的地址查询。(也就意味着0x01只会跟在终止ip后面)。 182 | 但是这里有一点要注意。当遇到0x01整体重定向到新的偏移地址时,依然可能会遇到0x02的局部重定向的情况(其实又是一次递归处理) 183 | 184 | 关于0x01整体依然可以使用递归处理。当遇到最复杂的情况(先是整体重定向,然后记录A和记录B分别局部重定向)也只会有个位数的递归调用,无需担心性能。相比使用if/else判断上述各种情况从代码实现上要简洁的多。 185 | 186 | ### 性能分析 187 | 188 | 至此为止,一次查询函数的调用流程就很清楚了: 189 | 190 | - 第1步:ip字符串转成数值形式 191 | - 第2步:查找所属ip区间及记录区偏移量 192 | - 第3步:根据偏移量去记录区读取相关信息 193 | 194 | 如果每次使用文件io的方式去查询效率就太低了。虽然节省内存,但使用场景受限。 195 | 196 | #### 常驻内存 197 | 198 | 所以一般会选择把整个ip库文件加载并常驻内存,以微服务的形式对外提供服务,代价就就是牺牲30mb内存。 199 | 200 | 此时基准测试结果如下:查询效率约为160w次/秒。 201 | 202 | ``` 203 | cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz 204 | BenchmarkFind 205 | BenchmarkFind-4 6350968 604.4 ns/op 568 B/op 6 allocs/op 206 | PASS 207 | ok github.com/yzchan/iploc 4.481s 208 | ``` 209 | 210 | #### GBK -> UTF8 211 | 212 | 如果要进一步提升查询效率,可以在第三步上想办法了。读取记录区数据主要包含两部分操作:定位和处理重定向、GBK解码。 213 | 214 | 先尝试不做GBK解码试试,直接返回string(buff)然后进行基准测试 215 | 216 | ``` 217 | cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz 218 | BenchmarkFind 219 | BenchmarkFind-4 14217972 240.7 ns/op 40 B/op 3 allocs/op 220 | PASS 221 | ok github.com/yzchan/iploc 3.746s 222 | ``` 223 | 224 | 可见每次查询的60%的时间在做GBK解码。优化思路就是把纯真ip库提前转化成UTF-8格式然后再载入内存。[转化方法可以看这个库](https://github.com/kayon/iploc) 225 | 此时查询效率达到了400w+次/秒。 226 | 227 | #### 使用hashmap存储解析结果 228 | 229 | 还有一种思路是在初始化的时候直接把所有的ip的记录值都读取出来保存到一个map中,利用hashmap高效的Get操作(时间复杂度O(1))来直接获取格式化后的记录值。 230 | 这样不仅节约了转编码的时间,还节省了读取内容时的各种通定向处理所需的时间。但内存占用从30M达到了160M。 231 | 232 | ``` 233 | cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz 234 | BenchmarkFind 235 | BenchmarkFind-4 18485007 188.5 ns/op 16 B/op 1 allocs/op 236 | PASS 237 | ok github.com/yzchan/iploc 6.134s 238 | ``` 239 | 240 | 此时查询效率可达500w+次/秒 -------------------------------------------------------------------------------- /download/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "github.com/cheggaaa/pb/v3" 10 | "golang.org/x/text/encoding/simplifiedchinese" 11 | "io" 12 | "io/ioutil" 13 | "log" 14 | "net/http" 15 | "net/url" 16 | "path/filepath" 17 | "strconv" 18 | "time" 19 | ) 20 | 21 | //type CopyWrite struct { 22 | // ZipTag [4]byte // 固定值[43 5a 49 50],即 CZIP 23 | // DateCnt uint32 // 1900.1.1到当前发布版本的天数 24 | // _ [4]byte // 未知 固定值 [01 00 00 00] 25 | // FileSize uint32 // 文件大小 26 | // _ [4]byte // 未知数据 27 | // Secret uint32 // 密钥 28 | // Version [128]byte // 版本信息 29 | // Link [128]byte // 官网链接 30 | //} 31 | 32 | func download(uri string) (stream []byte, err error) { 33 | u, err := url.Parse(uri) 34 | if err != nil { 35 | return 36 | } 37 | filename := filepath.Base(u.Path) 38 | 39 | client := http.DefaultClient 40 | client.Timeout = time.Second * 60 //设置超时时间 41 | resp, err := client.Get(uri) 42 | if err != nil { 43 | return 44 | } 45 | 46 | if resp.StatusCode != http.StatusOK { 47 | err = errors.New(fmt.Sprintf("[%s]下载失败,%s.", filename, resp.Status)) 48 | return 49 | } 50 | 51 | log.Printf("[INFO] 正在下载: [%s]", filename) 52 | 53 | size, err := strconv.Atoi(resp.Header.Get("Content-Length")) 54 | if err != nil { 55 | return 56 | } 57 | 58 | reader := io.LimitReader(resp.Body, int64(size)) 59 | writer := new(bytes.Buffer) 60 | // start new bar 61 | bar := pb.Full.Start64(int64(size)) 62 | 63 | // create proxy reader 64 | barReader := bar.NewProxyReader(reader) 65 | // copy from proxy reader 66 | n, err := io.Copy(writer, barReader) 67 | if err != nil || n != int64(size) { 68 | return 69 | } 70 | // finish bar 71 | bar.Finish() 72 | stream = writer.Bytes() 73 | return 74 | } 75 | 76 | func main() { 77 | log.Println("开始下载密钥文件") 78 | keyStream, err := download("http://update.cz88.net/ip/copywrite.rar") 79 | if err != nil { 80 | panic(err) 81 | } 82 | 83 | secret := binary.LittleEndian.Uint32(keyStream[20:24]) 84 | 85 | version := string(keyStream[24:142]) 86 | enc := simplifiedchinese.GBK.NewDecoder() 87 | decoded, err := enc.String(version) 88 | 89 | log.Println(decoded) 90 | 91 | log.Println("开始下载数据文件") 92 | dataStream, err := download("http://update.cz88.net/ip/qqwry.rar") 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | log.Println("开始解密文件") 98 | for i := 0; i < 512; i++ { // 处理前512字节 99 | secret = ((secret * 2053) + 1) & 0xFF // 密钥变换 100 | dataStream[i] = byte(uint32(dataStream[i]) ^ secret) // 做异或运算解密对应字节 101 | } 102 | 103 | reader, err := zlib.NewReader(bytes.NewReader(dataStream)) 104 | if err != nil { 105 | panic(err) 106 | } 107 | log.Println("开始保存qqwry.data数据文件...") 108 | qqwry, err := ioutil.ReadAll(reader) 109 | if err != nil { 110 | panic(err) 111 | } 112 | 113 | err = ioutil.WriteFile("qqwry.dat", qqwry, 0777) 114 | if err != nil { 115 | panic(err) 116 | } 117 | log.Println("文件保存成功!") 118 | } 119 | -------------------------------------------------------------------------------- /download/qqwry-2021.04.14.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yzchan/iploc/af8b427c6af2eaf60fec54ad519b9251c3f89828/download/qqwry-2021.04.14.dat -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yzchan/iploc 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/cheggaaa/pb/v3 v3.0.8 7 | golang.org/x/text v0.3.7 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= 2 | github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= 3 | github.com/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA= 4 | github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA= 5 | github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= 6 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 7 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= 8 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 9 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 10 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 11 | github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= 12 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 13 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 14 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 15 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 16 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 18 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A= 19 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 20 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 21 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 22 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 23 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | import ( 4 | "encoding/binary" 5 | "golang.org/x/text/encoding" 6 | "golang.org/x/text/encoding/simplifiedchinese" 7 | "io/ioutil" 8 | "net" 9 | "os" 10 | ) 11 | 12 | type Record struct { 13 | RecordA string 14 | RecordB string 15 | } 16 | 17 | type Result struct { 18 | StartIP net.IP 19 | StopIP net.IP 20 | Record 21 | } 22 | 23 | type QQWryParser struct { 24 | buffers []byte 25 | len int 26 | head uint32 27 | tail uint32 28 | enc *encoding.Decoder 29 | maps map[uint32]Record 30 | } 31 | 32 | func NewQQWryParser(filepath string) (q *QQWryParser, err error) { 33 | q = &QQWryParser{} 34 | 35 | f, err := os.OpenFile(filepath, os.O_RDONLY, 0400) 36 | if err != nil { 37 | return q, err 38 | } 39 | defer f.Close() 40 | 41 | buffer, err := ioutil.ReadAll(f) 42 | if err != nil { 43 | return q, err 44 | } 45 | 46 | q.buffers = buffer 47 | q.head = binary.LittleEndian.Uint32(buffer[:4]) 48 | q.tail = binary.LittleEndian.Uint32(buffer[4:8]) 49 | q.len = int((q.tail-q.head)/7) + 1 50 | q.enc = simplifiedchinese.GBK.NewDecoder() 51 | 52 | return q, nil 53 | } 54 | 55 | // Find 查询函数 56 | func (q *QQWryParser) Find(ipStr string) (recordA string, recordB string) { 57 | ip := binary.BigEndian.Uint32(net.ParseIP(ipStr).To4()) 58 | if len(q.maps) > 0 { 59 | return q.findInMap(ip) 60 | } 61 | _, _, areaOffset := q.searchIndex(ip) 62 | return q.readRecords(areaOffset) 63 | } 64 | 65 | // Query TODO 标准查询函数,接收 net.IP 类型的参数 66 | //func (q *QQWryParser) Query(ip net.IP) (recordA string, recordB string) { 67 | // return 68 | //} 69 | 70 | // Version 返回版本信息 71 | func (q *QQWryParser) Version() string { 72 | a, b := q.Find("255.255.255.0") 73 | return a + b 74 | } 75 | 76 | func (q *QQWryParser) FormatMap() { 77 | q.maps = make(map[uint32]Record, q.len) 78 | for i := q.head; i <= q.tail; i += 7 { 79 | recordA, recordB := q.readRecords(q.fillOffset(q.buffers[i+4 : i+7])) 80 | q.maps[binary.LittleEndian.Uint32(q.buffers[i:i+4])] = Record{recordA, recordB} 81 | } 82 | } 83 | 84 | func (q *QQWryParser) findInMap(ip uint32) (string, string) { 85 | _, ipu, _ := q.searchIndex(ip) 86 | r, ok := q.maps[ipu] 87 | if !ok { 88 | return "", "" 89 | } 90 | return r.RecordA, r.RecordB 91 | } 92 | 93 | /** 94 | * 纯真ip库中有的数据只占用3个byte,这里填充为4byte的uint32 95 | */ 96 | func (q *QQWryParser) fillOffset(b3 []byte) uint32 { 97 | return uint32(b3[0]) | uint32(b3[1])<<8 | uint32(b3[2])<<16 | 00<<24 98 | } 99 | 100 | /** 101 | * 根据索引区偏移量读取索引区数据 返回起始ip和记录区偏移量 102 | * 索引区每条索引是一个长度为7的[]byte,前4个byte表示起始ip 后3个byte表示记录区偏移量 103 | */ 104 | func (q *QQWryParser) readIndex(offset uint32) (startIp uint32, recordOffset uint32) { 105 | ip := q.buffers[offset : offset+4] 106 | startIp = binary.LittleEndian.Uint32(ip) 107 | recordOffset = q.fillOffset(q.buffers[offset+4 : offset+7]) 108 | return 109 | } 110 | 111 | func (q *QQWryParser) searchIndex(target uint32) (indexOffset uint32, startIp uint32, recordOffset uint32) { 112 | head := uint32(0) 113 | tail := (q.tail-q.head)/7 + 1 114 | 115 | mid := (head + tail) / 2 116 | var ipMid uint32 117 | for i := 0; ; i++ { 118 | ipMid, recordOffset = q.readIndex(mid*7 + q.head) 119 | if head == mid { 120 | indexOffset = mid*7 + q.head 121 | startIp = ipMid 122 | return 123 | } 124 | if target < ipMid { 125 | tail = mid 126 | } else { 127 | head = mid 128 | } 129 | mid = (head + tail) / 2 130 | } 131 | } 132 | 133 | /** 134 | * 根据偏移量整体读取记录A和记录B的值 135 | */ 136 | func (q *QQWryParser) readRecords(offset uint32) (textA string, textB string) { 137 | buff := q.buffers[offset : offset+8] 138 | //fmt.Printf("%#08x: %#x %#x [%#x][%#x]\n", offset, buff[:4], buff[4:], buff[0], buff[4]) 139 | if buff[4] == 0x01 { //记录模式245 140 | return q.readRecords(q.fillOffset(buff[5:]) - 4) // 非重定向模式 前4byte为指针 需要后移4位 141 | } 142 | 143 | var pos2 uint32 144 | textA, pos2 = q.readRecord(offset + 4) 145 | textB, _ = q.readRecord(pos2) 146 | if textB == " CZ88.NET" { 147 | textB = "" 148 | } 149 | return 150 | } 151 | 152 | /** 153 | * 读取记录A/记录B:需传入偏移量。返回记录A/记录B的值,同时返回新的偏移量 154 | * 因为记录A/记录B存储采用c语言字符数组的方式(遇到\0表示结束) 155 | * 所以读取记录B需要知道记录A的长度,所以在读取记录A的时候返回新的偏移量供读取记录B使用 156 | */ 157 | func (q *QQWryParser) readRecord(offset uint32) (record string, cursor uint32) { 158 | cursor = offset 159 | // 先预读4个byte的内容分析 如果第一个字节是0x02说明是重定向 160 | b4 := q.buffers[offset : offset+4] 161 | 162 | if b4[0] == 0x02 { 163 | record, cursor = q.readRecord(q.fillOffset(b4[1:])) 164 | return record, offset + 4 165 | } 166 | 167 | // 重新开始读取,遇到\0结束 168 | for { 169 | cursor++ 170 | if q.buffers[cursor] == 0x00 { 171 | break 172 | } 173 | } 174 | buff := q.buffers[offset:cursor] 175 | record, _ = q.enc.String(string(buff)) 176 | //Record = string(buff) 177 | cursor++ 178 | return 179 | } 180 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | const filePath = "download/qqwry.dat" 10 | 11 | func TestFind(t *testing.T) { 12 | var results = []struct { 13 | ip string 14 | recordA string 15 | recordB string 16 | }{ 17 | {"0.0.0.1", "IANA", "保留地址"}, 18 | {"127.0.0.1", "本机地址", ""}, 19 | {"255.255.255.1", "纯真网络", "2022年04月27日IP数据"}, 20 | } 21 | q, err := NewQQWryParser(filePath) 22 | if err != nil { 23 | t.Fatal("读取ip库文件失败") 24 | } 25 | q.FormatMap() // 格式化数据到map 26 | t.Log("开始测试Find函数") 27 | errFlag := false 28 | for index, result := range results { 29 | recordA, recordB := q.Find(result.ip) 30 | t.Logf("第[%d]组ip [%s]\n", index, result.ip) 31 | t.Logf(" |-预期结果:[%s] [%s]\n", result.recordA, result.recordB) 32 | t.Logf(" |-查询结果:[%s] [%s]\n", recordA, recordB) 33 | 34 | if recordA != result.recordA || recordB != result.recordB { 35 | errFlag = true 36 | } 37 | } 38 | if errFlag { 39 | t.Fatal("\x1b[31m测试失败!\x1b[0m") 40 | } 41 | t.Log("\x1b[32m测试通过!\x1b[0m") 42 | } 43 | 44 | //func BenchmarkFormat(b *testing.B) { 45 | // b.StopTimer() 46 | // 47 | // q, err := NewQQWryParser(filePath) 48 | // if err != nil { 49 | // panic(err) 50 | // } 51 | // 52 | // rand.Seed(time.Now().UnixNano()) 53 | // b.StartTimer() 54 | // for i := 0; i < b.N; i++ { 55 | // q.FormatMap() 56 | // } 57 | //} 58 | 59 | func BenchmarkFind(b *testing.B) { 60 | b.StopTimer() 61 | 62 | q, err := NewQQWryParser(filePath) 63 | if err != nil { 64 | panic(err) 65 | } 66 | //q.FormatMap() 67 | 68 | rand.Seed(time.Now().UnixNano()) 69 | b.StartTimer() 70 | for i := 0; i < b.N; i++ { 71 | q.Find("127.0.0.1") 72 | } 73 | } 74 | 75 | func BenchmarkFindParallel(b *testing.B) { 76 | b.StopTimer() 77 | q, err := NewQQWryParser(filePath) 78 | if err != nil { 79 | b.Fatal(err) 80 | } 81 | //q.FormatMap() 82 | rand.Seed(time.Now().UnixNano()) 83 | b.StartTimer() 84 | b.RunParallel(func(pb *testing.PB) { 85 | for pb.Next() { 86 | q.Find("127.0.0.1") 87 | } 88 | }) 89 | } 90 | --------------------------------------------------------------------------------