连接到 Internet 的设备称为节点,计算机节点称为主机。每个节点或主机都由至少一个唯一的数来标识,称为 IP 地址。IP 地址分为 IPv4(四字节)地址和 IPv6(十六字节)地址。IPv4 地址一般写为 4 个无符号字节,每个字节范围从 0~255,为方便人们查看,各字节用点号分隔。IPv6 地址通常写为冒号分隔的 8 个区块,每个区块是 4 个十六进制数字,前导的 0 不需要写,两个冒号表示多个 0 区块,但每个地址中双冒号最多出现一次。
image.png
IP 地址对于计算机来说很不错,但人们很难记忆长的数字。因此 Internet 的设计者发明了域名系统(Domain Name System),DNS 将人们可以记忆的主机名(server.example.com)与计算机可以记忆的 IP 地址关联起来。服务器通常至少有一个主机名,客户端往往有一个主机名,但也可能没有。

每台连接到 Internet 的计算机都应当能访问一个称为域名服务器的机器,该服务器管理了不同主机名和 IP 地址之间的映射关系。大多数域名服务器只知道其本地网络上主机的地址,以及其他网站中一些域名服务器的地址。如果客户端请求本地域名外的机器地址,本地域名服务器会询问远程域名服务器,再将答案转发给请求者。

InetAddress

java.net.InetAddress 是 Java 对 IP 地址的高层表示,代表了一个网络目标地址。地址可以由一个字符串来定义,这个字符串可以是数字型的地址也可以是主机名,主机名必须被解析成数字型地址才能用来通信。该类有两个子类:Inet4AddressInet6Address,分别对应了目前 IP 地址的两个版本。
InetAddress.png

1. 创建

InetAddress 没有公共的构造函数,它提供了一些静态工厂方法,可以连接到 DNS 服务器来解析主机名。最常用的是根据主机名获取地址的方法:

  1. public static InetAddress getByName(String host) throws UnknownHostException

实际上,这个方法会建立与本地 DNS 服务器的一个连接,来查找对应地址。如果 DNS 服务器找不到这个地址则抛出 UnknownHostException 异常,它是 IOException 的一个子类。

一个主机名可以对应多个 IP 地址,如果我们想得到一个主机的所有 IP 地址,可以调用以下方法:

  1. public static InetAddress[] getAllByName(String host)

最后,可以通过 getLocalHost 方法为运行这个代码的主机返回一个地址对象。这个方法也会尝试连接 DNS 来得到一个真正的主机名和 IP 地址,不过如果失败则返回 “localhost” 主机名和 “127.0.0.1” 地址。

  1. public static InetAddress getLocalHost() throws UnknownHostException

由于 DNS 查找的开销可能很大,所以 InetAddress 会缓存查找的结果。一旦得到一个给定主机的地址,就不会再次查找,即使你为同一个主机创建一个新的 InetAddress 对象,也不会再次查找地址。缓存时间可以通过系统属性 networkaddress.cache.ttlnetworkaddress.cache.negative.ttl 来控制,前者指定了成功的DNS 查询结果在缓存中保留的时间,后者指定了不成功的查找结果缓存的时间,单位为秒。在这些时限内,再次查找相同的主机会返回相同的值,-1 解释为用不过期。

除了在 InetAddress 中的缓存,本地主机、本地域名服务器和 Internet 上其他地方的 DNS 服务器也会缓存各种查询的结果。因此,在 Internet 上传播 IP 地址改变的信息可能要花费几个小时,期间程序可能会遇到各种异常,包括:UnknownHostException、NoRouteToHostException、ConnectException。

主机名比 IP 地址稳定的多,有些服务多年以来一直使用同一个主机名,但 IP 地址更换了很多次。如果要在使用主机名和使用 IP 地址之间做出选择,一定要选择主机名。

2. 获取

InetAddress 包含 4 个获取方法,可以将主机名作为字符串返回,将 IP 地址作为字符串或字节数组返回:

  1. public String getHostName()
  2. public String getCanonicalHostName()
  3. public byte[] getAddress()
  4. public String getHostAddress()

InetAddress 没有对应的 setHostname() 和 setAddress() 方法,这使得 InetAddress 实例一旦创建就是不可变的了,每个实例始终指向同一个地址,因此是线程安全的。

