Loading

网络编程-Java中的Internet查询

前提

在深入理解URL、URI等概念,或者学些Socket相关的知识之,有必要系统理解一下Internet相关的一些基础知识。

Internet地址

连接到Internet(因特网)的设备称为节点(node),而任意一个计算机节点称为主机(host)。每个节点或者主机都由至少一个唯一的数来标识,这称为Internet地址或者IP地址。

IP和域名

如果使用Java作为开发语言的话,不需要担心IP或者域名的工作原理,但是我们需要理解IP寻址的一些基础知识。我们目前常用的网络都是IPv4网络,每个计算机节点都是由一个4字节(32bit)的数字标识,这个数字标识的格式是点分四段(dotted quad,形式是:xxx.xxx.xx.xx),其中的每个数字都是一个无符号字节,取值范围是0到255。当数据通过网络传输的时候,数据包的首部中要包括要发往的机器地址(目的地址)和发送这个数据包的机器地址(源地址)。

可以使用的IPv4类型的IP地址总量大概是40亿多一点,因此无法做到地球上每个人都分配一个唯一的IPv4的IP地址,所以IPv6就诞生了,目前网络由IPv4向IPv6过度(不过这个过度过程相对缓慢,因素很多)。IPv6网络中的IP地址使用16字节(128bit)的数字标识。IPv6地址的表示形式通常是以英文冒号分隔的8个区域,每个区域都是4个十六进制的数字,举个例子:FEDC:BA98:7654:3210:FEDC:BA98:7654:3210就是一个合法的IPV6地址。而在IPv4和IPv6的混合网络中,IPv6地址的最后四个字节有时候表示形式为IPv4地址的点分四段地址。IPv6地址只在Jdk1.4以及之后的版本支持,换言之,Jdk1.3或者之前的版本只能使用IPv4地址。

虽然计算机可以轻松地处理数字,但是人脑的记忆对于数字并不敏感,因此开发了域名系统(Domain Name System,也就是DNS),用于将人脑易于记忆的主机名(如www.baidu.com)转换为数字Internet地址(如183.232.231.173)。这里不展开DNS的具体内容,作为开发者,我们可以简单理解为它就是一个巨型分布式数据库,用于映射主机名(域名)和IP地址,画个图大致如下:

j-i-q-1

端口

因为每台计算机都不是只做一件事,相当于计算机做的每一种业务逻辑需要从逻辑上隔离,例如FTP请求的处理要和电子邮件的处理分离,FTP请求处理也要和Web业务处理分离,所以每种业务逻辑的处理需要使用一个逻辑分离的标识,这个标识就是端口(port)。一般每台计算机有成千上万个逻辑端口(确切来说,每个传输层协议都有65535个端口,Windows系统中,11023号端口是系统端口,用户无法修改,102465534端口是系统为用户预留的端口,而65535号端口为系统保留端口),每个端口可以分配给一个特定的服务。例如Web的底层协议Http协议通讯一般使用80端口,使用浏览器URL访问服务器的80端口可以省略URL中的端口号。

Java对网络的抽象

InetAddress

单词InetAddress是Internet Address的缩写合并,代表因特网地址。java.net.InetAddress类是Java对IP地址(包括IPv4和IPv6地址)的高度抽象表示。大多数网络编程相关的类都会用到InetAddress,如Socket、ServerSocket等,InetAddress两个最核心的属性是主机名(host)和IP地址,对应属性hostName和address。

创建InetAddress实例

创建InetAddress实例主要依赖它的工厂方法(实际上InetAddress的构造函数是包私有的,也就是无法通过new关键字创建实例),比较常用的一个静态工厂方法是:

static InetAddress getByName(String host) throws UnknownHostException

其中参数可以为主机名(域名)或者点分四段地址,前者相当于通过主机名查找一个可连接的IP地址,后者相当于通过IP地址反查主机名,值得注意的是,这个方法调用的使用会建立与本地DNS服务器的连接进行主机名或者数字地址查找,如果DNS服务器找不到主机或者地址,会抛出UnknownHostException异常。

	public static void main(String[] args) throws Exception{
		InetAddress inetAddress = InetAddress.getByName("www.baidu.com");
		System.out.println(inetAddress);
	}

InetAddress覆写了toString方法,返回结果是hostName/address格式,上面的main方法执行的一个可能的结果是:

www.baidu.com/14.215.177.39

有些时候,我们知道数字IP地址,就可以由数字地址直接创建一个InetAddress实例,这样就可以不必使用getByName(String host)方法和DNS交互。

static InetAddress getByAddress(byte[] addr)throws UnknownHostException
static InetAddress getByAddress(String host,byte[] addr)throws UnknownHostException

这两个方法可以创建主机名不存在或者主机名无法解析的InetAddress实例。举个例子:

	public static void main(String[] args) throws Exception {
		byte[] bytes = {14, (byte) 215, (byte) 177, 39};
		InetAddress inetAddress = InetAddress.getByAddress("www.doge.com",bytes);
		System.out.println(inetAddress);
	}

实际上,域名www.doge.com并不存在,但是这个方法并不会抛出异常。

