├── .drone.yml ├── .editorconfig ├── .gitignore ├── .travis.yml ├── Dockerfile ├── Makefile ├── README.md ├── consts.go ├── docker-compose.yaml ├── download.go ├── download_test.go ├── go.mod ├── go.sum ├── main.go ├── qqwry.dat ├── qqwry.go ├── response.go └── screenshots.png /.drone.yml: -------------------------------------------------------------------------------- 1 | ### 2 | # File: .drone.yml 3 | # Author: Ming Cheng 4 | # 5 | # Created Date: Monday, March 9th 2020, 5:11:52 pm 6 | # Last Modified: Friday, July 15th 2022, 5:38:32 pm 7 | # 8 | # http://www.opensource.org/licenses/MIT 9 | ### 10 | 11 | kind: pipeline 12 | type: docker 13 | name: build_and_run_on_docker 14 | 15 | steps: 16 | - name: build 17 | image: golang:1.18 18 | environment: 19 | GOPROXY: 'https://goproxy.cn,direct' 20 | volumes: 21 | - name: docker-sock 22 | path: /var/run/docker.sock 23 | commands: 24 | - make clean test build 25 | 26 | - name: weekly build 27 | image: docker:stable 28 | when: 29 | branch: 30 | - master 31 | event: 32 | - push 33 | environment: 34 | DOCKER_USERNAME: 35 | from_secret: ghcr_docker_username 36 | DOCKER_PASSWORD: 37 | from_secret: ghcr_docker_password 38 | volumes: 39 | - name: docker-sock 40 | path: /var/run/docker.sock 41 | commands: 42 | - docker login -u=$DOCKER_USERNAME -p=$DOCKER_PASSWORD ghcr.io 43 | - docker build . -t ghcr.io/mingcheng/qqwry:`date +%Y%m%d` 44 | - docker push ghcr.io/mingcheng/qqwry:`date +%Y%m%d` 45 | - docker logout 46 | 47 | - name: publish-to-ghcr 48 | image: plugins/docker 49 | volumes: 50 | - name: docker-sock 51 | path: /var/run/docker.sock 52 | environment: 53 | when: 54 | branch: 55 | - master 56 | event: 57 | - push 58 | settings: 59 | registry: ghcr.io 60 | repo: ghcr.io/mingcheng/qqwry 61 | username: 62 | from_secret: ghcr_docker_username 63 | password: 64 | from_secret: ghcr_docker_password 65 | dockerfile: Dockerfile 66 | tags: 67 | - latest 68 | - 1.0.4 69 | 70 | volumes: 71 | - name: docker-sock 72 | host: 73 | path: /var/run/docker.sock 74 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.go] 12 | indent_style = tab 13 | indent_size = 2 14 | 15 | [Makefile] 16 | indent_style = tab 17 | indent_size = 4 18 | 19 | [.travis.yml] 20 | indent_style = space 21 | indent_size = 2 22 | 23 | [*.json] 24 | indent_style = space 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> Go 2 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 3 | *.o 4 | *.a 5 | *.so 6 | 7 | # Folders 8 | _obj 9 | _test 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | .idea 27 | qqwry.iml 28 | qqwry 29 | qqwry_linux_amd64 30 | 31 | launch.json 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.10.x 5 | - 1.11.x 6 | - 1.12.x 7 | 8 | before_install: 9 | - go get github.com/mitchellh/gox 10 | - go get 11 | 12 | install: 13 | - gox -osarch=darwin/386 14 | - gox -osarch=darwin/amd64 15 | - gox -osarch=linux/amd64 16 | - gox -osarch=linux/386 17 | - gox -osarch=windows/amd64 18 | - gox -osarch=windows/386 19 | 20 | script: 21 | - ls -l|grep qqwry 22 | 23 | deploy: 24 | provider: releases 25 | api_key: "$GH_TOKEN" 26 | file: 27 | - qqwry_darwin_386 28 | - qqwry_darwin_amd64 29 | - qqwry_linux_386 30 | - qqwry_linux_amd64 31 | - qqwry_windows_386.exe 32 | - qqwry_windows_amd64.exe 33 | skip_cleanup: true 34 | on: 35 | go: 1.12.x 36 | tags: true 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18 AS builder 2 | LABEL maintainer="Ming Cheng" 3 | 4 | ENV GOPATH /go 5 | ENV GOROOT /usr/local/go 6 | ENV PACKAGE github.com/freshcn/qqwry 7 | ENV GOPROXY https://goproxy.cn,direct 8 | ENV BUILD_DIR ${GOPATH}/src/${PACKAGE} 9 | 10 | # Build 11 | COPY . ${BUILD_DIR} 12 | WORKDIR ${BUILD_DIR} 13 | RUN go build -o /bin/qqwry ${BUILD_DIR} 14 | 15 | # ENTRYPOINT [ "/bin/qqwry" ] 16 | 17 | # Stage2 18 | FROM ubuntu:22.04 19 | 20 | COPY --from=builder /bin/qqwry /bin/qqwry 21 | 22 | EXPOSE 2060 23 | ENTRYPOINT ["/bin/qqwry"] 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT="qqwry" 2 | VERSION=$(shell date +%Y%m%d) 3 | COMMIT_HASH=$(shell git rev-parse --short HEAD) 4 | BINARY=qqwry 5 | 6 | GO_FLAGS=-ldflags=" \ 7 | -X 'main.version=$(VERSION)' \ 8 | -X 'main.commit=$(COMMIT_HASH)' \ 9 | -X 'main.date=$(shell date)'" 10 | GO=$(shell which go) 11 | 12 | all: build 13 | 14 | build: test 15 | @$(GO) build $(GO_FLAGS) -o ${BINARY} . 16 | 17 | test: clean 18 | @go test -v . 19 | 20 | docker_image: 21 | @docker-compose build 22 | 23 | clean: 24 | @$(GO) clean -testcache 25 | @rm -f ${BINARY} 26 | 27 | .PHONY: fmt install test clean docker_image 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 此项目不再更新,是因为 CZ88 官方已经从 2024.10 开始不提供 dat 格式的 IP 离线数据库文件 2 | 3 | Shame on you! 理解纯真公司基于商业盈利目的带来的各种营收压力,但是请您不要以社区的名义不实宣传 https://cz88.net/geo-public 4 | 5 | # qqwry.go 6 | 7 | 通过 HTTP 提供一个 IP 地址归属地查询支持的微服务,使用纯真的 IP 库(支持直接在线获取和更新)。 8 | 9 | ## 介绍 10 | 11 | ![Screenshots](screenshots.png) 12 | 13 | 我们大家做网站的时候,都会需要将用户的IP地址转换为归属地址功能,而之前的作法大都是从硬盘的数据文件中读取,这样不太高效。 14 | 15 | 这个微服务将纯真 IP 库直接加载到内存中,并以 HTTP 方式提供接口暴露出来(后续支持 GRPC),这样方便项目中所有的程序都能方便的接口 IP 查询功能。 16 | 17 | ## 安装 18 | 19 | ### Golang 的安装 20 | 21 | ``` 22 | go get github.com/freshcn/qqwry 23 | ``` 24 | 25 | ### 二进制包直接下载 26 | 27 | https://github.com/freshcn/qqwry/releases 28 | 29 | ### 在线获取最新的纯真 IP 库 30 | 31 | 默认支持在线获取最新的纯真 IP 库,解压缩到内村中并直接运行(需要网络的支持)。如果您想使用本地下载运行,请参考如下。 32 | 33 | ### 下载纯真IP库 34 | 35 | 访问 http://www.cz88.net 下载纯真IP库,需要在 Windows 中安装程序,然后在程序的安装目录可以找到 `qqwry.dat` 文件;也可以考虑运行使用 `download.go` 的 `GetOnline` 函数并将数据保存为本地文件。 36 | 37 | 将 dump 出来的 `qqwry` 文件放到和本程序同一目录(当然也可是其他目录,只是需要在运行的时候指定IP库目录),直接运行即可。 38 | 39 | ### 运行参数 40 | 41 | 运行 `./qqwry -h` 可以看到本服务程序的可用运行参数 42 | 43 | ``` 44 | -port string 45 | HTTP 请求监听端口号 (default "2060") 46 | -qqwry string 47 | 纯真 IP 库的地址 (default "./qqwry.dat") 48 | ``` 49 | 50 | ## 使用方法 51 | 52 | ``` 53 | http://127.0.0.1:2060?ip=8.8.8.8,114.114.114.114&callback=a 54 | ``` 55 | 56 | * ip - 要查询的IP地址,可使用半角逗号分隔查询多个IP地址。必填项 57 | * callback - jsonp回调函数名,当提交了这个参数,将会按jsonp格式返回。非必填 58 | 59 | ** 返回结果 ** 60 | 61 | ```json 62 | {"114.114.114.114":{"ip":"114.114.114.114","country":"江苏省南京市","area":"南京信风网络科技有限公司GreatbitDNS服务器"},"8.8.8.8":{"ip":"8.8.8.8","country":"美国","area":"加利福尼亚州圣克拉拉县山景市谷歌公司DNS服务器"}} 63 | ``` 64 | 65 | * ip - 输入的ip地址 66 | * country - 国家或地区 67 | * area - 区域(我实际测试得到还有可能是运营商) 68 | 69 | 70 | ### 感谢 71 | 72 | * 感谢[纯真IP库](http://www.cz88.net)一直以来坚持为大家提供免费的IP库资源 73 | * 感谢[yinheli](https://github.com/yinheli)的[qqwry](https://github.com/yinheli/qqwry)项目,为我提供了纯真ip库文件格式算法 74 | * 感谢 https://zhangzifan.com/update-qqwry-dat.html 提供在线获取 IP 库的方案 75 | 76 | `- eof -` 77 | -------------------------------------------------------------------------------- /consts.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | ) 7 | 8 | const ( 9 | // IndexLen 索引长度 10 | IndexLen = 7 11 | // RedirectMode1 国家的类型, 指向另一个指向 12 | RedirectMode1 = 0x01 13 | // RedirectMode2 国家的类型, 指向一个指向 14 | RedirectMode2 = 0x02 15 | ) 16 | 17 | // ResultQQwry 归属地信息 18 | type ResultQQwry struct { 19 | IP string `json:"ip"` 20 | Country string `json:"country"` 21 | Area string `json:"area"` 22 | } 23 | 24 | type fileData struct { 25 | Data []byte 26 | FilePath string 27 | Path *os.File 28 | IPNum int64 29 | } 30 | 31 | // QQwry 纯真ip库 32 | type QQwry struct { 33 | Data *fileData 34 | Offset int64 35 | } 36 | 37 | // Response 向客户端返回数据的 38 | type Response struct { 39 | r *http.Request 40 | w http.ResponseWriter 41 | } 42 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | obsync: 4 | build: 5 | context: . 6 | image: ghcr.io/mingcheng/qqwry 7 | ports: 8 | - 2060:2060 9 | restart: on-failure 10 | -------------------------------------------------------------------------------- /download.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "encoding/binary" 7 | "io/ioutil" 8 | "net/http" 9 | ) 10 | 11 | // @ref https://zhangzifan.com/update-qqwry-dat.html 12 | 13 | const Address = "http://update.cz88.net/ip/qqwry.rar" 14 | 15 | // getKey from cz88.net 16 | func getKey() (uint32, error) { 17 | resp, err := http.Get("http://update.cz88.net/ip/copywrite.rar") 18 | if err != nil { 19 | return 0, err 20 | } 21 | defer resp.Body.Close() 22 | 23 | if body, err := ioutil.ReadAll(resp.Body); err != nil { 24 | return 0, err 25 | } else { 26 | // @see https://stackoverflow.com/questions/34078427/how-to-read-packed-binary-data-in-go 27 | return binary.LittleEndian.Uint32(body[5*4:]), nil 28 | } 29 | } 30 | 31 | // GetOnline get data from online server 32 | func GetOnline() ([]byte, error) { 33 | resp, err := http.Get(Address) 34 | if err != nil { 35 | return nil, err 36 | } 37 | defer resp.Body.Close() 38 | 39 | if body, err := ioutil.ReadAll(resp.Body); err != nil { 40 | return nil, err 41 | } else { 42 | if key, err := getKey(); err != nil { 43 | return nil, err 44 | } else { 45 | for i := 0; i < 0x200; i++ { 46 | key = key * 0x805 47 | key++ 48 | key = key & 0xff 49 | 50 | body[i] = byte(uint32(body[i]) ^ key) 51 | } 52 | 53 | reader, err := zlib.NewReader(bytes.NewReader(body)) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return ioutil.ReadAll(reader) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /download_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | ) 7 | 8 | func Test_getKey(t *testing.T) { 9 | got, err := getKey() 10 | if err != nil { 11 | t.Errorf("getKey() error = %v", err) 12 | return 13 | } 14 | 15 | t.Log(got) 16 | } 17 | 18 | func TestGetOnline(t *testing.T) { 19 | got, err := GetOnline() 20 | if err != nil { 21 | t.Errorf("GetOnline() error = %v", err) 22 | return 23 | } 24 | 25 | if len(got) <= 0 { 26 | t.Errorf("GetOnline() error, response empty") 27 | return 28 | } 29 | 30 | err = ioutil.WriteFile("qqwry.dat", got, 0644) 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/freshcn/qqwry 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 7 | golang.org/x/text v0.3.8 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 h1:xoIK0ctDddBMnc74udxJYBqlo9Ylnsp1waqjLsnef20= 2 | github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= 3 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 4 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "runtime" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | var ( 14 | version = "dev" 15 | commit = "" 16 | date = "" 17 | ) 18 | 19 | func main() { 20 | fmt.Printf("version %s(%s) build on %s\n", version, commit, date) 21 | 22 | runtime.GOMAXPROCS(runtime.NumCPU()) 23 | datFile := flag.String("qqwry", "./qqwry.dat", "纯真 IP 库的地址") 24 | port := flag.String("port", "2060", "HTTP 请求监听端口号") 25 | flag.Parse() 26 | 27 | IPData.FilePath = *datFile 28 | 29 | startTime := time.Now().UnixNano() 30 | res := IPData.InitIPData() 31 | 32 | if v, ok := res.(error); ok { 33 | log.Panic(v) 34 | } 35 | endTime := time.Now().UnixNano() 36 | 37 | log.Printf("IP 库加载完成 共加载:%d 条 IP 记录, 所花时间:%.1f ms\n", IPData.IPNum, float64(endTime-startTime)/1000000) 38 | 39 | // 下面开始加载 http 相关的服务 40 | http.HandleFunc("/", findIP) 41 | 42 | log.Printf("开始监听网络端口:%s", *port) 43 | 44 | if err := http.ListenAndServe(fmt.Sprintf(":%s", *port), nil); err != nil { 45 | log.Println(err) 46 | } 47 | } 48 | 49 | // findIP 查找 IP 地址的接口 50 | func findIP(w http.ResponseWriter, r *http.Request) { 51 | res := NewResponse(w, r) 52 | 53 | ip := r.Form.Get("ip") 54 | 55 | if ip == "" { 56 | res.ReturnError(http.StatusBadRequest, 200001, "请填写 IP 地址") 57 | return 58 | } 59 | 60 | ips := strings.Split(ip, ",") 61 | 62 | qqWry := NewQQwry() 63 | 64 | rs := map[string]ResultQQwry{} 65 | if len(ips) > 0 { 66 | for _, v := range ips { 67 | rs[v] = qqWry.Find(v) 68 | } 69 | } 70 | 71 | res.ReturnSuccess(rs) 72 | } 73 | -------------------------------------------------------------------------------- /qqwry.dat: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /qqwry.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/binary" 6 | "net" 7 | "strings" 8 | 9 | "golang.org/x/text/encoding/simplifiedchinese" 10 | ) 11 | 12 | //go:embed "qqwry.dat" 13 | var tmpData []byte 14 | 15 | var IPData fileData 16 | 17 | // InitIPData 初始化ip库数据到内存中 18 | func (f *fileData) InitIPData() (rs interface{}) { 19 | 20 | //// 判断文件是否存在 21 | //if _, err := os.Stat(f.FilePath); err != nil && os.IsNotExist(err) { 22 | // log.Println("文件不存在,尝试从网络获取最新纯真 IP 库") 23 | // //tmpData, err = GetOnline() 24 | // if err != nil { 25 | // rs = err 26 | // return 27 | // } else { 28 | // if err := ioutil.WriteFile(f.FilePath, tmpData, 0644); err == nil { 29 | // log.Printf("已将最新的纯真 IP 库保存到本地 %s ", f.FilePath) 30 | // } 31 | // } 32 | //} else { 33 | // // 打开文件句柄 34 | // log.Printf("从本地数据库文件 %s 打开\n", f.FilePath) 35 | // f.Path, err = os.OpenFile(f.FilePath, os.O_RDONLY, 0400) 36 | // if err != nil { 37 | // rs = err 38 | // return 39 | // } 40 | // defer f.Path.Close() 41 | // 42 | // tmpData, err = ioutil.ReadAll(f.Path) 43 | // if err != nil { 44 | // log.Println(err) 45 | // rs = err 46 | // return 47 | // } 48 | //} 49 | 50 | f.Data = tmpData 51 | 52 | buf := f.Data[0:8] 53 | start := binary.LittleEndian.Uint32(buf[:4]) 54 | end := binary.LittleEndian.Uint32(buf[4:]) 55 | 56 | f.IPNum = int64((end-start)/IndexLen + 1) 57 | 58 | return true 59 | } 60 | 61 | // NewQQwry 新建 qqwry 类型 62 | func NewQQwry() QQwry { 63 | return QQwry{ 64 | Data: &IPData, 65 | } 66 | } 67 | 68 | // ReadData 从文件中读取数据 69 | func (q *QQwry) ReadData(num int, offset ...int64) (rs []byte) { 70 | if len(offset) > 0 { 71 | q.SetOffset(offset[0]) 72 | } 73 | nums := int64(num) 74 | end := q.Offset + nums 75 | dataNum := int64(len(q.Data.Data)) 76 | if q.Offset > dataNum { 77 | return nil 78 | } 79 | 80 | if end > dataNum { 81 | end = dataNum 82 | } 83 | rs = q.Data.Data[q.Offset:end] 84 | q.Offset = end 85 | return 86 | } 87 | 88 | // SetOffset 设置偏移量 89 | func (q *QQwry) SetOffset(offset int64) { 90 | q.Offset = offset 91 | } 92 | 93 | // Find ip地址查询对应归属地信息 94 | func (q *QQwry) Find(ip string) (res ResultQQwry) { 95 | 96 | res = ResultQQwry{} 97 | 98 | res.IP = ip 99 | if strings.Count(ip, ".") != 3 { 100 | return res 101 | } 102 | offset := q.searchIndex(binary.BigEndian.Uint32(net.ParseIP(ip).To4())) 103 | if offset <= 0 { 104 | return 105 | } 106 | 107 | var country []byte 108 | var area []byte 109 | 110 | mode := q.readMode(offset + 4) 111 | if mode == RedirectMode1 { 112 | countryOffset := q.readUInt24() 113 | mode = q.readMode(countryOffset) 114 | if mode == RedirectMode2 { 115 | c := q.readUInt24() 116 | country = q.readString(c) 117 | countryOffset += 4 118 | } else { 119 | country = q.readString(countryOffset) 120 | countryOffset += uint32(len(country) + 1) 121 | } 122 | area = q.readArea(countryOffset) 123 | } else if mode == RedirectMode2 { 124 | countryOffset := q.readUInt24() 125 | country = q.readString(countryOffset) 126 | area = q.readArea(offset + 8) 127 | } else { 128 | country = q.readString(offset + 4) 129 | area = q.readArea(offset + uint32(5+len(country))) 130 | } 131 | 132 | enc := simplifiedchinese.GBK.NewDecoder() 133 | res.Country, _ = enc.String(string(country)) 134 | res.Area, _ = enc.String(string(area)) 135 | 136 | return 137 | } 138 | 139 | // readMode 获取偏移值类型 140 | func (q *QQwry) readMode(offset uint32) byte { 141 | mode := q.ReadData(1, int64(offset)) 142 | return mode[0] 143 | } 144 | 145 | // readArea 读取区域 146 | func (q *QQwry) readArea(offset uint32) []byte { 147 | mode := q.readMode(offset) 148 | if mode == RedirectMode1 || mode == RedirectMode2 { 149 | areaOffset := q.readUInt24() 150 | if areaOffset == 0 { 151 | return []byte("") 152 | } 153 | return q.readString(areaOffset) 154 | } 155 | return q.readString(offset) 156 | } 157 | 158 | // readString 获取字符串 159 | func (q *QQwry) readString(offset uint32) []byte { 160 | q.SetOffset(int64(offset)) 161 | data := make([]byte, 0, 30) 162 | buf := make([]byte, 1) 163 | for { 164 | buf = q.ReadData(1) 165 | if buf[0] == 0 { 166 | break 167 | } 168 | data = append(data, buf[0]) 169 | } 170 | return data 171 | } 172 | 173 | // searchIndex 查找索引位置 174 | func (q *QQwry) searchIndex(ip uint32) uint32 { 175 | header := q.ReadData(8, 0) 176 | 177 | start := binary.LittleEndian.Uint32(header[:4]) 178 | end := binary.LittleEndian.Uint32(header[4:]) 179 | 180 | buf := make([]byte, IndexLen) 181 | mid := uint32(0) 182 | _ip := uint32(0) 183 | 184 | for { 185 | mid = q.getMiddleOffset(start, end) 186 | buf = q.ReadData(IndexLen, int64(mid)) 187 | _ip = binary.LittleEndian.Uint32(buf[:4]) 188 | 189 | if end-start == IndexLen { 190 | offset := byteToUInt32(buf[4:]) 191 | buf = q.ReadData(IndexLen) 192 | if ip < binary.LittleEndian.Uint32(buf[:4]) { 193 | return offset 194 | } 195 | return 0 196 | } 197 | 198 | // 找到的比较大,向前移 199 | if _ip > ip { 200 | end = mid 201 | } else if _ip < ip { // 找到的比较小,向后移 202 | start = mid 203 | } else if _ip == ip { 204 | return byteToUInt32(buf[4:]) 205 | } 206 | } 207 | } 208 | 209 | // readUInt24 210 | func (q *QQwry) readUInt24() uint32 { 211 | buf := q.ReadData(3) 212 | return byteToUInt32(buf) 213 | } 214 | 215 | // getMiddleOffset 216 | func (q *QQwry) getMiddleOffset(start uint32, end uint32) uint32 { 217 | records := ((end - start) / IndexLen) >> 1 218 | return start + records*IndexLen 219 | } 220 | 221 | // byteToUInt32 将 byte 转换为uint32 222 | func byteToUInt32(data []byte) uint32 { 223 | i := uint32(data[0]) & 0xff 224 | i |= (uint32(data[1]) << 8) & 0xff00 225 | i |= (uint32(data[2]) << 16) & 0xff0000 226 | return i 227 | } 228 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/pquerna/ffjson/ffjson" 8 | ) 9 | 10 | // NewResponse 创建一个新的response对象 11 | func NewResponse(w http.ResponseWriter, r *http.Request) Response { 12 | r.ParseForm() 13 | return Response{ 14 | w: w, 15 | r: r, 16 | } 17 | } 18 | 19 | // ReturnSuccess 返回正确的信息 20 | func (r *Response) ReturnSuccess(data interface{}) { 21 | r.Return(data, 200) 22 | } 23 | 24 | // ReturnError 返回错误信息 25 | func (r *Response) ReturnError(statuscode, code int, errMsg string) { 26 | r.Return(map[string]interface{}{"errcode": code, "errmsg": errMsg}, statuscode) 27 | } 28 | 29 | // Return 向客户返回回数据 30 | func (r *Response) Return(data interface{}, code int) { 31 | jsonp := r.IsJSONP() 32 | 33 | rs, err := ffjson.Marshal(data) 34 | if err != nil { 35 | code = 500 36 | rs = []byte(fmt.Sprintf(`{"errcode":500, "errmsg":"%s"}`, err.Error())) 37 | } 38 | 39 | r.w.WriteHeader(code) 40 | if jsonp == "" { 41 | r.w.Header().Add("Content-Type", "application/json") 42 | r.w.Write(rs) 43 | } else { 44 | r.w.Header().Add("Content-Type", "application/javascript") 45 | r.w.Write([]byte(fmt.Sprintf(`%s(%s)`, jsonp, rs))) 46 | } 47 | } 48 | 49 | // IsJSONP 是否为jsonp 请求 50 | func (r *Response) IsJSONP() string { 51 | if r.r.Form.Get("callback") != "" { 52 | return r.r.Form.Get("callback") 53 | } 54 | return "" 55 | } 56 | -------------------------------------------------------------------------------- /screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingcheng/qqwry/82ced2e01dbb0cf07865046dbf4c5b116ab18d7a/screenshots.png --------------------------------------------------------------------------------