十二、访问网络
应用通常需要访问网络来获取资源(例如图像)或与远程可执行实体(例如 web 服务)通信。一个网络是一组互联的节点(计算设备如平板电脑和外围设备如扫描仪或激光打印机),可以在网络用户之间共享。
注意内部网是位于一个组织内部的网络,而互联网是将组织相互连接起来的网络。互联网是网络的全球网络。
内部网和互联网经常使用TC P/IP()在节点之间进行通信。TCP/IP 包括传输控制协议(TCP) ,是面向连接的协议;用户数据报协议(UDP) ,无连接协议;以及互联网协议(IP) ,这是 TCP 和 UDP 执行其任务的基本协议。
java.net 包提供了支持在相同或不同主机(基于计算机的 TCP/IP 节点)上运行的进程(执行应用)之间的 TCP/IP 的类型。在这一章中,我首先介绍了执行基于套接字和基于 URL 的通信的类型。然后,我将介绍底层网络接口和接口地址类型以及面向 cookie 的类型。
注意安卓应用必须获得访问网络的许可。在清单文件中包含即可获得权限。
面向网络的应用经常要处理字节序()这个话题,指的是在一个较大的数据项的表示中,可单独寻址的子组件的排序。例如,给定一个 16 位的短整数,你是先传输最高有效字节还是最低有效字节?
通过套接字访问网络
两个进程通过套接字进行通信,套接字是这些进程之间通信链路的端点。每个端点由一个标识主机的 IP 地址和一个标识运行在该主机上的进程的端口号来标识。
IP 地址和端口号
IP 地址是 32 位或 128 位无符号整数,其唯一地标识网络主机或一些其他网络节点(例如,路由器)。
通常将 32 位 IP 地址指定为以句点分隔的十进制记数法表示的四个 8 位整数部分,其中每个部分是范围从 0 到 255 的十进制整数,并且通过句点(例如,127.0.0.1)与下一个部分分隔开。一个 32 位的 IP 地址通常被称为互联网协议第 4 版(IPv4)地址 (参见【http://en.wikipedia.org/wiki/IPv4】)。
通常将 128 位 IP 地址指定为冒号分隔的十六进制表示法中的八个 16 位整数部分,其中每个部分都是从 0 到 FFFF 的十六进制整数,并通过冒号与下一个部分分隔开(例如,1080:0:0:0:8:800:200C:417A)。一个 128 位的 IP 地址通常被称为互联网协议第 6 版(IPv6)地址 (参见【http://en.wikipedia.org/wiki/IPv6】)。
一个端口号 是一个 16 位的整数,唯一地标识一个进程,该进程是消息的最终来源或接收者。小于 1024 的端口号保留给标准进程。例如,端口号 25 传统上识别用于发送电子邮件的简单邮件传输协议(SMTP)进程,尽管端口号 587 已经在很大程度上淘汰了这个旧端口号(参见 http://en.wikipedia.org/wiki/Smtp)。
一个进程将一个消息(一个字节序列)写入它的套接字。底层平台的网络管理软件部分将消息分解成一系列的数据包(可寻址的消息块,通常被称为 IP 数据报 ),并将它们转发到另一个进程的套接字,在那里它们被重新组合成原始消息进行处理。
图 12-1 显示了两个套接字如何在 TCP/IP 环境中通信。
图 12-1T3。两个进程使用套接字进行通信
在图 12-1 的上下文中,假设进程 A 想要向进程 b 发送一条消息。进程 A 将该消息发送到其套接字,套接字带有进程 b 的目的套接字地址。主机 A 的网络管理软件(通常称为协议栈 )获得该消息,并将其简化为一系列数据包,每个数据包都包括目的主机的 IP 地址和端口号。然后,网络管理软件通过主机 A 的网络接口卡(NIC) 将这些数据包发送到主机 b。
注意网卡的各种网络接口是计算机和网络之间的连接。
主机 B 的协议栈通过网卡接收数据包,并将它们重新组装成原始消息(数据包可能接收顺序错误),然后通过套接字提供给进程 B。当进程 B 与进程 a 进行通信时,这种情况正好相反。
网络管理软件使用 TCP 在两台主机之间建立持续对话,在对话中来回发送消息。在此对话发生之前,这些主机之间会建立连接。建立连接后,TCP 进入一种模式,在这种模式下,它发送消息包并等待它们正确到达的回复(或者当回复由于某种网络问题而没有到达时,等待超时)。这种模式重复并保证可靠的连接。有关此模式的详细信息,请查看en . Wikipedia . org/wiki/Tcp _ receive _ window # Flow _ control
。
因为建立连接需要时间,发送数据包也需要时间(因为接收应答确认是必要的,也因为超时),所以 TCP 很慢。另一方面,不需要连接和数据包确认的 UDP 要快得多。缺点是 UDP 不太可靠(无法保证数据包的传送、排序或防止重复数据包,尽管 UDP 使用校验和来验证数据是否正确),因为没有确认。此外,UDP 仅限于单包对话。
java.net 包提供了套接字、服务器套接字和其他套接字后缀类,用于执行基于 TCP 或基于 UDP 的通信。在研究这些类之前,您需要理解套接字地址和套接字选项。
套接字地址
一个套接字后缀类的实例与一个由 IP 地址和端口号组成的套接字地址 相关联。这些类通常依靠 InetAddress 类来表示套接字地址的 IPv4 或 IPv6 地址部分,并分别表示端口号。
注意 InetAddress 依赖其 Inet4Address 子类来表示 IPv4 地址,并依赖其 Inet6Address 子类来表示 IPv6 地址。
InetAddress 声明了几个类方法来获得一个 InetAddress 实例。这些方法包括以下内容:
- inet address[]getAllByName(String host)返回一个 InetAddress es 数组,该数组存储了与主机相关联的 IP 地址。您可以向此参数传递域名(如" tutortutor.ca “)或 IP 地址(如"70.33.247.10”)参数。(要了解域名,查看维基百科的“域名”条目[
en.wikipedia.org/wiki/Domain_name
])。传递 null 以获得一个 InetAddress 实例,该实例存储了 loopback 接口(一个基于软件的网络接口,传出数据作为传入数据返回)的 IP 地址。当找不到指定的主机的 IP 地址或者为全局 IPv6 地址指定了作用域标识符时,该方法抛出未知主机异常。 - InetAddress getby address(byte[]addr)返回给定原始 IP 地址的 InetAddress 对象。传递给 addr 的参数按照网络字节顺序(最高有效字节在前),其中最高顺序字节存储在 addr[0] 中。对于 IPv4 地址, addr 数组的长度必须是 4 个字节,对于 IPv6 地址必须是 16 个字节。当数组有另一个长度时,这个方法抛出 UnknownHostException 。
- inet address getby address(String hostName,byte[] ipAddress) 根据主机名和 IP 地址参数返回一个 InetAddress 实例。当数组的长度既不是 4 也不是 16 时,该方法抛出 UnknownHostException 。
- inet address get by name(String host)基于 host 参数返回一个 InetAddress 实例,该参数可以是一个机器名(如 “tutortutor.ca” )或其 IP 地址的文本表示。向主机传递 null 会导致返回一个表示环回接口地址的 InetAddress 实例。
- inet address get LocalHost()返回本地主机(当前主机)的地址,用主机名 localhost 或 IP 地址表示,通常表示为 127.0.0.1 (IPv4)或::1 (IPv6)。当本地主机不能被解析成地址时,这个方法抛出 UnknownHostException 。
在您获得一个 InetAddress 实例后,您可以通过调用实例方法来询问它,例如 byte[] getAddress() ,它返回这个 InetAddress 对象的原始 IP 地址(按照网络字节顺序),以及 boolean is loopbackaddress(),它确定这个 InetAddress 实例是否代表一个环回地址。
Java 1.4 引入了抽象的 SocketAddress 类来表示“没有协议附件”的套接字地址(这个类的创建者可能已经预料到 Java 最终会支持低级别的通信协议,而不是广泛流行的 Internet 协议。)
SocketAddress 由具体的 InetSocketAddress 类子类化,将套接字地址表示为 IP 地址和端口号。它还可以表示主机名和端口号,并将尝试解析主机名。
InetSocketAddress 实例是通过调用 InetSocketAddress(inet address addr,int port) 等构造函数创建的。创建实例后,可以调用 InetAddress getAddress() 和 int getPort() 等方法返回套接字地址组件。
插座选项
一个套接字后缀类的实例共享了套接字选项的概念,这些选项是用于配置套接字行为的参数。套接字选项由在 SocketOptions 接口中声明的常量描述:
- IP_MULTICAST_IF :指定组播包的出网接口(在多宿主【多网卡】主机上)。Android 没有实现这个选项。
- IP_MULTICAST_IF2 :使用接口索引指定多播数据包的输出网络接口。
- IP_MULTICAST_LOOP :启用或禁用组播数据报的本地回环。
- IP_TOS :为 TCP 或 UDP 套接字设置 IP 报头中的服务类型(IPv4)或流量类别(IPv6)字段。
- SO_BINDADDR :获取套接字的本地地址绑定。Android 没有实现这个选项。
- SO_BROADCAST :启用套接字发送广播消息。
- SO_KEEPALIVE :开启 socket keepalive。
- SO_LINGER :指定当还有一些缓冲数据要发送时,关闭套接字时等待的秒数。
- SO_OOBINLINE :启用 TCP 紧急数据的内联接收。
- SO_RCVBUF :设置或获取最大套接字接收缓冲区大小(以字节为单位)。
- SO_REUSEADDR :启用套接字的重用地址。
- SO_SNDBUF :设置或获取最大套接字发送缓冲区大小(以字节为单位)。
- SO_TIMEOUT :指定阻塞接受或读取/接收(但不是写入/发送)套接字操作的超时时间(以毫秒为单位)。(永远不要挡!)
- TCP_NODELAY :禁用 Nagle 算法()。换句话说,这个选项允许您在这个套接字上立即发送数据(但可能同样有效)。
SocketOptions 还声明了以下设置和获取这些选项的方法:
- void setOption(int optID,Object value)
- 物体补片(int optID)
optID 是前述常数之一,值是合适类的对象(例如, java.lang.Boolean )。
SocketOptions 由抽象的 SocketImpl 和 DatagramSocketImpl 类实现。这些类的具体实例由各种套接字后缀的类包装。因此,您不能调用这些方法。相反,您使用由套接字提供的类型安全的 setter 和 getter 方法来设置和获取这些选项。
例如, Socket 声明 void setKeepAlive(布尔 keepAlive) 用于设置 SO_KEEPALIVE 选项, ServerSocket 声明 void setSoTimeout(int time out)用于设置 SO_TIMEOUT 选项。查看关于套接字后缀类的文档,了解这些和其他套接字选项方法。
注意适用于 DatagramSocket 的 Socket 选项方法也适用于它的 MulticastSocket 子类。
套接字和服务器套接字
Socket 和 ServerSocket 类支持客户端进程(例如,运行在平板电脑上的应用)和服务器进程(例如,运行在互联网服务供应器的计算机上的应用,提供对万维网的访问)之间基于 TCP 的通信。因为套接字与 java.io.InputStream 和 java.io.OutputStream 类相关联,所以基于套接字类的套接字通常被称为流套接字 。
Socket 支持客户端 Socket 的创建。为此,它声明了几个构造函数,包括下面的一对:
- Socket(inet address dst address,int dstPort) 创建一个流套接字,在指定的 IP 地址(由 dstAddress 描述)连接到指定的端口号(由 dstPort 描述)。当创建套接字时发生 I/O 错误时,该构造函数抛出 Java . io . io exception;Java . lang . illegalargumentexception 当传递给 dstPort 的参数不在端口值的有效范围内,即 0 到 65535;以及 dstAddress 为 null 时的 Java . lang . nullpointerexception。
- Socket(String dstName,int dstPort) 创建一个流套接字,并将其连接到由 dstName 标识的主机上由 dstPort 标识的端口。当 dstName 为 null 时,这个构造函数相当于调用 Socket(inet address . get byname(null),port) 。它抛出与前面的构造函数相同的 IOException 和 IllegalArgumentException 实例。但是,它不是抛出 NullPointerException ,而是在无法确定主机的 IP 地址时抛出 unknown hoste exception。
在通过这些构造函数创建了一个套接字实例之后,在连接到远程主机套接字地址之前,它被绑定到一个任意的本地主机套接字地址。绑定使客户机套接字地址对服务器套接字可用,以便服务器进程可以通过服务器套接字与客户机进程通信。
套接字提供了额外的构造函数。例如, Socket() 和 Socket(Proxy proxy) 创建未绑定和未连接的套接字。在使用这些套接字之前,必须通过调用 void bind(Socket address local addr)将其绑定到本地套接字地址,然后必须通过调用 Socket 的 connect() 方法(例如 void connect(Socket address remote addr))进行连接。
注意代理是出于安全目的位于内部网和互联网之间的主机。代理设置通过代理类的实例来表示,帮助套接字通过代理进行通信。
另一个构造函数是 Socket(inet address dst address,int dstPort,InetAddress localAddr,int localPort) ,它让你通过 localAddr 和 localPort 指定自己的本地主机套接字地址。该构造函数自动绑定到本地套接字地址,然后尝试连接到 dstAddress 上的远程 dstPort 。
在创建了一个 Socket 实例,并可能在该实例上调用了 bind() 和 connect() 之后,应用调用 Socket 的 InputStream getInputStream()和 output stream getOutputStream()方法来获取从 Socket 读取字节的输入流和向 Socket 写入字节的输出流。此外,应用经常调用套接字的 void close() 方法 来关闭不再需要 I/O 的套接字。
以下示例演示了如何在本地主机上创建一个绑定到端口号 9999 的套接字,然后访问其输入和输出流—为简洁起见,将忽略异常:
Socket socket = new Socket("localhost", 9999);
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
// Do some work with the socket.
socket.close();
ServerSocket 支持创建服务器端套接字。为此,它声明了以下四个构造函数:
- ServerSocket() 创建未绑定的服务器套接字。通过调用 ServerSocket 的两个 bind() 方法中的任何一个,可以将这个套接字绑定到一个特定的套接字地址(客户端套接字与之通信)。绑定使服务器套接字地址对客户端套接字可用,以便客户端进程可以通过客户端套接字与服务器进程通信。当试图打开套接字时发生 I/O 错误时,该构造函数抛出 IOException 。
- ServerSocket(int port) 创建一个服务器套接字,该套接字绑定到指定的端口值和与主机的一个网卡相关联的 IP 地址。当您通过 0 到端口时,会选择一个任意端口号。可以通过调用 int getLocalPort() 来检索端口号。来自客户端的传入连接请求的最大队列长度设置为 50。如果连接请求在队列已满时到达,连接将被拒绝。当试图打开套接字时发生 I/O 错误时,该构造函数抛出 IOException ,当 port 的值超出指定的有效端口值范围(0 到 65535,包括 0 和 65535)时,该构造函数抛出 IllegalArgumentException。
- ServerSocket(int port,int backlog) 相当于前面的构造函数,但是它也允许您通过向 backlog 传递一个正整数来指定传入连接的最大队列长度。
- ServerSocket(int port,int backlog,InetAddress localAddress) 相当于前面的构造函数,但它也允许您指定服务器套接字绑定到的不同 IP 地址。(当通过 null 时,选择任何地址。)这个构造函数对于有多个网卡的机器很有用,并且您希望在特定的网卡上监听连接请求。
通过这些构造函数创建服务器套接字后,服务器应用进入一个循环,首先调用服务器套接字的套接字接受()方法 来监听连接请求,并返回一个套接字实例,让它与相关的客户端套接字进行通信。然后,它与客户端套接字进行通信,以执行某种处理。当处理完成时,服务器套接字调用客户端套接字的 close() 方法 来终止它与客户端的连接。
注意 ServerSocket 声明了一个 void close() 方法,用于在终止服务器应用之前关闭服务器套接字。当应用终止时,未关闭的套接字会自动关闭。
以下示例演示了如何在当前主机上创建一个绑定到端口 9999 的服务器套接字,侦听传入的连接请求,返回它们的套接字,在这些套接字上执行工作,以及关闭套接字—为简洁起见,将忽略异常:
ServerSocket ss = new ServerSocket(9999);
while (true)
{
Socket socket = ss.accept();
// obtain socket input/output streams and communicate with socket
socket.close();
}
accept() 方法 调用阻塞,直到有连接请求可用,然后返回一个套接字对象,以便服务器应用可以与其关联的客户端通信。通信发生后,套接字被关闭。当应用退出时,服务器套接字会自动关闭。
这个例子假设套接字通信发生在服务器应用的主线程上,这在处理需要时间来执行时是一个问题,因为服务器对传入连接请求的响应时间减少了。为了加快响应时间,通常需要与工作线程上的套接字进行通信,如下例所示:
ServerSocket ss = new ServerSocket(9999);
while (true)
{
final Socket s = ss.accept();
new Thread(new Runnable()
{
@Override
public void run()
{
// obtain socket input/output streams and communicate with socket
try { s.close(); } catch (IOException ioe) {}
}
}).start();
}
每当一个连接请求到达, accept() 返回一个 socket 实例,然后创建一个 java.lang.Thread 对象,它的 runnable 访问那个 Socket,以便与工作线程上的 Socket 进行通信。
提示虽然这个例子使用了线程类,但是你也可以使用一个执行器(参见第十章)来代替。
我已经创建了 EchoClient 和 EchoServer 应用,演示了 Socket 和 ServerSocket 。清单 12-1 展示了 EchoClient 的源代码。
清单 12-1。向服务器回显数据并从服务器接收数据
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class EchoClient
{
public static void main(String[] args)
{
if (args.length != 1)
{
System.err.println("usage : java EchoClient message");
System.err.println("example: java EchoClient \"This is a test.\"");
return;
}
try
{
Socket socket = new Socket("localhost", 9999);
OutputStream os = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os);
PrintWriter pw = new PrintWriter(osw);
pw.println(args[0]);
pw.flush();
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
System.out.println(br.readLine());
}
catch (UnknownHostException uhe)
{
System.err.println("unknown host: " + uhe.getMessage());
}
catch (IOException ioe)
{
System.err.println("I/O error: " + ioe.getMessage());
}
}
}
EchoClient 首先验证它已经收到了一个命令行参数,然后创建一个套接字,该套接字将连接到本地主机的端口 9999 上运行的进程。
创建套接字后, EchoClient 获得一个输出流,用于向套接字写入字符串。因为输出流只能处理一个字节序列,所以 Java . io . output streamwriter 和 java.io.PrintWriter 类(参见第十一章)用于将输出字符的编写器连接到面向字节的输出流。
在实例化了 PrintWriter 之后, EchoClient 调用其 void println(String str) 方法来编写后跟换行符的字符串。随后调用 void flush() 方法 以确保所有未决数据都被写入服务器。
EchoClient 现在获得一个输入流,用于读取作为字节序列的字符串。然后它通过实例化 Java . io . inputstreamreader 和 java.io.BufferedReader 将阅读器(输入字符)连接到面向字节的输入流(参见第十一章)。
最后, EchoClient 调用 BufferedReader 的 String readLine() 方法从套接字中读取后跟换行符的字符。( readLine() 在返回的字符串中不包含换行符。)这些字符后跟一个换行符,然后被写入标准输出。
注意在一个长时间运行的应用中,当不再需要套接字时,可以通过调用其 void close() 方法来显式关闭套接字实例。为了简洁起见,我选择在这个和大多数剩余的 Socket 后缀的类示例中不这样做。
清单 12-2 展示了 EchoServerT4 的源代码。
清单 12-2。从客户端接收数据并将其发送回客户端
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer
{
public static void main(String[] args) throws IOException
{
System.out.println("Starting echo server. . .");
ServerSocket ss = new ServerSocket(9999);
while (true)
{
Socket s = ss.accept();
try
{
InputStream is = s.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String msg = br.readLine();
System.out.println(msg);
OutputStream os = s.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os);
PrintWriter pw = new PrintWriter(osw);
pw.println(msg);
pw.flush();
}
catch (IOException ioe)
{
System.err.println("I/O error: " + ioe.getMessage());
}
finally
{
try
{
s.close();
}
catch (IOException ioe)
{
assert false; // shouldn't happen in this context
}
}
}
}
}
EchoServer 首先向标准输出输出一个介绍性的消息,然后创建一个服务器套接字来监听端口 9999 上的连接。然后它进入一个无限循环,其中每次迭代调用 ServerSocket 的 Socket accept() 方法进行阻塞,直到接收到一个连接,然后返回一个代表这个连接的 Socket 对象。
获得套接字后, EchoServer 获得一个输入流,用于从套接字中读取。因为输入流只能处理一个字节序列,所以使用 InputStreamReader 和 BufferedReader 类将输入字符的阅读器连接到面向字节的输入流。
EchoServer 现在获得一个输出流,用于将字符串写成一个字节序列。然后它通过实例化 OutputStreamWriter 和 PrintWriter 将输出字符的编写器连接到面向字节的输出流。
将消息输出到标准输出后, EchoServer 调用 flush() 将输出刷新到客户端。然后关闭客户端套接字。
为了试验这些应用,将 EchoClient.java 和 EchoServer.java 复制到同一个目录,并打开两个当前目录的控制台窗口。编译每个源文件并在一个窗口中执行 Java echo server—您应该会看到一条介绍性消息,尽管您可能首先需要在防火墙(en . Wikipedia . org/wiki/Firewall _(computing
)上启用端口 9999。启动服务器后,回显以下命令,将文本回显到两个窗口:
java EchoClient "This is a test."
你应该注意到这是一个测试。"在两个窗口中。
DatagramSocket 和多播 Socket
DatagramSocket 和 MulticastSocket 类让您可以在一对主机( DatagramSocket )或多个主机( MulticastSocket )之间执行基于 UDP 的通信。使用任何一个类,您都可以通过数据报包 传递单向消息,这些数据报包是与 DatagramPacket 类的实例相关联的字节数组。
注意虽然你可能认为 Socket 和 ServerSocket 就是你所需要的,但是 DatagramSocket (及其 MulticastSocket 子类)有它们的用途。例如,考虑一个场景,其中一组机器需要偶尔告诉服务器它们还活着。偶尔丢失消息或者消息没有按时到达都没有关系。另一个例子是周期性广播股票价格的低优先级股票报价机。当一个包裹没有到达时,很可能下一个包裹会到达,然后你会收到最新价格的通知。在实时应用中,及时的交付比可靠或有序的交付更重要。
DatagramPacket 声明了几个构造函数,其中 DatagramPacket(byte[] buf,int length) 是最简单的。这个构造函数要求你将字节数组和整数参数传递给 buf 和 length ,其中 buf 是存储要发送或接收的数据的数据缓冲区, length (必须小于或等于 buf.length )指定要发送/接收的字节数(从 buf[0】开始)。
下面的示例演示了此构造函数:
byte[] buffer = new byte[100];
DatagramPacket dgp = new DatagramPacket(buffer, buffer.length);
注意额外的构造函数让你在 buf 中指定一个偏移量,用来标识第一个传出或传入字节的存储位置,和/或让你指定一个目的套接字地址。
DatagramSocket 描述了 UDP 通信链路的客户端或服务器端的套接字。虽然这个类声明了几个构造函数,但我发现在本章中使用客户端的 DatagramSocket() 构造函数和服务器端的 DatagramSocket(int port) 构造函数很方便。当无法创建数据报套接字或将数据报套接字绑定到本地端口时,任一构造函数都会抛出 SocketException 。
应用实例化 DatagramSocket 后,调用 void send(datagram packet DGP)和 void receive(datagram packet DGP)发送和接收数据报包。
清单 12-3 展示了服务器环境中的 DatagramPacket 和 DatagramSocket 。
清单 12-3。从客户端接收数据报数据包,并将它们回显到客户端
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class DGServer
{
final static int PORT = 10000;
public static void main(String[] args) throws SocketException
{
System.out.println("Server is starting");
DatagramSocket dgs = new DatagramSocket(PORT);
try
{
System.out.println("Send buffer size = " + dgs.getSendBufferSize());
System.out.println("Receive buffer size = " +
dgs.getReceiveBufferSize());
byte[] data = new byte[100];
DatagramPacket dgp = new DatagramPacket(data, data.length);
while (true)
{
dgs.receive(dgp);
System.out.println(new String(data));
dgs.send(dgp);
}
}
catch (IOException ioe)
{
System.err.println("I/O error: " + ioe.getMessage());
}
}
}
清单 12-3 的 main() 方法首先创建一个 DatagramSocket 对象,并将套接字绑定到本地主机上的端口 10000。然后它调用 DatagramSocket 的 int getSendBufferSize() 和 int getReceiveBufferSize()方法来获取 SO_SNDBUF 和 SO_RCVBUF 套接字选项的值,然后输出这些值。
注意套接字与底层平台发送和接收缓冲区相关联,通过调用 getSendBufferSize() 和 getReceiveBufferSize() 来访问它们的大小。同样,它们的大小可以通过调用 DatagramSocket 的 void setReceiveBufferSize(int size)和 void setSendBufferSize(int size)方法来设置。虽然您可以调整这些缓冲区的大小来提高性能,但是 UDP 有一个实际的限制。在 IPv4 下,可以发送或接收的 UDP 数据包的最大大小是 65,507 字节,这是从 65,535 减去 8 字节 UDP 头和 20 字节 IP 头值得出的。虽然您可以指定一个更大的发送/接收缓冲区值,但这样做是浪费,因为最大的数据包被限制为 65,507 字节。此外,试图发送或接收缓冲区长度超过 65,507 字节的数据包会导致 IOException 。
main() 接下来实例化 DatagramPacket 以准备从客户端接收数据报分组,然后将该分组回送到客户端。它假设数据包的大小不超过 100 字节。
最后, main() 进入一个无限循环,接收一个包,输出包内容,并将包发送回客户端——客户端的寻址信息存储在 DatagramPacket 中。
编译清单 12-3(javac DGServer.java)并运行应用( java DGServer )。您应该观察到与如下所示相同或相似的输出:
Server is starting
Send buffer size = 8192
Receive buffer size = 8192
清单 12-4 演示了客户端上下文中的 DatagramPacket 和 DatagramSocket 。
列表 12-4。向服务器发送数据报数据包并从服务器接收数据包
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
public class DGClient
{
final static int PORT = 10000;
final static String ADDR = "localhost";
public static void main(String[] args) throws SocketException
{
System.out.println("client is starting");
DatagramSocket dgs = new DatagramSocket();
try
{
byte[] buffer;
buffer = "Send me a datagram".getBytes();
InetAddress ia = InetAddress.getByName(ADDR);
DatagramPacket dgp = new DatagramPacket(buffer, buffer.length, ia,
PORT);
dgs.send(dgp);
byte[] buffer2 = new byte[100];
dgp = new DatagramPacket(buffer2, buffer.length, ia, PORT);
dgs.receive(dgp);
System.out.println(new String(dgp.getData()));
}
catch (IOException ioe)
{
System.err.println("I/O error: " + ioe.getMessage());
}
}
}
清单 12-4 类似于清单 12-3 ,但是有一个很大的不同。我使用 DatagramPacket(byte[] buf,int length,InetAddress,int port) 构造函数在数据报包中指定服务器的目的地,这个目的地恰好是本地主机上的端口 10000。 send() 方法调用将数据包路由到这个目的地。
编译清单 12-4(javac DGClient.java)并运行应用( java DGClient )。假设 DGServer 也在运行,您应该在 DGClient 的命令窗口中观察到以下输出(以及 DGServer 的命令窗口中该输出的最后一行):
client is starting
Send me a datagram
多播套接字 描述基于 UDP 的多播会话的客户端或服务器端的套接字。两个常用的构造函数是 MulticastSocket() (创建一个不绑定到端口的组播套接字)和 MulticastSocket(int port) (创建一个绑定到指定端口的组播套接字)。当发生 I/O 错误时,任一构造函数都会抛出 IOException 。
什么是多播?
前面的例子已经演示了单播,它发生在服务器向单个客户端发送消息的时候。然而,也可以向多个客户端广播相同的消息(例如,向已经向在线程序注册以接收该通知的一组家长的所有成员发送“学校因恶劣天气关闭”通知);这个活动被称为多播。
服务器通过向一个特殊的 IP 地址(称为多播组地址 )和一个特定的端口(由端口号指定)发送一系列数据报分组来进行多播。想要接收这些数据报数据包的客户端创建一个使用该端口号的多播套接字。他们通过指定特殊 IP 地址的加入群组操作请求加入群组。此时,客户端可以接收发送到该组的数据报数据包,甚至可以向其他组成员发送数据报数据包。在客户端已经读取了它想要读取的所有数据报分组之后,它通过应用指定特殊 IP 地址的离开组操作将自己从组中移除。
IPv4 地址 224.0.0.1 到 239.255.255.255(含)保留用作多播组地址。
清单 12-5 展示了一个组播服务器。
清单 12-5。多播数据报数据包
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
public class MCServer
{
final static int PORT = 10000;
public static void main(String[] args)
{
try
{
MulticastSocket mcs = new MulticastSocket();
InetAddress group = InetAddress.getByName("231.0.0.1");
byte[] dummy = new byte[0];
DatagramPacket dgp = new DatagramPacket(dummy, 0, group, PORT);
int i = 0;
while (true)
{
byte[] buffer = ("line " + i).getBytes();
dgp.setData(buffer);
dgp.setLength(buffer.length);
mcs.send(dgp);
i++;
}
}
catch (IOException ioe)
{
System.err.println("I/O error: " + ioe.getMessage());
}
}
}
清单 12-5 的 main() 方法首先通过 MulticastSocket() 构造函数创建一个 MulticastSocket 实例。多播套接字不需要绑定到端口号,因为端口号是作为随后创建的 DatagramPacket 实例的一部分与多播组的 IP 地址(231.0.0.1)一起指定的。(虚拟数组的存在是为了防止 NullPointerException 对象从 DatagramPacket 构造函数中抛出——该数组不用于存储要广播的数据。)
此时, main() 进入一个无限循环,首先从一个 java.lang.String 实例创建一个字节数组,并使用平台的默认字符编码(参见第十一章)将 Unicode 字符转换成字节。(尽管无关的 java.lang.StringBuilder 和 String 对象是通过表达式 “line” + i 在每次循环迭代中创建的,但我并不担心它们对这个简短的一次性应用中的垃圾收集的影响。)
随后通过调用 void setData(byte[] buf) 方法将该数据缓冲区分配给 DatagramPacket 实例,然后将数据报数据包广播给与端口 10000 和组播 IP 地址 231.0.0.1 相关联的组的所有成员。
编译清单 12-5(javac MCServer.java)并运行这个应用( java MCServer )。你不应该观察任何输出。
清单 12-6 展示了一个组播客户端。
清单 12-6。接收多播数据报数据包
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
public class MCClient
{
final static int PORT = 10000;
public static void main(String[] args)
{
try
{
MulticastSocket mcs = new MulticastSocket(PORT);
InetAddress group = InetAddress.getByName("231.0.0.1");
mcs.joinGroup(group);
for (int i = 0; i < 10; i++)
{
byte[] buffer = new byte[256];
DatagramPacket dgp = new DatagramPacket(buffer, buffer.length);
mcs.receive(dgp);
byte[] buffer2 = new byte[dgp.getLength()];
System.arraycopy(dgp.getData(), 0, buffer2, 0, dgp.getLength());
System.out.println(new String(buffer2));
}
mcs.leaveGroup(group);
}
catch (IOException ioe)
{
System.err.println("I/O error: " + ioe.getMessage());
}
}
}
清单 12-6 的 main() 方法首先通过 MulticastSocket(int port) 构造函数创建一个绑定到端口 10000 的 MulticastSocket 实例。
然后它获得一个包含组播组 IP 地址 231.0.0.1 的 InetAddress 对象,并通过调用 MulticastSocket 的 void join group(inet address mcastaddr)方法使用该对象加入该地址的组。
main() 接下来接收 10 个数据报包,打印它们的内容,并通过调用 MulticastSocket 的 void leave group(inet address mcastaddr)方法以相同的组播 IP 地址作为参数来离开组。
注意 joinGroup() 和 leaveGroup() 当试图加入或离开组时发生 I/O 错误,或者当 IP 地址不是多播 IP 地址时,抛出 IOException 。
因为客户端不知道字节数组到底有多长,所以它假设 256 个字节来确保数据缓冲区能够容纳整个数组。如果它试图打印出返回的数组,在实际数据被打印出来后,您会看到许多空白空间。
为了消除这个空间,客户端调用 DatagramPacket 的 int getLength() 方法来获得数组的实际长度,创建具有该长度的第二个字节数组( buffer2 ),并使用 system . array copy()—第八章中讨论的方法——将这个字节复制到 buffer2 。在将这个字节数组转换成一个字符串对象后(通过字符串(byte[] bytes) 构造函数,它使用平台的默认字符集——参见第十一章了解字符集),它将结果字符打印到标准输出流。
编译清单 12-6(【MCClient.java】??)并运行这个应用( java MCClient )。您应该观察到类似如下的输出:
line 462615
line 462616
line 462617
line 462618
line 462619
line 462620
line 462621
line 462622
line 462623
line 462624
通过 URL 访问网络
统一资源定位符(URL) 是指定资源(例如,网页)在基于 TCP/IP 的网络(例如,因特网)上的位置的字符串。此外,它还提供了检索该资源的方法。例如,tutortutor . ca
是一个定位我的网站主页的 URL。 http:// 前缀指定必须使用超文本传输协议(http) 来检索位于 tutortutor.ca 的网页,该协议是位于 TCP/IP 之上的用于定位 HTTP 资源(例如网页)的高级协议。
urn 和 uri
统一资源名称(URN) 是一个字符串,它命名一个资源,但不提供访问该资源的方法(该资源可能不可用)。例如, urn:isbn:9781430231561 识别出一本名为Learn Java for Android Development的进度书,仅此而已。
urn 和 URL 是统一资源标识符(URIs) 的例子,是用于标识名称(urn)和资源(URL)的字符串。每个骨灰盒和网址也是一个 URI。
java.net 包提供了用于访问基于 URL 的资源的 URL 和 URLConnection 类。它还提供了用于编码和解码 URL 的 URLEncoder 和 URLDecoder 类,以及用于执行基于 URI 的操作(例如,相对化)并返回包含结果的 URL 实例的 URI 类。为了简洁起见,我在本章中不讨论 URI 。
URL 和 URLConnection
URL 类表示 URL,并提供对它们所引用的资源的访问。每个 URL 实例明确地标识一个互联网资源。
URL 声明了几个构造函数,其中 URL(String s) 是最简单的。该构造函数从传递给 s 的字符串参数中创建一个 URL 实例,演示如下:
try
{
URL url = new URL(" [`tutortutor.ca`](http://tutortutor.ca) ");
}
catch (MalformedURLException murle)
{
// handle the exception
}
本例创建一个使用 HTTP 访问位于 的网页的 URL 对象 http://tutortutor.ca 。如果我指定了一个非法的 URL(例如, foo ,构造函数将抛出 malformedurexception(一个 IOException 子类)。
虽然您通常会指定 http:// 作为协议前缀,但这不是您唯一的选择。例如,当资源位于本地主机上时,您还可以指定文件:/// 。此外,当资源存储在 jar 文件中时,您可以将 jar: 添加到 http:// 或 file:/// 的前面,如下所示:
jar:file:///C:./rt.jar!/java/util/Timer.class
jar: 前缀表示您想要访问一个 jar 文件资源(例如,一个存储的类文件)。 file:/// 前缀标识本地主机的资源位置,在本例中是 Windows C:硬盘上当前目录中的 rt.jar (Java 5 的运行时 jar 文件)。
JAR 文件的路径后面跟一个感叹号(!)将 JAR 文件路径与 JAR 资源路径分开,JAR 资源路径恰好是这个 JAR 文件中的/Java/util/timer . classclass file 条目(需要前导的 / 字符)。
注意Oracle 的 Java 参考实现中的 URL 类支持附加协议,包括 ftp 。
在创建了一个 URL 对象之后,您可以调用各种 URL 方法来访问 URL 的各个部分。例如, String getProtocol() 返回 URL 的协议部分(例如, http )。还可以通过调用 InputStream openStream() 方法来检索资源。
openStream() 创建一个到资源的连接,并返回一个 InputStream 实例,用于从该连接读取资源数据,如下所示:
InputStream is = url.openStream();
int ch;
while ((ch = is.read()) != −1)
System.out.print((char) ch);
注意对于 HTTP 连接,会创建一个内部套接字,该套接字连接到通过 URL 的域名/IP 地址标识的服务器上的 HTTP 端口 80,除非您在域名/IP 地址后附加一个不同的端口号(例如,HTTP://tutortutor . ca:8080)。
我已经创建了一个 ListResource 应用,通过使用这个类获取资源并列出其内容来演示 URL 。清单 12-7 展示了 ListResource 的源代码。
清单 12-7。列出通过 URL 命令行参数识别的资源的内容
import java.io.InputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
public class ListResource
{
public static void main(String[] args)
{
if (args.length != 1)
{
System.err.println("usage: java ListResource url");
return;
}
try
{
URL url = new URL(args[0]);
InputStream is = url.openStream();
try
{
int ch;
while ((ch = is.read()) != −1)
System.out.print((char) ch);
}
catch (IOException ioe)
{
is.close();
}
}
catch (MalformedURLException murle)
{
System.err.println("invalid URL");
}
catch (IOException ioe)
{
System.err.println("I/O error: " + ioe.getMessage());
}
}
}
ListResource 首先验证它已经收到了一个命令行参数,然后试图用这个参数实例化 URL 。假设 URL 是有效的,这意味着 malformedurexception 没有被抛出, ListResource 调用 URL 实例上的 openStream() ,并继续将资源内容列出到标准输出中。
编译这段源代码(javac ListResource.java)并执行 Java list resourcetutortutor . ca
。以下输出显示了返回网页的短前缀:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" " [`www.w3.org/TR/html4/strict.dtd`](http://www.w3.org/TR/html4/strict.dtd) ">
<html>
<head>
<title>
TutorTutor -- /main
</title>
<link rel="stylesheet" href="/shared/styles.css" media="screen">
. . .
openStream() 是一个调用 openConnection()的便捷方法。getInputStream() 。每个 URL 的 URL connection open connection()和 URL connection open connection(Proxy Proxy)方法都返回一个 URLConnection 类的实例,它代表应用和 URL 之间的通信链接。
URLConnection 为您提供了对客户端/服务器通信的额外控制。例如,您可以使用此类将内容输出到接受内容的各种资源。相比之下, URL 只让你通过 openStream() 输入内容。
URLConnection 声明了各种方法,包括:
- InputStream getInputStream()返回从这个打开的连接中读取的输入流。
- output stream get output stream()返回写入这个打开的连接的输出流。
- void setDoInput(boolean doInput)指定这个 URLConnection 对象支持(传递真到 doInput )或者不支持(传递假到 doInput )输入。因为 true 是默认值,所以您只需将 true 传递给该方法,以记录您执行输入的意图。
- void setdoooutput(boolean doooutput)指定这个 URLConnection 对象支持(传递真到 doooutput)或者不支持(传递假到 doooutput)输出。因为默认值为 false,所以必须先调用此方法,然后才能执行输出。
- void setRequestProperty(String field,String newValue) 设置请求属性(如 HTTP 的 accept 属性)。当字段已经存在时,其值将被指定的值覆盖。
下面的例子展示了如何从预先创建的 url 变量引用的 URL 对象中获取一个 URLConnection 对象,启用其 dooput 属性,并获取一个输出流以写入资源:
URLConnection urlc = url.openConnection();
urlc.setDoOutput(true);
OutputStream os = urlc.getOutputStream();
URLConnection 被 HttpURLConnection和 JarURLConnection 子类化。这些类声明特定于使用 HTTP 协议或与基于 JAR 的资源交互的常量和/或方法。
注意为了简洁起见,我建议您参考关于 URLConnection 、 HttpURLConnection 和 JarURLConnection 的 Java 文档,以了解关于这些类的更多信息。
URLEncoder 和 URLDecoder
超文本标记语言(HTML)允许您将表单引入网页,向页面访问者请求信息。填写完表单的字段后,访问者单击表单的 Submit 按钮(它可以指定除 Submit 之外的内容),表单内容(字段名称和值)被发送到服务器程序。在发送表单内容之前,web 浏览器通过替换空格和其他 URL 非法字符对该数据进行编码,并将内容的 Internet 媒体类型(也称为多用途 Internet 邮件扩展[MIME]类型)设置为 application/x-www-form-urlencoded。
注意数据是为 HTTP POST 和 HTTP GET 操作编码的。与 POST 不同,GET 需要一个查询字符串 (a?-包含编码内容的前缀字符串)附加到服务器程序的 URL。
java.net 包提供了 URLEncoder 和 URLDecoder 类来帮助你完成编码和解码表单内容的任务。
URLEncoder 应用以下编码规则:
- 字母数字字符“A”到“Z”、“A”到“Z”以及“0”到“9”保持不变。
- 特殊字符。“、“-”、“*”和“_”保持不变。
- 空格字符“”被转换为加号“+”。
- 所有其他字符都是不安全的,首先使用某种编码方案将其转换为 1 个或更多字节。然后,每个字节由三个字符的字符串% xy 表示,其中 xy 是该字节的 2 位十六进制表示。推荐使用的编码方案是 UTF-8。然而,出于兼容性原因,当没有指定编码时,使用平台的默认编码。
例如,使用 UTF-8 作为编码方案,字符串 “string ü@foo-bar” 转换为" string+% C3 % BC % 40 foo-bar "。在 UTF-8 中,字符ü编码为 2 字节 C3(十六进制)和 BC(十六进制);字符@被编码为 1 字节 40(十六进制)。
URLEncoder 声明了以下用于编码字符串的类方法:
String encode(String s, String enc)
该方法使用由 enc 指定的编码方案,将传递给 s 的字符串参数翻译成 application/x-www-form-urlencoded 格式。它使用提供的编码方案来获取不安全字符的字节,并在不支持 enc 的值时抛出 Java . io . unsupportedencodingexception。
URLDecoder 应用以下解码规则:
- 字母数字字符“A”到“Z”、“A”到“Z”以及“0”到“9”保持不变。
- 特殊字符。“、“-”、“*”和“_”保持不变。
- 加号“+”被转换为空格字符“”。
- % xy 形式的序列将被视为表示一个字节,其中 xy 是 8 位的 2 位十六进制表示。然后,所有连续包含一个或多个这些字节序列的子字符串将被其编码将产生这些连续字节的字符所替换。可以指定用于解码这些字符的编码方案;未指定时,使用平台的默认编码。
URLDecoder 声明了以下用于解码编码字符串的类方法:
String decode(String s, String enc)
这个方法使用由 enc 指定的编码方案解码一个 application/x-www-form-urlencoded 字符串。提供的编码用于确定% xy 形式的任何连续序列代表什么字符。不支持 enc 的值时抛出 UnsupportedEncodingException。
解码器处理非法编码的字符串有两种可能的方式。它要么不处理非法字符,要么抛出 IllegalArgumentException 。解码器采用哪种方法由实现来决定。
注环球网联盟推荐(www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars)UTF-8 作为 encode() 和 decode() 的编码方案。不这样做可能会引入不兼容性。
我已经创建了一个 ED (编码/解码)应用,在前面的 “string ü@foo-bar” 和" string+% C3 % BC % 40 foo-bar "示例的上下文中演示了 URLEncoder 和 URLDecoder 。清单 12-8 给出了应用的源代码。
清单 12-8。对编码字符串进行编码和解码
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
public class ED
{
public static void main(String[] args) throws UnsupportedEncodingException
{
String encodedData = URLEncoder.encode("string ü@foo-bar", "UTF-8");
System.out.println(encodedData);
System.out.println(URLDecoder.decode(encodedData, "UTF-8"));
}
}
当您运行此应用时,它会生成以下输出:
string+%C3%BC%40foo-bar
string ü@foo-bar
注意查看维基百科的“百分比编码”主题(【http://en.wikipedia.org/wiki/Percent-encoding】)以了解更多关于 URL 编码的信息(以及更准确的百分比编码术语)。
访问网络接口和接口地址
NetworkInterface 类根据名称(例如 le0 )和分配给该接口的 IP 地址列表来表示网络接口。虽然网络接口通常在物理网卡上实现,但它也可以在软件中实现;例如环回接口(这对测试客户端很有用)。
表 12-1 给出了网络接口的方法。
表 12-1 。网络接口方法
方法 | 描述 |
---|---|
布尔等于(对象对象) | 将这个网络接口对象与对象进行比较。当且仅当对象不是 null 并且表示与该对象相同的网络接口时,结果为真。(当两个 NetworkInterface 对象的名称和地址相同时,表示同一个网络接口。) |
静态网络接口 getByInetAddress(InetAddress 地址) | 返回给定的地址对应的网络接口,如果没有接口有该地址则返回空。该方法在发生 I/O 错误时抛出 SocketException ,在地址为 null 时抛出 NullPointerException 。 |
静态网络接口 getByName(字符串接口名称) | 返回带有指定名称的网络接口,如果没有该网络接口则返回 null。该方法在 I/O 错误时抛出 SocketException ,当 interfaceName 为 null 时抛出 NullPointerException 。 |
字符串 getDisplayName() | 返回这个网络接口的显示名称(描述网络设备的可读字符串)。在 Android 上,这是由 getName() 返回的相同字符串。 |
字节[]get 硬件地址() | 返回包含该网络接口硬件地址的字节数组,该地址通常被称为媒体访问控制(MAC) 地址。当接口没有 MAC 地址,或者无法访问该地址时(可能用户没有足够的权限),该方法返回 null。当一个 I/O 错误发生时,这个方法抛出 SocketException 。 |
枚举getinetddress() | 返回一个枚举(一次迭代的结果),其中包含绑定到该网络接口的所有地址或其子集。 |
清单<介面位址> getInterfaceAddresses() | 返回一个包含这个网络接口的接口地址的 java.util.List 。 |
int get tu() | 返回该网络接口的最大传输单位(MTU) 。当一个 I/O 错误发生时,这个方法抛出 SocketException 。 |
字符串 getName() | 返回该网络接口的名称(如 eth0 或 lo )。 |
静态枚举<网络接口> getNetworkInterfaces() | 返回这台机器上的所有网络接口,或者当找不到网络接口时返回 null。当一个 I/O 错误发生时,这个方法抛出 SocketException 。 |
网络接口 get arent() | 当此网络接口是子接口时,返回此网络接口的父接口 NetworkInterface 。当此网络接口没有父接口时,或者当它是物理(非虚拟)接口时,此方法返回 null。(一个物理网络接口在逻辑上可以分为多个虚拟子接口,常用于路由和交换。这些子接口可以组织成一个层次结构,其中物理网络接口作为根接口。) |
枚举<网络接口>获取子接口() | 返回包含连接到此网络接口的虚拟子接口的枚举。例如, eth0:1 是 eth0 的子接口。 |
int hashCode() | 此方法被覆盖,因为等于()被覆盖。 |
布尔值回溯() | 当此网络接口将传出数据作为传入数据反射回自身时,返回 true。当一个 I/O 错误发生时,这个方法抛出 SocketException 。 |
boolean ispointtopint() | 当此网络接口是点对点(例如,通过调制解调器的 PPP 连接)时,返回 true。当一个 I/O 错误发生时,这个方法抛出 SocketException 。 |
布尔 isUp() | 当该网络接口 up (路由条目已建立)运行(平台资源已分配)时返回 true。当一个 I/O 错误发生时,这个方法抛出 SocketException 。 |
布尔 isVirtual() | 当该网络接口是虚拟子接口时,返回 true。在某些平台上,虚拟子接口是作为物理网络接口的子接口创建的网络接口,并具有不同的设置(例如,地址或 MTU)。通常,接口的名称是父接口的名称,后跟一个冒号(:)和一个标识子接口的数字,因为一个物理网络接口可以连接多个虚拟子接口。 |
布尔支持多播() | 当该网络接口支持组播时返回 true。当一个 I/O 错误发生时,这个方法抛出 SocketException 。 |
字符串 toString() | 返回这个网络接口的字符串表示。 |
您可以使用这些方法来收集有关平台网络接口的有用信息。例如,清单 12-9 给出了一个应用,它遍历所有网络接口,调用表 12-1 中列出的方法,这些方法获取网络接口的名称和显示名称,确定网络接口是否是环回接口,确定网络接口是否已启动并正在运行,获取 MTU,确定网络接口是否支持多播,并枚举所有网络接口的虚拟子接口。
清单 12-9。枚举所有网络接口
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Collections;
import java.util.Enumeration;
public class NetInfo
{
public static void main(String[] args) throws SocketException
{
Enumeration<NetworkInterface> eni;
eni = NetworkInterface.getNetworkInterfaces();
for (NetworkInterface ni: Collections.list(eni))
{
System.out.println("Name = " + ni.getName());
System.out.println("Display Name = " + ni.getDisplayName());
System.out.println("Loopback = " + ni.isLoopback());
System.out.println("Up and running = " + ni.isUp());
System.out.println("MTU = " + ni.getMTU());
System.out.println("Supports multicast = " + ni.supportsMulticast());
System.out.println("Sub-interfaces");
Enumeration<NetworkInterface> eni2;
eni2 = ni.getSubInterfaces();
for (NetworkInterface ni2: Collections.list(eni2))
System.out.println(" " + ni2);
System.out.println();
}
}
}
提示Java . util . collections 类的 ArrayListlist(EnumerationEnumeration)方法对于将遗留枚举转换为现代数组列表非常有用。
编译清单 12-9(Java NetInfo.java)并执行这个应用( java NetInfo )。当我在 Windows 7 平台上运行 NetInfo 时,我观察到以以下输出开始的信息:
Name = lo
Display Name = Software Loopback Interface 1
Loopback = true
Up and running = true
MTU = −1
Supports multicast = true
Sub-interfaces
Name = net0
Display Name = WAN Miniport (SSTP)
Loopback = false
Up and running = false
MTU = −1
Supports multicast = true
Sub-interfaces
完整的输出揭示了一些网络接口的不同 MTU 大小。每个大小代表一条消息的最大长度,该消息可以装入一个 IP 数据报 中,而不需要将消息分割成多个 IP 数据报。这种片段化对性能有影响,尤其是在网络游戏的环境中。仅仅因为这个原因, getMTU() 方法就是 NetworkInterface 的一个有价值的成员。
getInterfaceAddresses() 方法 返回一列 InterfaceAddress 对象,每个对象包含一个网络接口的 IP 地址以及广播地址和子网掩码(IPv4)或网络前缀长度(IPv6)。
表 12-2 给出了接口地址的方法 ??。
表 12-2 。接口地址方法
方法 | 描述 |
---|---|
布尔等于(对象对象) | 将这个接口地址对象与对象进行比较。当对象也是接口地址并且两个对象包含相同的接口地址,相同的子网掩码/网络前缀长度(取决于 IPv4 或 IPv6),以及相同的广播地址时,返回 true。 |
inetaddress get ddress() | 返回这个接口地址的 IP 地址,作为一个地址对象。 |
InetAddress get broadcast() | 返回此接口地址的广播地址(IPv4)或 null(IPv6);IPv6 不支持广播地址。 |
短 getNetworkPrefixLength() | 返回此接口地址的网络前缀长度(IPv6)或子网掩码(IPv4)。Oracle 的 Java 文档显示 128 (::1/128)和 10 (fe80::203:baff:fe27:1243/10)是典型的 IPv6 值。典型的 IPv4 值为 8 (255.0.0.0)、16 (255.255.0.0)和 24 (255.255.255.0)。 |
int hashCode() | 返回这个接口地址的哈希码。哈希码是内地址的哈希码、广播地址(如果存在)哈希码和网络前缀长度的组合。 |
字符串 toString() | 返回这个接口地址的字符串表示。这种表示形式为地址/网络前缀长度【广播地址】。 |
清单 12-10 ,它扩展了清单 12-9 (去掉了几行),枚举所有网络接口,输出它们的显示名称,枚举每个网络接口的接口地址,输出接口地址信息。
清单 12-10。枚举所有网络接口和接口地址
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
public class NetInfo
{
public static void main(String[] args) throws SocketException
{
Enumeration<NetworkInterface> eni;
eni = NetworkInterface.getNetworkInterfaces();
for (NetworkInterface ni: Collections.list(eni))
{
System.out.println("Name = " + ni.getName());
List<InterfaceAddress> ias = ni.getInterfaceAddresses();
Iterator<InterfaceAddress> iter = ias.iterator();
while (iter.hasNext())
System.out.println(iter.next());
System.out.println();
}
}
}
编译清单 12-10(javac NetInfo.java)并执行这个应用 ( java NetInfo )。当我在 Windows 7 平台上运行 NetInfo 时,我观察到以下信息:
Name = lo
/127.0.0.1/8 [/127.255.255.255]
/0:0:0:0:0:0:0:1/128 [null]
Name = net0
Name = net1
Name = net2
Name = ppp0
Name = eth0
Name = eth1
Name = eth2
Name = ppp1
Name = net3
Name = eth3
/192.xxx.xxx.xxx/xx [/192.xxx.xxx.xxx]
/fe80:0:0:0:xxxx:xxxx:xxxx:xxxx%xx/xx [null]
Name = net4
/fe80:0:0:0:0:xxxx:xxxx:xxxx%xx/xxx [null]
Name = net5
/2001:0:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/x [null]
/fe80:0:0:0:xxxx:xxxx:xxxx:xxxx%xx/xx [null]
Name = eth4
Name = eth5
Name = eth6
Name = eth7
Name = eth8
管理 Cookies
服务器应用通常使用 HTTP cookies (状态对象)——cookies来保存客户端上的少量信息。例如,购物车中当前所选商品的标识符可以存储为 cookies。将 cookie 存储在客户机上比存储在服务器上更可取,因为可能会有数百万个 cookie(取决于网站的受欢迎程度)。在这种情况下,不仅服务器需要大量的存储空间来存储 cookie,而且搜索和维护 cookie 也非常耗时。
注意查看维基百科的“HTTP cookie”条目()快速复习一下 cookie。
服务器应用将 cookie 作为 HTTP 响应的一部分发送给客户端。客户端(例如,网络浏览器)将 cookie 作为 HTTP 请求的一部分发送给服务器。在 Java 5 之前,应用使用 URLConnection 类(及其 HttpURLConnection 子类)来获取 HTTP 响应的 cookie 并设置 HTTP 请求的 cookie。字符串 getHeaderFieldKey(int n)和字符串 getHeaderField(int n) 方法用于访问响应的 Set-Cookie 头,而 void setRequestProperty(String key,String value) 方法用于创建请求的 Cookie 头。
注 RFC 2109: HTTP 状态管理机制(【www.ietf.org/rfc/rfc2109.txt】)描述了 Set-Cookie 和 Cookie 头。
Java 5 引入了抽象的 CookieHandler 类作为回调机制,将 HTTP 状态管理连接到 HTTP 协议处理程序(想想具体的 HttpURLConnection 子类)。一个应用通过 CookieHandler 类的 void set default(CookieHandler cHandler)类方法安装一个具体的 CookieHandler 子类作为系统范围的 cookie 处理程序。一个伴随的 CookieHandler get default()类方法返回这个 cookie 处理程序,当一个系统范围的 cookie 处理程序没有被安装时,它为空。
HTTP 协议处理器访问响应和请求头。这个处理程序调用系统范围的 cookie 处理程序的 void put(URI uri,映射<字符串,列表<字符串> > responseHeaders) 方法将响应 cookie 存储在 cookie 缓存中,并调用映射<字符串,列表<字符串> > get(URI uri,映射<字符串,列表<字符串> >请求头)方法从该缓存中获取请求 cookie。与 Java 5 不同,Java 6 引入了一个具体的实现 CookieHandler ,以便 HTTP 协议处理程序和应用可以使用 cookies。
具体的 CookieManager 类扩展了 CookieHandler 来管理 cookies。一个 CookieManager 对象被初始化如下:
- 用饼干店 来存放饼干。cookie store 基于 CookieStore 界面。
- 使用 cookie 策略 来确定接受哪些 cookie 进行存储。cookie 策略基于 CookiePolicy 接口。
通过调用 CookieManager() 构造函数或 CookieManager(CookieStore store,CookiePolicy policy) 构造函数来创建 cookie 管理器。 CookieManager() 构造函数使用默认的内存中 cookie 存储和默认的仅从原始服务器接受 cookie 策略,用 null 参数调用后一个构造函数。除非您计划创建自己的 CookieStore 和 CookiePolicy 实现,否则您很可能会使用默认构造函数。以下示例创建并建立一个新的 CookieManager 对象作为系统范围的 cookie 处理程序:
CookieHandler.setDefault(new CookieManager());
除了前面提到的构造函数, CookieManager 声明了下面的方法:
- Map < String,List>get(URI uri,Map < String,List>request headers)返回从 cookie store 获得的路径与 uri 的路径匹配的 Cookie 的 Cookie 和 Cookie2 请求头的不可变映射。虽然这个方法的默认实现没有使用 requestHeaders ,但是子类可以使用它。发生 I/O 错误时,抛出 IOException 。
- CookieStore getCookieStore()返回 cookie 管理器的 cookie 存储。
- void put(URI uri,Map < String,List>response headers)存储所有适用的 cookie,这些 Cookie 的 Set-Cookie 和 Set-Cookie2 响应头是从指定的 uri 值中检索的,并(与所有其他响应头一起)放在 Cookie 存储中不可变的 responseHeaders 映射中。发生 I/O 错误时,抛出 IOException 。
- void setCookiePolicy(CookiePolicy CookiePolicy)将 cookie 管理器的 cookie 策略设置为 CookiePolicy 之一。ACCEPT_ALL (接受所有 cookies), CookiePolicy。ACCEPT_NONE (不接受 cookies),或者 CookiePolicy。ACCEPT_ORIGINAL_SERVER (仅接受来自原始服务器的 cookies 这是默认设置)。向该方法传递空值对当前策略没有影响。
与由 HTTP 协议处理程序调用的 get() 和 put() 方法相反,应用使用 getCookieStore() 和 setCookiePolicy() 方法 。考虑清单 12-11 中的。
清单 12-11。列出特定域的所有 Cookies】
import java.io.IOException;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.HttpCookie;
import java.net.URL;
import java.util.List;
public class ListAllCookies
{
public static void main(String[] args) throws IOException
{
if (args.length != 1)
{
System.err.println("usage: java ListAllCookies url");
return;
}
CookieManager cm = new CookieManager();
cm.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
CookieHandler.setDefault(cm);
new URL(args[0]).openConnection().getContent();
List<HttpCookie> cookies = cm.getCookieStore().getCookies();
for (HttpCookie cookie: cookies)
{
System.out.println("Name = " + cookie.getName());
System.out.println("Value = " + cookie.getValue());
System.out.println("Lifetime (seconds) = " + cookie.getMaxAge());
System.out.println("Path = " + cookie.getPath());
System.out.println();
}
}
}
清单 12-11 描述了一个命令行应用,它从它的单个域名参数中获取并列出所有的 cookies。
在创建 cookie 管理器并调用 setCookiePolicy() 来设置 cookie 管理器的策略以接受所有 cookie 之后,listallcokies 安装 cookie 管理器作为系统范围的 cookie 处理程序。接下来,它连接到由命令行参数标识的域并读取内容(通过 URL 的对象 getContent() 方法 )。
Cookie 存储通过 getCookieStore() 获得,并通过其 Listget cookies()方法检索所有未过期的 cookie。对于这些 HttpCookie 、 String getName() 、 String getValue() 和其他 HttpCookie 方法被调用来返回特定于 Cookie 的信息。
调用 Java list all cookies【http://java.net】得到如下输出:
Name = SESSe2db433431725a35762565c526a602d3
Value = 29va73kqorof3k2tmchn1fka11
Lifetime (seconds) = 3971
Path = /
注意有关 cookie 管理的更多信息,包括展示如何创建自己的 CookiePolicy 和 CookieStore 实现的示例,请查看Java 教程的“使用 cookie”课程(docs . Oracle . com/javase/Tutorial/networking/Cookies/index . html
)。
练习
以下练习旨在测试您对第十二章内容的理解:
- 定义网络。
- 什么是内部网,什么是互联网?
- 内部网和互联网经常用什么在节点之间通信?
- 定义主机。
- 什么是插座?
- 如何识别套接字?
- 定义 IP 地址。
- 什么是数据包?
- 套接字地址由哪些元素组成?
- 识别用于表示 IPv4 和 IPv6 地址的 InetAddress 子类。
- 什么是环回接口?
- 是非判断:在网络字节顺序中,最低有效字节排在最前面。
- 本地主机是如何表示的?
- 定义套接字选项。
- 如何描述插座选项?
- 是非判断:通过调用 void setOption(int optID,Object value) 方法来设置套接字选项。
- 为什么基于套接字类的套接字通常被称为流套接字?
- 在一个套接字实例的上下文中,绑定完成了什么?
- 定义代理。Java 如何表示代理设置?
- 是非判断: ServerSocket() 构造函数创建一个绑定的服务器套接字。
- DatagramSocket 和 MulticastSocket 类之间有什么区别?
- 什么是数据报包?
- 单播和组播有什么区别?
- 什么是 URL?
- 什么是骨灰盒?
- 是非判断:URL 和 urn 也是 URIs。
- 当你将 null 传递给 s 时, URL(String s) 构造函数做什么?
- openStream() 等价于什么?
- 是非判断:您需要调用 URLConnection 的 void setDoInput(boolean doInput)方法,并将 true 作为参数,然后才能从 web 资源输入内容。
- URLEncoder 遇到空格字符怎么办?
- 网络接口类完成什么?
- 什么是 MAC 地址?
- MTU 代表什么,目的是什么?
- 是非判断: NetworkInterface 的 getName() 方法返回一个人类可读的名称。
- IPv4 下 InterfaceAddress 的 getNetworkPrefixLength() 方法返回什么?
- 定义 HTTP Cookie。
- 为什么将 cookies 存储在客户机上比存储在服务器上更可取?
- 确定用于处理 cookies 的四种 java.net 类型。
- 修改清单 12-1 的 EchoClient 源代码以显式关闭套接字。
- 修改清单 12-2 的 EchoServer 源代码,当一个名为 kill 的文件出现在服务器启动的目录中时,退出 while 循环并显式关闭服务器套接字。在这个文件出现之后,服务器可能不会立即死亡,因为它很可能在等待(通过 accept() 调用)一个传入的客户端连接。但是,它应该在服务下一个传入连接后终止。
摘要
网络是一组可以在网络用户之间共享的互连节点。内部网是位于组织内部的网络,而互联网是将组织相互连接起来的网络。互联网是网络的全球网络。
java.net 包提供了支持运行在相同或不同主机上的进程之间的 TCP/IP 的类型。两个进程通过套接字进行通信,套接字是这些进程之间通信链路的端点。每个端点由标识主机的 IP 地址和标识该主机上运行的进程的端口号来标识。
一个进程将消息写入其套接字,底层操作系统的网络管理软件部分将消息分解成一系列数据包,并将其转发给另一个进程的套接字,另一个进程将接收到的数据包重新组合成原始消息以供自己处理。
网络管理软件使用 TCP 在两台主机之间建立持续对话,在对话中来回发送消息。在此对话发生之前,这些主机之间会建立连接。建立连接后,TCP 进入一种模式,在这种模式下,它发送消息包并等待消息包正确到达的回复(或者当回复由于某种网络问题而没有到达时等待超时)。这种模式重复并保证可靠的连接。
因为建立连接需要时间,发送数据包也需要时间(因为接收应答确认是必要的,也因为超时),所以 TCP 很慢。另一方面,不需要连接和数据包确认的 UDP 要快得多。缺点是 UDP 不太可靠(无法保证数据包的传送、排序或防止重复数据包,尽管 UDP 使用校验和来验证数据是否正确),因为没有确认。此外,UDP 仅限于单包对话。
一个套接字后缀类的实例与一个由 IP 地址和端口号组成的套接字地址相关联。这些类通常依靠 InetAddress 类来表示套接字地址的 IPv4 或 IPv6 地址部分,并分别表示端口号。
一个 Socket 后缀类的实例共享 socket options 的概念,Socket options 是配置 Socket 行为的参数。套接字选项由在 SocketOptions 接口中声明的常量描述。
套接字和服务器套接字类支持客户端进程和服务器进程之间基于 TCP 的通信。 Socket 支持创建客户端套接字,而 ServerSocket 支持创建服务器端套接字。
通过 DatagramSocket 和 MulticastSocket 类,您可以在一对主机( DatagramSocket )或任意多的主机( MulticastSocket )之间执行基于 UDP 的通信。无论使用哪一类,您都可以通过数据报数据包传递单向消息。
两个通过套接字通信的进程演示了低级网络访问。Java 还支持通过 URL 进行高级访问,这些 URL 标识资源并指定它们在基于 TCP/IP 的网络上的位置。
URL 由 URL 类表示,该类提供对它们所引用的资源的访问。 URLConnection 为您提供了对客户端/服务器通信的额外控制。例如,您可以使用此类将内容输出到接受内容的各种资源。相比之下, URL 只允许你通过 openStream() 输入内容。
HTML 允许您将表单引入网页,向页面访问者请求信息。java.net 包提供了 URLEncoder 和 URLDecoder 类来帮助你完成编码和解码表单内容的任务。
NetworkInterface 类根据名称(例如 le0 )和分配给该接口的 IP 地址列表来表示网络接口。 NetworkInterface 的 get interface addresses()方法返回一列 InterfaceAddress 对象,每个对象包含一个网络接口的 IP 地址以及广播地址和子网掩码(IPv4)或网络前缀长度(IPv6)。
服务器应用通常使用 HTTP cookies(状态对象)——简称 cookie——在客户机上保存少量信息。Java 提供了用于处理 cookies 的 CookieHandler 和 CookieManager 类以及 CookiePolicy 和 CookieStore 接口。
本章主要讨论网络环境中的 I/O。新的 I/O 使您能够以更高性能的方式执行基于文件和基于网络的 I/O。第十三章向你介绍 Java 新的 I/O API。
十三、迁移到新的 I/O
第十一章和第十二章向你介绍了 Java 的经典 I/O API。第十一章从 java.io.RandomAccessFile 类、流和写/读程序的角度介绍了经典的 I/O。第十二章介绍了套接字和 URL 方面的经典 I/O。
现代操作系统提供了 Java 经典 I/O API 不支持的强大 I/O 特性。特性包括内存映射文件 I/O (能够将进程的部分虚拟内存[参见【http://en.wikipedia.org/wiki/Virtual_memory】]映射到文件的某个部分,以便写入或读取进程内存空间的该部分实际上是写入/读取文件的相关部分), 就绪选择(非阻塞 I/O 之上的一个步骤,将检查 I/O 流就绪以执行读写操作所涉及的工作卸载给操作系统),以及文件锁定(一个进程阻止其他进程访问文件或以某种方式限制访问的能力)。
Java 1.4 引入了更强大的 I/O 架构,支持内存映射文件 I/O、就绪选择、文件锁定等等。这个架构由缓冲区、通道、选择器、正则表达式和字符集组成,通常被称为新 I/O (NIO )。
注意正则表达式是 NIO 的一部分(参见 http://jcp.org/en/jsr/detail?id=51 51—),因为 NIO 完全是关于性能的,正则表达式对于以高性能的方式扫描文本(从 I/O 源读取)非常有用。(NIO 中还包含了一个简单的 printf 样式的格式化工具,它基于 java.util.Formatter 类【参见第十章】。)
第十三章从缓冲区、通道和正则表达式的角度向您介绍 NIO。(为了简洁起见,我不讨论选择器和字符集。)
注意 NIO 是一个庞大的架构;一个全面的讨论可以占据一整本书。为了简洁起见,我在这一章中省略了许多细节。
使用缓冲区
NIO 基于缓冲区。缓冲器 是存储要发送到 I/O 服务(用于执行输入/输出的装置)或从其接收的固定量数据的对象。它位于一个应用和一个通道之间,通道将缓冲的数据写入服务,或者从服务读取数据并将其存入缓冲区。
缓冲液具有四种特性:
- 容量 :缓冲区可以存储的数据项总数。容量是在创建缓冲区时指定的,以后不能更改。
- Limit :缓冲区中“活”数据项的数量。不应写入或读取从零开始的任何项目。
- 位置 :可以读取的下一个数据项的从零开始的索引或者可以写入数据项的位置。
- 标记 :可以召回的零基位置。该标记最初没有定义。
这四个属性的关系如下:0 <=标记< =位置< =限制< =容量。
图 13-1 展示了一个新创建的面向字节的缓冲器。
图 13-1T3。面向字节的缓冲器的逻辑布局包括未定义的标记、当前位置、极限和容量
图 13-1 的缓冲区最多可以存储 7 个元素。标记最初是未定义的,位置最初被设置为 0,限制最初被设置为容量,该容量指定可以存储在缓冲区中的最大字节数。您只能访问位置 0 到 6。
缓冲区及其子级
缓冲区由从抽象 java.nio.Buffer 类派生的类实现。表 13-1 描述了缓冲的方法
表 13-1。 缓冲的方法
方法 | 描述 |
---|---|
对象数组() | 返回支持该缓冲区的数组。该方法旨在允许数组支持的缓冲区更有效地传递给本机代码。具体的子类覆盖这个方法,并通过协变返回类型提供更强类型的返回值(在第四章中讨论)。当该缓冲区由数组支持但为只读时,该方法抛出 Java . nio . readonlybufferexception,当该缓冲区不由可访问的数组支持时,该方法抛出 Java . lang . unsupportedoperationexception。 |
int arrayOffset() | 返回该缓冲区的后备数组中第一个缓冲区元素的偏移量。当该缓冲区由数组支持时,缓冲区位置 p 对应数组索引 p + arrayOffset() 。 在调用此方法之前调用 hasArray() 以确保此缓冲区具有可访问的后备数组。当该缓冲区由数组支持但为只读时,该方法抛出 ReadOnlyBufferException ,当该缓冲区不由可访问的数组支持时,该方法抛出 UnsupportedOperationException。 |
int capacity() | 返回这个缓冲区的容量。 |
缓冲清除() | 清空这个缓冲区。将位置设置为零,将限制设置为容量,并丢弃标记。这个方法并不删除缓冲区中的数据,但它被命名为删除缓冲区中的数据,因为它最常用于这种情况。 |
缓冲翻转() | 翻转这个缓冲器。极限被设置为当前位置,然后该位置被设置为零。当标记被定义时,它被丢弃。 |
boolean hasArray() | 当此缓冲区由数组支持并且不是只读的时,返回 true 否则,返回 false。当该方法返回 true 时, array() 和 arrayOffset() 可以被安全调用。 |
布尔哈斯剩余() | 当至少一个元素保留在该缓冲区中时(即,在当前位置和限制之间),返回 true 否则,返回 false。 |
布尔 isDirect() | 当这个缓冲区是直接字节缓冲区时返回 true(在本节后面讨论);否则,返回 false。 |
布尔 is readonly() | 当此缓冲区为只读时返回 true 否则,返回 false。 |
int limit() | 返回此缓冲区的限制。 |
缓冲区限制(int newLimit) | 将该缓冲区的限制设置为新限制。当位置大于新限时,位置被设置为新限。当标记被定义并且大于新限制时,标记被丢弃。当 newLimit 为负或大于该缓冲区的容量时,该方法抛出 Java . lang . illegalargumentexception;否则,它返回这个缓冲区。 |
缓冲标记() | 将该缓冲区的标记设置在其位置,并返回该缓冲区。 |
int position() | 返回这个缓冲区的位置。 |
缓冲位置(int newPosition) | 将该缓冲器的位置设置为新位置。当标记被定义且大于新位置时,标记被丢弃。当 newPosition 为负或大于该缓冲区的当前限制时,该方法抛出 IllegalArgumentException;否则,它返回这个缓冲区。 |
int 剩余() | 返回当前位置和限制之间的元素数。 |
缓冲复位 () | 将该缓冲器的位置重置到先前标记的位置。调用这个方法既不会改变也不会丢弃标记的值。该方法在标记未设置时抛出 Java . nio . invalidmackexception;否则,它返回这个缓冲区。 |
缓冲倒带() | 倒带然后返回这个缓冲区。该位置被设置为零,并且该标记被丢弃。 |
.
表 13-1 显示许多缓冲区的方法返回缓冲区引用,这样你就可以将实例方法调用链接在一起(第三章讨论方法调用链接)。例如,不要指定以下三行。
buf.mark();
buf.position(2);
buf.reset();
您可以更方便地指定以下行:
buf.mark().position(2).reset();
表 13-1 还显示了所有的缓冲区都可以被读取,但不是所有的缓冲区都可以被写入——例如,一个由只读的内存映射文件支持的缓冲区。不得写入只读缓冲区;否则,抛出 ReadOnlyBufferException。当您在尝试写入缓冲区之前不确定该缓冲区是否可写时,调用 isReadOnly() 。
注意缓冲区不是线程安全的。当您想要从多个线程访问一个缓冲区时,您必须使用同步。
java.nio 包包含了几个扩展 Buffer 的抽象类,除了 Boolean 之外,每个原语类型一个: ByteBuffer , CharBuffer , DoubleBuffer , FloatBuffer , IntBuffer , LongBuffer 和 ShortBuffer 。此外,这个包包括作为抽象字节缓冲子类的 MappedByteBuffer 。
注意操作系统执行面向字节的 I/O,您使用 ByteBuffer 创建面向字节的缓冲区,用于存储写入目标或从源读取的字节。其他原始类型的缓冲区类允许您创建多字节视图缓冲区(稍后讨论),这样您就可以在概念上根据字符、双精度浮点值、32 位整数等执行 I/O。然而,I/O 操作实际上是作为字节流来执行的。
清单 13-1 从字节缓冲区、容量、极限、位置和剩余元素方面展示了缓冲区类。
清单 13-1。演示面向字节的缓冲区
import java.nio.Buffer;
import java.nio.ByteBuffer;
public class BufferDemo
{
public static void main(String[] args)
{
Buffer buffer = ByteBuffer.allocate(7);
System.out.println("Capacity: " + buffer.capacity());
System.out.println("Limit: " + buffer.limit());
System.out.println("Position: " + buffer.position());
System.out.println("Remaining: " + buffer.remaining());
System.out.println("Changing buffer limit to 5");
buffer.limit(5);
System.out.println("Limit: " + buffer.limit());
System.out.println("Position: " + buffer.position());
System.out.println("Remaining: " + buffer.remaining());
System.out.println("Changing buffer position to 3");
buffer.position(3);
System.out.println("Position: " + buffer.position());
System.out.println("Remaining: " + buffer.remaining());
System.out.println(buffer);
}
}
清单 13-1 的 main() 方法首先需要获得一个缓冲区。它不能实例化缓冲区类,因为该类是抽象的。而是使用 ByteBuffer 类及其 allocate() 类方法来分配图 13-1 所示的 7 字节缓冲区。 main() 然后调用各种 Buffer 方法来演示容量、限制、位置和剩余元素。
编译清单 13-1(Java BufferDemo.java)并运行这个应用( java BufferDemo )。您应该观察到以下输出:
Capacity: 7
Limit: 7
Position: 0
Remaining: 7
Changing buffer limit to 5
Limit: 5
Position: 0
Remaining: 5
Changing buffer position to 3
Position: 3
Remaining: 2
java.nio.HeapByteBuffer[pos=3 lim=5 cap=7]
最终的输出行显示分配给缓冲区的字节缓冲区实例实际上是包私有 java.nio.HeapByteBuffer 类的一个实例。
深度缓冲区
前面对 Buffer 类的讨论已经让您对 NIO 缓冲区有了一些了解。然而,还有更多要探索的。本节通过探索缓冲区创建、写入和读取缓冲区、缓冲区翻转、缓冲区标记、缓冲区子类操作、字节排序和直接缓冲区,带您深入了解缓冲区。
注意虽然原始类型的缓冲区类有相似的能力,但是 ByteBuffer 是最大的和最通用的。毕竟,字节是操作系统用来传输数据项的基本单位。因此,我将使用字节缓冲来演示大多数缓冲操作。我还将使用 CharBuffer 来增加多样性。
缓冲区创建
ByteBuffer 和其他原始类型的缓冲区类声明了创建该类型缓冲区的各种类方法。例如, ByteBuffer 声明了下面的类方法来创建 ByteBuffer 实例:
- byte buffer allocate(int capacity)用指定的容量值分配一个新的字节缓冲区。它的位置是 0,它的极限是它的容量,它的标记是未定义的,每个元素都被初始化为 0。它有一个后备数组,数组偏移量为 0。当容量为负时,该方法抛出 IllegalArgumentException 。
- byte buffer allocated direct(int capacity)用指定的容量值分配一个新的直接字节缓冲区。它的位置是 0,它的极限是它的容量,它的标记是未定义的,每个元素都被初始化为 0。它是否有支持数组是未知的。当容量为负时,该方法抛出 IllegalArgumentException 。
- byte buffer wrap(byte[]array)将一个字节数组包装到一个缓冲区中。新的缓冲区由数组支持;也就是说,对缓冲区的修改将导致数组被修改,反之亦然。新缓冲区的容量和限制被设置为 array.length ,其位置被设置为 0,其标志未定义。它的数组偏移量是 0。
- byte buffer wrap(byte[]array,int offset,int length) 将一个字节数组包装到一个缓冲区中。新的缓冲器由阵列支持。新缓冲区的容量设置为 array.length ,位置设置为 offset ,限制设置为 offset + length ,标志未定义。它的数组偏移量是 0。当偏移量为负数或大于数组长度或长度为负数或大于数组长度偏移量时,该方法抛出 Java . lang . indexoutofboundsexception。
这些方法展示了两种创建字节缓冲区的方法:创建字节缓冲区对象并分配一个存储容量字节的内部数组,或者创建字节缓冲区对象并使用指定的数组来存储这些字节。考虑这些例子:
ByteBuffer buffer = ByteBuffer.allocate(10);
byte[] bytes = new byte[200];
ByteBuffer buffer2 = ByteBuffer.wrap(bytes);
第一行创建一个字节缓冲区,其中包含一个最多存储 10 个字节的内部字节数组,第二和第三行创建一个字节数组和一个使用该数组存储最多 200 个字节的字节缓冲区。
现在,考虑下面的示例,它扩展了前面的示例:
buffer = ByteBuffer.wrap(bytes, 10, 50);
这个例子创建了一个字节缓冲区,位置为 10 ,限制为 50 ,容量为 bytes.length (恰好是 200)。虽然看起来缓冲区只能访问这个数组的一个子范围,但它实际上可以访问整个数组: 10 和 50 的值只是位置和限制的初始值。
通过 allocate() 或 wrap() 创建的字节缓冲区(和其他原始类型的缓冲区)是非直接字节缓冲区——稍后您将了解直接字节缓冲区。非直接字节缓冲区有后备数组,只要 hasArray() 返回 true,就可以通过 array() 方法(恰好在 ByteArray 类中声明为 byte[] array() )访问这些后备数组。(当 hasArray() 返回 true 时,需要调用 arrayOffset() 来获取数组中第一个数据项的位置。)
清单 13-2 展示了缓冲分配和包装。
清单 13-2。通过分配和包装创建面向字节的缓冲区
import java.nio.ByteBuffer;
public class BufferDemo
{
public static void main(String[] args)
{
ByteBuffer buffer1 = ByteBuffer.allocate(10);
if (buffer1.hasArray())
{
System.out.println("buffer1 array: " + buffer1.array());
System.out.println("Buffer1 array offset: " + buffer1.arrayOffset());
System.out.println("Capacity: " + buffer1.capacity());
System.out.println("Limit: " + buffer1.limit());
System.out.println("Position: " + buffer1.position());
System.out.println("Remaining: " + buffer1.remaining());
System.out.println();
}
byte[] bytes = new byte[200];
ByteBuffer buffer2 = ByteBuffer.wrap(bytes);
buffer2 = ByteBuffer.wrap(bytes, 10, 50);
if (buffer2.hasArray())
{
System.out.println("buffer2 array: " + buffer2.array());
System.out.println("Buffer2 array offset: " + buffer2.arrayOffset());
System.out.println("Capacity: " + buffer2.capacity());
System.out.println("Limit: " + buffer2.limit());
System.out.println("Position: " + buffer2.position());
System.out.println("Remaining: " + buffer2.remaining());
}
}
}
编译清单 13-2(Java BufferDemo.java)并运行这个应用( java BufferDemo )。您应该观察到以下输出:
buffer1 array: B@15e565bd
Buffer1 array offset: 0
Capacity: 10
Limit: 10
Position: 0
Remaining: 10
buffer2 array: [B@77a6686
Buffer2 array offset: 0
Capacity: 200
Limit: 60
Position: 10
Remaining: 50
除了管理存储在外部数组中的数据元素(通过 wrap() 方法),缓冲区还可以管理存储在其他缓冲区中的数据。当您创建一个缓冲区来管理另一个缓冲区的数据时,创建的缓冲区被称为视图缓冲区 。在任一缓冲区中所做的更改都会反映在另一个缓冲区中。
视图缓冲区是通过调用缓冲区子类的 duplicate() 方法 来创建的。结果视图缓冲区相当于原始缓冲区;两个缓冲器共享相同的数据项,并且具有相等的容量。但是,每个缓冲区都有自己的位置、限制和标记。当被复制的缓冲区是只读的或直接的时,视图缓冲区也是只读的或直接的。
考虑以下示例:
ByteBuffer buffer = ByteBuffer.allocate(10);
ByteBuffer bufferView = buffer.duplicate();
由 bufferView 标识的 ByteBuffer 实例与 buffer 共享相同的 10 个元素的内部数组。目前,这些缓冲器具有相同的位置、极限和(未定义的)标记。但是,一个缓冲区中的这些属性可以独立于另一个缓冲区中的属性进行更改。
视图缓冲区也是通过调用 ByteBuffer 的 asxBuffer()方法之一创建的。例如, LongBuffer asLongBuffer() 返回一个视图缓冲区,它将字节缓冲区概念化为长整数的缓冲区。
注意只读视图缓冲区 可以通过调用 byte buffer asReadOnlyBuffer()等方法来创建。任何改变只读视图缓冲区内容的尝试都会导致 ReadOnlyBufferException 。但是,原始缓冲区内容(假设它不是只读的)可以更改,只读视图缓冲区将反映这些更改。
写入和读取缓冲器
ByteBuffer 和其他原始类型的缓冲区类声明了几个重载的 put() 和 get() 方法,用于将数据项写入缓冲区和从缓冲区读取数据项。这些方法在需要索引参数时是绝对的,在不需要索引时是相对的。
例如, ByteBuffer 声明绝对 ByteBuffer put(int index,byte b) 方法以将字节 b 存储在缓冲区中的索引值处,并声明绝对字节 get(int index) 方法以获取位于位置索引处的字节。该类还声明了相关的 ByteBuffer put(byte b) 方法,用于将字节 b 存储在缓冲区的当前位置,然后递增当前位置,还声明了相关的 byte get() 方法,用于获取位于缓冲区的当前位置的字节,并递增当前位置。
绝对 put() 和 get() 方法 throwIndexOutOfBoundsException 当 index 为负或者大于等于缓冲区的限制时。相对 put() 方法在当前位置大于等于限制时抛出 Java . nio . bufferoverflowexception,相对 get() 方法在当前位置大于等于限制时抛出 Java . nio . bufferunderflowexception。此外,当缓冲区为只读时,绝对和相对 put() 方法会抛出 ReadOnlyBufferException。
[清单 13-3 演示了相对 put() 方法和绝对 get() 方法。
清单 13-3。向缓冲区写入字节,并从缓冲区读取
import java.nio.ByteBuffer;
public class BufferDemo
{
public static void main(String[] args)
{
ByteBuffer buffer = ByteBuffer.allocate(7);
System.out.println("Capacity = " + buffer.capacity());
System.out.println("Limit = " + buffer.limit());
System.out.println("Position = " + buffer.position());
System.out.println("Remaining = " + buffer.remaining());
buffer.put((byte) 10).put((byte) 20).put((byte) 30);
System.out.println("Capacity = " + buffer.capacity());
System.out.println("Limit = " + buffer.limit());
System.out.println("Position = " + buffer.position());
System.out.println("Remaining = " + buffer.remaining());
for (int i = 0; i < buffer.position(); i++)
System.out.println(buffer.get(i));
}
}
编译清单 13-3(Java BufferDemo.java)并运行这个应用( java BufferDemo )。您应该观察到以下输出:
Capacity = 7
Limit = 7
Position = 0
Remaining = 7
Capacity = 7
Limit = 7
Position = 3
Remaining = 4
10
20
30
提示为了获得最大效率,您可以使用 ByteBuffer put(byte[] src) 、 ByteBuffer put(byte[] src,int offset,int length) 、 ByteBuffer get(byte[] dst) 和 ByteBuffer get(byte[] dst,int offset,int length) 方法来读写字节数组,从而执行批量数据传输。
翻转缓冲器
填充缓冲液后,必须准备好通过通道排出。当您按原样传递缓冲区时,通道将访问当前位置以外的未定义数据。
要解决这个问题,您可以将位置重置为 0,但是通道如何知道何时到达插入数据的末尾呢?解决方案是使用 limit 属性,该属性指示缓冲区活动部分的结束。基本上,您将限制设置为当前位置,然后将当前位置重置为 0。
您可以通过执行以下代码来完成此任务,这些代码还会清除任何已定义的标记:
buffer.limit(buffer.position()).position(0);
然而,有一种更简单的方法来完成同样的任务,如下所示:
buffer.flip();
在任一情况下,缓冲区都准备好被排空。
清单 13-4 展示了在字符缓冲区的环境下缓冲区翻转。
清单 13-4。向字符缓冲区写入字符和从中读取字符
import java.nio.CharBuffer;
public class BufferDemo
{
public static void main(String[] args)
{
String[] poem =
{
"Roses are red",
"Violets are blue",
"Sugar is sweet",
"And so are you."
};
CharBuffer buffer = CharBuffer.allocate(50);
for (int i = 0; i < poem.length; i++)
{
// Fill the buffer.
for (int j = 0; j < poem[i].length(); j++)
buffer.put(poem[i].charAt(j));
// Flip the buffer so that its contents can be read.
buffer.flip();
// Drain the buffer.
while (buffer.hasRemaining())
System.out.print(buffer.get());
// Empty the buffer to prevent BufferOverflowException.
buffer.clear();
System.out.println();
}
}
}
编译清单 13-4(Java BufferDemo.java)并运行这个应用( java BufferDemo )。您应该观察到以下输出:
Roses are red
Violets are blue
Sugar is sweet
And so are you.
注 倒带()类似于翻转()但忽略了极限。此外,调用 flip() 两次也不会将您返回到初始状态。相反,缓冲区的大小为零。调用 put() 方法导致 BufferOverflowException,调用 get() 方法导致 BufferUnderflowExceptionor(对于 get(int))IndexOutOfBoundsException。
标记缓冲器
您可以通过调用 mark() 方法来标记缓冲区,稍后通过调用 reset() 返回到标记的位置。例如,假设您已经执行了 byte buffer buffer = byte buffer . allocate(7);,后面是 buffer.put((字节)10)。put((字节)20)。put((字节)30)。put((字节)40);,后面是 buffer . limit(4);。当前位置和限制被设置为 4。
继续,假设您执行 buffer.position(1)。标记()。位置(3);。如果你发送这个缓冲到一个通道,字节 40 将被发送(当前位置是 3,因为位置(3) )并且位置将前进到 4。如果你随后执行了 buffer . reset();并将此缓冲区发送到通道,该位置将被设置为标志(1);并且字节 20、30 和 40(从当前位置到低于限制的一个位置的所有字节)将被发送到通道(并且以此顺序)。
清单 13-5 展示了这种标记/重置场景。
清单 13-5。标记当前缓冲位置,并将当前位置复位到标记位置
import java.nio.ByteBuffer;
public class BufferDemo
{
public static void main(String[] args)
{
ByteBuffer buffer = ByteBuffer.allocate(7);
buffer.put((byte) 10).put((byte) 20).put((byte) 30).put((byte) 40);
buffer.limit(4);
buffer.position(1).mark().position(3);
System.out.println(buffer.get());
System.out.println();
buffer.reset();
while (buffer.hasRemaining())
System.out.println(buffer.get());
}
}
编译清单 13-5(javac BufferDemo.java)并运行这个应用( java BufferDemo )。您应该观察到以下输出:
40
20
30
40
注意不要混淆复位()和清除()。 clear() 方法将缓冲区标记为空,而 reset() 将缓冲区的当前位置更改为之前设置的标记,或者在没有之前设置的标记时抛出 InvalidMarkException 。
缓冲子类操作
ByteBuffer 和其他原始类型的缓冲区类声明了一个 compact() 方法,该方法通过将当前位置和限制之间的所有字节复制到缓冲区的开头来压缩缓冲区。索引 p = position() 处的字节被复制到索引 0,索引 p + 1 处的字节被复制到索引 1,依此类推,直到索引 limit()–1 处的字节被复制到索引 n = limit()–1–p。然后,缓冲器的当前位置被设置为 n + 1 ,其限制被设置为其容量。定义后的标记将被丢弃。
在从一个缓冲区写入数据之后,您调用 compact() 来处理缓冲区的内容没有全部写入的情况。考虑下面的例子,它通过缓冲区 buf 将内容从 in 通道复制到 out 通道:
buf.clear(); // Prepare buffer for use
while (in.read(buf) != −1)
{
buf.flip(); // Prepare buffer for draining.
out.write(buf); // Write the buffer.
buf.compact(); // Do this in case of a partial write.
}
当没有指定 compact() 时, compact() 方法调用将未写入的缓冲区数据移动到缓冲区的开头,以便下一个 read() 方法调用将读取的数据追加到缓冲区的数据中,而不是覆盖该数据。
您可能偶尔需要比较缓冲区的相等性或顺序。除了 ByteBuffer 的 MappedByteBuffer 子类之外的所有 Buffer 子类都覆盖了 equals() 和 compareTo() 方法来执行这些比较——MappedByteBuffer 从其 ByteBuffer 超类继承了这些方法。以下示例显示了如何比较字节缓冲区字节缓冲区 1 和字节缓冲区 2 的相等性和排序:
System.out.println(bytBuf1.equals(bytBuf2));
System.out.println(bytBuf1.compareTo(bytBuf2));
ByteBuffer 的 equals() 契约声明,当且仅当两个字节缓冲区具有相同的元素类型时,它们才相等;它们具有相同数量的剩余元素;和剩余元素的两个序列,独立于它们的起始位置考虑,各自是相等的。这个合同对于其他缓冲子类也是一样的。
ByteBuffer 的 compareTo() 方法声明,通过按字典顺序比较剩余元素的序列来比较 2 字节缓冲区的顺序,而不考虑每个序列在其对应缓冲区中的起始位置。像调用 Byte.compare(byte,byte) 一样比较成对的字节元素。类似的描述也适用于其他缓冲子类。
字节排序
除了 Boolean(可能用一个位或一个字节表示)以外的非字节原语类型由几个字节组成:一个字符或一个短整型占用 2 个字节,一个 32 位整型或浮点值占用 4 个字节,一个长整型或双精度浮点值占用 8 个字节。这些多字节类型之一的每个值都存储在一系列连续的内存位置中。然而,这些字节的顺序可能因平台而异。
例如,考虑 32 位长整数 0x10203040。这个值的 4 个字节可以作为 10、20、30、40 存储在存储器中(从低位地址到高位地址);这种安排被称为大端顺序 (最高有效字节,即“大”端,存储在最低地址)。或者,这些字节可以存储为 40、30、20、10;这种安排被称为小端顺序 (最低有效字节,即“小”端,存储在最低地址)。
Java 提供了 java.nio.ByteOrder 类来帮助您处理在多字节缓冲区中写入/读取多字节值时的字节顺序问题。 ByteOrder 声明了一个 ByteOrder nativeOrder() 方法,该方法将平台的字节顺序作为一个 ByteOrder 实例返回。因为该实例是 ByteOrder 的 BIG_ENDIAN 和 LITTLE_ENDIAN 常量之一,并且因为不能创建其他 ByteOrder 实例,所以可以通过 == 或将 nativeOrder() 的返回值与这些常量之一进行比较!= 操作员。
同样,每个多字节类(例如, FloatBuffer )声明一个 ByteOrder order() 方法 ,该方法返回该缓冲区的字节顺序。这个方法返回 ByteOrder。BIG_ENDIAN 或 ByteOrder。小 _ 端。
从 order() 返回的 ByteOrder 值可以根据缓冲区的创建方式采用不同的值。如果多字节缓冲区(例如,浮点缓冲区)是通过分配或包装现有数组创建的,则缓冲区的字节顺序是底层平台的本机顺序。然而,如果多字节缓冲区是作为字节缓冲区的视图创建的,那么视图缓冲区的字节顺序就是创建视图时字节缓冲区的顺序。视图缓冲区的字节顺序不能随后更改。
ByteBuffer 在字节顺序方面不同于多字节类。它的默认字节顺序总是大端,即使底层平台的字节顺序是小端。 ByteBuffer 默认为 big endian,因为 Java 的默认字节顺序也是 big endian,这使得类文件和序列化对象跨虚拟机一致地存储数据。
因为这个大端默认设置会影响小端平台的性能, ByteBuffer 还声明了一个 byte buffer order(byte order bo)方法来改变字节缓冲区的字节顺序。
虽然改变字节缓冲区的字节顺序似乎不太常见(在这里只访问单字节数据项),但该方法非常有用,因为 ByteBuffer 还声明了几个方便的方法来读写多字节值(例如,byte buffer putInt(int value)和 int getInt() )。这些方便的方法根据字节缓冲区的当前字节顺序写入这些值。此外,您可以随后调用 ByteBuffer 的 long Buffer aslong Buffer()或另一个 asxBuffer()方法来返回一个视图缓冲区,其顺序将反映字节缓冲区更改后的字节顺序。
直接字节缓冲器
与多字节缓冲区不同,字节缓冲区可以作为基于通道的 I/O 的源和/或目标。这并不奇怪,因为操作系统在 8 位字节(不是浮点值,也不是 32 位整数,等等)的连续序列的内存区域上执行 I/O。
操作系统可以直接访问进程的地址空间。例如,操作系统可以直接访问虚拟机进程的地址空间,以基于字节数组执行数据传输操作。但是,虚拟机可能不会连续存储字节数组,或者其垃圾收集器可能会将字节数组移动到另一个位置。由于这些限制,直接字节缓冲区应运而生。
直接字节缓冲区是一个与通道和本机代码交互以执行 I/O 的字节缓冲区。直接字节缓冲区试图将字节元素存储在通道用于通过本机代码执行直接(原始)访问的内存区域中,本机代码告诉操作系统直接清空或填充内存区域。
直接字节缓冲区是在虚拟机上执行 I/O 的最有效方式。虽然您也可以将非直接字节缓冲区传递给通道,但可能会出现性能问题,因为非直接字节缓冲区并不总是能够充当本机 I/O 操作的目标。
当通道通过非直接字节缓冲区时,通道可能必须创建一个临时直接字节缓冲区,将非直接字节缓冲区的内容复制到直接字节缓冲区,对临时直接字节缓冲区执行 I/O 操作,并将临时直接字节缓冲区的内容复制到非直接字节缓冲区。然后,临时直接字节缓冲区将接受垃圾收集。
虽然直接字节缓冲区对于 I/O 来说是最佳的,但创建它的成本可能会很高,因为虚拟机堆之外的内存需要由操作系统来分配,并且设置和/或拆除该内存所需的时间可能会比缓冲区位于堆内时更长。在您的代码运行之后,如果您想尝试性能优化,您可以通过调用我前面讨论过的 ByteBuffer 的 allocated direct()方法 ,轻松获得一个直接字节缓冲区。
使用频道
通道与缓冲区合作以实现高性能 I/O。通道 是一个对象,它表示与硬件设备、文件、网络套接字、应用组件或其他能够执行写、读和其他 I/O 操作的实体的开放连接。通道在字节缓冲区和 I/O 服务源或目的地之间高效地传输数据。
注意通道是访问本地 I/O 服务的网关。通道使用字节缓冲区作为发送和接收数据的端点。
在操作系统文件句柄或文件描述符和通道之间通常存在一一对应关系。当您在文件上下文中使用通道时,通道通常会连接到一个打开的文件描述符。尽管通道比文件描述符更抽象,但它们仍然能够模拟操作系统的本机 I/O 设施。
频道及其子频道
Java 通过其 java.nio.channels 和 java.nio.channels.spi 包支持通道。应用与位于前一个包中的类型进行交互;定义新选择器提供程序的开发人员使用后一个包。
所有通道都是最终实现 Java . nio . channels . channel 接口的类的实例。通道声明如下方法:
- void close() :关闭本通道。当该通道已经关闭时,调用 close() 不起作用。当另一个线程已经调用了 close() 时,一个新的 close() 调用会阻塞,直到第一个调用完成,之后 close() 返回而没有效果。当发生 I/O 错误时,该方法抛出 IOException 。在通道关闭后,任何对其调用 I/O 操作的进一步尝试都会导致抛出 Java . nio . channels . closedchannelexception。
- 布尔 isOpen() :返回该通道的打开状态。当通道打开时,此方法返回 true 否则,它返回 false。
这些方法表明只有两个操作是所有通道共有的:关闭通道并确定通道是打开还是关闭。为了支持 I/O,通道由 Java . nio . channels . writable bytechannel 和 Java . nio . channels . readable bytechannel 接口扩展:
- WritableByteChannel 声明了一个抽象的 int write(byte buffer buffer)方法,该方法将一个字节序列从缓冲区写入当前通道。此方法返回实际写入的字节数。当通道未打开进行写操作时,它抛出 Java . nio . channels . nonwritablechannelexception,当通道关闭时抛出 Java . nio . channels . closedchannelexception,当另一个线程在写操作期间关闭通道时抛出 Java . nio . channels . asynchronouscloseexception,当另一个线程在写操作期间中断当前线程时抛出 Java . nio . channels . closedbyinterruptexception
- ReadableByteChannel 声明了一个抽象的 int read(byte buffer buffer)方法,将字节从当前通道读入缓冲区。此方法返回实际读取的字节数(如果没有更多字节要读取,则返回 1)。当通道没有打开读取时,抛出 Java . nio . channels . nonreadablechannelexception;关闭通道时的 ClosedChannelException;AsynchronousCloseException 当另一个线程在读取期间关闭通道时;ClosedByInterruptException 当另一个线程在写操作正在进行时中断当前线程,从而关闭通道并设置当前线程的中断状态;以及当一些其他 I/O 错误发生时的 IOException 。
注意一个类只实现可写字节通道或可读字节通道的通道是单向的。试图从可写字节通道读取或写入可读字节通道会导致引发异常。
您可以使用操作符的 instanceof 来确定通道实例是否实现任一接口。因为对这两个接口进行测试有些困难,所以 Java 提供了 Java . nio . channels . bytechannel 接口,这是一个空的标记接口,它子类化了 WritableByteChannel 和 ReadableByteChannel 。当您需要了解一个通道是否是双向的时,指定一个表达式更方便,比如字节通道的通道实例。
随着可写字节通道和可读字节通道 , 中断字节通道直接扩展通道。InterruptibleChannel 描述了一个可以异步关闭和中断的通道。该接口覆盖了其通道超级接口的 close() 方法 头,为通道的该方法契约提供了以下附加规定:当前在该通道上的 I/O 操作中被阻塞的任何线程都将接收到异步关闭异常(一个 io 异常后代)。
实现这个接口的通道是异步可关闭的 :当一个线程在一个可中断通道上的 I/O 操作中被阻塞时,另一个线程可能会调用该通道的 close() 方法。这导致被阻塞的线程接收到一个抛出的 AsynchronousCloseException 实例。
实现这个接口的通道也是可中断的:当一个线程在一个可中断通道上的 I/O 操作中被阻塞时,另一个线程可能会调用被阻塞线程的中断()方法。这样做会导致通道关闭,被阻塞的线程接收到一个抛出的 ClosedByInterruptException 实例,并且被阻塞的线程的中断状态被设置。(当一个线程的中断状态已经被设置并且它调用一个通道上的阻塞 I/O 操作时,该通道被关闭并且该线程将立即接收一个抛出的 ClosedByInterruptException 实例;其中断状态将保持设置。)
NIO 的设计者选择在阻塞线程被中断时关闭通道,因为他们无法找到一种跨平台以相同方式可靠处理中断的 I/O 操作的方法。保证确定性行为的唯一方法是关闭通道。
提示您可以通过在一个表达式中使用 instanceof 操作符来决定一个通道是否支持异步关闭和中断,比如通道 instance of interruptible channel。
您之前了解到,您必须调用一个 Buffer 子类的类方法来获得一个缓冲区。关于频道,有两种方法可以获得频道:
-
java.nio.channels 包提供了一个 Channels 工具类,该类提供了两种从流中获取通道的方法——对于以下每种方法,当通道关闭时,底层流也将关闭,并且通道不会被缓冲:
-
WritableByteChannel new channel(output stream output stream)返回给定 outputStream 的可写字节通道。
-
ReadableByteChannel new channel(InputStream InputStream)返回给定 inputStream 的可读字节通道。
-
各种经典的 I/O 类已经被改造以支持通道创建。例如, RandomAccessFile 声明了一个用于返回文件通道实例的 FileChannel getChannel() 方法, java.net.Socket 声明了一个用于返回套接字通道的 socket channel get channel()方法。
清单 13-6 使用通道类获得标准输入和输出流的通道,然后使用这些通道将字节从输入通道复制到输出通道。
清单 13-6。将字节从输入通道复制到输出通道
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
public class ChannelDemo
{
public static void main(String[] args)
{
ReadableByteChannel src = Channels.newChannel(System.in);
WritableByteChannel dest = Channels.newChannel(System.out);
try
{
copy(src, dest); // or copyAlt(src, dest);
}
catch (IOException ioe)
{
System.err.println("I/O error: " + ioe.getMessage());
}
finally
{
try
{
src.close();
dest.close();
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}
static void copy(ReadableByteChannel src, WritableByteChannel dest)
throws IOException
{
ByteBuffer buffer = ByteBuffer.allocateDirect(2048);
while (src.read(buffer) != −1)
{
buffer.flip();
dest.write(buffer);
buffer.compact();
}
buffer.flip();
while (buffer.hasRemaining())
dest.write(buffer);
}
static void copyAlt (ReadableByteChannel src, WritableByteChannel dest)
throws IOException
{
ByteBuffer buffer = ByteBuffer.allocateDirect(2048);
while (src.read(buffer) != −1)
{
buffer.flip();
while (buffer.hasRemaining())
dest.write(buffer);
buffer.clear();
}
}
}
清单 13-6 展示了两种将字节从标准输入流复制到标准输出流的方法。在第一种方法中,以 copy() 方法为例,目标是最小化本机 I/O 调用(通过 write() 方法调用),尽管由于 compact() 方法调用,更多的数据可能最终被复制。在第二种方法中,如 copyAlt() 所示,目标是消除数据复制,尽管可能会出现更多的本机 I/O 调用。
每个 copy() 和 copyAlt() 首先分配一个直接字节缓冲区(回想一下,直接字节缓冲区是在虚拟机上执行 I/O 的最有效方式),并进入一个 while 循环,该循环不断地从源通道读取字节,直到输入结束( read() 返回 1)。在读取之后,缓冲区被翻转,以便可以清空。这就是方法分歧的地方。
- 这个 copy() 方法 while 循环对 write() 进行一次调用。因为 write() 可能不会完全清空缓冲区,所以在下一次读取之前,调用 compact() 来压缩缓冲区。压缩确保未写入的缓冲区内容在下一次读取操作中不会被覆盖。在 while 循环之后, copy() 翻转缓冲区以准备清空任何剩余的内容,然后使用 hasRemaining() 和 write() 来完全清空缓冲区。
- copyAlt() 方法 while 循环包含一个嵌套的 while 循环,它与 hasRemaining() 和 write() 一起继续清空缓冲区,直到缓冲区为空。接下来是一个 clear() 方法调用,该方法调用清空缓冲区,以便在下一个 read() 调用时填充缓冲区。
注意重要的是要认识到一个单独的 write() 方法调用可能不会输出一个缓冲区的全部内容。类似地,单个 read() 调用可能不会完全填满一个缓冲区。
编译清单 13-6 ( 贾瓦茨 ChannelDemo.java)并运行这个应用( java ChannelDemo 和 Java channel demo<ChannelDemo.java>channel demo . bak 就是例子)来验证标准输入被复制到标准输出。测试完 copy() 方法后,替换 copy(src,dest);带 copyAlt(src,dest);并重复。
深度频道
前面对通道接口及其直接派生的讨论已经让您对 NIO 通道有了一些了解。然而,还有更多要探索的。本节通过探索分散/聚集 I/O 和文件通道,带您深入了解通道。不幸的是,简洁的需要限制了我探索套接字通道。
分散/收集 I/O
通道提供跨多个缓冲器执行单个 I/O 操作的能力。这种能力被称为分散/收集 I/O (也被称为矢量 I/O )。
在写入操作的上下文中,几个缓冲区的内容按顺序聚集(排干),然后通过通道发送到目的地—这些缓冲区不需要具有相同的容量。在读取操作的上下文中,一个通道的内容依次被分散(填充)到多个缓冲区;每个缓冲区被填充到其极限,直到通道为空或总的缓冲区空间被用尽。
注意现代操作系统提供支持矢量 I/O 的 API,以消除(或至少减少)系统调用或缓冲副本,从而提高性能。例如,Win32/win 64 API 为此提供了 readfile scatter()和 WriteFileGather() 函数 。
Java 提供了 Java . nio . channels . scatteringbytechannel 接口来支持散射,提供了 Java . nio . channels . gatherengbytechannel 接口来支持聚集。
ScatteringByteChannel 提供了以下方法:
- 长读(ByteBuffer[] buffers,int offset,int length)
- 长读取(ByteBuffer[] buffers)
GatheringByteChannel 提供了以下方法:
- long write(byte buffer[]buffers,int offset,int length)
- 长写(ByteBuffer[] buffers)
第一个 read() 方法 和第一个 write() 方法 让您通过向 offset 传递一个基于零的偏移量来标识要读/写的第一个缓冲区,并通过向 length 传递一个值来标识要读/写的缓冲区数量。第二个 read() 方法和第二个 write() 方法按顺序读/写所有缓冲区。
清单 13-7 演示了读(字节缓冲区[]缓冲区)和写(字节缓冲区[]缓冲区)。
清单 13-7。演示分散/聚集
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.ScatteringByteChannel;
public class ChannelDemo
{
public static void main(String[] args) throws IOException
{
ScatteringByteChannel src;
src = (ScatteringByteChannel) Channels.newChannel(new FileInputStream("x.dat"));
ByteBuffer buffer1 = ByteBuffer.allocateDirect(5);
ByteBuffer buffer2 = ByteBuffer.allocateDirect(3);
ByteBuffer[] buffers = { buffer1, buffer2 };
src.read(buffers);
buffer1.flip();
while (buffer1.hasRemaining())
System.out.printf("%c%n", buffer1.get());
System.out.println();
buffer2.flip();
while (buffer2.hasRemaining())
System.out.printf("%c%n", buffer2.get());
buffer1.rewind();
buffer2.rewind();
GatheringByteChannel dest;
dest = (GatheringByteChannel) Channels.newChannel(new FileOutputStream("y.dat"));
buffers[0] = buffer2;
buffers[1] = buffer1;
dest.write(buffers);
}
}
清单 13-7 的 main() 方法首先通过实例化 java.io.FileInputStream 并将此实例传递给 Channels 类的 ReadableByteChannel new channel(InputStream InputStream)方法来获得一个分散的字节通道。返回的 ReadableByteChannel 实例被强制转换为 ScatteringByteChannel ,因为这个实例实际上是一个实现 ScatteringByteChannel 的文件通道(稍后讨论)。
接下来, main() 创建一对直接字节缓冲区;第一缓冲器具有 5 字节的容量,第二缓冲器具有 3 字节的容量。这些缓冲区随后被存储在一个数组中,该数组被传递给 read(ByteBuffer[]) 以填充它们。
在填充缓冲区之后, main() 翻转它们,以便可以将它们的内容输出到标准输出。输出这些内容后,缓冲区将被倒带,准备通过收集操作清空。
main() 现在通过实例化 java.io.FileOutputStream 并将此实例传递给 Channels 类的 writable bytechannel new channel(output stream output stream)方法来获得一个聚集字节通道。返回的 WritableByteChannel 实例被强制转换为 GatheringByteChannel ,因为这个实例实际上是一个实现 GatheringByteChannel 的文件通道(稍后讨论)。
最后, main() 将这些缓冲区按照最初分配的相反顺序分配给 buffers 数组,然后将该数组传递给 write(ByteBuffer[]) 以清空它们。
注意system . out . printf()方法调用中的 %n 格式说明符是指定行结束符(指定行尾的一个或两个字符的序列)的一种可移植方式。指定 \n 并不是一个好主意,因为一些平台要求 \r\n 作为行结束符,而其他平台要求 \r 。
创建一个名为 x.dat 的文件,并将以下文本存储在该文件中:
12345abcdefg
现在编译清单 13-7(javac ChannelDemo.java)并运行这个应用( java ChannelDemo )。您应该观察到以下输出:
1
2
3
4
5
a
b
c
此外,您应该看到一个新创建的 y.dat 文件,其内容如下:
abc12345
文件频道
我之前提到过, RandomAccessFile 声明了一个用于返回文件通道实例的 FileChannel getChannel() 方法。原来 FileInputStream 和 FileOutputStream 也提供了同样的方法。相比之下, FileReader 和 filerewriter 不提供获取文件通道的方法。
注意从 FileInputStream 的 getChannel() 方法返回的文件通道是只读的,从 file output stream’sget channel()方法返回的文件通道是只写的。试图写入只读文件通道或从只写文件通道读取会导致异常。
抽象 Java . nio . channels . file channel 类描述了一个文件通道。因为这个类扩展了抽象的 Java . nio . channels . SPI . abstractinterruptiblechannel 类,实现了 InterruptibleChannel 接口,所以文件通道是可中断的。因为这个类实现了字节通道、 GatheringByteChannel 和 ScatteringByteChannel 接口,所以您可以对底层文件进行读写和分散 I/O 操作。然而,还有更多。
注意与非线程安全的缓冲区不同,文件通道是线程安全的。
一个文件通道维护一个指向文件的虚拟指针,称为文件指针 ,文件通道让你获得并改变文件指针值。它还允许您获取通道下的文件大小,尝试锁定整个文件或文件的一个区域,执行内存映射文件 I/O ,请求将缓存的数据强制传输到磁盘,并以可能被平台优化的方式将数据直接传输到另一个通道。
表 13-2 描述了几个文件通道的方法。
表 13-2 。文件通道方法
方法 | 描述 |
---|---|
无效力(布尔元数据) | 请求将对此文件通道的所有更新提交到存储设备。当此方法返回时,当文件驻留在本地存储设备上时,对作为此通道基础的平台文件所做的所有修改都已提交。然而,当文件不在本地托管时(例如,它在网络文件系统上),应用不能确定修改已经被提交。(不保证使用其他地方定义的方法对文件所做的更改会被提交。例如,通过映射字节缓冲区进行的更改可能不会被提交。)当通过了真时,元数据值指示更新是否应该包括文件的元数据(例如,最后修改时间和最后访问时间),或者当通过了假时,更新是否不包括文件的元数据。传递 true 可以调用对操作系统的底层写入(如果平台正在维护元数据,例如上次访问时间),即使当通道作为只读通道打开时。当通道已经关闭时,该方法抛出 ClosedChannelException ,当任何其他 I/O 错误发生时,抛出 IOException 。 |
FileLock lock() | 获取此文件通道的基础文件的排他锁。这个方便的方法相当于执行 fileChannel.lock(0L,Long。MAX_VALUE,false);,其中文件通道引用一个文件通道。这个方法返回一个代表锁定区域的 Java . nio . channels . file lock 对象。文件通道关闭时抛出 ClosedChannelException;非可写通道异常当通道未打开写入时;Java . nio . channels . overlappingfilelockexception 当已经持有与该锁请求重叠的锁或者另一个线程正在等待获取将与该请求重叠的锁时;Java . nio . channels . filelockinterruptionexception 当调用线程在等待获取锁时被中断;AsynchronousCloseException 当通道在调用线程等待获取锁时被关闭;以及 IOException 当获取请求的锁时发生另一个 I/O 错误。 |
映射字节缓冲图 (文件通道)。MapMode 模式,长位置,长大小) | 根据以下三种模式之一将该文件通道的文件的一个区域直接映射到内存:* 只读:任何修改结果缓冲区的尝试都会导致 ReadOnlyBufferException 抛出( MapMode)。READ_ONLY ) * 读/写:对结果缓冲区所做的更改将最终传播到文件;它们可能对已经映射了相同文件的其他程序可见,也可能不可见。READ _ WRITE)*private:对结果缓冲区所做的更改不会传播到文件中,并且对映射了相同文件的其他程序不可见;相反,它们将导致创建缓冲区的修改部分的私有副本( MapMode。PRIVATE )对于只读映射,该通道必须已经打开进行读取;对于读/写或私有映射,此通道必须已打开以进行读写。传递给位置的值标识了文件中映射区域开始的位置。传递给 size 的值标识映射区域的长度。成功时,此方法返回映射的字节缓冲区。返回的映射字节缓冲区的位置为 0,极限和容量为大小;它的标记将是未定义的。缓冲区和它所代表的映射将保持有效,直到缓冲区本身被垃圾收集。如果不成功,此方法将引发异常。当模式为 READ_ONLY 时,抛出 unreadablechannelexception,但该通道没有打开读取;当模式为 READ_WRITE 或 PRIVATE 时,出现不可写通道异常,但该通道并未同时打开读写;当模式不是文件通道定义的常量之一时 IllegalArgumentException。MapMode 类,传递给位置的值为负值,或者传递给大小的值为负值或者大于 Java . lang . integer . max _ VALUE;以及 IOException 当任何其他 I/O 错误发生时。一旦建立,映射就不依赖于用来创建它的文件通道。特别是,关闭通道对映射的有效性没有影响。内存映射文件的许多细节本质上依赖于底层操作系统,因此是不确定的。当请求的区域未完全包含在此通道的文件中时,此方法的行为是未指定的。此应用或另一个应用对基础文件的内容或大小所做的更改是否会传播到缓冲区是未指定的。对缓冲区的更改传播到文件的速率未指定。对于大多数操作系统来说,将一个文件映射到内存中比通过通常的读/写方法读写几十千字节的数据更昂贵。从性能角度来看,通常只需要将相对较大的文件映射到内存中。 |
长仓() | 返回该文件通道的文件指针的当前值,该值相对于零。该方法在文件通道关闭时抛出 ClosedChannelException ,在另一个 I/O 错误发生时抛出 IOException 。 |
文件通道位置(长偏移) | 将该文件通道的文件指针设置为偏移量。参数是从文件开头开始计数的字节数。位置不能设置为负值。新位置可以设置为超出当前文件大小。如果设置超过当前文件大小,尝试读取将返回文件结尾。写操作将会成功,但是它们将用所需数量的(未指定的)字节值填充文件当前结尾和新位置之间的字节。该方法在偏移量为负时抛出 IllegalArgumentException ,在文件通道关闭时抛出 ClosedChannelException ,在另一个 I/O 错误发生时抛出 IOException 。 |
int read(ByteBuffer 缓冲) | 将字节从这个文件通道读入给定的缓冲区。调用方法时,将读取的最大字节数是缓冲区中剩余的字节数。字节将从缓冲区的当前位置开始复制到缓冲区中。当其他线程也试图从此通道读取时,调用可能会阻塞。完成后,缓冲区的位置被设置为已读取字节的末尾。缓冲区的限制不会改变。该方法返回实际读取的字节数,并抛出与前面讨论的 ReadableByteChannel 相同的异常。 |
长尺寸() | 返回此文件通道下文件的大小(以字节为单位)。该方法在文件通道关闭时抛出 ClosedChannelException ,在另一个 I/O 错误发生时抛出 IOException 。 |
文件通道截断(长尺寸) | 将此文件通道下的文件截断为大小。任何超出给定的大小的字节都将从文件中删除。当没有超过给定大小的字节时,文件内容不被修改。当文件指针当前大于给定的大小时,它被设置为新的大小。 |
【file lock try lock() | 尝试在不阻止的情况下获取此文件通道的基础文件的排他锁。这个方便的方法相当于执行 fileChannel.tryLock(0L,Long。MAX_VALUE,false);其中文件通道引用一个文件通道。该方法返回一个代表锁定区域的文件锁对象,或者当该锁与另一个操作系统进程中现有的独占锁重叠时返回 null。文件通道关闭时抛出 ClosedChannelException;OverlappingFileLockException 当与所请求区域重叠的锁已经被该虚拟机持有时,或者当另一个线程已经在该方法中被阻塞并试图锁定重叠区域时;以及 IOException 当在获取所请求的锁时发生另一个 I/O 错误时。 |
int write(ByteBuffer 缓冲) | 从给定的缓冲区向该文件通道写入一个字节序列。字节从通道的当前文件位置开始写入,除非通道处于追加模式,在这种情况下,位置首先前进到文件的末尾。文件增长(必要时)以容纳写入的字节,然后用实际写入的字节数更新文件位置。否则,该方法的行为完全按照可写字节通道接口的指定。该方法返回实际写入的字节数,并抛出与前面讨论的可写字节通道相同的异常。 |
表 13-2 提供了很多理解的材料。为了帮助你获得这些知识,清单 13-8 展示了其中的几种方法
清单 13-8。演示文件通道
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class ChannelDemo
{
public static void main(String[] args) throws IOException
{
if (args.length != 1)
{
System.out.println("usage: java ChannelDemo newfilespec");
return;
}
FileOutputStream fos = new FileOutputStream(args[0]);
FileChannel fc = fos.getChannel();
System.out.println("position: " + fc.position());
System.out.println("size: " + fc.size());
String msg = "This is a test message.";
ByteBuffer buffer = ByteBuffer.allocateDirect(msg.length() * 2);
buffer.asCharBuffer().put(msg);
fc.write(buffer);
System.out.println("position: " + fc.position());
System.out.println("size: " + fc.size());
fc.truncate(24L);
fc.close();
FileInputStream fis = new FileInputStream(args[0]);
fc = fis.getChannel();
System.out.println("size: " + fc.size());
buffer.clear();
fc.read(buffer);
buffer.flip();
while (buffer.hasRemaining())
System.out.print(buffer.getChar());
System.out.println();
System.out.println(buffer.getChar(0));
System.out.println(buffer.getChar(1));
System.out.println(buffer.getChar(2));
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, 4);
System.out.println(mbb.getChar(0));
System.out.println(mbb.getChar(2));
System.out.println(mbb.getChar(4));
fc.close();
}
}
清单 13-8 的 main() 方法首先验证你已经指定了一个命令行参数,这是一个将要被创建和覆盖的文件的名称。然后,它创建一个文件输出流,获取该流的文件通道,输出有关该通道的一些信息,向文件输出一些内容,再次输出通道信息,截断文件,并关闭文件。
main() 接下来创建文件输入流,获取该流的文件通道,输出通道信息,将文件内容读入缓冲区,输出缓冲区内容。最后,获得一个映射字节缓冲区(它提供了文件的一个内存映射区域),并输出内容。
编译清单 13-8(javac ChannelDemo.java)并运行这个应用(例如 java ChannelDemo x.dat )。您应该观察到以下输出:
position: 0
size: 0
position: 46
size: 46
size: 24
This is a te
T
?
h
T
H
Exception in thread "main" java.lang.IndexOutOfBoundsException
at java.nio.Buffer.checkIndex(Unknown Source)
at java.nio.DirectByteBuffer.getChar(Unknown Source)
at ChannelDemo.main(ChannelDemo.java:46)
这个输出中有两个有趣的项目:问号 ( ?)而例外。问号来自于通过 system . out . println(buffer . getchar(1))输出一个不可显示的 Unicode 字符;,其中 buffer.getChar(1) 返回从字节索引 0 开始的 2 字节 Unicode 字符的后半部分和从字节索引 2 开始的 2 字节 Unicode 字符的前半部分。
异常源于 mbb.getChar(4) 试图访问字节索引 4 和 5 处的 Unicode 字符。但是,映射字节缓冲区中唯一有效的字节索引是索引 0 到 3。
使用正则表达式
文本处理应用经常需要将文本与模式(简明描述被认为是匹配的字符串集合的字符串)进行匹配。例如,应用可能需要在文本文件中找到特定单词模式的所有匹配项,以便用另一个单词替换那些匹配项。NIO JSR 包括正则表达式,以帮助文本处理应用高性能地执行模式匹配。
模式、模式同步异常和匹配器
一个正则表达式 (也称为 regex 或 regexp )是一个基于字符串的模式,表示匹配这个模式的一组字符串。该模式由字面字符和元字符组成,元字符是具有特殊含义的字符,而不是字面含义。
正则表达式 API 提供了 java.util.regex.Pattern 类来通过编译的正则表达式表示模式。出于性能原因编译正则表达式;通过编译后的正则表达式进行模式匹配比不编译正则表达式要快得多。表 13-3 描述了模式的方法。
表 13-3。 花样方法
方法 | 描述 |
---|---|
静态模式编译(字符串正则表达式) | 编译 regex 并返回其模式对象。当 regex 的语法无效时,该方法抛出 Java . util . regex . patternsynctaxexception。 |
静态模式编译(字符串 regex,int 标志) | 根据给定的标志编译 regex (由模式的 CANON_EQ 、不区分大小写、注释、dotoll、文字、多行、 UNICODE_CASE 和 UNIX_LINES 的某种组合组成的位集当 regex 的语法无效时,该方法抛出 PatternSyntaxException ,当 flags 中设置了与定义的匹配标志不同的位值时,抛出 IllegalArgumentException。 |
int 标志() | 返回这个模式对象的匹配标志。这个方法为通过编译(String) 创建的模式实例返回 0,为通过编译(String,int) 创建的模式实例返回标志位集。 |
匹配(序列输入)?? | 返回一个匹配器,它将根据这个模式的编译正则表达式匹配输入。 |
静态布尔匹配(字符串正则表达式,字符序列输入) | 编译正则表达式,并尝试将输入与编译后的正则表达式进行匹配。有匹配时返回 true 否则,返回 false。这个方便的方法相当于 Pattern.compile(regex)。匹配器(输入)。matches() 并在 regex 的语法无效时抛出 PatternSyntaxException。 |
字符串模式() | 返回这个模式的未编译的正则表达式。 |
静弦报(弦 s) | 使用“ \Q ”和“ \E ”引用 s ,使所有其他元字符失去其特殊含义。当返回的 java.lang.String 对象后来被编译成模式实例时,只能进行字面匹配。 |
字符串【拆分】(CharSequence 输入) | 在这个模式的编译正则表达式的匹配项周围分割输入,并返回一个包含匹配项的数组。 |
String[]split(char sequence input,int limit) | 在这个模式的编译正则表达式的匹配项周围拆分输入; limit 控制编译后的正则表达式被应用的次数,从而影响结果数组的长度。 |
String toString() | 返回这个模式的未编译的正则表达式。 |
表 13-3 揭示了 java.lang.CharSequence 接口,它描述了一个可读且不可变的 char 值序列——底层实现可能是可变的。实现该接口的任何类的实例(例如, String 、 java.lang.StringBuffer 和 Java . lang . stringbuilder)可以传递给采用 CharSequence 参数的模式方法(例如, split(CharSequence) )。
表 13-3 还揭示了模式的 compile() 方法及其 matches() 方法(该方法调用 compile(String) 方法)在编译模式参数时遇到语法错误时会抛出 patternsynctaxexception。表 13-4 描述了 PatternSyntaxException 的方法。
表 13-4。 PatternSyntaxException 方法
方法 | 描述 |
---|---|
字符串 getDescription() | 返回语法错误的描述。 |
int getiindex() | 返回模式中出现语法错误的位置的近似索引,如果索引未知,则返回 1。 |
字符串 getMessage() | 返回一个多行字符串,其中包含语法错误的描述及其索引、错误模式以及模式中错误索引的可视指示。 |
字串 get atten() | 返回错误的模式。 |
最后,表 13-4 的匹配器 匹配器(CharSequence input) 方法揭示了正则表达式 API 还提供了 java.util.regex.Matcher 类,其匹配器试图将编译后的正则表达式与输入文本进行匹配。 Matcher 声明以下方法执行匹配操作:
- 布尔匹配()尝试将整个区域与模式匹配。当匹配成功后,可以通过调用 Matcher 的 start() 、 end() 和 group() 方法获得更多信息。例如, int start() 返回前一个匹配的起始索引, int end() 返回前一个匹配后的第一个字符的偏移量, String group() 返回前一个匹配的输入子序列。当尚未尝试匹配或之前的匹配尝试失败时,每个方法都会抛出 Java . lang . illegalstateexception。
- boolean lookingAt() 尝试匹配输入序列,从区域的开头开始,对照模式。与 matches() 一样,这种方法总是从区域的开始处开始。与 matches() 不同, lookingAt() 不需要匹配整个区域。当匹配成功后,可以通过调用 Matcher 的 start() 、 end() 和 group() 方法获得更多信息。
- boolean find() 试图找到输入序列中与模式匹配的下一个子序列。它从这个匹配器的区域的开始处开始,或者,如果先前对此方法的调用成功,并且匹配器此后没有被重置(通过调用匹配器的匹配器重置()或匹配器重置(CharSequence input) 方法),则在先前匹配没有匹配的第一个字符处开始。当匹配成功后,可以通过调用 Matcher 的 start() 、 end() 和 group() 方法获得更多信息。
注意一个匹配器在其输入的一个称为区域的子集中寻找匹配。默认情况下,该区域包含匹配器的所有输入。可以通过调用 Matcher 的 Matcher region(int start,int end) 方法修改区域(设置该 Matcher 的区域的限制),通过调用 Matcher 的 int regionStart() 和 int regionEnd() 方法查询区域。
我创建了一个简单的应用,演示了模式、模式同步异常和匹配器。清单 13-9 展示了这个应用的源代码 。
清单 13-9。玩弄正则表达式
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
public class RegExDemo
{
public static void main(String[] args)
{
if (args.length != 2)
{
System.err.println("usage: java RegExDemo regex input");
return;
}
try
{
System.out.println("regex = " + args[0]);
System.out.println("input = " + args[1]);
Pattern p = Pattern.compile(args[0]);
Matcher m = p.matcher(args[1]);
while (m.find())
System.out.println("Located [" + m.group() + "] starting at "
+ m.start() + " and ending at " + (m.end() - 1));
}
catch (PatternSyntaxException pse)
{
System.err.println("Bad regex: " + pse.getMessage());
System.err.println("Description: " + pse.getDescription());
System.err.println("Index: " + pse.getIndex());
System.err.println("Incorrect pattern: " + pse.getPattern());
}
}
}
编译清单 13-9(javac RegExDemo.java)并通过 Java regex demo ox 运行这个应用。您将发现以下输出:
regex = ox
input = ox
Located [ox] starting at 0 and ending at 1
find() 通过将正则表达式字符与输入字符按照从左到右的顺序 进行比较来搜索匹配,并返回 true,因为 o 等于 o 并且 x 等于 x 。
继续,执行 java RegExDemo box ox 。这一次,您将发现以下输出:
regex = box
input = ox
find() 首先将正则表达式字符 b 与输入字符 o 进行比较。因为这些字符不相等,并且因为输入中没有足够的字符来继续搜索, find() 不会输出“Located ”消息来指示匹配。然而,如果您执行 java RegExDemo ox box ,您会发现一个匹配:
regex = ox
input = box
Located [ox] starting at 1 and ending at 2
ox regex 由文字字符组成。更复杂的正则表达式结合了文字字符和元字符(例如,句点[ )。 ])和其他正则表达式构造。
提示要指定一个元字符 作为文字字符,在元字符前加一个反斜杠(如)。)或将元字符置于 \Q 和 \E 之间(如 \Q.\E )。在这两种情况下,当转义元字符出现在字符串文字中时,请确保将反斜杠字符加倍;比如“\ \ .”或 “\Q.\E” 。
句点元字符匹配除行结束符之外的所有字符。例如,每个 java RegExDemo。ox box 和 java RegExDemo。牛狐报告匹配,因为周期匹配框中的 b 和狐中的 f 。
注意 模式识别以下行终止符:回车符( \r )、换行符(换行符)( \n )、回车符后紧跟换行符( \r\n )、下一行( \u0085 )、行分隔符( \u2028 )和段落分隔符( \u2029 )。通过指定模式,可以使句点元字符也匹配这些行终止符。调用 Pattern.compile(String,int) 时的 DOTALL 标志。
人物类
一个字符类是出现在和之间的一组字符。有六种字符类:
- 一个简单字符类由并排放置的文字字符组成,并且只匹配这些字符。比如【ABC】由人物 a 、 b 、 c 组成。另外,Java regex demo t[爱欧]ck tack 报告一个匹配,因为 a 是【爱欧】的成员。因为 i 、 o 和 u 是成员,所以当输入为勾号、库存或塔克时,它也报告匹配。
- 一个否定字符类由一个扬抑符元字符( ^ )组成,后跟并排放置的文字字符,匹配除类中字符之外的所有字符。比如【^abc】由除 a 、 b 、 c 以外的所有字符组成。另外,Java regex demo "【^b]ox】框不报告匹配,因为 b 不是【^b】的成员,而 Java regex demo "【^b]ox】fox 报告匹配,因为 f 是成员。(在我的 Windows 7 平台上,【^b]ox】周围的双引号是必要的,因为 ^ 在命令行被特殊对待。)
- 范围字符类由连续的文字字符组成,表示为起始文字字符、连字符元字符()和结束文字字符,并匹配该范围内的所有字符。例如,【a-z】由从 a 到 z 的所有角色组成。此外,Java regex demo[h-l]house house 报告匹配,因为 h 是该类的成员,而 Java regex demo[h-l]ouse mouse 不报告匹配,因为 m 位于范围之外,因此不是该类的一部分。通过将多个范围并排放置,可以在同一个范围字符类中组合多个范围;例如,【A-Za-z】全部由大小写拉丁字母组成。
- 一个并集字符类 由多个嵌套的字符类组成,匹配属于最终并集的所有字符。比如【ABC[u-z]】由人物 a 、 b 、 c 、 u 、 v 、 w 、 x 、 y 、 z 组成。另外,Java regex demo[[0–9][A-F][A-F]]e 报告一个匹配,因为 e 是一个十六进制字符。(我也可以通过组合多个范围将这个字符类表示为【0-9A-Fa-f】。)
- 一个交集字符类由多个 & & 组成——分隔的嵌套字符类,匹配这些嵌套字符类共有的所有字符。比如【a-c&&【c-f】】由字符 c 组成,这是【a-c】【c-f】唯一共有的字符。另外,Java regex demo “[aeiouy&&【y】]” y 报告一个匹配,因为 y 对于类【aeiouy】和【y】是通用的。
- 一个减法字符类由多个 & & 分隔的嵌套字符类组成,其中至少一个嵌套字符类是否定字符类,并且匹配除了由否定字符类指示的那些字符之外的所有字符。比如【a-z&&【^x-z】由字符 a 到 w 组成。(^x-z 周围的方括号是必需的;否则, ^ 被忽略,结果类仅由 x 、 y 和 z 组成。)还有,Java regex demo “[a-z&&【^aeiou】]” g 报告匹配,因为 g 是辅音,只有辅音属于这个类。(我在忽略 y ,有时被当做辅音,有时被当做元音。)
一个预定义的字符类是一个通常指定的字符类的正则表达式。表 13-5 标识了图案的预定义字符类别。
表 13-5。 预定义的人物类
预定义字符类 | 描述 |
---|---|
\d | 匹配任何数字字符。 \d 相当于【0–9】。 |
\D | 匹配任何非数字字符。 \D 相当于【^\d】。 |
\s | 匹配任何空白字符。 \s 相当于 [\t\n\x0B\f\r ] 。 |
\S | 匹配任何非白色空间字符。 \S 相当于【^\s】。 |
\w | 匹配任何单词字符。 \w 相当于【a-zA-Z0-9】。 |
\W | 匹配任何非单词字符。 \W 相当于【^\w】。 |
例如, java RegExDemo \wbc abc 报告一个匹配,因为 \w 匹配 abc 中的单词字符 a 。
捕获组
一个捕获组保存一个匹配的字符供以后在模式匹配时调用,并被表示为一个由括号元字符 ( 和 ) 包围的字符序列。捕获组中的所有字符被视为一个单元。比如(安卓)捕捉组,将 A 、 n 、 d 、 r 、 o 、 i 、 d 组合成一个单位。它匹配输入中所有出现的 Android 的 Android 模式。每次匹配都用下一次匹配的 Android 字符替换前一次匹配保存的 Android 字符。
捕获组可以出现在其他捕获组中。例如,捕获组(A)(B©)出现在捕获组 ((A)(B©)) 内部,捕获组 © 出现在捕获组 (B©) 内部。每个嵌套或非嵌套捕获组都有自己的编号,编号从 1 开始,捕获组从左到右进行编号。例如, ((A)(B©)) 赋 1, (A) 赋 2, (B©) 赋 3, © 赋 4。
捕获组通过后向引用保存其匹配项,后向引用是一个反斜杠字符,后跟一个表示捕获组编号的数字字符。反向引用使得匹配器使用反向引用的捕获组号来调用捕获组保存的匹配,然后使用该匹配的字符来尝试进一步的匹配。以下示例使用反向引用来确定输入是否由两个连续的 Android 模式组成:
java RegExDemo "(Android) \1" "Android Android"
RegExDemo 报告一个匹配,因为匹配器在输入中检测到 Android ,后跟一个空格,然后是 Android 。
边界匹配器和零长度匹配器
边界匹配器是一个 regex 构造,用于识别行首、单词边界、文本结尾和其他常见的边界。参见表 13-6 。
表 13-6。 边界匹配器
边界匹配器 | 描述 |
---|---|
^ | 匹配行首。 |
$ | 匹配行尾。 |
\b | 匹配单词边界。 |
\B | 匹配非单词边界。 |
\A | 匹配文本开头。 |
\G | 前一场比赛的结束。 |
\Z | 匹配除行结束符(如果存在)之外的文本结尾。 |
\z | 匹配文本结尾。 |
例如,Java regex demo \ b \ b " I think "报告了几个匹配项,如以下输出所示:
regex = \b\b
input = I think
Located [] starting at 0 and ending at −1
Located [] starting at 1 and ending at 0
Located [] starting at 2 and ending at 1
Located [] starting at 7 and ending at 6
这个输出揭示了几个零长度匹配。当出现零长度匹配时,开始和结束索引相等,尽管输出显示结束索引比开始索引小一,因为我在清单 13-9 中指定了 end() - 1 (这样匹配的结束索引标识非零长度匹配的最后一个字符,而不是非零长度匹配的最后一个字符之后的字符)。
注意零长度匹配出现在空输入文本中、输入文本的开头、输入文本的最后一个字符之后或该文本的任意两个字符之间。零长度匹配很容易识别,因为它们总是在相同的索引位置开始和结束。
量词
我给出的最后一个 regex 构造是量词,一个隐式或显式绑定到模式的数值。量词分为贪婪、勉强和占有三类:
- 一个贪婪量词 ( ?、 * 或 + )试图找到最长的匹配。指定X?查找一个或没有出现的 X ,X**查找零个或多个出现的 X , X + 查找一个或多个出现的 X ,X{n}查找 *X{n,} 找到至少 n (可能更多)出现的 X ,X{n, m } 找到至少n**
** 一个舍不得量词 ( ??、 ?,还是 +?)试图找到最短的匹配。指定X*??查找 X , X 的一个或不出现?查找零个或多个出现的 X 、X+?查找 X ,X{n}?查找 n 出现的 X ,X{n,}?找出至少 n (也可能更多)出现的 X ,和X*{n, m }?找出至少 n 但不超过 m 个 X 的出现次数。* 一个所有格量词 ( ?+ 、 + 或 ++ )除了所有格量词只进行一次寻找最长匹配的尝试,而贪婪量词可以进行多次尝试之外,与贪婪量词类似。指定X*?+ 查找一个或没有出现的 X , X + 查找零个或多个出现的 X ,X+查找一个或多个出现的 X ,X{n}+到 X{n,}+ 查找至少出现 X 的X*{n, m }+ 查找至少出现*
**对于贪婪量词的例子,执行 java RegExDemo。*end “wend rend end” 。您将发现以下输出:
regex = .*end
input = wend rend end
Located [wend rend end] starting at 0 and ending at 12
贪婪的量词 ( )。* )匹配以结尾结束的最长字符序列。它首先消耗所有的输入文本,然后被迫后退,直到发现输入文本以这些字符结束。
对于不情愿量词的例子,执行 java RegExDemo。*?end【wend rend end】。您将发现以下输出:
regex = .*?end
input = wend rend end
Located [wend] starting at 0 and ending at 3
Located [ rend] starting at 4 and ending at 8
Located [ end] starting at 9 and ending at 12
不情愿的量词()。*?)匹配以结尾结束的最短字符序列。它开始不消耗任何东西,然后慢慢消耗字符,直到找到匹配。然后继续,直到输入完所有文本。
对于所有格量词的例子,执行 java RegExDemo。*+end “wend rend end” 。您将发现以下输出:
regex = .*+end
input = wend rend end
所有格量词()。*+ )没有检测到匹配,因为它消耗了整个输入文本,没有留下任何内容来匹配正则表达式末尾的 end 。与贪婪量词不同,所有格量词不会后退。
使用量词时,您可能会遇到零长度匹配。比如执行 java RegExDemo 1?101101 :
regex = 1?
input = 101101
Located [1] starting at 0 and ending at 0
Located [] starting at 1 and ending at 0
Located [1] starting at 2 and ending at 2
Located [1] starting at 3 and ending at 3
Located [] starting at 4 and ending at 3
Located [1] starting at 5 and ending at 5
Located [] starting at 6 and ending at 5
这个贪婪量词的结果是在输入文本的位置 0、2、3 和 5 检测到了 1 ,而在位置 1、4 和 6 没有检测到任何内容(零长度匹配)。
这次执行 java RegExDemo 1??101101 :
regex = 1??
input = 101101
Located [] starting at 0 and ending at −1
Located [] starting at 1 and ending at 0
Located [] starting at 2 and ending at 1
Located [] starting at 3 and ending at 2
Located [] starting at 4 and ending at 3
Located [] starting at 5 and ending at 4
Located [] starting at 6 and ending at 5
这个输出可能看起来令人惊讶,但是请记住,不情愿的量词寻找最短的匹配,这(在本例中)根本不是匹配。
最后执行 java RegExDemo 1+?101101 :
regex = 1+?
input = 101101
Located [1] starting at 0 and ending at 0
Located [1] starting at 2 and ending at 2
Located [1] starting at 3 and ending at 3
Located [1] starting at 5 and ending at 5
这个所有格量词只匹配输入文本中检测到 1 的位置。它不执行零长度匹配。
注意查看关于模式类的 Java 文档,了解更多的正则表达式构造。** **实用的正则表达式
除了帮助您掌握如何使用各种正则表达式构造之外,前面的大多数正则表达式示例都不实用。相反,下面的例子显示了一个正则表达式,它匹配形式为 (ddd) ddd-dddd 或 ddd-dddd 的电话号码。在 (ddd) 和 ddd 之间出现一个空格;连字符两边都没有空格。
java RegExDemo "(\(\d{3}\))?\s*\d{3}-\d{4}" "(800) 555-1212"
regex = (\(\d{3}\))?\s*\d{3}-\d{4}
input = (800) 555–1212
Located [(800) 555–1212] starting at 0 and ending at 13
java RegExDemo "(\(\d{3}\))?\s*\d{3}-\d{4}" 555–1212
regex = (\(\d{3}\))?\s*\d{3}-\d{4}
input = 555–1212
Located [555–1212] starting at 0 and ending at 7
注意要了解关于正则表达式的更多信息,请查看Java 教程中的“课程:正则表达式”(download . Oracle . com/javase/tutorial/essential/regex/index . html
)。
练习
以下练习旨在测试您对第十三章内容的理解:
- 定义新的 I/O。
- 什么是缓冲?
- 确定缓冲区的四个属性。
- 当您在由只读数组支持的缓冲区上调用 Buffer 的 array() 方法时会发生什么?
- 当你在一个缓冲区上调用 Buffer 的 flip() 方法时会发生什么?
- 当你在一个没有设置标记的缓冲区上调用 Buffer 的 reset() 方法时会发生什么?
- 是非判断:缓冲区是线程安全的。
- 识别扩展抽象缓冲区类的类。
- 如何创建一个字节缓冲区?
- 定义视图缓冲区。
- 如何创建视图缓冲区?
- 如何创建只读视图缓冲区?
- 识别 ByteBuffer 在字节缓冲区存储单个字节和从字节缓冲区获取单个字节的方法。
- 什么原因导致 BufferOverflowException 或 BufferUnderflowException 发生?
- 执行 buffer.flip()相当于什么;?
- 是非判断:调用 flip() 两次会将您返回到初始状态。
- Buffer 的 clear() 和 reset() 方法有什么区别?
- ByteBuffer 的 compact() 方法完成了什么?
- ByteOrder 类的用途是什么?
- 定义直接字节缓冲区。
- 你如何获得一个直接字节缓冲区?
- 什么是渠道?
- 通道接口提供什么功能?
- 确定直接扩展通道的三个接口。
- 是非判断:实现 InterruptibleChannel 的通道是异步关闭的。
- 确定获得渠道的两种方式。
- 定义分散/聚集 I/O。
- 为实现分散/聚集 I/O 提供了哪些接口?
- 定义文件通道。
- 是非判断:文件通道不支持分散/聚集 I/O。
- FileChannel 提供了什么方法将文件的一个区域映射到内存中?
- FileChannel 的 lock() 和 tryLock() 方法的根本区别是什么?
- 定义正则表达式。
- 模式类完成什么?
- 当模式的编译()方法在它们的正则表达式参数中发现非法语法时,它们会做什么?
- Matcher 类完成什么?
- Matcher 的 matches() 和 lookingAt() 的方法有什么区别?
- 定义字符类。
- 识别不同种类的字符类别。
- 定义捕获组。
- 什么是零长度匹配?
- 定义量词。
- 贪心量词和勉强量词有什么区别?
- 所有格量词和贪心量词有什么区别?
- 重构清单 11-8 ( 第十一章的复制应用)以使用字节缓冲和文件通道类与文件输入流和文件输出流合作。
- 创建一个 ReplaceText 应用,该应用接受输入文本、指定要替换的文本的模式和替换文本命令行参数,并使用匹配器的 String replaceAll(字符串替换)方法用替换文本替换模式的所有匹配(传递给替换)。例如, java ReplaceText “太多嵌入空格” " \s+" " " 应该输出太多嵌入空格连续单词之间只有一个空格字符。
摘要
Java 1.4 引入了更强大的 I/O 架构,支持内存映射文件 I/O、就绪选择、文件锁定等等。这种架构由缓冲区、通道、选择器、正则表达式和字符集组成,通常被称为新 I/O (NIO)。
缓冲区是一个对象,它存储要发送到 I/O 服务或从 I/O 服务接收的固定数量的数据,或者已经从 I/O 服务接收的数据。它位于应用和向服务写入缓冲数据或从服务读取数据并将其存入缓冲区的通道之间。
通道是一个对象,它表示到硬件设备、文件、网络套接字、应用组件或其他能够执行写、读和其他 I/O 操作的实体的开放连接。通道在缓冲区和其他实体之间传输数据。
正则表达式(也称为 regex 或 regexp)是一种基于字符串的模式,表示与该模式匹配的字符串集。该模式由字面字符和元字符组成,元字符是具有特殊含义而非字面含义的字符。
第十四章重点介绍数据库访问。您首先会接触到 Java DB 和 SQLite 数据库产品,然后学习如何使用 JDBC API 来创建/访问它们的数据库。**