其中,getHostName() 方法返回主机名,getCanonicalHostName() 方法也类似,区别在于 getHostName 只有在不知道主机名的时候才会联系 DNS,而 getCanonicalHostName 知道主机名时也会联系 DNS,可能会替换原来缓存的主机名。如果名字解析失败,这两个方法都将返回数字型地址。

getAddress() 方法返回一个适当长度的字节数组,代表 IP 地址的二进制的形式。如果是一个 Inet4Address 实例,则该数组的大小为 4 个字节;如果是 Inet6Address 实例,则为 16 个字节。因此可以根据该方法返回的字节长度确定一个 IP 地址是 IPv4 还是 IPv6,这比使用 instanceof 要快得多。

3. 地址类型

有些 IP 地址和地址模式有特殊含义。例如 127.0.0.1 是本地回送地址,224.0.0.0 到 239.255.255.255 范围内的 IPv4 地址是组播地址,可以同时发送到多个订购的主机。InetAddress 提供了 10 个测试方法:

  1. public boolean isMulticastAddress()
  2. public boolean isAnyLocalAddress()
  3. public boolean isLoopbackAddress()
  4. public boolean isLinkLocalAddress()
  5. public boolean isSiteLocalAddress()
  6. public boolean isMCGlobal()
  7. public boolean isMCNodeLocal()
  8. public boolean isMCLinkLocal()
  9. public boolean isMCSiteLocal()
  10. public boolean isMCOrgLocal()
  • isMulticastAddress:判断地址是否是组播地址,组播会将内容广播给所有预定的计算机。在 IPv4 中组播地址都在 224.0.0.0 到 239.255.255.255 范围内;在 IPv6 中组播地址都以字节 FF 开头

  • isAnyLocalAddress:判断地址是否是通配地址,通配地址可以匹配本地系统中的任何地址。在 IPv4 中通配地址是 0.0.0.0;在 IPv6 中通配地址是 0:0:0:0:0:0:0:0,又写作 ::

  • isLoopbackAddress:判断地址是否是回送地址,回送地址直接在 IP 层连接同一台计算机,而不使用任何物理硬件。在 IPv4 中回送地址是 127.0.0.1;在 IPv6 中回送地址是 0:0:0:0:0:0:0:1,又写作 ::1

  • isLinkLocalAddress:判断地址是否是 IPv6 本地链接地址,IPv6 本地链接地址可以用于帮助 IPv6 网络实现自配置,与 IPv4 网络上的 DHCP 非常相似。

  • isSiteLocalAddress:判断地址是否是 IPv6 本地网站地址,本地网站地址与本地链接地址相似,不过本地网站地址可以由路由器在网站或校园内转发。

  • isMCGlobal:判断地址是否是全球组播地址。

  • isMCNodeLocal:判断地址是否是本地接口组播地址。

  • isMCLinkLocal:判断地址是否是子网范围组播地址。

  • isMCSiteLocal:判断地址是否是网站范围组播地址。

  • isMCOrgLocal:判断地址是否是组织范围组播地址。

    4. 可达性

    InetAddress 有两个 isReachable 方法,可以测试一个特定节点对当前主机是否可达。连接可能由于很多原因而阻塞,包括防火墙、代理服务器、线缆断开等。

    1. public boolean isReachable(int timeout) throws IOException
    2. public boolean isReachable(NetworkInterface netif, int ttl, int timeout) throws IOException

    如果主机在 timeout 毫秒内响应,则方法返回 true。如果出现网络错误则抛出 IOException 异常。第二个方法还可以指定从哪个本地网络接口建立连接。

    NetworkInterface

    NetworkInterface 表示一个本地 IP 地址,可以是一个物理接口也可以是一个虚拟接口。NetworkInterface 提供了一些方法可以枚举所有本地地址,并由它们创建 InetAddress 对象,然后用于创建 Socket 等。

    1. 创建

    由于 NetworkInterface 表示物理硬件和虚拟地址,所以不能任意构造。与 InetAddress 一样,它提供了一些静态工厂方法可以返回与某个网络接口关联的 NetworkInterface 对象。

    1. public static NetworkInterface getByName(String name) throws SocketException
    2. public static NetworkInterface getByIndex(int index) throws SocketException
    3. public static NetworkInterface getByInetAddress(InetAddress addr) throws SocketException
    4. public static Enumeration<NetworkInterface> getNetworkInterfaces() throws SocketException

    getByName 返回一个 NetworkInterface 对象,表示有指定名称的网络接口,如果没有则返回 null。如果在查找相关网络接口时底层网络栈遇到问题,会抛出 SocketException 异常。名字的格式与平台有关,在典型的 UNIX 系统上,以太网接口名的形式为 eth0、eth1 等。

