IP数据库生成器
代碼地址如下:
http://www.demodashi.com/demo/12688.html
項目放在github上,python版本ipdb_creator,java版本ip-locator。
項目代碼結(jié)構(gòu)
項目結(jié)構(gòu)圖
IP數(shù)據(jù)庫生成
首先要知道IP的分配是一直變化的,所以不會存在絕對準確的IP庫。IP庫需要經(jīng)常更新才能保證較高的準確度。IP的分配由國際非盈利性組織ICANN負責,所以要生成最新的IP庫首先需要從這里下載5個最新原始分配文件,分別是delegated-arin-latest delegated-ripencc-latest delegated-lacnic-latest delegated-afrinic-latest delegated-apnic-latest。
我們需要處理的是文件中ipv4的記錄,每條記錄的格式如下:
apnic|AU|ipv4|1.0.0.0|256|20110811|assigned- AU: 表示澳大利亞的簡稱
- ipv4: 表示記錄的ip類型
- 1.0.0.0: 表示記錄的起始IP
- 256: 表示記錄從起始IP往后256個地址
- 20110811: 表示分配時間
- assigned(allocated): 表示已分配
國家縮寫與名字的對應(yīng)關(guān)系,可以直接看python項目中的country_code文件。在大部分應(yīng)用場景下,國內(nèi)IP需要精確到省或者市級別,國外IP大部分只需要精確到國家級別。那怎么才能得到比較準確的國內(nèi)IP庫呢?
現(xiàn)在網(wǎng)上有很多免費的IP查詢工具,有的比較友好提供了HTTP的查詢接口。經(jīng)過長時間的查詢對比發(fā)現(xiàn),其中IP淘寶和IPIP.NET的準確率相對比較高。為了得到最全面的數(shù)據(jù),我把delegated-apnic-latest中分配給CN的所有記錄拿出來,然后對每條記錄中的每個24網(wǎng)段進行掃描,最后把得到的中國全部24網(wǎng)段IP地址進行合并,就得到了國內(nèi)IP庫。對于IP分配中一些沒有指明國家碼的記錄也可以用同樣地方法。
要注意,大部分免費提供的IP查詢接口都是對頻率有限制的,如上面說的兩個都是限制每個來源IP每秒10次的頻率。
CN記錄拆分為24網(wǎng)段
以一條記錄為例 apnic|CN|ipv4|1.0.8.0|2048|20110412|allocated,把記錄轉(zhuǎn)換成CIDR格式1.0.8.0/21,以java為例:
String[] params = line.split("\\|"); // do filter ...String baseIP = params[3];int masklen = 32 - (int) (log(Integer.parseInt(params[4]), 2));String netcidr = baseIP + "/" + masklen;if (masklen > 24) masklen = 24;IPv4Network networks = new IPv4Network(prefix);for (String subnet : networks.getSubnet(24)) {// query ...}可以看到關(guān)鍵的方法就是getSubnet(24),簡單地說就是,從起始地址開始,每隔256個IP截斷,最后就得到了對應(yīng)的24網(wǎng)段列表。來看它的實現(xiàn):
public List<String> getSubnet(int masklen) {if (masklen > 32 || masklen < 8 || masklen < numericCIDR) {throw new NumberFormatException("masklen can not be greater than 32");}int numberOfIPs = 1 << (32 - masklen);Long startIP = baseIPnumeric & netmaskNumeric;List<String> list = new ArrayList<String>();for (int i=0; i<Math.pow(2, masklen-numericCIDR); i++) {String subnet = IPUtil.ipLong2String(startIP) + "/" + masklen;startIP += numberOfIPs;list.add(subnet);}return list;}查詢及頻率限制
以淘寶IP查詢?yōu)槔?#xff0c;接口可以在瀏覽器輸入 http://ip.taobao.com/service/getIpInfo.php?ip=1.0.8.1 查看返回的結(jié)果,返回結(jié)果為json格式,
private IpData queryFromTaobao(String ip) throws Exception {limitRate.check();String ret = HttpClientPool.getInstance().getMethod(TAOBAO_URL + "?ip=" + ip, 5000);if (ret == null) {return null;} else {JSONObject json = JSON.parseObject(ret);if (json.getInteger("code") == 0) {JSONObject dataJson = json.getJSONObject("data");IpData ipData = new IpData();ipData.setCountry(dataJson.getString("country"));ipData.setProvince(dataJson.getString("region"));ipData.setCity(dataJson.getString("city"));ipData.setIsp(dataJson.getString("isp"));ipData.setIp(ip);return ipData;} else {return null;}}}其中LimitRate是本地實現(xiàn)的一個簡單頻率控制,通過Queue
public void check() throws InterruptedException {if (queue.size() < limit)return;Long first = queue.peek();if (first == null)return;long now = System.currentTimeMillis();if (now - first <= duration) {logger.info("limit rate checked, sleep a while");Thread.sleep(duration - now + first + 1);}queue.offer(now);}雖然對查詢頻率做了限制,但這并不保證接口的每一次查詢都能正確返回結(jié)果,所以查詢結(jié)果無效時應(yīng)該重新查詢,直到得到有效結(jié)果為止。
IP網(wǎng)段合并
最后需要對掃描的結(jié)果進行合并,由于掃描時全部拆分成24網(wǎng)段,而IP的分配又是不連續(xù)的,所以合并的時候要仔細,不要出錯。首先要對掃描結(jié)果按IP排序,然后依次取出每一條結(jié)果,如果第n條與第n-1條的結(jié)果是相同的,則存入臨時隊列,直到當n與n-1的結(jié)果不同,這時把臨時隊列中的數(shù)據(jù)進行合并,合并結(jié)果存入最終的輸出隊列,并清空臨時隊列,循環(huán)此過程,最后就可以得到合并的結(jié)果。
以下面三條結(jié)果的合并為例:
(1) 首先對每一個網(wǎng)段的IP范圍,如1.0.1.0/24的IPRange是1.0.1.0~1.0.1.255對應(yīng)的long型范圍是16777472-16777727,
1.0.2.0/24對應(yīng)16777728-16777983,如果16777728 - 1 <= 16777727,則說明兩個網(wǎng)段是連續(xù)的,則合并成新的IPRange:16777472-16777983,以此類推,最后得到16777472-16778239(如果網(wǎng)段中存在不連續(xù)的情況,則會得到多個IPRange)。
(2) 接著處理得到的IPRange(s),先把IPRange轉(zhuǎn)換成能包含它本身的最小IP網(wǎng)段,16777472-16778239的startIP為16777472,endIP為16778239,n從1開始,n++直到滿足
$$endIP - 2^n <= startIP$$
$$endIP - 2^{n-1} > startIP$$
得到結(jié)果startIP/(32-n)轉(zhuǎn)換成可讀形式:1.0.0.0/22。
(3) 最后,由于合并后網(wǎng)段包含范圍超出了原本的三個網(wǎng)段,所以要對該結(jié)果再進行拆分。如果合并后的網(wǎng)段的起始IP小于合并前的起始IP,則以合并前的最小網(wǎng)段為界,把合并后網(wǎng)段拆分為小于,等于,大于合并前的最小網(wǎng)段的三個范圍(合并后的網(wǎng)段的最大IP大于合并前的最大IP情況,也同理可推),這里的實現(xiàn)稍微有點復(fù)雜,通過代碼來理解會比較容易一些,對應(yīng)方法為IPUtil.cidrPartition()。最后得到合并后的網(wǎng)段:
1.0.1.0/24;中國;福建省;福州市;電信;1.0.3.247;256 1.0.2.0/23;中國;福建省;福州市;電信;1.0.3.247;512IP數(shù)據(jù)庫使用
完整的數(shù)據(jù)庫已經(jīng)生成,那么如何使用它呢?
RadixTree
RadixTree(基樹)是通用的字典類型數(shù)據(jù)結(jié)構(gòu),在Linux內(nèi)核及Nginx中被用于路由表的設(shè)計。RadixTree與傳統(tǒng)的二叉樹差不多,只是在尋找方式上,利用比如一個unsigned int的類型的每一個比特位作為樹節(jié)點的判斷。比如一個數(shù) 10001010101010100101010100101010按照Radix樹的插入就是在根節(jié)點,如果遇到0,就指向左節(jié)點,如果遇到1就指向右節(jié)點,在插入過程中構(gòu)造樹節(jié)點,在刪除過程中刪除樹節(jié)點。
插入
由于java中沒有無符號整型,為了能表示最大的ipv4,我們用long型的低32位代替。key為ip的主機字節(jié)序,mask為網(wǎng)段的子網(wǎng)掩碼,value為該網(wǎng)段的信息。以1.0.1.0/24為例,key=0x01000100,mask=0xFFFFFF00。從最高位開始,判斷key的每一個位,1則前往右節(jié)點,0則前往左節(jié)點。如果當前節(jié)點不存在,則創(chuàng)建新的節(jié)點。
public void put(long key, long mask, IpData value) {long bit = 0x80000000L; // 128.0.0.0int node = ROOT_PTR;int next = ROOT_PTR;// 從最高位開始,判斷key的每一個位,1則前往右節(jié)點,0則前往左節(jié)點while ((bit & mask) != 0) { next = ((key & bit) != 0) ? rights[node] : lefts[node]; if (next == NULL_PTR) // 節(jié)點不存在,跳出循環(huán)break;bit >>= 1; node = next;}if (next != NULL_PTR) {// next不為NULL,是因bit&mask為0,也就是已經(jīng)判斷過key的最后一位,而退出上面的while的,則覆蓋當前節(jié)點的值values[node] = value;return;}while ((bit & mask) != 0) {if (size == allocatedSize)expandAllocatedSize();next = size; // 新增一個空節(jié)點values[next] = NO_VALUE;rights[next] = NULL_PTR;lefts[next] = NULL_PTR;if ((key & bit) != 0) {rights[node] = next;} else {lefts[node] = next;}bit >>= 1;node = next;size++;}values[node] = value; // 最后走完key的所有位,到達目標節(jié)點,存入value}查找
如果明白插入的原理,那么查找就比較簡單了。給定一個ip,首先將ip地址轉(zhuǎn)換成主機字節(jié)序的四個字節(jié),從32位的key的最高位開始,0就轉(zhuǎn)向左節(jié)點,1就轉(zhuǎn)向右節(jié)點,這樣從樹的根節(jié)點開始,直到找到對應(yīng)的葉子節(jié)點為止,在此查找路徑上最后一個值不為NO_VALUE的node的value就是查找的結(jié)果。
public IpData selectValue(long key) {long bit = 0x80000000L;IpData value = NO_VALUE;int node = ROOT_PTR;while (node != NULL_PTR) {if (values[node] != NO_VALUE)value = values[node];node = ((key & bit) != 0) ? rights[node] : lefts[node];bit >>= 1;}return value;}結(jié)束
為了省點買IP付費數(shù)據(jù)庫的錢,也不容易啊。方案還在進一步完善中,目前由于是單臺機器,在1秒10次的頻率限制下,完整跑一次需要的時間較長,正在考慮設(shè)置代理請求,加快查詢頻率,如果出口IP夠多的話,可以大幅提高速度。
代碼地址如下:
http://www.demodashi.com/demo/12688.html
注:本文著作權(quán)歸作者,由demo大師代發(fā),拒絕轉(zhuǎn)載,轉(zhuǎn)載需要作者授權(quán)
總結(jié)
- 上一篇: 我的嵌入式软硬件学习(三)
- 下一篇: 智能合约从入门到精通:JIDE集成开发工