https://github.com/miraclesu/ipipgit
工做中常常會有經過IP匹配用戶信息的需求,如肯定用戶所在的地區(國家/省份/城市)、運營商、時區、經緯度等等。前一陣有個Golang開發的項目也有這樣的需求,因而簡單實現了一個包,最近忙裏偷閒又包了一個支持HTTP和GRPC方式調用的服務,並開源在GitHub上了。本文主要介紹IP地址信息查詢的實現細節和使用方式。github
首先交代一下GitHub地址:golang
歡迎你們在項目中使用(已經過N億日PV服務的考驗),有任何問題或建議,請提交Issue反饋或Fork到本身名下修改後提交Pull Request。shell
IP數據文件存放IP地址段和數據信息的映射關係,是IP地址信息查詢中最重要的部分,格式上要求可擴展,數據上則須要準確甚至精確。真正意義上完美的IP數據文件是不存在的,而要想讓數據文件保持可用,須要按期對數據文件作維護更新。數據庫
IP數據文件一般是純文本形式的,映射關係按行存放,每行的各項之間用」\t」分隔,前面兩列是IP段的起始和結束點轉換成無符號32位整型的值(只考慮IPV4),後面的部分則是各項信息,可根據實際須要擴充;各行之間要求是按IP段從小到大升序排列的。示例格式以下:json
1033224192 1033228287 北京 北京 朝陽 聯通 1033228288 1033232383 北京 北京 海淀 聯通 1033232384 1033233407 北京 北京 昌平 聯通
IP信息數據主要有3個來源:數組
此外也能經過必定的技術或人工手段自行獲取維護IP信息數據庫,可是成本會很是高。安全
有了數據文件,要實現信息查詢並不難,簡單方式是直接將數據文件加載到內存數組中,查找時將IP地址轉換成無符號32位整型,而後用二分查找法查找整型所在的區間,找到後則返回對應的數據,沒找到則返回失敗。Golang中核心代碼以下:網絡
將IP地址字符串轉成無符號32位整型:curl
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 }
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包提供了較完善的單元測試,克隆代碼到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
如文章開頭所說,這個項目基於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接口支持返回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接口須要使用以你熟悉的語言編寫客戶端,下面的代碼是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/,轉載請註明。