如果要查询一个主机名的所有IP地址,可以使用:

static InetAddress[] getAllByName(String host) throws UnknownHostException

如果需要查询本机的主机名和IP地址,可以使用:

static InetAddress getLocalHost() throws UnknownHostException

注意这个方法会尝试连接DNS去查询本地计算机的真正的主机名和IP地址,如果查询失败,它就会返回回送地址,也就是主机名是"localhost",IP地址是点分四段地址"127.0.0.1"。

InetAddress缓存

DNS查找的开销可能相当大(如果请求需要经过多个中间服务器或者尝试解析一个不可达的主机,可能需要耗费几秒的时间),所以InetAddress会缓存DNS查询结果,也就是一旦得到一个给定主机的地址,就不会再次查找,即使为同一个主机创建多个InetAddress实例,也不会再次进行DNS查询。这样的缓存机制对于性能来说是有好处的,但是也会带来负面影响:

  • 程序运行期间连接的主机的IP地址很大可能会发生变化,已缓存的IP有可能不可用。
  • 刚开始尝试解析一个主机时候是失败的,但是随后尝试解析的时候会成功,但是缓存了首次解析失败的记录。
  • 远程DNS服务器发送的信息还在传输,第一次尝试超时,下一次请求即可成功。

因此,Java对于不成功的DNS查询结果仅仅缓存10秒,而且可以通过下面两个系统变量控制缓存的时间:

  • networkaddress.cache.ttl:成功的DNS查询结果的缓存时间(秒数),-1表示不会超时。
  • networkaddress.cache.negative.ttl:成功的DNS查询结果的缓存时间(秒数),-1表示不会超时。

InetAddress提供的基本属性获取方法

InetAddress提供四个基本属性获取方法,用于获取当前InetAddress表示的主机名和IP地址。

public String getHostName();
public String getCanonicalHostName();
public byte[] getAddress();
public String getHostAddress();

注意上面的几个方法只有Getter,没有Setter方法,说明这几个属性的设置权限是java.net包中的类库。

  • getHostName:返回当前InetAddress实例的主机名,如果对应的机器没有主机名或者安全管理器阻止确定主机名,则会返回点分四段数字IP地址。
  • getCanonicalHostName:getCanonicalHostName与getHostName,不过getHostName方法只是在不知道主机名的情况下才连接DNS进行查询,getCanonicalHostName方法总是连接DNS查询主机名并且替换缓存值,所以这个方法调用会比较耗时。
  • getAddress:返回当前InetAddress实例的数字IP地址的byte数组,注意因为Java中没有无符号的byte,因此负数byte值要+256变成int类型才是无符号的byte值。
  • getHostAddress:实际上就返回getAddress方法中的byte数组转换成的IP地址点分四段表示形式的字符串,也就是IP地址字符串。

上面的getAddress()方法还有一个特殊的判断使用场景,就是它的返回值byte数组的长度如果是4,那么InetAddress一定是Inet4Address的实例,如果长度为16,那么那么InetAddress一定是Inet6Address的实例,由此可以判断InetAddress中的IP地址到底是IPv4还是IPv6。

InetAddress提供的地址类型判断方法

