网站如何判断客户端是在国内还是国外

有些网站需要根据用户的位置,自动选择不同的语言,或者根据不同的国家,设置不同的时间显示方式,又或者跳转到不同的服务器路由。对于这个问题,我们该如何实现呢?这里我们讲一些非付费的方式,土豪请自我忽略。

方法一:根据客户端的语言来判断

这种方法是利用用户使用的浏览器的语言偏好来判断,这个方法简单快速,但是这个显然是不准确的。

var language = navigator.language; //navigator 中的language表示客户端用户偏好的第一语言,而languages是浏览器的语言列表,如果是zh-CN,则表示是中文,而en-US则表示美国。

方法二:利用header 里的Accept-Language来判断

一般网页发送到服务器时,会携带一个Accept-Language的头部信息,它可以通过逗号分割来携带多国语言。第一个会是首选的语言,其它语言会携带一个“q”值,来表示用户对该语言的喜好程度(0~1)。 这个方法和上面的同样是不准确的。

string languages = HttpContext.Request.Headers["Accept-Language"]; C#获取

方法三:利用IP地址来判断

根据客户端的IP地址来确定是相对来说比较准确的方法。原理就是通过IP地址所在的区域确定国家信息,互联网构建时的IP地址区域分配是有一定的数据规划的,不同的区域给的IP地址会是一段一段的,通过这个段位数据的字典查询就能知道客户所在的区域了。这个也是有可能不准确,比如当通过VPN来访问时,或者代理访问时都有可能不准确,但相对来说,这种情况少见,也是可以接受的。

直接通过第三方服务来获取信息

比较强大的第三库,不得不推荐MaxMind的GeoIP®Databases and Services,他们有自己的IP库,提供各种准确的接口,付费的可以根据定位很准确,不付费的只可以模糊定位到国家,不过已经符合我们的需求。

javascript端的实现过程可以查看文档:

https://dev.maxmind.com/geoip/geolocate-an-ip/client-side-javascript

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div>
    目前所在:<span id="result"></span>
  </div>
  <script src="//geoip-js.com/js/apis/geoip2/v2.1/geoip2.js" type="text/javascript"></script>
  <script>
    var test = (function () {
      var onSuccess = function (geoipResponse) {
        /* There's no guarantee that a successful response object
         * has any particular property, so we need to code defensively. */
        if (!geoipResponse.country.iso_code) {
          return;
        }
        /* ISO country codes are in upper case. */
        var code = geoipResponse.country.iso_code.toLowerCase();
        document.getElementById('result').innerHTML = code;
      };
      var onError = function (error) {
      };
      return function () {
        geoip2.country(onSuccess, onError);
      };
    }());
    test();
  </script>
</body>

</html>

那如果是自己实现该怎么做呢?

关键是两个问题:一个是IP地址的获取 ,另外一个是IP区域数据库的获取。

IP地址的获取

IP地址在后端一般都有提供,如C# ASP.NET的可以通过如下的方式获取:

/// <summary>
/// 获取客户端IP地址
/// </summary>
/// <returns>若失败则返回回送地址</returns>
public static string GetIP()
{
    //如果客户端使用了代理服务器,则利用HTTP_X_FORWARDED_FOR找到客户端IP地址
    string userHostAddress = HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"].ToString().Split(',')[0].Trim();
    //否则直接读取REMOTE_ADDR获取客户端IP地址
    if (string.IsNullOrEmpty(userHostAddress))
    {
        userHostAddress = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];
    }
    //前两者均失败,则利用Request.UserHostAddress属性获取IP地址,但此时无法确定该IP是客户端IP还是代理IP
    if (string.IsNullOrEmpty(userHostAddress))
    {
        userHostAddress = HttpContext.Current.Request.UserHostAddress;
    }
    //最后判断获取是否成功,并检查IP地址的格式(检查其格式非常重要)
    if (!string.IsNullOrEmpty(userHostAddress) && IsIP(userHostAddress))
    {
        return userHostAddress;
    }
    return "127.0.0.1";
}