getByInetAddress 返回一个 NetworkInterface 对象,表示与指定 IP 地址绑定的网络接口。如果主机上没有网络接口与这个 IP 地址绑定就返回 null,如果发生异常则抛出 SocketException 异常。

getNetworkInterfaces 返回本地主机上的所有网络接口,包括不能够向网络中的其他主机发送或接收消息的虚拟回环接口,因此在使用前应先进行属性检查,判断这个地址不是回环地址,不是本地链接地址等。

  1. public static void main(String[] args) throws Exception {
  2. Enumeration<NetworkInterface> list = NetworkInterface.getNetworkInterfaces();
  3. while (list.hasMoreElements()) {
  4. NetworkInterface networkInterface = list.nextElement();
  5. System.out.println("NetworkInterface name is :" + networkInterface.getName());
  6. }
  7. }

2. 获取

有了 NetworkInterface 对象,就可以查询其 IP 地址和名字。

  1. public String getName()
  2. public Enumeration<InetAddress> getInetAddresses()

getName 用于返回名称,如 eth0 或 lo0。

一个网络接口可以绑定多个 IP 地址,这种情况不太常见但确实是存在的。getInetAddresses 返回与这个接口绑定的每一个 IP 地址,使用示例如下:

  1. public static void main(String[] args) throws Exception {
  2. NetworkInterface networkInterface = NetworkInterface.getByName("eth0");
  3. Enumeration<InetAddress> list = networkInterface.getInetAddresses();
  4. while (list.hasMoreElements()) {
  5. System.out.println("address :" + list.nextElement().getHostAddress());
  6. }
  7. }

URI 组成

统一资源标识符(Uniform Resource Identifier)是采用一种特定语法标识一个资源的字符串。所标识的资源可能是服务器上的一个文件,也可能是一个邮件地址、新闻、图书或者任何其他内容。URI 类的作用之一是解析标识符并将它分解成各种不同的组成部分

URI 的语法由一个模式(scheme)和一个模式特定部分组成,中间用一个冒号进行分割,如下所示:

  1. 模式:模式特定部分

模式特定部分的语法取决于所用的模式。当前模式包括:

  • file:本地磁盘上的文件
  • ftp:FTP 服务器
  • http:使用超文本传输协议的国际互联网服务器
  • mailto:电子邮件地址
  • telnet:与基于 Telnet 的服务的连接

此外,Java 还大量使用了一些非标准的定制模式,如 rmi、jar、jndi 和 doc,来实现各种不同用途。而 URI 中的模式特定部分并没有特别的语法,不过很多都采用了一种层次结构形式,如:

//authority/path?query

这个 URI 的 authority 部分指定了负责解析该 URI 其余部分的授权机构,比如 http://www.ietf.org/rfc/39.txt 这个 URI 的模式为 http,授权机构为 www.ietf.org,路径为 /rfc/39.txt。这表示位于 www.ietf.org 的服务器负责将路径映射到一个资源。

路径是授权机构用来确定所标识资源的字符串,不同的授权机构可能会把相同的路径解释为指向不同的资源。路径可以是分层的,在这种情况下,各个部分之间用斜线分隔,”.” 和 “..” 操作符用于在这个层次结构中导航,这是从 UNIX 操作系统的路径名语法继承而来的。

整个模式特定部分由 ASCII 字母数字符号组成,即字母 A-Z、a-z 和数字 0-9,此外还可以使用一些标点符号如 - _ . ! ~ * ‘ ,,界定符 / ? & @ # + $ = ; 有其预定义的用途。

