存储IP地址的数据类型

13

在Java中是否有一种特定的数据类型可以存储IP地址?我有一个具体的功能需求:

  1. 给定一个IP范围和一个IP,如果该IP在范围内则返回true,否则返回false。例如:范围为10.10.10.1-10.10.11.255,IP为10.10.10.192,则应返回true。

我知道有java.net.inetaddress,但我相信它不能提供这个功能。有什么建议吗?


你能不将其转换为字符串的情况下完成这个吗? - Nitesh Verma
7个回答

15
我会使用java.net.InetAddress或其子类,并编写自定义比较器和范围类:
  • 使用显式类型InetAddress而不仅仅是longs,维护和调试更容易:您的调试器实际上将显示“10.10.10.1”,而不是“168430081”
  • IPv6要么不是问题,要么可以在不增加太多麻烦的情况下实现。
InetAddress的一个缺点是getByName会导致DNS访问。如果您想避免DNS的惩罚,您可以查看Guavacom.google.common.net.InetAddresses帮助类。
public enum InetAddressComparator implements Comparator<InetAddress> {

  INSTANCE;

  public int compare(InetAddress first, InetAddress second) {
    byte[] firstBytes = first.getAddress();
    byte[] secondBytes = second.getAddress();
    if (firstBytes.length != secondBytes.length) {
      throw new IllegalArgumentException("Cannot compare IPv4 and IPv6 addresses");
    }
    // getAddress returns bytes in network byte order:
    // the least significant byte is at the last index
    for (int i = firstBytes.length - 1; i >= 0; i--) {
      // translate the byte to an int with only the last 8 bits set,
      // effectively treating it as unsigned
      int a = firstBytes[i] & 0xff;
      int b = secondBytes[i] & 0xff;
      if (a < b) {
        return -1;
      } else if (a > b) {
        return 1;
      }
    }
    return 0;
  }

}

public class Range<T> {

  private T lower;
  private T upper;
  private Comparator<T> comparator;

  public Range(T lower, T upper, Comparator<T> comparator) {
    if (comparator.compare(lower, upper) <= 0) {
      this.lower = lower;
      this.upper = upper;
    } else {
      this.lower = upper;
      this.upper = lower;
    }
    this.comparator = comparator;
  }

  public boolean contains(T element) {
    return comparator.compare(lower, element) <= 0 &&
      comparator.compare(upper, element) >= 0;
  }

}

public class Main {
  public static void main(String[] args) throws Exception {
    InetAddress start = InetAddress.getByName("10.10.10.1");
    InetAddress end = InetAddress.getByName("10.10.11.255");
    InetAddress test = InetAddress.getByName("10.10.10.192");
    assert InetAddressComparator.INSTANCE.compare(start, test) == -1;
    assert InetAddressComparator.INSTANCE.compare(end, test) == 1;
    assert InetAddressComparator.INSTANCE.compare(test, test) == 0;
    assert new Range<InetAddress>(start, end, InetAddressComparator.INSTANCE)
      .contains(test);
  }
}

Ray Tayek 的回答和这个回答看起来都是不错的解决方案。我想知道自那时起已经过去了两年,是否有任何新的想法。值得一提的是,似乎 Java 7 在 InetAddress.getByName() 中不会对 IP 地址进行 DNS 检查。 - isapir

13

IP(IPv4)是32位(与Java中的int大小相同)。由于您希望使用无符号整数进行比较(如果需要支持128.0.0.0以上的IP),因此需要改用long。

10.10.10.1 is: (10 << 24) + (10 << 16) + (10 << 8) + 1 = 168430081
10.10.11.255 is: (10 << 24) + (10 << 16) + (11 << 8) + 255 = 168430591

10.10.10.192 is: (10 << 24) + (10 << 16) + (10 << 8) + 192 = 168430272

由于 168430081 <= 168430272 && 168430272 <= 168430591,(换句话说,168430272在168430081和168430272之间),您的IP位于该范围内。


2
只有在进行跨越127.255.255.255和128.0.0.0边界的范围比较时,才需要使用无符号整数,此时您可以在编码中翻转最高位。 - Peter Lawrey
你可以手动检测它,但我认为使用长整型更容易,而且除了32个浪费的位(如果他在内存中存储了很多这些东西),我看不出有什么问题。 - Paul
在我看来,你应该更新你的示例以使用“long”,因为目前你的示例只使用了我建议的“int”。;) - Peter Lawrey
2
在这个例子中,你如何将其转换回字符串? - user1732480
你可以将其转换回来:`StringBuilder sb = new StringBuilder(); int a = (ipv4 >> 24) & 127; sb.append(a); a = (ipv4 >> 16) & 255; sb.append("." + a); a = (ipv4 >> 8) & 255; sb.append("." + a); a = ipv4 & 255; sb.append("." + a); return sb.toString();` - jbarrameda

3

如果您的IP地址范围可以用常见的CIDR格式表示,您可以使用Apache Common的SubnetUtils

SubnetUtils.SubnetInfo subnet = new SubnetUtils("192.168.0.3/31");
return subnet.isInRange(testIpAddress);

