IP地址在数据库中的存储姿势

发布时间:

最后更新:

本文为博主原创文章,发表于https://blog.kaciras.com/article/7/how-to-store-ip-address-in-database,未经许可禁止转载

IP AddressIP Address

在数据库中保存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>

IPv4-mapped IPv6 addressIPv4-mapped IPv6 address

在IPV6的16个字节中,前10字节全是0,第11,12字节是0xFF,剩下的4个字节与对应的IPV4字节相同,例如本地环回地址可以表示为::FFFF:127.0.0.1

在放进数据库前,把IPV4地址用这个格式转换成IPV6即可。下面附上转换的JAVA代码

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;
	}
评论加载中