所有其他字符应用百分号 % 进行转义,其后是该字符按 UTF-8 编码的十六进制码。例如汉字 “木” 的 Unicode 码为 0x6728,在 UTF-8 中,这会编码为 3 字节 E6、9C 和 A8,因此它在 URI 中编码为 %E6%9C%A8。

URL 组成

URL 是 URI 的一个特例,URL 可以唯一标识一个资源在 Internet 上的位置,是最常见的 URI。除了标识一个资源,还会为资源提供一个特定的网络位置,客户端可以用它来获取这个资源的一个表示。而通常 URI 只可以告诉你一个资源是什么,但是无法告诉你它在哪里,以及如何得到这个资源。

在 Java 类库中,URI 类并不包含任何用于访问资源的方法,它的唯一作用就是解析。但是,URL 类可以打开一个连接到资源的流。因此,URL 类只能作用于那些 Java 类库知道该如何处理的模式,例如 http:、https:、ftp:、本地文件系统(file:)和 JAR 文件(jar:)。

URL 中的网址位置通常包括用来访问服务器的协议(如 FTP、HTTP)、服务器的主机名或 IP 地址,以及文件在该服务器上的路径。URL 的语法为:

protocol://userInfo@host:port/path?query#fragment
  • protocol 是对 URI 中模式(scheme)的另一种叫法

  • userInfo 是服务器的登录信息(可选)

  • host 部分是提供所需资源的服务器的名字,可以是一个主机名也可以是服务器的 IP 地址

  • port 也是可选的,如果服务在其默认端口(HTTP 服务器的默认端口是 80)运行就不需要指定。用户信息、主机和端口合在一起构成授权机构(authority)

  • path 指向指定服务器上的一个特定目录

  • query 向服务器提供附加参数,一般只在 http URL 中使用,用于将表单数据提供给服务器上的程序

  • fragment(片段)指向远程资源的某个部分,比如一篇文章的指定章节

image.png

1. java.net.URL 类

java.net.URL 类是对统一资源定位符的抽象,是 Java 程序在网络上定位和获取数据的最简单的方法,用户无需考虑所使用协议的细节,也不用担心如何与服器通信。只要把 URL 告诉 Java,它就会为你获得数据。

public final class URL implements java.io.Serializable {
    private String protocol;
    private String host;
    private int port = -1;
    // file is defined as {@code path[?query]}
    private String file;
    private transient String path;
    private transient String query;
    ......
}

1.1 创建

与 InetAddress 对象不同,我们可以直接通过构造函数来构造 URL 的实例:

public URL(String spec) throws MalformedURLException
public URL(String protocol, String host, String file) throws MalformedURLException
public URL(String protocol, String host, int port, String file) throws MalformedURLException

使用哪个构造函数取决于你有哪些信息以及信息的形式。如果试图为一个不支持的协议创建 URL 对象,或者如果 URL 的语法不正确,所有这些构造函数都会抛出一个 MalformedURLException 异常。

第一个构造函数只接收一个字符串形式的绝对 URL 作为唯一参数,第二个构造函数将端口设置为 -1,表示使用该协议的默认端口。file 参数应当以斜线开头,包括路径和文件名。如果默认端口不正确,第三个构造函数还允许用一个 int 显式指定端口。

除了构造函数,Java 类库中的其他一些方法也返回 URL 对象。java.io.File 类有一个 toURL() 方法,它返回与指定文件匹配的 file URL,这个方法返回 URL 的具体格式与平台相关。例如在 Windows 上它可能返回 file:/D:/Users/xulei/ToURLTest.java,在 Linux 和其他 UNIX 上可能是 file:/Users/xulei/ToURLTest.java

类加载器(ClassLoader)不仅用于加载类,也能加载资源。它提供了如下两个静态方法:

public static URL getSystemResource(String name)
public static Enumeration<URL> getSystemResources(String name) throws IOException

通过方法返回的 URL 可以读取指定的资源。此外还提供了两个实例方法:

public URL getResource(String name)
public Enumeration<URL> getResources(String name)

实例方法会在所引用类加载器使用的路径中搜索指定资源的 URL,返回的 URL 可能是 file URL、http URL 或其他模式。

1.2 获取数据

当获取 URL 后我们更关心 URL 所指向的文档中包含的数据,URL 类提供以下方法从 URL 中获取数据:

public final InputStream openStream() throws IOException

public URLConnection openConnection() throws IOException
public URLConnection openConnection(Proxy proxy) throws IOException

这些方法中,最常用的是 openStream 方法,该方法连接到 URL 所引用的资源,在客户端和服务器之间完成必要的握手,返回一个 InputStream,可以由此读取数据。但从这个 InputStream 获得的数据是 URL 引用的原始内容,它不包括任何 HTTP 首部或与协议有关的任何其他信息。

如果需要更多地控制下载过程,应该调用 openConnection 方法,该方法为指定的 URL 打开一个 socket,并返回一个 URLConnection 对象,表示一个网络资源的打开的连接。通过调用它的 getInputStream() 方法返回一个通用的 InputStream,可以读取和解析服务器发送的数据。此外 URLConnection 还提供了一些访问 HTTP 首部信息的方法:

public String getContentType()    
public int getContentLength()   
public long getContentLengthLong()
public String getContentEncoding()
public long getDate()
public long getExpiration()
public long getLastModified()

// 获取任意首部字段
public String getHeaderField(String name)  
public String getHeaderFieldKey(int n)
public String getHeaderField(int n)    
public Map<String,List<String>> getHeaderFields()

1.3 分解 URL

URL 由以下 5 部分组成:

  • 协议,即模式
  • 授权机构
  • 路径
  • 查询字符串
  • 片段标识符

例如,在 URL http://www.ibiblio.org/java/books/index.html?isbn=156384627#doc 中,模式是 http,授权机构是 www.ibiblio.org,路径是 /java/books/index.html,查询字符串是 isbn=156384627,片段标识符是 doc。授权机构可以进一步划分为用户信息、主机和端口。

URL 提供了如下方法用于对这些信息进行访问:

public String getProtocol()
public String getAuthority()
public String getUserInfo()
public String getHost() 
public int getPort()    
public String getPath()
public String getQuery()
public String getRef()

1.4 判等

URL 重写了 equals() 和 hashCode() 方法,当且仅当两个 URL 都指向相同主机、端口和路径上的相同资源,而且有相同的片段标识符和查询字符串,才认为这两个 URL 是相等的。实际上 equals() 方法会尝试用 DNS 解析主机,来判断两个主机是否相同,这是一个阻塞的 I/O 操作,因此应尽量避免将 URL 存储在依赖 equals() 的数据结构中,如 HashMap。更好的选择是 URI,可以在必要时通过 toURI() 方法将 URL 转换成 URI。

URL 还有一个 sameFile() 方法,可以检查两个 URL 是否指向相同的资源。该方法与 equals() 方法基本相同,也会进行 DNS 查询,但 sameFile() 方法不会考虑片段标识符。

2. URLEncoder

Java 提供了 URLEncoder 类可以对字符串进行编码,通过一个静态的 encode 方法完成编码:

public static String encode(String s, String enc) throws UnsupportedEncodingException

该方法会对所有非 ASCII 字符编码,其中 ‘A’-‘Z’,’a’-‘z’,’0’-‘9’,’-‘,’_’,’·’ 和 ‘‘ 等字符保持不变,空格被编码成 ‘+’,所有其他字符被编码成 “%XY” 形式的字节序列,其中 0xXY 为该字节十六进制数。尽管这个方法允许指定字符集,但是最好只选择 *UTF-8,因为与其他任何编码方式相比,UTF-8 与 URI 类、现代 Web 浏览器和其他软件更兼容。

注意,该方法也会对一些特殊界定符 / & = : 进行编码,它不会去判断这些字符在 URL 中如何使用,因此我们在使用时最好逐部分地对 URL 进行编码,而不是在一个方法调用中对整个 URL 编码。

3. URLDecoder

对应的 URLDecoder 类有一个静态的 decode 方法,它会对用 x-www-form-urlencoded 格式编码的字符串进行解码。也就是说,将所有百分号转义字符转换成对应的字符:

public static String decode(String s, String enc) throws UnsupportedEncodingException

如果字符串包含一个百分号,但其后没有两个十六进制数字,或者字符串解码为无效的序列,就会抛出一个 IllegalArgumentException 异常。由于这个方法对非转义字符不做处理,所以可以传入整个 URL。