你知道抖音的IP归属地是怎么实现的吗 抖音ip属地原理

1.背景最近刷抖音发现上线了 IP 属地的功能,小伙伴在发表动态、发表评论以及聊天的时候 , 都会显示自己的 IP 属地信息,其核心意义是让用户更具有真实性,减少虚假欺骗事件 。正好最近本人开发获取客户端ip , 做一些接口限流,黑白名单等需求功能,顺路就研究了一下怎么解析IP获取归属地问题 。
接下来,就着重讲解一下Java后端怎么实现IP归属地的功能 , 其实只需要以下两大步骤:
2.获取客户端ip接口做过web开发都知道,无论移动端还是pc端的请求接口都会被封装成为一个HttpServletRequest对象,该对象包含了客户端请求信息包括请求的地址,请求的参数 , 提交的数据等等 。
如果服务器直接把IP暴露出去,那么request.getRemoteAddr()就能拿到客户端ip 。
但目前流行的架构中,基本上服务器都不会直接把自己的ip暴露出去,一般前面还有一层或多层反向代理,常见的nginx居多 。加了代理后,相当于服务器和客户端中间还有一层,这时·request.getRemoteAddr()拿到的就是代理服务器的ip了 , 并不是客户端的ip 。所以这种情况下 , 一般会在转发头上加X-Forwarded-For等信息,用来跟踪原始客户端的ip 。
X-Forwarded-For: 这是一个 Squid 开发的字段,只有在通过了HTTP代理或者负载均衡服务器时才会添加该项 。格式为X-Forwarded-For:client1,proxy1,proxy2,一般情况下,第一个ip为客户端真实ip,后面的为经过的代理服务器ip 。上面的代码注释也说的很清楚,直接截取拿到第一个ip 。Proxy-Client-IP/WL- Proxy-Client-IP: 这个一般是经过apache http服务器的请求才会有,用apache http做代理时一般会加上Proxy-Client-IP请求头 , 而WL-Proxy-Client-IP是他的weblogic插件加上的头 。这种情况也是直接能拿到 。HTTP_CLIENT_IP: 有些代理服务器也会加上此请求头 。X-Real-IP: nginx一般用这个 。
但是在日常开发中,并没有规范规定用以上哪一个头信息去跟踪客户端,所以都有可能,只能一一尝试,直到获取到为止 。代码如下:
【你知道抖音的IP归属地是怎么实现的吗 抖音ip属地原理】@Slf4jpublic class IpUtils {   private static final String UNKNOWN_VALUE = "https://www.44400.cn/unknown";   private static final String LOCALHOST_V4 = "127.0.0.1";   private static final String LOCALHOST_V6 = "0:0:0:0:0:0:0:1";?   private static final String X_FORWARDED_FOR = "X-Forwarded-For";   private static final String X_REAL_IP = "X-Real-IP";   private static final String PROXY_CLIENT_IP = "Proxy-Client-IP";   private static final String WL_PROXY_CLIENT_IP = "WL-Proxy-Client-IP";   private static final String HTTP_CLIENT_IP = "HTTP_CLIENT_IP";?   private static final String IP_DATA_PATH = "/Users/shepherdmy/Desktop/ip2region.xdb";   private static  byte[] contentBuff;   /**    * 获取客户端ip地址    * @param request    * @return    */   public static String getRemoteHost(HttpServletRequest request) {       String ip = request.getHeader(X_FORWARDED_FOR);       if (StringUtils.isNotEmpty(ip) && !UNKNOWN_VALUE.equalsIgnoreCase(ip)) {           // 多次反向代理后会有多个ip值 , 第一个ip才是真实ip           int index = ip.indexOf(",");           if (index != -1) {               return ip.substring(0, index);         } else {               return ip;         }     }       ip = request.getHeader(X_REAL_IP);       if (StringUtils.isNotEmpty(ip) && !UNKNOWN_VALUE.equalsIgnoreCase(ip)) {           return ip;     }       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {           ip = request.getHeader(PROXY_CLIENT_IP);     }       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {           ip = request.getHeader(WL_PROXY_CLIENT_IP);     }       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {           ip = request.getRemoteAddr();     }?       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {           ip = request.getHeader(HTTP_CLIENT_IP);     }?       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {           ip = request.getRemoteAddr();     }       return ip.equals(LOCALHOST_V6) ? LOCALHOST_V4 : ip; }?}项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理 。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔 。严格控制包依赖和统一版本管理 , 做到最少化依赖 。注重代码规范和注释 , 非常适合个人学习和企业使用
Github地址:github.com/plasticene/…
Gitee地址:gitee.com/plasticene3…
3.获取ip归属地通过上面我们就能获取到客户端用户的ip地址,接下来就可以通过ip解析获取归属地了 。
如果我们在网上搜索资料教程,大部分都是说基于各大平台(eg:淘宝,新浪)提供的ip库进行查询,不过不难发现这些平台已经不怎么维护这个功能 , 现在处于“半死不活”的状态,根本不靠谱,当然有些平台提供可靠的获取ip属地接口,但是收费、收费、收费 。
本着作为一个程序员的严谨:“能白嫖的就白嫖,避免出现要买的是你,不会用也是你的尴尬遭遇” 。扯远了言归正传 , 为了寻求可靠有效的解决方案,只能去看看github有没有什么项目能满足需求,果然功夫不负有心人,发现一个宝藏级项目:ip2region,一个准确率 99.9% 的离线 IP 地址定位库,0.0x 毫秒级查询 , ip2region.db 数据库只有数 MB的项目,提供了众多主流编程语言的 xdb 数据生成和查询客户端实现,这里只能说:开源真香,开源万岁 。
3.1 Ip2region 特性标准化的数据格式
每个 ip 数据段的 region 信息都固定了格式:国家|区域|省份|城市|ISP,只有中国的数据绝大部分精确到了城市,其他国家部分数据只能定位到国家,其余选项全部是0 。
数据去重和压缩
xdb 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大 。
极速查询响应
即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:

  1. vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间 。
  2. xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率 。
IP 数据管理框架
v2.0 格式的 xdb 支持亿级别的 IP 数据段行数 , region 信息也可以完全自定义,例如:你可以在 region 中追加特定业务需求的数据 , 例如:GPS信息/国际统一地域信息编码/邮编等 。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据 。
99.9% 准确率
数据聚合了一些知名 ip 到地名查询提供商的数据,这些是他们官方的的准确率 , 经测试着实比经典的纯真 IP 定位准确一些 。
ip2region 的数据聚合自以下服务商的开放 API 或者数据(升级程序每秒请求次数 2 到 4 次):
  • 01,>80%,淘宝IP地址库 , ip.taobao.com/%5C
  • 02,≈10%,GeoIP,geoip.com/%5C
  • 03,≈2% , 纯真 IP 库 , www.cz88.net/%5C
备注:如果上述开放 API 或者数据都不给开放数据时 ip2region 将停止数据的更新服务 。
3.2 整合Ip2region客户端进行查询提供了众多主流编程语言的 xdb 数据生成和查询客户端实现 , 已经集成的客户端有:java、C#、php、c、python、nodejs、php扩展(php5 和 php7)、golang、rust、lua、lua_c , nginx 。这里讲一下java的客户端 。
首先我们需要引入依赖:
<dependency> <groupId>org.lionsoul</groupId> <artifactId>ip2region</artifactId> <version>2.6.5</version></dependency>接下来我们需要先去下载数据文件ip2region.xdb到本地,然后基于数据文件进行查询,下面查询方法文件路径改为你本地路径即可,ip2region提供三种查询方式:
完全基于文件的查询
import org.lionsoul.ip2region.xdb.Searcher;import java.io.*;import java.util.concurrent.TimeUnit;?public class SearcherTest {   public static void main(String[] args) {       // 1、创建 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、关闭资源       searcher.close();              // 备注:并发使用 , 每个线程需要创建一个独立的 searcher 对象单独使用 。 }}缓存 VectorIndex 索引
我们可以提前从 xdb 文件中加载出来 VectorIndex 数据,然后全局缓存,每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作 , 从而加速查询,减少 IO 压力 。
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 中预先加载 VectorIndex 缓存,并且把这个得到的数据作为全局变量,后续反复使用 。       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 创建带 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);     }              // 4、关闭资源       searcher.close();?       // 备注:每个线程需要单独创建一个独立的 Searcher 对象,但是都共享全局的制度 vIndex 缓存 。 }}缓存整个 xdb 数据
我们也可以预先加载整个 ip2region.xdb 的数据到内存,然后基于这个数据创建查询对象来实现完全基于文件的查询 , 类似之前的 memory search 。
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 到内存 。       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 创建一个完全基于内存的查询对象 。       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);     }              // 4、关闭资源 - 该 searcher 对象可以安全用于并发,等整个服务关闭的时候再关闭 searcher       // searcher.close();?       // 备注:并发使用 , 用整个 xdb 数据缓存创建的查询对象可以安全的用于并发 , 也就是你可以把这个 searcher 对象做成全局对象去跨线程访问 。 }}3.3 springboot整合示例首先我们也需要像上面一样引入maven依赖 。然后就可以基于上面的查询方式进行封装成工具类了,我这里选择了上面的第三种方式:缓存整个 xdb 数据
@Slf4jpublic class IpUtils {   private static final String IP_DATA_PATH = "/Users/shepherdmy/Desktop/ip2region.xdb";   private static  byte[] contentBuff;?   static {       try {           // 从 dbPath 加载整个 xdb 到内存 。           contentBuff = Searcher.loadContentFromFile(IP_DATA_PATH);     } catch (IOException e) {           e.printStackTrace();     } }      /**    * 根据ip查询归属地,固定格式:中国|0|浙江省|杭州市|电信    * @param ip    * @return    */   public static IpRegion getIpRegion(String ip) {       Searcher searcher = null;       IpRegion ipRegion = new IpRegion();       try {           searcher = Searcher.newWithBuffer(contentBuff);           String region = searcher.search(ip);           String[] info = StringUtils.split(region, "|");           ipRegion.setCountry(info[0]);           ipRegion.setArea(info[1]);           ipRegion.setProvince(info[2]);           ipRegion.setCity(info[3]);           ipRegion.setIsp(info[4]);     } catch (Exception e) {           log.error("get ip region error: ", e);     } finally {           if (searcher != null) {               try {                   searcher.close();             } catch (IOException e) {                   log.error("close searcher error:", e);             }         }     }       return ipRegion; }?}

    推荐阅读