基于Golang的IP地址信息查詢服務

LatiaFaerbe 7年前發布 | 72K 次閱讀 Go語言 Google Go/Golang開發 gRPC

工作中經常會有通過IP匹配用戶信息的需求,如確定用戶所在的地區(國家/省份/城市)、運營商、時區、經緯度等等。前一陣有個Golang開發的項目也有這樣的需求,于是簡單實現了一個包,最近忙里偷閑又包了一個支持HTTP和GRPC方式調用的服務,并開源在GitHub上了。本文主要介紹IP地址信息查詢的實現細節和使用方式。

首先交代一下GitHub地址:

歡迎大家在項目中使用(已通過N億日PV服務的考驗),有任何問題或建議,請提交Issue反饋或Fork到自己名下修改后提交Pull Request。

IP數據文件

IP數據文件存放IP地址段和數據信息的映射關系,是IP地址信息查詢中最重要的部分,格式上要求可擴展,數據上則需要準確甚至精確。真正意義上完美的IP數據文件是不存在的,而要想讓數據文件保持可用,需要定期對數據文件做維護更新。

數據格式

IP數據文件通常是純文本形式的,映射關系按行存放,每行的各項之間用”\t”分隔,前面兩列是IP段的起始和結束點轉換成無符號32位整型的值(只考慮IPV4),后面的部分則是各項信息,可根據實際需要擴充;各行之間要求是按IP段從小到大升序排列的。示例格式如下:

1033224192    1033228287  北京  北京  朝陽  聯通
1033228288  1033232383  北京  北京  海淀  聯通
1033232384  1033233407  北京  北京  昌平  聯通

數據來源

IP信息數據主要有3個來源:

  • 具有一定規模的公司大都會自己維護一份IP庫,如果你在這些公司工作,可以直接使用
  • 網絡上有一些免費的IP庫(如純真IP庫)
  • 購買商業的IP庫(如IPIP.NET)

此外也能通過一定的技術或人工手段自行獲取維護IP信息數據庫,但是成本會非常高。

IP地址信息查詢的原理

有了數據文件,要實現信息查詢并不難,簡單方式是直接將數據文件加載到內存數組中,查找時將IP地址轉換成無符號32位整型,然后用二分查找法查找整型所在的區間,找到后則返回對應的數據,沒找到則返回失敗。Golang中核心代碼如下:

將IP地址字符串轉成無符號32位整型:

func ip2Long(ip string) uint32 {
    var long uint32
    binary.Read(bytes.NewBuffer(net.ParseIP(ip).To4()), binary.BigEndian, &long)
    return long
}

主要結構體:

type IpRange struct {
    Begin uint32
    End   uint32
    Data  []byte
}

type IpData []*IpRange

二分查找:

func (id *IpData) getIpRange(ip string) (*IpRange, error) {
    var low, high int = 0, (id.Length() - 1)

    ipdt := *id
    il := ip2Long(ip)
    if il <= 0 {
        return nil, ErrorIpRangeNotFound
    }

    for low <= high {
        var middle int = (high-low)/2 + low

        ir := ipdt[middle]

        if il >= ir.Begin && il <= ir.End {
            return ir, nil
        } else if il < ir.Begin {
            high = middle - 1
        } else {
            low = middle + 1
        }
    }

    return nil, ErrorIpRangeNotFound
}

Golang ipquery包介紹

ipquery包的用法

ipquery包(https://github.com/tabalt/ipquery/)用起來很簡單,導入包后通過 ipquery.Load() 方法初始化加載IP數據文件,然后就可以使用 ipquery.Find() 方法來查詢IP地址對應的信息了。示例代碼如下:

package main

import (
    "fmt"
    "github.com/tabalt/ipquery"
)

func main() {
    df := "testdata/test_10000.data"
    err := ipquery.Load(df)
    if err != nil {
        fmt.Println(err)
    }

    ip := "61.149.208.1"
    dt, err := ipquery.Find(ip)

    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(ip, string(dt))
    }
}

如果你想在程序運行過程中安全地更新數據文件,請使用 ipquery.ReLoad() 方法; ipquery.Length() 則可以獲取到加載到內存的數據總條數。

上面介紹的方法其實都是為了方便使用而包裝的快捷方法,也可以直接使用 ipquery.NewIpData() 方法返回的IpData結構體獲得更大的靈活性。如給IpData結構體的Load或ReLoad方法傳入一個自定義的io.Reader可以從非文本文件的數據源初始化ipquery包。