/// <summary>
/// 检查IP地址格式
/// </summary>
/// <param name="ip"></param>
/// <returns></returns>
public static bool IsIP(string ip)
{
    return System.Text.RegularExpressions.Regex.IsMatch(ip, @"^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$");
}
以上引用了 https://www.cnblogs.com/stay-foolish/archive/2012/05/01/2475071.html,里面提到了为何如此获取的原因,有相关的知识可以参考。里面还提供了进一步知识的文章:
https://community.broadcom.com/symantecenterprise/communities/community-home/librarydocuments/viewdocument?DocumentKey=9d18fc06-b229-4c4a-8ca5-7386d0870c01&CommunityKey=1ecf5f55-9545-44d6-b0f4-4e4a7f5f5e68&tab=librarydocuments

前端Javascript获取的话,需要通过一些提供该类服务的网站API来获取,具体可以查看文章:

https://www.delftstack.com/zh/howto/javascript/get-ip-address-javascript/

1.   https://api.ipify.org?format=json  //只返回地址信息
2.   https://ipinfo.io/json  //还会返回国家地区信息,但每天有次数限制
3.   https://ipgeolocation.abstractapi.com/v1/?api_key=<your_api_key>
对于上面的第2个API是可以直接拿到国家信息的,如果可以接受这个限制,那就可以直接通过这个来达到目的。

IP区域信息库的获取

IP的区域库信息可以在apnic的官方网站上可以下载 http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest

apnic|KR|ipv4|27.35.0.0|65536|20100324|allocated
apnic|CN|ipv4|27.36.0.0|262144|20100323|allocated
apnic|CN|ipv4|27.40.0.0|524288|20100323|allocated
apnic|IN|ipv4|27.48.0.0|65536|20100311|allocated
apnic|PH|ipv4|27.49.0.0|65536|20100311|allocated
apnic|PH|ipv4|27.50.0.0|1024|20100611|allocated
apnic|IN|ipv4|27.50.4.0|1024|20100311|allocated
apnic|AU|ipv4|27.50.8.0|1024|20160913|allocated
apnic|JP|ipv4|27.50.12.0|1024|20100317|allocated
apnic|ID|ipv4|27.50.16.0|4096|20100311|allocated
apnic|HK|ipv4|27.50.32.0|2048|20100316|allocated
apnic|CN|ipv4|27.50.40.0|2048|20110412|allocated
apnic|SG|ipv4|27.50.48.0|4096|20100317|allocated
apnic|AU|ipv4|27.50.64.0|8192|20100317|allocated
apnic|JP|ipv4|27.50.96.0|8192|20110304|allocated
apnic|CN|ipv4|27.50.128.0|32768|20100317|allocated
apnic|TW|ipv4|27.51.0.0|65536|20100312|allocated

在IP库中,所有的IP不分国家,按照IP的顺序从上而下排列着;

   apnic|CN|ipv4|222.126.128.0|32768|20060830|allocated
    -    -    -        -         -    
    |    |    |        |         |    
    |    |    |        |         +----- 代表着该IP段下有32768个地址
    |    |    |        +--------------- IP地址
    |    |    +------------------------ ipv4
    |    +----------------------------- CN代表中国
    +---------------------------------- 代表apnic
    
那么为什么`222.126.128.0`下有`32768`个地址呢,是怎么计算的呢?
那么我们需要根据下一个IP判断,如上图所示下一个IP是    
`apnic|PH|ipv4|222.127.0.0|32768|20060913|allocated`
PH是菲律宾,说明这个IP就是菲律宾了。

`apnic|CN|ipv4|222.126.128.0|32768|20060830|allocated`
`apnic|PH|ipv4|222.127.0.0|32768|20060913|allocated`

IP:
IP是Internet Protocol(网际互连协议)的缩写,是TCP/IP体系中的网络层协议;
`IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数`;
所有a,b,c,d都有2的32次方随机匹配地址数。
所以:
从`222.126.128.0`到`222.127.0.0`,有多少种可能?
`a相同,b=126到b=127地址数加1,c=128到c=0剩余128个选择,d有256种选择`
`计算公式=1*128*256=32768个地址`

