有些网站需要根据用户的位置,自动选择不同的语言,或者根据不同的国家,设置不同的时间显示方式,又或者跳转到不同的服务器路由。对于这个问题,我们该如何实现呢?这里我们讲一些非付费的方式,土豪请自我忽略。
方法一:根据客户端的语言来判断
这种方法是利用用户使用的浏览器的语言偏好来判断,这个方法简单快速,但是这个显然是不准确的。
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
}
1967

被折叠的 条评论
为什么被折叠?