ipquery包的壓測結果

ipquery包提供了較完善的單元測試,克隆代碼到GOPATH中后,進入$GOPATH/ipqeury目錄,執行 go test 相關命令即可執行測試代碼:

[tabalt@localhost ipquery] go test -v
=== RUN   TestIpData_Load
--- PASS: TestIpData_Load (0.01s)
=== RUN   TestIpData_Find
--- PASS: TestIpData_Find (0.01s)
=== RUN   TestIpData_Parallel_Find
--- PASS: TestIpData_Parallel_Find (0.01s)
PASS
ok      ipquery 0.051s

從壓測結果上看ipquery包的性能是相當不錯的,在一臺2核4G CentOS 6.2 Golang 1.7.1虛擬機開發機上,初始化23M的數據文件平均耗時500ms左右,執行查找平均耗時0.012ms,具體數據如下:

[tabalt@localhost ipquery] go test -bench=.
BenchmarkIpData_Load-2                 3         452223279 ns/op        97439626 B/op    1780052 allocs/op
BenchmarkIpData_Find-2            100000             11472 ns/op            1118 B/op         21 allocs/op
PASS
ok      ipquery 33.488s
[tabalt@localhost ipquery] go test -bench=.
BenchmarkIpData_Load-2                 3         500309108 ns/op        97439621 B/op    1780052 allocs/op
BenchmarkIpData_Find-2            100000             11809 ns/op            1118 B/op         21 allocs/op
PASS
ok      ipquery 33.498s
[tabalt@localhost ipquery] go test -bench=.
BenchmarkIpData_Load-2                 3         436756760 ns/op        97439621 B/op    1780052 allocs/op
BenchmarkIpData_Find-2            100000             12574 ns/op            1118 B/op         21 allocs/op
PASS
ok      ipquery 34.510s

IP地址信息查詢服務介紹

如文章開頭所說,這個項目基于ipquery包提供HTTP和GRPC接口,名字也就很俗的取為ipqueryd。個人習慣項目級的Go代碼不放在全局的GOPATH里,而是使用shell腳本來動態修改GOPATH為項目目錄后執行go命令,因此可以使用如下步驟運行本項目:

[tabalt@localhost ~] git clone https://github.com/tabalt/ipqueryd.git ~/$NOT_YOUR_GOPATH/
[tabalt@localhost ipqueryd] cd ~/$NOT_YOUR_GOPATH/ipqueryd
[tabalt@localhost ipqueryd] ./ctrl.sh run

配置文件

項目中conf目錄下有個ipqueryd.json的配置文件,可以配置PID文件、HTTP服務端口、GRPC服務端口、數據文件路徑等內容,可以根據需求修改;服務端口可以只配其中一個也可以兩個都配上。

{
    "pid_file": "./tmp/ipqueryd.pid",
    "http_server_port": ":12101",
    "grpc_server_port": ":12102",
    "data_file": "./data/ip_data.txt"
}

HTTP接口

HTTP接口支持返回JSON格式和JSONP格式的響應,下面使用命令行測試:

[tabalt@localhost ipqueryd] curl "http://127.0.0.1:12101/find?ip=1.1.8.1"
{"data":["廣東省電信"]}

[tabalt@localhost ipqueryd] curl "http://127.0.0.1:12101/find?ip=1.1.8.1&_callback=showip"
showip({"data":["廣東省電信"]});

GRPC接口

GRPC接口需要使用以你熟悉的語言編寫客戶端,下面的代碼是Golang中的簡單使用:

package main

import (
    "log"

    "golang.org/x/net/context"
    "google.golang.org/grpc"

    "pb"
)

func main() {
    conn, err := grpc.Dial("127.0.0.1:12102", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()

    iqc := pb.NewIpQueryClient(conn)

    ip := "1.1.8.1"

    r, err := iqc.Find(context.Background(), &pb.IpFindRequest{Ip: ip})
    if err != nil {
        log.Fatalf("could not find: %v", err)
    }
    log.Printf("ip data: %s", r.Data)
}

更多內容等你來發現和貢獻!

 

來自:http://tabalt.net/blog/ipquery-server-by-golang/

 

 本文由用戶 LatiaFaerbe 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!