3
对于IPv4地址,我会使用int类型的值。将IP地址转换为数字,然后可以使用数字操作。
如果您想进行比较,并覆盖127.x.x.x(环回局域网)到128.x.x.x这样的范围,虽然不太可能,但是如果确实需要,您可以翻转最高位,范围比较仍将起作用。
以@user1的示例为例,检查210.210.210.192是否在210.210.210.1和210.210.211.255之间。
210.210.210.1 is: (210 << 24) + (210 << 16) + (210 << 8) + 1 = -757935615
210.210.211.255 is: (210 << 24) + (210 << 16) + (211 << 8) + 255 = -757935105

210.210.210.192 is: (210 << 24) + (210 << 16) + (210 << 8) + 192 = -757935424

最后一个IP地址在范围内,因为-757935615 <= -757935424 && -757935424 <= -757935105。


你需要使用 long 数据类型来轻松处理 IPv4,因为你不想在 Java 中使用有符号比较时将以字节大于 127 开头的 IP 地址进行比较。 - Paul
此外,long类型只有64位,而IPv6使用128位,因此long类型不足以满足需求。 - nd.
@user1 在我看来,这只是个例外情况下的问题,而且有一个简单的解决方法。 - Peter Lawrey

2

2

试试这个:

import static org.junit.Assert.*;
import org.junit.Test;
import java.math.BigInteger;
import java.net.*;
class IpRange {
    IpRange(InetAddress from, InetAddress to) {
        if(!from.getClass().equals(to.getClass())) throw new RuntimeException("different versions of ip address!");
        this.from = new BigInteger(from.getAddress());
        this.to = new BigInteger(to.getAddress());
    }
    boolean isInRange(InetAddress inetAddress) {
        BigInteger bigInteger = new BigInteger(inetAddress.getAddress());
        return !(from.compareTo(bigInteger) == 1 || bigInteger.compareTo(to) == 1);
    }
    final BigInteger from, to;
}
public class IpRangeTestCase {
    @Test public void testInRange() throws UnknownHostException {
        InetAddress from = InetAddress.getByAddress(new byte[] { 10, 10, 10, 1 });
        InetAddress x = InetAddress.getByAddress(new byte[] { 10, 10, 10, 42 });
        InetAddress to = InetAddress.getByAddress(new byte[] { 10, 10, 10, (byte) 192 });
        IpRange ipRange = new IpRange(from, to);
        assertTrue(ipRange.isInRange(from));
        assertTrue(ipRange.isInRange(x));
        assertTrue(ipRange.isInRange(to));
        InetAddress toSmall = InetAddress.getByAddress(new byte[] { 10, 10, 9, 1 });
        assertFalse(ipRange.isInRange(toSmall));
        InetAddress toBig = InetAddress.getByAddress(new byte[] { 10, 10, 10, (byte) 193 });
        assertFalse(ipRange.isInRange(toBig));
        InetAddress fromv6=InetAddress.getByAddress(new byte[] {(byte)0xfe,(byte)0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x02,(byte)0xb3,(byte)0xff,(byte)0xfe,0x1e,(byte)0x83,0x20});
        InetAddress xv6=InetAddress.getByAddress(new byte[] {(byte)0xfe,(byte)0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x02,(byte)0xb3,(byte)0xff,(byte)0xfe,0x1e,(byte)0x83,0x29});
        InetAddress tov6=InetAddress.getByAddress(new byte[] {(byte)0xfe,(byte)0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x02,(byte)0xb3,(byte)0xff,(byte)0xfe,0x1e,(byte)0x83,0x40});
        IpRange ipRangev6 = new IpRange(fromv6, tov6);
        assertTrue(ipRangev6.isInRange(xv6));
    }
    @Test (expected=RuntimeException.class) public void testInRangeThrows() throws UnknownHostException {
        InetAddress v4 = InetAddress.getByAddress(new byte[] { 10, 10, 10, 1 });
        InetAddress v6=InetAddress.getByAddress(new byte[] {(byte)0xfe,(byte)0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x02,(byte)0xb3,(byte)0xff,(byte)0xfe,0x1e,(byte)0x83,0x29});
        new IpRange(v4, v6);
    }
}

我最初考虑在我的答案中使用 BigInteger 来作为比较器,但后来决定不这样做:它可以很好地将 IPv4 和 IPv6 地址进行比较,而这可能并不是你想要的。 - nd.
我的程序似乎可以使用IPv6,但无法检测其中一个是v4还是v6。 - Ray Tayek
我添加了一个针对不同版本IP地址的测试。 - Ray Tayek
它可以处理任意字节长度的互联网地址(即IPv4和IPv6),但如果您将v4与v6进行比较,则使用::v4-address别名v4地址。这实际上与“IPv4兼容”地址相同,这可能正是您想要的,但由于还有其他v4<->v6转换系统,因此可能是错误的选择。 - nd.
这很棒,但请注意应该使用考虑符号的BigInteger构造函数,即BigInteger(1, byte[])。否则,像255.255.255.255这样的地址将被表示为-1。 - Noky

0

只需使用Java的原始数据类型long


1
IP地址包含4个数字,每个数字可以存储在1个字节(0-255)中。因此,您需要4个字节来存储整个IP地址。在获得minIp和maxIp值之后,您可以简单地测试testIp > minIp && testIp < maxIp以查看它是否在范围内。有关如何实现转换的详细信息,请参见@user1的答案。 - bezmax
@Max IPv6怎么办?“long”可以处理通用情况。 - Bohemian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接