解析純真IP位址庫

2022-05-07 04:45:08 字數 3540 閱讀 8633

一周以來,一直在做 ip位址庫的解析。從調研到編碼到優化,大概花了有七八天的時間。感覺很好玩。總結一下整個做的過程。

1、關於ip 位址庫的解析方式

目前主要的解析方式有兩種:通過api,或通過ip資料庫。

ip資料庫方式相對來講複雜一點,需要有完善的資料庫,還要建立相應的查詢服務。優缺點則跟api方式正好相反:優點是查詢快,不受網路和**的限制,缺點是編碼相對複雜,而且要一直維護資料庫。資料庫國內最著名的是純真網路,ipip,國外更加著名的geoip等等。

我們在權衡利弊之後,決定採取資料庫方式。聽說geoip對國外ip 資料很完善,但是對於國內的ip還是不太全的。因此,我們初步選用純真ip資料庫來解析。

2、儲存先普及個常識,那就是ip位址實際上是乙個unsigned int值。在群裡詢問做法的時候我發現很多人居然都不知道這一點。我們看到的ip位址,是4個0~255之間的數,而實際上在計算機中ip位址的表示是32位二進位制。 01010101.10101010.00110011.11001100醬嬸的。32位二進位制,當然就是乙個unsigned int的取值範圍。ip解析也是一樣,把ip轉化成int 進行儲存和查詢,是最節省空間、最效率的方法。

三個字段,start, end, address,從start到end之間的ip都是address這個位置。然而仔細觀察可以發現,end其實並沒有什麼卵用。因為end跟下乙個start是連線的,中間沒有斷開的ip。所以只需要記錄start: address就好了,所以——我們得到了乙個鍵值對。啊哈,關於鍵值對我們能用的**就多了,最典型的可以用redis這樣的資料庫或者直接用字典。

那麼查詢怎麼查?最簡單的方法,提取keys,順序排列,二分查詢。45萬條資料最多19次比較即可。注意這裡我們要找的是「小於等於給定ip的最大值」。

什麼?你說每一條ip對應乙個address ,把所有ip的address寫成乙個列表?唔……也不是不可以,不過首先你的伺服器得有200g記憶體。沒錯,200g。記憶體。

3、演算法演進

3.1、首先考慮redis

我需要保持程式一直執行,即需要乙個server,裡面是儲存好的位址結構,當我需要查詢一條ip的時候,只需要傳送一條請求即可。那麼,如果使字典保持在記憶體裡,就必須要程式一直執行,需要我寫乙個server。tcp還是udp還是http的無所謂。然而,我懶。所以首先考慮redis。畢竟人家儲存結構都寫好了,我都不用動腦筋,往裡存就好了。

然而事實證明我想錯了。

3.1.1、普通鍵值對

先用最簡單的方法,set ip addr,全部存進去;然後查詢的時候讀取keys,型別轉換,排序,二分,查到小於等於給定ip的最大值,行雲流水的一套下來——3.2秒。泥煤這速度還不如直接發api請求呢!仔細想想,自己確實是犯二了,40多萬數型別轉換再排序,能快就見鬼了。

3.1.2、有序集合

考慮下一方案,一要存整數,二要有序。存整數是不可能了,在網上查到redis中的資料型別,根本沒有數字相關的,只有字串和各種序列型別。經人介紹選定有序集合。zadd ip2addr ip addr新增好。然而查詢時候始終有錯誤。莫名其妙了好久,終於查到原因了:假設一條ip1對應位址是addr ,過不久一條ip2對應的位址也是addr,那麼ip2就會把ip1覆蓋掉。這不科學啊!唉,只能拋棄有序集合了。

(其實後來有大神查到還是可以用的,如果相同的addr會覆蓋,那就人為的讓它不同,例如可以儲存addr@ip這樣的形式。當時著急了,也沒多想想。)

3.1.3、列表

讓ip有序,最合適的還是列表。於是在redis裡面我建立了兩個列表,乙個是ip,另乙個是對應位置的addr。查詢時候先獲得ip列表,查到給定ip,用這個ip的索引去查詢對應位置的addr。願望是美好的,現實是殘酷的。由於redis中的列表採用的是雙向鍊錶,要獲取全部40多萬資料也是夠慢,這就造成了結果查詢一條資料要360ms。而且有個看似奇怪的特性:ip值較小的,比如1.2.3.4,查詢結果就4ms,而ip值大到222.222.222.222這樣的就要接近400ms了。

這仍然是乙個慢到不能忍的結果。

3.1.4、列表+分塊