有些IP地址和地址模式有特殊的含义,例如127.0.0.1是本地回送地址,244.0.0.0到239.255.255.255范围内的IPv4地址是组播地址。InetAddress中提供10个公有实例方法来判断InetAddress对象是否符合这些地址模式:

  • 1、boolean isAnyLocalAddress():如果地址是通配地址则返回true,所谓通配地址就是可以匹配本地系统中的任何地址,在IPv4中的通配地址是0.0.0.0,在IPv6中的通配地址是0:0:0:0:0:0:0:0(:😃。
  • 2、boolean isLoopbackAddress():如果地址是回送地址则返回true,在IPv4中的回送地址是127.0.0.1,在IPv6中的回送地址是0:0:0:0:0:0:0:1(::1)。
  • 3、boolean isLinkLocalAddress():如果地址是一个IPv6本地链接地址则返回true。
  • 4、boolean isSiteLocalAddress():如果地址是一个IPv6本地网站地址则返回true。
  • 5、boolean isMulticastAddress():如果地址是一个组播地址则返回true。
  • 6、boolean isMCGlobal():如果地址是一个全球组播地址则返回true。
  • 7、boolean isMCOrgLocal():如果地址是一个组织范围组播地址则返回true。
  • 8、boolean isMCSiteLocal():如果地址是一个网站范围组播地址则返回true。
  • 9、boolean isMCLinkLocal():如果地址是一个子网范围组播地址则返回true。
  • 10、boolean isMCNodeLocal():如果地址是一个本地接口组播地址则返回true。

实际上,我们很少用到这十个方法。

InetAddress的可达性测试

InetAddress提供两个isReaachable()方法用于测试可达性。其实就是测试一个特定的节点对当前主机是否可达(两者是否能够建立一个网络连接)。因为网络连接有可能因为多种原因阻塞,列举一些原因如下:

  • 防火墙拦截。
  • 代理服务器拦截。
  • 不能正常服务的路由器。
  • 断开的网络线缆。
  • 尝试连接的远程计算机没有开机。
public boolean isReachable(int timeout) throws IOException
public boolean isReachable(NetworkInterface netif, int ttl, int timeout) throws IOException

这两个方法会尝试使用Traceroute查看指定地址是否可达。Traceroute程序使用ICMP报文和IP首部中的TTL字段(一般为64)。TTL字段的目的是防止数据报在选路时候无休止的在网络中流动(当路由故障的时候,可能在两个路由循环)。可以理解TTL字段用于控制连接被丢弃之前的网络最大跳数。第一个方法isReachable(int timeout)只有一个参数控制检测可达性的超时毫秒数,第二个方法可以控制指定本地的网络接口、TTL参数和超时时间进行可达性测试。

	public static void main(String[] args) throws Exception{
		InetAddress inetAddress = InetAddress.getByName("www.baidu.com");
		System.out.println(inetAddress.isReachable(2000));
	}

比较不同的InetAddress

InetAddress中覆盖了equalshashCode方法,比较的时候,实际上比较的是address属性,也就是IP地址,换言之,只要两个InetAddress的IP地址一致,这两个InetAddress对象就是相等的,并不要求两个InetAddress对象的主机名一致。举个例子:

	public static void main(String[] args) throws Exception {
		while (true){
			Thread.sleep(500);
			InetAddress inetAddress = InetAddress.getByName("www.baidu.com");
			InetAddress other = InetAddress.getByName("14.215.177.39");
			System.out.println(inetAddress.equals(other));
		}
	}

上面的main方法执行之后,基本上打印true,取决于DNS的处理。

Inet4Address和Inet6Address

Inet4Address和Inet6Address两个类都继承自InetAddress,它们分别是IPv4和IPv6的IP地址的高度抽象表示。但是,一般情况下,开发者使无须感知使用或者连接的IP地址到底是IPv4和IPv6的IP地址,因为通常我们都是通过主机名(host)去访问。

本地网络接口

NetworkInterface是本地网络接口,实际上它可以表示一个物理接口,如以太网卡,它也可以表示一个虚拟接口,与机器的其他IP地址绑定到同一个物理硬件,最常见的就是现在的虚拟化容器如Docker提供的网卡。NetworkInterface类提供的一些方法可以枚举所有的本地地址,通过这些本地地址创建的InetAddress对象,创建出来的这些InetAddress对象就可以使用在客户端Socket或者服务端Socket。

NetworkInterface的创建方法

static NetworkInterface getByName(String name) throws SocketException 

getByName(String name)方法可以指定网络接口名字获取对应的网络接口实例NetworkInterface,如果没有这样名字的网络接口则返回null。网络接口的名字格式和平台相关,例如典型的Unix系统上,以太网接口名字的形式为eth0、eth1等等,在Windows系统中名字类似于"CE31"、"ELX100"等字符串,"lo"一般是本地回送地址的网络接口名字。

另外,可以通过InetAddress返回指定IP绑定的网络接口(或者说返回的网络接口处理指定的IP地址),如果本地主机没有网络接口和传入的IP地址绑定则返回null,如果发生错误则抛出一个SocketException。

static NetworkInterface getByInetAddress(InetAddress addr) throws SocketException 

最后,可以使用下面的方法枚举本地主机上的所有网络接口。

static Enumeration<NetworkInterface> getNetworkInterfaces() throws SocketException

笔者用的是Windows10系统,尝试枚举一下所有的网络接口:

	public static void main(String[] args) throws Exception{
		Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
		while (networkInterfaces.hasMoreElements()){
			NetworkInterface networkInterface = networkInterfaces.nextElement();
			System.out.println(networkInterface);
		}
	}

执行结果部分如下:

name:lo (Software Loopback Interface 1)
name:ppp0 (WAN Miniport (PPPOE))
name:net0 (Microsoft ISATAP Adapter #2)
name:net1 (Microsoft ISATAP Adapter)
name:net2 (WAN Miniport (L2TP))
name:net3 (WAN Miniport (IKEv2))
...

NetworkInterface提供的属性获取方法

NetworkInterface提供一个实例方法public Enumeration<InetAddress> getInetAddresses()用于获取绑定在一个网络接口上面的所有IP地址,虽然这种情况不常见,但是确实存在。

实例方法public String getName()返回NetworkInterface实例的对象名称,例如eth0、lo等。

实例方法public String getDsiplayName()也是返回NetworkInterface实例的对象名称,不过表示的形式更加友好,例如"Ethernet Card 0"(eth0),但是在Unix系统中和getName()相同。

小结

只有理解网络编程中的一些基础概念,才能铺好学习URI(URL)、TCP协议、HTTP协议和套接字(Socket)的路。

(本文完 c-2-d)

技术公众号(《Throwable文摘》),不定期推送笔者原创技术文章(绝不抄袭或者转载):

娱乐公众号(《天天沙雕》),甄选奇趣沙雕图文和视频不定期推送,缓解生活工作压力:

posted @ 2018-09-03 23:13  throwable  阅读(1152)  评论(0编辑  收藏  举报