以前只知道计算机使用反码来进行计算,但是没有想到,也没有想过计算机存储数字的时候是用什么格式存储的,当然它是二进制的,我的意思是它是原码,反码,补码中的哪一种。今天因为学习ServerSocketChannel,涉及到了这个问题,才把这个知识点摸透,是以反码形式存储的。认识到这一点有什么作用呢,且听我说。
用byte数组表示IP地址
编写服务器程序的时候,需要启动server socket监听某个端口,以及绑定一张网卡(也就是服务器上的网卡,当你的服务器有多张网卡的时候,你可以选择将socket绑定到其中一张,此时客户端到来的请求,只有网卡和端口都相同,才会被这个socket识别)。我有一张网卡的地址是:192.168.1.78。
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 绑定一个地址
serverSocketChannel.bind(socketAddress);
} catch (IOException e) {
e.printStackTrace();
}
上面代码中的socketAddress是一个SocketAddress接口的实例,java提供了对该接口的实现类InetSocketAddress,该实现类有构造函数:
public InetSocketAddress(InetAddress addr, int port)
InetAddress是一个public类,但它的构造函数是默认权限,也就是包内或子类可访问,它在java.net包下,但它提供了静态方法来获取其实例:
public static InetAddress getByAddress(byte[] addr) throws UnknownHostException
当然它还有其他一些静态方法来获取实例,并不是一定要用byte数组才行。而我决定使用它,并因此发现了自己知识图谱中的一个缺陷。
首先我们知道:
- 计算机一定是使用二进制存储数字的
- java的数字是有符号位的
- ip地址是32位的
- 一个byte是8位,所以这里要我们传入的应该是4个byte的数组
- 一个byte的范围是[-128-127]
然后我当时自以为,java是以原码存储数字的。
在这样的知识背景下,我来算一算,我应该传哪4个byte。我要绑定的网卡的IP地址是192.168.1.78,所以最低位是78,在byte的表达范围内,不需要特殊处理;第二位是1,也不需要处理;第三位是168,168在java里用byte已经表达不了了,因为java的byte的最高位用来表达符号,所以它实际上只有7位能表达数值。怎么回事,难道这个方法创建的InetAddress实例只能表示[0.0.0.0-127.127.127.127]内的IP地址?
不对劲,不对劲,我去看一看这个方法的内部是怎么实现的。
public static InetAddress getByAddress(byte[] addr)
throws UnknownHostException {
return getByAddress(null, addr);
}
看来它调用了另一个方法,我们再去看看它是怎么回事。
public static InetAddress getByAddress(String host, byte[] addr)
throws UnknownHostException {
if (host != null && host.length() > 0 && host.charAt(0) == '[') {
if (host.charAt(host.length()-1) == ']') {
host = host.substring(1, host.length() -1);
}
}
if (addr != null) {
if (addr.length == Inet4Address.INADDRSZ) {
return new Inet4Address(host, addr); // into here
} else if (addr.length == Inet6Address.INADDRSZ) {
byte[] newAddr
= IPAddressUtil.convertFromIPv4MappedAddress(addr);
if (newAddr != null) {
return new Inet4Address(host, newAddr);
} else {
return new Inet6Address(host, addr);
}
}
}
throw new UnknownHostException("addr is of illegal length");
}
从第一段代码我们能看出来调用getByAddress方法时,传入的host是null,所以第一个if直接跳过不看,我们传入的addr参数,是一个长度为4的byte数组,所以代码执行到了into here注释所在的那一行。继续:
Inet4Address(String hostName, byte addr[]) {
holder().hostName = hostName;
holder().family = IPv4;
if (addr != null) {
if (addr.length == INADDRSZ) {
int address = addr[3] & 0xFF;
address |= ((addr[2] << 8) & 0xFF00);
address |= ((addr[1] << 16) & 0xFF0000);
address |= ((addr[0] << 24) & 0xFF000000);
holder().address = address;
}
}
holder().originalHostName = hostName;
}
原来!在这里,通过移位运算,byte数组被转换成了一个int值。左移位是不会影响符号位的,所以在将byte转换成int的过程中,符号位实际上被当成了一个数值位(符号位成为了int中低位上的数值位,但最后一个byte的符号位成为了int的符号位)。int是4字节=32位=ip地址长,这样代表IP地址,可以的。
所以InetAddress是可以表示[0.0.0.0-255.255.255.255]的,只不过你要将byte中的最高位也考虑上。让我们来算一算,无符号的168用有符号的谁来表示。windows自带程序员计算器,搬出来计算一下,168的二进制表示是10101000。java的byte的最高位是符号位,所以它表示一个负的00101000。而00101000是40,所以我应该用有符号的-40表示无符号的168。同理计算出192用-64表示。
综上,我的代码应该是
// 192.168.1.78
InetAddress inetAddress = InetAddress.getByAddress(new byte[]{-64, -40, 1, 78});
System.out.println(inetAddress)
然而,println输出的却是:/192.216.1.78
怎么回事?216是什么鬼?检查了一通,发现-40没有问题,为什么-64就正确的表达了192,-40就叉屁了呢?
debug一下看看吧
11000000_11011000_00000001_01001110,-40的10101000,到这里怎么就变成11011000了呢?其他都是正确的。到这里我其实没看出来导致这个问题的根本原因,但是计算机里的二进制确实有原码,反码,补码一说,我来套一套看看。
如果数据的存储不是原码,而是反码呢?
10101000 原
11010111 反(符号位不变,其余取反)
11011000 补(反码+1)
我去,柳暗花明又一村,得来全不费工夫。新的问题是,-64怎么就对上192了呢
11000000 原
10111111 反
11000000 补
居然还有这么巧的事情。我差一点就因为-64和192的对应是正确的而没往这个方向想。差点因为一个巧合走向了错误的思路。也给自己提个醒,有些看似不应该有问题的地方,也有可能是存在问题的。
至此我也是才发现一个很重要的知识点,java在存储数值的时候,使用的是反码,并不是以前我以为的,用原码存储,用反码计算。这一点平时开发可能基本遇不到,但当你使用 >> << >>> 这种移位运算符,或者用 $ | 运算符的时候就会用到这个知识点。
最后,要想表达168,直接使用它的二进制补码11011000就可以了,将他转换为有符号的byte是-88。
InetAddress inetAddress = InetAddress.getByAddress(new byte[]{-64, -88, 1, 78});
System.out.println(inetAddress) // /192.168.1.78