新來個技術(shù)總監(jiān)要我做一個 IP 屬地功能~
點擊上方“碼農(nóng)突圍”,馬上關(guān)注 這里是碼農(nóng)充電第一站,回復(fù)“666”,獲取一份專屬大禮包 真愛,請設(shè)置“星標(biāo)”或點個“在看”
文章來源:juejin.cn/post/7118954784853327903
背景
HttpServletRequest 獲取 IP
Ip2region
Ip2region V2.0 特性
ip2region xdb java 查詢客戶端實現(xiàn)
IDEA 中做個測試
背景
細(xì)心的朋友應(yīng)該會發(fā)現(xiàn),最近,繼新浪微博之后,頭條、騰訊、抖音、知乎、快手、小紅書等各大平臺陸陸續(xù)續(xù)都上線了“網(wǎng)絡(luò)用戶 IP 地址顯示功能”,境外用戶顯示的是國家,國內(nèi)的用戶顯示的省份,而且此項顯示無法關(guān)閉,歸屬地強(qiáng)制顯示。
作為技術(shù)人,那!這個功能要怎么實現(xiàn)呢?
HttpServletRequest 獲取 IP
下面,我就來講講,Java 中是如何獲取 IP 屬地的,主要分為以下幾步:
通過 HttpServletRequest 對象,獲取用戶的 「IP」 地址 通過 IP 地址,獲取對應(yīng)的省份、城市
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
/**
* 常用獲取客戶端信息的工具
*/
public class NetworkUtil {
/**
* 獲取ip地址
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 本機(jī)訪問
if ("localhost".equalsIgnoreCase(ip) || "127.0.0.1".equalsIgnoreCase(ip) || "0:0:0:0:0:0:0:1".equalsIgnoreCase(ip)){
// 根據(jù)網(wǎng)卡取本機(jī)配置的IP
InetAddress inet;
try {
inet = InetAddress.getLocalHost();
ip = inet.getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
// 對于通過多個代理的情況,第一個IP為客戶端真實IP,多個IP按照','分割
if (null != ip && ip.length() > 15) {
if (ip.indexOf(",") > 15) {
ip = ip.substring(0, ip.indexOf(","));
}
}
return ip;
}
/**
* 獲取mac地址
*/
public static String getMacAddress() throws Exception {
// 取mac地址
byte[] macAddressBytes = NetworkInterface.getByInetAddress(InetAddress.getLocalHost()).getHardwareAddress();
// 下面代碼是把mac地址拼裝成String
StringBuilder sb = new StringBuilder();
for (int i = 0; i < macAddressBytes.length; i++) {
if (i != 0) {
sb.append("-");
}
// mac[i] & 0xFF 是為了把byte轉(zhuǎn)化為正整數(shù)
String s = Integer.toHexString(macAddressBytes[i] & 0xFF);
sb.append(s.length() == 1 ? 0 + s : s);
}
return sb.toString().trim().toUpperCase();
}
}
通過此方法,從請求 Header 中獲取到用戶的 IP 地址。
https://ip.taobao.com/




可以看到日志 log 文件中,大量的 the request over max qps for user 問題。

Ip2region
下面,給大家介紹下之前在 Github 沖浪時發(fā)現(xiàn)的今天的主角:Ip2region 開源項目。
https://github.com/lionsoul2014/ip2region
目前最新已更新到了 v2.0 版本,ip2region v2.0 是一個離線 IP 地址定位庫和 IP 定位數(shù)據(jù)管理框架,10 微秒級別的查詢效率,準(zhǔn)提供了眾多主流編程語言的 xdb 數(shù)據(jù)生成和查詢客戶端實現(xiàn)。
①99.9% 準(zhǔn)確率
數(shù)據(jù)聚合了一些知名 ip 到地名查詢提供商的數(shù)據(jù),這些是他們官方的的準(zhǔn)確率,經(jīng)測試著實比經(jīng)典的純真 IP 定位準(zhǔn)確一些。
ip2region 的數(shù)據(jù)聚合自以下服務(wù)商的開放 API 或者數(shù)據(jù)(升級程序每秒請求次數(shù) 2 到 4 次):
01,>80%,淘寶IP地址庫,http://ip.taobao.com/%5C
02,≈10%,GeoIP,https://geoip.com/%5C
03,≈2%,純真 IP 庫,http://www.cz88.net/%5C
備注:如果上述開放 API 或者數(shù)據(jù)都不給開放數(shù)據(jù)時 ip2region 將停止數(shù)據(jù)的更新服務(wù)。
②多查詢客戶端的支持

