IP数据库生成器
代碼地址如下:
http://www.demodashi.com/demo/12688.html
項目放在github上,python版本ipdb_creator,java版本ip-locator。
項目代碼結構
項目結構圖
IP數據庫生成
首先要知道IP的分配是一直變化的,所以不會存在絕對準確的IP庫。IP庫需要經常更新才能保證較高的準確度。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): 表示已分配
國家縮寫與名字的對應關系,可以直接看python項目中的country_code文件。在大部分應用場景下,國內IP需要精確到省或者市級別,國外IP大部分只需要精確到國家級別。那怎么才能得到比較準確的國內IP庫呢?
現在網上有很多免費的IP查詢工具,有的比較友好提供了HTTP的查詢接口。經過長時間的查詢對比發現,其中IP淘寶和IPIP.NET的準確率相對比較高。為了得到最全面的數據,我把delegated-apnic-latest中分配給CN的所有記錄拿出來,然后對每條記錄中的每個24網段進行掃描,最后把得到的中國全部24網段IP地址進行合并,就得到了國內IP庫。對于IP分配中一些沒有指明國家碼的記錄也可以用同樣地方法。
要注意,大部分免費提供的IP查詢接口都是對頻率有限制的,如上面說的兩個都是限制每個來源IP每秒10次的頻率。
CN記錄拆分為24網段
以一條記錄為例 apnic|CN|ipv4|1.0.8.0|2048|20110412|allocated,把記錄轉換成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 ...}可以看到關鍵的方法就是getSubnet(24),簡單地說就是,從起始地址開始,每隔256個IP截斷,最后就得到了對應的24網段列表。來看它的實現:
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查詢為例,接口可以在瀏覽器輸入 http://ip.taobao.com/service/getIpInfo.php?ip=1.0.8.1 查看返回的結果,返回結果為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是本地實現的一個簡單頻率控制,通過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);}雖然對查詢頻率做了限制,但這并不保證接口的每一次查詢都能正確返回結果,所以查詢結果無效時應該重新查詢,直到得到有效結果為止。
IP網段合并
最后需要對掃描的結果進行合并,由于掃描時全部拆分成24網段,而IP的分配又是不連續的,所以合并的時候要仔細,不要出錯。首先要對掃描結果按IP排序,然后依次取出每一條結果,如果第n條與第n-1條的結果是相同的,則存入臨時隊列,直到當n與n-1的結果不同,這時把臨時隊列中的數據進行合并,合并結果存入最終的輸出隊列,并清空臨時隊列,循環此過程,最后就可以得到合并的結果。
以下面三條結果的合并為例:
(1) 首先對每一個網段的IP范圍,如1.0.1.0/24的IPRange是1.0.1.0~1.0.1.255對應的long型范圍是16777472-16777727,
1.0.2.0/24對應16777728-16777983,如果16777728 - 1 <= 16777727,則說明兩個網段是連續的,則合并成新的IPRange:16777472-16777983,以此類推,最后得到16777472-16778239(如果網段中存在不連續的情況,則會得到多個IPRange)。
(2) 接著處理得到的IPRange(s),先把IPRange轉換成能包含它本身的最小IP網段,16777472-16778239的startIP為16777472,endIP為16778239,n從1開始,n++直到滿足
$$endIP - 2^n <= startIP$$
$$endIP - 2^{n-1} > startIP$$
得到結果startIP/(32-n)轉換成可讀形式:1.0.0.0/22。
(3) 最后,由于合并后網段包含范圍超出了原本的三個網段,所以要對該結果再進行拆分。如果合并后的網段的起始IP小于合并前的起始IP,則以合并前的最小網段為界,把合并后網段拆分為小于,等于,大于合并前的最小網段的三個范圍(合并后的網段的最大IP大于合并前的最大IP情況,也同理可推),這里的實現稍微有點復雜,通過代碼來理解會比較容易一些,對應方法為IPUtil.cidrPartition()。最后得到合并后的網段:
1.0.1.0/24;中國;福建省;福州市;電信;1.0.3.247;256 1.0.2.0/23;中國;福建省;福州市;電信;1.0.3.247;512IP數據庫使用
完整的數據庫已經生成,那么如何使用它呢?
RadixTree
RadixTree(基樹)是通用的字典類型數據結構,在Linux內核及Nginx中被用于路由表的設計。RadixTree與傳統的二叉樹差不多,只是在尋找方式上,利用比如一個unsigned int的類型的每一個比特位作為樹節點的判斷。比如一個數 10001010101010100101010100101010按照Radix樹的插入就是在根節點,如果遇到0,就指向左節點,如果遇到1就指向右節點,在插入過程中構造樹節點,在刪除過程中刪除樹節點。
插入
由于java中沒有無符號整型,為了能表示最大的ipv4,我們用long型的低32位代替。key為ip的主機字節序,mask為網段的子網掩碼,value為該網段的信息。以1.0.1.0/24為例,key=0x01000100,mask=0xFFFFFF00。從最高位開始,判斷key的每一個位,1則前往右節點,0則前往左節點。如果當前節點不存在,則創建新的節點。
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則前往右節點,0則前往左節點while ((bit & mask) != 0) { next = ((key & bit) != 0) ? rights[node] : lefts[node]; if (next == NULL_PTR) // 節點不存在,跳出循環break;bit >>= 1; node = next;}if (next != NULL_PTR) {// next不為NULL,是因bit&mask為0,也就是已經判斷過key的最后一位,而退出上面的while的,則覆蓋當前節點的值values[node] = value;return;}while ((bit & mask) != 0) {if (size == allocatedSize)expandAllocatedSize();next = size; // 新增一個空節點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的所有位,到達目標節點,存入value}查找
如果明白插入的原理,那么查找就比較簡單了。給定一個ip,首先將ip地址轉換成主機字節序的四個字節,從32位的key的最高位開始,0就轉向左節點,1就轉向右節點,這樣從樹的根節點開始,直到找到對應的葉子節點為止,在此查找路徑上最后一個值不為NO_VALUE的node的value就是查找的結果。
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;}結束
為了省點買IP付費數據庫的錢,也不容易啊。方案還在進一步完善中,目前由于是單臺機器,在1秒10次的頻率限制下,完整跑一次需要的時間較長,正在考慮設置代理請求,加快查詢頻率,如果出口IP夠多的話,可以大幅提高速度。
代碼地址如下:
http://www.demodashi.com/demo/12688.html
注:本文著作權歸作者,由demo大師代發,拒絕轉載,轉載需要作者授權
總結
- 上一篇: 我的嵌入式软硬件学习(三)
- 下一篇: wamp mysql 启动 mysql_