我们还可以这么假设,
由于c*d=256*256=65536,当`apnic|CN|ipv4|222.160.0.0|131072|20031212|allocated`中`131072`大于65536说明,a.b.*.*也就是222.160.*.*无论都两个整数是什么,都属于中国IP; 并且由于131072=65536*2,说明b+1后的a.b+1.*.*也就是222.161.*.*全部是中国IP;
那么这个结果正确吗,让我们从IP库数据看下:
`apnic|CN|ipv4|222.160.0.0|131072|20031212|allocated`
`apnic|CN|ipv4|222.162.0.0|65536|20031212|allocated`
下一条数据222.162.0.0,所以我们的假设是正确的。虽然不知道为什么Apnic没有把222.162.0.0一并合并到222.160.0.0,但是我们的假设是正确的,第五位的数字代表着基于当前IP按正顺序下的地址数。

所以我们可以这么处理数据:
`首先判断第五位的数字,`

`如果第五位数字超过65536,` 
`代表a.b.*.*都是中国IP,如果是65536的N倍,那么代表着`
a.b.*.*
a.b+1.*.*
....
a.b+n-1.*.*
`全部都是中国IP,可以标记为ALL, 后续比较前两位就可以判断是否是中国IP`。
存储数据结构为:
{
  a: {
    b: "all",
    b+1: "all",
    ....
    b+n-1: "all"
  }
}

`如果第五位数字不超过65536呢?`
apnic|CN|ipv4|1.2.16.0|4096|20110412|allocated
apnic|CN|ipv4|1.2.32.0|8192|20110412|allocated
apnic|CN|ipv4|1.2.64.0|16384|20110412|allocated

IP转换十进制(a.b.c.d)= a*256^3+b*256^2+c*256+d
这里以当前IP转化为十进制为起始范围,加上第五位地址书为终止范围。
如果要判断的IP转化的十进制数后,判断是否在这个范围内,范围内则为中国IP;
并且同一个a.b,后面会有多端地址,如:
apnic|CN|ipv4|14.1.0.0|1024|20110414|allocated
apnic|JP|ipv4|14.1.4.0|1024|20100910|allocated
apnic|JP|ipv4|14.1.8.0|2048|20100910|allocated
apnic|AU|ipv4|14.1.16.0|1024|20100916|allocated
apnic|HK|ipv4|14.1.20.0|1024|20100920|allocated
apnic|CN|ipv4|14.1.24.0|1024|20151214|allocated
中间还掺杂着其他国家的IP,所以需要分段存储如下:
存储数据结构为:
{
  a: {
    b: [
        "16777472-16777728"
            "16777728-16778240"
            "16779264-16781312"
            "16785408-16793600"
    ]
    
  }
}

`全部数据存储在Redis里面,2小时有效期(再次读取自动生成的IP库更新数据),结构如下:`
{
  a: {
    b: "all",
    b+1: "all",
    ....
    b+n-1: "all",
    b: [
        "16777472-16777728"
            "16777728-16778240"
            "16779264-16781312"
            "16785408-16793600"
    ]
  }
}
那么如何根据以上的数据判断一个IP是否属于国内IP呢?
`首先根据a查询是否存在,再根据b查询是否存在;`
`如果b存在,如果是all, 直接返回true;`
`如果是数组,需要判断地址是否存在于数组中的某一个范围内,存在就返回true;`
`其他返回false;`

这样的话,查询效率变得快了很多。
每次被查询的IP结果也会被存储在redis中,下次查询同样的IP,可以直接返回结果。

从上面的信息可以看到,我们只要解析出中国大陆CN(还有澳门MO,台湾TW和香港HK)就可以判断是国内还是国外了。

补充一下关于如何判断的算法问题

对于此列表,我们可以将其解析为数值来判断即可,IP地址本身就是一个32位的数值,那对于列表中的数据,因为它本身是一个范围,我们需要两个数据来判断,当给定一个IP地址,将其转为数值,然后用减法运算即可算出,在语言里,用位运算,可轻易将IP转为数值:

function IpAddressToNumber(IPAddr)
{
    string[] IPAddrArr = IpAddr.split('.');
    int IpAddr_Number = 0;
    for(int i = 0;i < 4;i++){
        IpAddr_Number += (Convert.ToInt32(IPAddrArr[i]) << (4 - i - 1));
     }
}

我们使用同样的方法,对列表中的每一行解析一下(列表中给出了IP的起始值和增量值),得到两个数据,在给定一个IP时,将其转为数值后比较:

if (UserIpNumber - ListStartIpNumber < ListIncreNumber){
   //Match This List output Result
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值