Ip2region V2.0 特性
①標(biāo)準(zhǔn)化的數(shù)據(jù)格式
每個 ip 數(shù)據(jù)段的 region 信息都固定了格式:國家|區(qū)域|省份|城市|ISP,只有中國的數(shù)據(jù)絕大部分精確到了城市,其他國家部分?jǐn)?shù)據(jù)只能定位到國家,后前的選項全部是 0。
②數(shù)據(jù)去重和壓縮
xdb 格式生成程序會自動去重和壓縮部分?jǐn)?shù)據(jù),默認(rèn)的全部 IP 數(shù)據(jù),生成的 ip2region.xdb 數(shù)據(jù)庫是 11MiB,隨著數(shù)據(jù)的詳細(xì)度增加數(shù)據(jù)庫的大小也慢慢增大。
③極速查詢響應(yīng)
即使是完全基于 xdb 文件的查詢,單次查詢響應(yīng)時間在十微秒級別。
可通過如下兩種方式開啟內(nèi)存加速查詢:
vIndex 索引緩存:使用固定的 512KiB 的內(nèi)存空間緩存 vector index 數(shù)據(jù),減少一次 IO 磁盤操作,保持平均查詢效率穩(wěn)定在 10-20 微秒之間。 xdb 整個文件緩存:將整個 xdb 文件全部加載到內(nèi)存,內(nèi)存占用等同于 xdb 文件大小,無磁盤 IO 操作,保持微秒級別的查詢效率。
④極速查詢響應(yīng)
v2.0 格式的 xdb 支持億級別的 IP 數(shù)據(jù)段行數(shù),region 信息也可以完全自定義,例如:你可以在 region 中追加特定業(yè)務(wù)需求的數(shù)據(jù),例如:GPS信息/國際統(tǒng)一地域信息編碼/郵編等。也就是你完全可以使用 ip2region 來管理你自己的 IP 定位數(shù)據(jù)。
ip2region xdb java 查詢客戶端實現(xiàn)
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.6.4</version>
</dependency>
代碼如下:
import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;
public class SearcherTest {
public static void main(String[] args) {
// 1、創(chuàng)建 searcher 對象
String dbPath = "ip2region.xdb file path";
Searcher searcher = null;
try {
searcher = Searcher.newWithFileOnly(dbPath);
} catch (IOException e) {
System.out.printf("failed to create searcher with `%s`: %s\n", dbPath, e);
return;
}
// 2、查詢
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ip, e);
}
// 3、備注:并發(fā)使用,每個線程需要創(chuàng)建一個獨立的 searcher 對象單獨使用。
}
}
import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;
public class SearcherTest {
public static void main(String[] args) {
String dbPath = "ip2region.xdb file path";
// 1、從 dbPath 中預(yù)先加載 VectorIndex 緩存,并且把這個得到的數(shù)據(jù)作為全局變量,后續(xù)反復(fù)使用。
byte[] vIndex;
try {
vIndex = Searcher.loadVectorIndexFromFile(dbPath);
} catch (Exception e) {
System.out.printf("failed to load vector index from `%s`: %s\n", dbPath, e);
return;
}
// 2、使用全局的 vIndex 創(chuàng)建帶 VectorIndex 緩存的查詢對象。
Searcher searcher;
try {
searcher = Searcher.newWithVectorIndex(dbPath, vIndex);
} catch (Exception e) {
System.out.printf("failed to create vectorIndex cached searcher with `%s`: %s\n", dbPath, e);
return;
}
// 3、查詢
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ip, e);
}
// 備注:每個線程需要單獨創(chuàng)建一個獨立的 Searcher 對象,但是都共享全局的制度 vIndex 緩存。
}
}
import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;
public class SearcherTest {
public static void main(String[] args) {
String dbPath = "ip2region.xdb file path";
// 1、從 dbPath 加載整個 xdb 到內(nèi)存。
byte[] cBuff;
try {
cBuff = Searcher.loadContentFromFile(dbPath);
} catch (Exception e) {
System.out.printf("failed to load content from `%s`: %s\n", dbPath, e);
return;
}
// 2、使用上述的 cBuff 創(chuàng)建一個完全基于內(nèi)存的查詢對象。
Searcher searcher;
try {
searcher = Searcher.newWithBuffer(cBuff);
} catch (Exception e) {
System.out.printf("failed to create content cached searcher: %s\n", e);
return;
}
// 3、查詢
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ip, e);
}
// 備注:并發(fā)使用,用整個 xdb 數(shù)據(jù)緩存創(chuàng)建的查詢對象可以安全的用于并發(fā),也就是你可以把這個 searcher 對象做成全局對象去跨線程訪問。
}
}
IDEA 中做個測試

