IP地址在数据库中的存储姿势
发布时间:
最后更新:
本文为博主原创文章,发表于https://blog.kaciras.com/article/7/how-to-store-ip-address-in-database,未经许可禁止转载
在数据库中保存IP地址是一个很常见的需求,特别是在Web系统中用于保存访问记录。但除了一些功能复杂的数据库(例如PostgreSQL)以外,大多数据库并没有专门存储IP地址的数据类型,这就需要我们自己设计一个编码来保存IP地址。
而近年来,IPV6的普及也正式开始了,虽然目前大多都是IPV4地址,为了向前兼容,就要求数据库中的IP字段能够同时保存IPV4和IPV6。本站在开发时就遇到了这种需求,这里就简单研究下如何存储IP地址。
1.保存为字符串 #
IPV4能够表示为点分十进制格式,比如123.45.67.89
,四组数字在0-255之间,最大需要15个字符;而IPV6也有冒分十六进制表示法,比如ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
,最长39个字符。所以可以定义一个varchar(39)
类型的字段,直接将地址转成字符串存进去。
该方法优点在于简单直观,而且字符串格式是可读的,无需加工处理即可显示在前台界面。缺点很明显,浪费空间,而且两种格式不同无法进行比较,也就没法建索引(如果统一转成IPV6地址还是能比较的),所以一般不选择这种方案。
2.保存为整数 #
计算机中的任何信息都是二进制数,IP也不例外,所以就可以把它转换为整数类型存储,IPV4有4个字节,正好对应int
类型;而IPV6则有16个字节,一般的数据库字段类型没法存这么大的整数,不过可以把它分成两个bigint
或4个int
。
由于整数不能直接转换为IP地址,所以在存取前需要先做一个到byte[]的转换,不过也并不是很复杂,一些语言比如python内置了转换库struct,即便没有,自己写一个将byte[]转成int也不需要多少行代码。
该方法的优点就是便于排序,缺点是IPV6需要多个字段才能存下来。
3.保存为字节数组 #
直接把IP的原始字节存在数据库里面,使用16个字节的Binary(16)
类型即可,目前本站就是用的这种格式。
这种方法跟整数存储一样有着最小的空间占用,而且各种语言也都能够直接从IP地址对象获取其字节形式的数据,无需做跟整数一样的转换。缺点是字节类型在比较时的速度要比整数慢一些
通过对比,最好的方式应该是直接保存字节数组,它只需要一个字段,还不占用多余的空间,对于访问日志这种量大的数据非常重要,并且也能够做排序。保存成字符串虽然可读,但是一个完整的系统很少有直接看数据库中原始数据的需求,一般都是在读取到前台界面显示,所以可读性没什么意义。
附:IPV4与IPV6的转换 #
上面提到的三种方式,除了字符串(IPV4以英文句点隔开,IPV6是冒号),其他两种对IPV4和IPV6的存储格式都是一样的,那么如何区分他们呢?其实在IPV6设计时已经考虑到了兼容性。在RFC3493 第3.7节 定义了一个称作 IPv4-mapped IPv6 address 的地址段,用来在IPV6中表示IPV4地址,其格式如下:::FFFF:<IPv4-address>
在IPV6的16个字节中,前10字节全是0,第11,12字节是0xFF,剩下的4个字节与对应的IPV4字节相同,例如本地环回地址可以表示为::FFFF:127.0.0.1
在放进数据库前,把IPV4地址用这个格式转换成IPV6即可。下面附上转换的JAVA代码
public final class IpAddressConvertor {
private static final byte[] prefix = new byte[12];
static { prefix[10] = prefix[11] = (byte)0xFF; }
/**
* 把字节数组转换为InetAddress
*
* @param bytes 字节数组
* @return InetAddress
* @throws SQLDataException 数据不是IP地址
*/
public InetAddress decode(byte[] bytes) {
try {
//InetAddress.getByAddress()能自动识别IPv4-mapped addresses
return InetAddress.getByAddress(bytes);
} catch (UnknownHostException e) {
throw new RuntimeException("数据不是IP地址", e);
}
}
/**
* 把InetAddress转换为16字节的数组
*
* @param address 地址
* @return 字节数组
*/
public byte[] encode(InetAddress address) {
if(address instanceof Inet6Address) {
return address.getAddress();
}
return mappingToIPv6(address.getAddress());
}
/**
* 使用IPv4-mapped addresses,将IPv4的4字节地址转换成IPv6的16字节地址
*
* @see <a href="https://tools.ietf.org/html/rfc3493#section-3.7">IPv4-mapped addresses</a>
* @param ipv4 表示IPv4地址的4个字节
* @return IPv4-mapped IPv6 Address bytes
*/
private byte[] mappingToIPv6(byte[] ipv4) {
byte[] ipv6 = new byte[16];
System.arraycopy(ipv4, 0, ipv6, 12, 4);
System.arraycopy(prefix, 0, ipv6, 0, 12);
return ipv6;
}