列表做出來的結果大概在300ms多,還是太慢,我在redis裡面大概掃了一遍,沒有什麼更合適的資料結構了。那麼就只能進行演算法層面的優化了。觀察了一下ip位址的結構,前22萬條資料應該包含了前面一半ip,剩下的ip在後一半資料裡,試了試從ip列表裡只提取一半資料進行查詢,果然時間也縮到了一半,大概170ms。那麼,能否更精確的定位ip所在的位置?

想象一下42億個ip,分散在44萬條資料中,每塊裡面有多少個ip?肯定不是平均分布的,但是數量是可以統計的。我把int範圍切分,每10^7作為乙個塊,那麼42億多的int數可以切分出來430塊(例如ip值小於10^7放在第0區,小於2*10^7大於10^7放在第1區,等等),這樣就統計出來每個塊中ip的數量。下一步進行累加,算出前0塊共有多少個ip,前1塊有多少ip,前2塊共有多少ip……舉個栗子,統計ip數量的列表為[a1,a2,a3,a4...],那麼累加的列表為[a1,a1+a2, a1+a2+a3, a1+a2+a3+a4...]。這個列表即ip索引。這樣可以精確的定位ip。查詢的時候,先計算ip屬於哪個塊,然後找到對應的索引,最後通過索引來找到對應的ip範圍。雖然多查詢了一次,但是極大地縮減了從redis中取數的數量。經測試,速度已經達到了65ms左右。

然而這個演算法有兩個問題:首先是塊大小的設定,需要人為干預,塊大小涉及到每個塊裡的ip數量,還涉及到塊的數量,也就是索引列表的大小。這個完全靠經驗,沒有什麼理論。另乙個問題是塊中ip數量為0的情況。還用剛才的栗子,有個ip列表[a1,0,0,0,a3,a4...],索引列表為[a1,a1,a1,a1,a1+a3, a1+a3+a4...] ,也就是乙個ip在a1範圍內,而下一條ip已經在a3範圍內了。現在我查詢一條ip,本應查詢範圍是[a1, a1+a3],而現在查詢的範圍變成了[a1, a1],這樣必然結果錯誤。我也沒有太好的辦法解決,現在想到的只能是再記錄一下ip數量表,現查詢一下ip所在塊是不是0,如果是,就去找到在這之前第乙個不為0的塊。這樣效能肯定是要下降的。

3.2、記憶體

3.2.1、有序字典

碰到問題後,詢問了一下q群裡的大神們。幾個做過的人都是自己寫服務的。唉,本想偷懶,折騰了一圈反倒把自己坑了。於是自己寫socket來做。儲存結構為了保持整數和有序,使用ordereddict來儲存。像以前那樣拿到keys,二分,查詢,看眼時間,哭了,怎麼還是50ms?

3.2.2、字典+列表

繼續請教大神們,怎麼做的,得到的答案是用列表。恍然大悟。用dict.iteritems()這種形式的列表,既能保持字典的鍵值對形狀,又是有序的。ordereddict內部使用雙向鍊錶,當然怎麼算都是列表更快。在原來的基礎上簡單的改了改,重新測一下,1ms。1ms?!對比了一下ip庫,似乎結果並沒有錯。

那好吧,就到這裡了,這個問題就可以暫時告一段落了。從個人來講我覺得最有趣的是中間redis列表+分塊那個演算法,不能應用實在可惜,因為在後面的演算法中,主要瓶頸在於socket的傳輸速度,而不是列表資料的多少,單純查詢過程的速度已經達到了10^-5s的級別。遺留了幾個小問題吧,知道思路就好,反正也沒法用到最優解中去。

純真IP庫PHP查詢

class ip 檢查ip的合法性 public function checkip ip return true 讀取little endian編碼的4個位元組轉化為長整型數 public function getlong4 讀取little endian編碼的3個位元組轉化為長整型數 public...

IP位址解析

一 ip位址 internet依靠tcp ip協議,在全球範圍內實現不同硬體結構 不同作業系統 不同網路系統的互聯。在internet上,每乙個節點都依靠唯一的ip位址互相區分和相互聯絡。傳統的ip位址是乙個32位二進位制數的位址,也叫ipv4,由4個8位欄位組成。ipv6採用128位位址長度,8個...

用Python指令碼查詢純真IP庫

usr bin env python coding utf 8 用python指令碼查詢純真ip庫 qqwry.dat的格式如下 檔案頭 8位元組 記錄區 不定長 索引區 大小由檔案頭決定 檔案頭 4位元組開始索引偏移值 4位元組結尾索引偏移值 對於國家記錄,可以有三種表示方式 字串形式 ip記錄第...