ip 屬地國內(nèi)的話,會展示省份,國外的話,只會展示國家。可以通過如下圖這個方法進(jìn)行進(jìn)一步封裝,得到獲取 IP 屬地的信息。

②編譯測試程序
# cd 到 java binding 的根目錄
cd binding/java/
mvn compile package
然后會在當(dāng)前目錄的 target 目錄下得到一個 ip2region-{version}.jar 的打包文件。
③查詢測試
? java git:(v2.0_xdb) ? java -jar target/ip2region-2.6.0.jar search
java -jar ip2region-{version}.jar search [command options]
options:
--db string ip2region binary xdb file path
--cache-policy string cache policy: file/vectorIndex/content
? java git:(v2.0_xdb) ? java -jar target/ip2region-2.6.0.jar search --db=../../data/ip2region.xdb
ip2region xdb searcher test program, cachePolicy: vectorIndex
type 'quit' to exit
ip2region>> 1.2.3.4
{region: 美國|0|華盛頓|0|谷歌, ioCount: 7, took: 82 μs}
ip2region>>
輸入 ip 即可進(jìn)行查詢測試,也可以分別設(shè)置 cache-policy 為 file/vectorIndex/content 來測試三種不同緩存實現(xiàn)的查詢效果。
④bench 測試
? java git:(v2.0_xdb) ? java -jar target/ip2region-2.6.0.jar bench
java -jar ip2region-{version}.jar bench [command options]
options:
--db string ip2region binary xdb file path
--src string source ip text file path
--cache-policy string cache policy: file/vectorIndex/content
? java git:(v2.0_xdb) ? java -jar target/ip2region-2.6.0.jar bench --db=../../data/ip2region.xdb --src=../../data/ip.merge.txt
Bench finished, {cachePolicy: vectorIndex, total: 3417955, took: 8s, cost: 2 μs/op}
可以通過分別設(shè)置 cache-policy 為 file/vectorIndex/content 來測試三種不同緩存實現(xiàn)的效果。
@Note:注意 bench 使用的 src 文件要是生成對應(yīng) xdb 文件相同的源文件。
“到這里獲取用戶 IP 屬地已經(jīng)完成啦,這篇文章介紹的 v2.0 版本,有興趣的小伙伴可以登錄上門的 github 地址了解下 v1.0 版本。
(完)
碼農(nóng)突圍資料鏈接
1、臥槽!字節(jié)跳動《算法中文手冊》火了,完整版 PDF 開放下載!
2、計算機(jī)基礎(chǔ)知識總結(jié)與操作系統(tǒng) PDF 下載
3、艾瑪,終于來了!《LeetCode Java版題解》.PDF
4、Github 10K+,《LeetCode刷題C/C++版答案》出爐.PDF歡迎添加魚哥個人微信:smartfish2020,進(jìn)粉絲群或圍觀朋友圈。
