Java7 入门手册(七)
九、与网络和数据库交互
访问应用外部的数据有三个目标:文件系统、网络和数据库。第八章向您介绍了面向文件系统的数据访问,而本章向您介绍了通过网络和数据库的数据访问。
与网络互动
一个网络是相互连接的节点(计算机和外围设备【例如打印机】)的集合,这些节点可以在用户之间共享硬件和软件。一个内部网是一个组织内部的网络,一个互联网是一个将组织连接在一起的网络。互联网是网络的全球网络。
注意内部网和互联网通常使用传输控制协议(TCP)、用户数据报协议(UDP)和互联网协议(IP)在节点之间进行通信。TCP 是双向通信协议,UDP 是单向通信协议,IP 是 TCP 和 UDP 执行其通信任务的基本通信协议。TCP、UDP 和 IP 与其他协议结合成一个模型,称为 TCP/IP(见
[en.wikipedia.org/wiki/TCP/IP_model](http://en.wikipedia.org/wiki/TCP/IP_model))。
java.net包提供了支持运行在相同或不同主机(基于计算机的 TCP/IP 节点)上的进程(执行应用)之间的 TCP/IP 通信的各种类。在向您介绍了这些类之后,本节将介绍身份验证和 cookie 管理。
通过套接字通信
一个套接字是两个进程之间通信链路的一个端点。端点由一个识别主机的 IP 地址和一个识别在该网络节点上运行的进程的端口号组成。
一个进程将消息(字节序列)写入套接字,套接字将该消息分解成一系列数据包(可寻址消息块,通常称为 IP 数据报),并将这些数据包转发给另一个进程的套接字,套接字将它们重新组合成原始消息供该进程使用。图 9-1 显示了这种情况。
***图 9-1。*两个进程使用套接字进行通信。
根据图 9-1 ,主机 A 上的进程 A 向 socket 发送消息。主机 A 的网络管理软件,通常被称为协议栈,将该消息分解成一系列数据包(每个数据包包括目的主机的 IP 地址和端口号),并通过主机 A 的网络接口卡(NIC)将这些数据包发送到目的主机,即图中的主机 B。主机 B 的协议栈通过网卡接收数据包,并将它们重组为原始消息,然后提供给进程 B。当进程 B 与进程 a 通信时,情况正好相反。
IP 地址和端口号
IP 地址是 32 位或 128 位无符号整数,唯一标识网络主机和其他节点。32 位 IP 地址通常以句点分隔的十进制表示法指定为四个 8 位整数组成部分,其中每个组成部分都是范围从 0 到 255 的十进制整数,并通过句点与下一个组成部分分隔开(例如,127.0.0.1)。相比之下,128 位 IP 地址通常被指定为冒号分隔的十六进制表示法中的八个 16 位整数部分,其中每个部分是范围从 0 到 FFFF 的十六进制整数,并且通过冒号与下一个部分分隔开(例如,1080:0:0:0:8:800:200C:417A)。32 位 IP 地址通常被称为互联网协议第 4 版(IPv4)地址(参见[en.wikipedia.org/wiki/IPv4](http://en.wikipedia.org/wiki/IPv4))。类似地,128 位 IP 地址通常被称为互联网协议第 6 版(IPv6)地址(参见[en.wikipedia.org/wiki/IPv6](http://en.wikipedia.org/wiki/IPv6))。
端口号是 16 位无符号整数,唯一标识进程,即消息的来源或接收者。小于 1024 的端口号保留给标准进程。例如,端口号 25 传统上标识发送电子邮件的简单邮件传输协议(SMTP)进程,尽管端口号 587 目前普遍使用(见[en.wikipedia.org/wiki/Smtp](http://en.wikipedia.org/wiki/Smtp))。
TCP 用于通过来回发送消息来创建两台主机之间的持续对话。在此对话发生之前,必须在这些主机之间建立连接。建立连接后,TCP 进入一种模式,发送一个信息包并等待信息包正确到达的应答(或者当由于网络问题应答没有到达时等待超时)。这种发送/回复循环保证了可靠的连接。
因为建立连接需要时间,而且因为需要接收应答确认(或超时)而发送数据包也需要时间,所以 TCP 相当慢。UDP 不需要连接和数据包确认,比 TCP 快得多。然而,UDP 不像 TCP 那样可靠(不能保证数据包会正确到达,甚至到达),因为没有确认。此外,UDP 仅限于单包单向会话。
java.net包提供了用于执行基于 TCP 的通信的Socket和ServerSocket类。它还提供了用于执行 UDP 通信的DatagramSocket、DatagramPacket和MulticastSocket类。MulticastSocket是DatagramSocket的子类。
套接字地址
后缀为Socket的类的实例与由 IP 地址和端口号组成的套接字地址相关联。
Socket类依赖于java.net.InetAddress类来表示套接字地址的 IPv4 或 IPv6 地址部分。它单独代表端口号。(另一个Socket-后缀类也利用了InetAddress。)
注意
InetAddress依赖其java.net.Inet4Address子类来表示 IPv4 地址,依赖其java.net.Inet6Address子类来表示 IPv6 地址。
InetAddress声明了几个获取InetAddress实例的类方法。这些方法包括以下内容:
InetAddress[] getAllByName(String host)返回一个存储与host相关的 IP 地址的InetAddress数组。您可以向此参数传递域名(例如“tutortutor.ca”)或 IP 地址(例如“70.33.247.10”)参数。(查看维基百科的“域名”词条[[en.wikipedia.org/wiki/Domain_name](http://en.wikipedia.org/wiki/Domain_name)]了解域名。)传递null会产生一个InetAddress实例,该实例存储环回接口的 IP 地址(稍后定义)。当找不到指定的host的 IP 地址,或者为全局 IPv6 地址指定了作用域标识符时,该方法抛出java.net.UnknownHostException。InetAddress getByAddress(byte[] addr)为给定的原始 IP 地址返回一个InetAddress对象。传递给addr的参数按照网络字节顺序(最高有效字节优先),其中最高顺序字节在addr[0]。对于 IPv4 地址,addr数组的长度必须是 4 个字节,对于 IPv6 地址必须是 16 个字节。当数组的长度既不是 4 也不是 16 时,这个方法抛出UnknownHostException。InetAddress getByAddress(String host, byte[] addr)根据提供的主机名和 IP 地址返回一个InetAddress实例。当数组的长度既不是 4 也不是 16 时,这个方法抛出UnknownHostException。InetAddress getByName(String host)相当于指定getAllByName(host)[0]。InetAddress getLocalHost()返回本地主机(当前主机)的地址,该地址由主机名 local host 或 IP 地址(通常为 127.0.0.1 [IPv4]或::1[IPv6])表示。当 localhost 不能被解析成地址时,这个方法抛出UnknownHostException。InetAddress getLoopbackAddress()返回回送地址(一个特殊的 IP 地址,允许网络管理软件将传出消息视为传入消息)。返回的InetAddress实例表示 IPv4 环回地址 127.0.0.1 或 IPv6 环回地址:1。返回的 IPv4 环回地址只是许多 127 格式地址中的一个。..,其中是范围从 0 到 255 的通配符。
一旦有了一个InetAddress实例,就可以通过调用实例方法来询问它,例如byte[] getAddress()(返回这个InetAddress对象的原始 IP 地址(按照网络字节顺序)和boolean isLoopbackAddress()(确定这个InetAddress实例是否代表一个回送地址)。
Java 1.4 引入了抽象的java.net.SocketAddress类来表示“没有协议附件”的套接字地址也许这个类的创建者预见到 Java 最终会支持低级别的通信协议,而不是广泛流行的互联网协议。
SocketAddress由具体的java.net.InetSocketAddress类子类化,该类将套接字地址表示为 IP 地址和端口号。它还可以表示主机名和端口号,并将尝试解析主机名。
InetSocketAddress实例是通过调用InetSocketAddress(InetAddress addr, int port)等构造函数创建的。创建实例后,可以调用InetAddress getAddress()和int getPort()等方法来返回套接字地址组件。
插座选项
除了共享套接字地址的概念,各种带后缀的类也共享套接字选项的概念。套接字选项是用于配置套接字行为的参数。下面的 C 语言常量通过各种方法来标识Socket后缀类支持的套接字选项:
TCP_NODELAY:禁用 Nagle 的算法 ([en.wikipedia.org/wiki/Nagle's_algorithm](http://en.wikipedia.org/wiki/Nagle's_algorithm))。该选项对Socket有效。SO_LINGER:指定持续关闭超时。该选项对Socket有效。SO_TIMEOUT:指定阻塞套接字操作的超时时间。(永远不要挡!)该选项对Socket、ServerSocket和DatagramSocket有效。SO_BINDADDR:获取套接字的本地地址绑定。该选项对Socket、ServerSocket和DatagramSocket有效。SO_REUSEADDR:启用套接字重用地址。该选项对Socket、ServerSocket和DatagramSocket有效。SO_BROADCAST:启用套接字发送广播消息。该选项对DatagramSocket有效。SO_SNDBUF:设置或获取最大套接字发送缓冲区,以字节为单位。该选项对Socket、ServerSocket和DatagramSocket有效。SO_RCVBUF:设置或获取最大套接字接收缓冲区,以字节为单位。该选项对Socket、ServerSocket和DatagramSocket有效。SO_KEEPALIVE:打开 socket keepalive。该选项对Socket有效。SO_OOBINLINE:启用 TCP 紧急数据的内嵌接收。该选项对Socket有效。IP_MULTICAST_IF:指定组播数据包的输出接口(在多宿主【例如,多个 NIC】主机上)。该选项仅对MulticastSocket有效。IP_MULTICAST_LOOP:启用或禁用组播数据报的本地回环。该选项仅对MulticastSocket有效。IP_TOS:为 TCP 或 UDP 套接字设置 IP 报头中的服务类型或流量类别字段。该选项对Socket和DatagramSocket有效。
带后缀的类提供了设置/获取这些选项的 setter 和 getter 方法。例如,Socket声明void setKeepAlive(boolean on)设置SO_KEEPALIVE选项,MulticastSocket声明void setLoopbackMode(boolean disable)设置IP_MULTICAST_LOOP选项。查看关于java.net的Socket后缀类的 JDK 文档,了解这些和其他套接字选项方法,了解更多关于各种套接字选项的信息。
注意适用于
DatagramSocket的套接字选项也适用于它的MulticastSocket子类。
套接字和服务器套接字
Socket和ServerSocket类允许您在客户端进程(例如,在您的桌面上运行的应用)和服务器进程(例如,在您的互联网服务提供商的计算机上运行的提供对万维网的访问的应用)之间执行基于 TCP 的通信。因为Socket与java.io.InputStream和java.io.OutputStream类相关联,所以基于Socket类的套接字通常被称为流套接字。
Socket用于在客户端创建一个套接字。它声明了几个构造函数,包括下面的一对:
Socket(InetAddress address, int port)创建一个流套接字,并通过指定的 IPaddress将其连接到指定的port号。当创建套接字时出现 I/O 错误时,该构造函数抛出java.io.IOException,当传递给port的参数超出端口值的有效范围(从 0 到 65535)时抛出java.lang.IllegalArgumentException,当address为null时抛出java.lang.NullPointerException。Socket(String host, int port)创建一个流套接字,并将其连接到指定的host上的指定port号。当host为null时,这个构造函数相当于调用Socket(InetAddress.getByName(null), port)。它抛出与前面的构造函数相同的IOException和IllegalArgumentException实例。然而,当无法确定主机的 IP 地址时,它并不抛出NullPointerException,而是抛出UnknownHostException。
当通过这些构造函数创建一个Socket实例时,它在连接到远程主机套接字地址之前绑定到一个任意的本地主机套接字地址。绑定使客户机套接字地址对服务器套接字可用,以便服务器进程可以通过服务器套接字与客户机进程通信。
Socket提供额外的构造函数给你灵活性。例如,Socket()和Socket(Proxy proxy)创建未绑定和未连接的套接字。在使用这些套接字之前,需要通过调用void bind(SocketAddress bindpoint)将它们绑定到本地套接字地址,然后通过调用Socket的connect()方法进行连接,比如void connect(SocketAddress endpoint)。
注意代理是一台出于安全目的位于内部网和互联网之间的计算机。代理设置由
java.net.Proxy类的实例表示,帮助套接字通过代理进行通信。
另一个构造函数是Socket(InetAddress address, int port, InetAddress localAddr, int localPort),它让你通过localAddr和localPort指定你自己的本地主机套接字地址。这个构造函数自动绑定到本地套接字地址,然后尝试连接到远程address。
在创建了一个Socket实例,并可能在该实例上调用bind()和connect()之后,应用通常会调用Socket的InputStream getInputStream()和OutputStream getOutputStream()方法来获得从套接字读取字节的输入流和向套接字写入字节的输出流。此外,一旦不再需要执行输入或输出操作,应用通常会调用Socket的void close()方法来关闭套接字。
以下示例演示了如何在本地主机上创建一个绑定到端口号 1500 的套接字,然后访问其输入和输出流—为简洁起见,将忽略异常:
Socket socket = new Socket("localhost", 1500);
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
我创建了一个GetTime应用,通过创建一个套接字连接到美国国家标准协会&技术(NIST)时间服务器来检索和输出当前时间,从而演示了Socket类。清单 9-1 展示了这个应用的源代码。
**清单 9-1。**根据 NIST 实施的白天协议获取并输出当前时间
`import java.io.InputStream; import java.io.IOException;
import java.net.Socket; import java.net.UnknownHostException;
class GetTime
{
public static void main(String[] args)
{
if (args.length != 1)
{ System.err.println("usage : java GetTime server");
System.err.println("example: java GetTime time.nist.gov");
return;
}
try (Socket socket = new Socket(args[0], 13))
{
InputStream is = socket.getInputStream();
int ch;
while ((ch = is.read()) != -1)
System.out.print((char) ch);
}
catch (UnknownHostException uhe)
{
System.err.println("unknown host: "+uhe.getMessage());
}
catch (IOException ioe)
{
System.err.println("I/O error: "+ioe.getMessage());
}
}
}`
清单 9-1 描述了一个创建一个Socket实例的应用,该实例在端口 13 上连接到一个远程服务器,该端口是为互联网的日间协议保留的。根据该协议,客户端套接字在端口 13 上连接到服务器进程,并且实现 Daytime 的进程立即向客户端套接字返回包含当前日期和时间的 ASCII ( [en.wikipedia.org/wiki/Ascii](http://en.wikipedia.org/wiki/Ascii))字符串。
注互联网工程任务组发布备忘录,描述适用于互联网和互联网连接系统工作的方法、行为、研究或创新。这些备忘录统称为意见征询(RFC) 文件。RFC 867 描述了日间互联网协议(
[tools.ietf.org/html/rfc867](http://tools.ietf.org/html/rfc867)),并没有强制要求 ASCII 字符串的特定语法(其实现者可以自由使用他们自己的语法)。
各种时间服务器实现 Daytime(例如运行在与互联网域名time.nist.gov相关联的计算机上的服务器)。认识到这一事实后,GetTime要求您将时间服务器域名指定为命令行参数。例如,当您指定java GetTime time.nist.gov时,您将收到类似如下所示的输出:
55811 11-09-07 22:03:15 50 0 0 816.1 UTC(NIST) *
此输出符合日间协议的以下 NIST 语法:
JJJJJ YR-MO-DA HH:MM:SS TT L H msADV UTC(NIST) OTM
这些字段具有以下含义:
JJJJJ指定儒略日。YR-MO-DA以年/月/日格式指定日期。HH:MM:SS以小时/分钟/秒格式指定时间。该时间以协调世界时(UTC)表示—参见[en.wikipedia.org/wiki/UTC](http://en.wikipedia.org/wiki/UTC)。TT表示定时服务器是标准时间(ST)还是夏令时(DST),其中 00 表示标准时间,50 表示 DST。L表示如何处理月末的闰秒;它是 0(无闰秒)、+1(加一个闰秒)或-1(减一个闰秒)中的一个。H表示时间服务器的健康状况。它是 0(健康)或正整数(不健康)之一。msADV表示为补偿网络延迟时间提前的毫秒数。UTC(NIST)标识msADV值的发起者。OTM表示准时标记。
查看位于[www.nist.gov/pml/div688/…](http://www.nist.gov/pml/div688/grp40/its.cfm)的网页,了解关于这种语法的更多信息。
虽然您可以通过InputStream和OutputStream引用从套接字读取字节或向套接字写入字节,但是您通常会通过将这些引用包装在java.io.BufferedReader和java.io.PrintWriter类的实例中,将它们用作更方便的字符 I/O 流的基础,如下所示:
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
PrintWriter pw = new PrintWriter(os);
第一行创建了一个读取器,它将一个传入的字节流连接到一个传出的字符流,后者是根据默认的字符编码从字节中解码出来的(参见第八章)。然后将返回的读取器传递给BufferedReader以提高性能并获得对BufferedReader的String readLine()方法的访问,该方法可以方便地让您读取由换行符('\n')、回车符('\r')或回车符后紧跟换行符中的任何一个结束的字符串。
第三行使用PrintWriter(OutputStream out)构造函数创建一个PrintWriter实例,用于将字符串写入输出流,并通过内部创建的设置为默认字符编码的输出流编写器实例将这些字符转换为字节流。
当您调用这个构造函数时,当您调用一个println()方法时,它不会自动将字节刷新到输出流。为了确保输出字节,您需要在println()之后调用flush()方法。但是,您可以通过使用PrintWriter(OutputStream out, boolean autoFlush)构造函数并将true传递给autoFlush来确保刷新发生。
ServerSocket用于创建服务器端的一个 TCP 连接。服务器套接字等待通过网络传入的请求。它根据请求执行一些操作,然后可能向请求者返回一个结果。
当服务器套接字处理请求时,可能会有其他请求到达。这些请求存储在队列中,供后续处理。
ServerSocket声明了四个构造函数:
ServerSocket()创建一个未绑定的服务器套接字。您可以通过调用ServerSocket的两个bind()方法之一将这个套接字绑定到一个特定的套接字地址(客户端套接字与之通信)。绑定使服务器套接字地址对客户端套接字可用,以便客户端进程可以通过客户端套接字与服务器进程通信。当试图打开套接字时发生 I/O 错误时,该构造函数抛出IOException。ServerSocket(int port)创建绑定到指定的port值和与主机网卡之一相关联的 IP 地址的服务器套接字。当您将0传递到port时,会选择一个任意端口号。可以通过调用int getLocalPort()来检索端口号。传入连接指示(来自客户端的连接请求)的最大队列长度设置为 50。如果连接指示在队列已满时到达,则连接被拒绝。当试图打开套接字时发生 I/O 错误时,该构造函数抛出IOException,当port的值超出指定的有效端口值范围(0 到 65535,包括 0 和 65535)时,该构造函数抛出IllegalArgumentException。ServerSocket(int port, int backlog)相当于前面的构造函数,但是它也允许您通过向backlog传递一个正整数来指定最大队列长度。ServerSocket(int port, int backlog, InetAddress bindAddr)相当于前面的构造函数,但是它也允许您指定服务器套接字绑定到的不同 IP 地址。此构造函数对于具有多个 NIC 的计算机非常有用,并且您希望侦听特定 NIC 上的连接指示。
创建服务器套接字后,服务器应用进入一个循环,首先调用ServerSocket的Socket accept()方法监听连接指示,并返回一个Socket实例,让它与相关的客户端套接字通信。然后,它与客户端套接字进行通信,以执行某种处理。当处理完成时,服务器套接字调用客户机套接字的close()方法来终止它与客户机的连接。
注意
ServerSocket声明了一个void close()方法,用于在终止服务器应用之前关闭服务器套接字。
以下示例演示了如何创建一个绑定到当前主机上的端口 1500 的服务器套接字,侦听传入的连接指示,返回它们的套接字,在这些套接字上执行工作,以及关闭套接字—为简洁起见,将忽略异常:
ServerSocket ss = new ServerSocket(1500);
while (true)
{
Socket socket = ss.accept();
// obtain socket input/output streams and communicate with socket
socket.close();
}
accept()方法调用一直阻塞,直到有连接指示可用,然后返回一个Socket对象,以便服务器应用可以与其关联的客户端通信。通信发生后,套接字被关闭。
此示例假设套接字通信发生在服务器应用的主线程上,当处理需要时间来执行时,这是一个问题,因为服务器对传入连接指示的响应时间减少了。为了加快响应时间,通常需要与工作线程上的套接字进行通信,如下例所示:
ServerSocket ss = new ServerSocket(1500);
while (true)
{
final Socket s = ss.accept();
new Thread(new Runnable()
{
private volatile Socket socket = s;
@Override
public void run()
{
// obtain socket input/output streams and communicate with socket
try { socket.close(); } catch (IOException ioe) {}
}
}).start();
}
每次连接指示到达时,accept()返回一个Socket实例,然后创建一个java.lang.Thread对象,它的 runnable 访问那个套接字,以便与工作线程上的套接字通信。
因为套接字分配(socket = s)发生在服务器应用的主线程上,并且因为socket也在工作线程上被访问,所以socket必须被声明为volatile,以解决主线程和工作线程在不同的处理器或内核上运行并且拥有它们自己的socket引用变量的缓存副本的情况。
提示虽然这个例子使用了
Thread类,但是你也可以使用一个执行器(参见第六章)来代替。
为了演示ServerSocket和Socket,我创建了ChatServer和ChatClient应用,让多个用户进行通信。清单 9-2 展示了ChatServer的源代码。
***清单 9-2。*让多个用户通信
`import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.IOException; import java.io.PrintWriter;
import java.net.ServerSocket; import java.net.Socket;
import java.util.ArrayList; import java.util.List;
class ChatServer
{
private final static int PORT_NO = 8010;
private ServerSocket listener;
private List clients;
ChatServer() throws IOException
{
listener = new ServerSocket(PORT_NO);
clients = new ArrayList<>();
System.out.println("listening on port "+PORT_NO);
}
void runServer()
{
try
{
while (true)
{
Socket socket = listener.accept();
System.out.println("accepted connection");
Connection con = new Connection(socket);
synchronized(clients)
{
clients.add(con);
con.start();
if (clients.size() == 1)
con.send("welcome...you're the first user");
else
con.send("welcome...you're the latest of "+clients.size()+
" users");
}
}
}
catch (IOException ioe)
{
System.err.println("I/O error: "+ioe.getMessage());
return;
}
}
private class Connection extends Thread
{
private volatile BufferedReader br;
private volatile PrintWriter pw;
private String clientName;
Connection(Socket s) throws IOException
{
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
pw = new PrintWriter(s.getOutputStream(), true);
}
@Override
public void run() {
String line;
try
{
clientName = br.readLine();
sendClientsList();
while ((line = br.readLine()) != null)
broadcast(clientName+": "+line);
}
catch (IOException ioe)
{
System.err.println("I/O error: "+ioe.getMessage());
}
finally
{
System.out.println(clientName+": "+"finished");
synchronized(clients)
{
clients.remove(this);
broadcast("now "+clients.size()+" users");
sendClientsList();
}
}
}
private void broadcast(String message)
{
System.out.println("broadcasting "+message);
synchronized(clients)
{
for (Connection con: clients)
con.send(message);
}
}
private void send(String message)
{
pw.println(message);
}
private void sendClientsList()
{
StringBuilder sb = new StringBuilder();
synchronized(clients)
{
for (Connection con: clients)
{
sb.append(con.clientName);
sb.append(" ");
}
broadcast("!"+sb.toString());
}
}
}
public static void main(String[] args) {
try
{
System.out.println("ChatServer starting");
new ChatServer().runServer();
}
catch (IOException ioe)
{
System.err.println("unable to create server socket");
}
}
}`
清单 9-2 的ChatServer类由私有常量/非常数字段、一个构造函数、一个void runServer()方法、一个私有的Connection嵌套类(该嵌套类子类化Thread)和一个main()方法组成,该方法通过方法调用链调用该构造函数,后跟runServer()(参见第二章)。
构造函数试图创建服务器套接字;当成功时,它创建一个数组列表,存储代表来自聊天客户端的传入连接指示的Connection对象。
runServer()方法进入一个无限循环,首先调用accept()来等待连接指示,并返回一个Socket实例来与相关的客户端通信。然后它创建一个链接到Socket实例的Connection对象,将Connection对象添加到clients数组,启动Connection线程,并向与Connection对象的套接字关联的客户端发送问候消息。
当Connection线程的run()方法开始运行时,它首先通过readLine()方法调用获得客户端的名称(运行客户端应用的用户的名称)。然后它调用Connection的void sendClientsList()方法来通知所有客户端关于最新加入聊天的客户端。
sendClientsList()通过首先建立一个感叹号(!)-以空格分隔的客户端名称为前缀的字符串,然后调用Connection的void broadcast(String message)方法将该字符串广播给所有参与聊天的客户端。
反过来,broadcast()对存储在clients数组中的每个Connection对象调用Connection的void send(message)方法。
然后,Connection线程的run()方法进入一个循环,该循环使用readLine()从客户端读取每一行,然后将这一行以客户端名称作为前缀广播给所有客户端。
在某些时候,当用户选择退出聊天时,客户端的套接字将被关闭。这个动作导致readLine()返回 null,从而结束循环并执行 try 语句的 finally 子句。该子句从clients数组中删除客户端的Connection对象,并广播标识剩余客户端数量及其名称的消息。
虽然ChatServer在概念上很简单,但是它对volatile和线程同步的使用使它显得更加困难。
我在任何可以被多线程访问的地方声明一个变量volatile。这个想法是为了确保ChatServer能够在多核/多处理器机器上工作,这些机器包含变量的独立缓存副本。
我使用同步来确保客户端对聊天服务器的状态有一致的看法。例如,runServer()在一个同步块中执行clients.add(con);到con.send("welcome...you're the latest of "+clients.size()+" users");,并且还在另一个同步同一clients对象的同步块中执行clients.remove(this);到sendClientsList();,从而在添加客户端和向该客户端发送关于当前客户端数量的消息之间不能删除客户端,并且在删除客户端和通知所有剩余客户端当前客户端数量之间不能添加客户端。
编译这个源代码(javac ChatServer.java)并运行应用(java ChatServer)。它通过在其命令窗口中显示以下输出来做出响应:
ChatServer starting
listening on port 8010
清单 9-3 展示了ChatClient的源代码。
***清单 9-3。*访问聊天服务器
`import java.awt.BorderLayout; import java.awt.EventQueue; import java.awt.GridLayout;
import java.awt.event.ActionEvent; import java.awt.event.ActionListener;
import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.IOException; import java.io.PrintWriter;
import java.net.Socket;
import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextField;
import javax.swing.border.Border; import javax.swing.border.EtchedBorder;
class ChatClient
{
final static String SERVER_ADDR = "localhost";
final static int SERVER_PORT = 8010;
static Socket socket;
static volatile BufferedReader br;
static PrintWriter pw;
static JButton btnSend;
static JPanel createGUI()
{
JPanel pnlLayout = new JPanel();
pnlLayout.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
pnlLayout.setLayout(new BorderLayout());
JPanel pnlLeft = new JPanel();
pnlLeft.setLayout(new BorderLayout());
final JTextField txtUsername = new JTextField(30);
pnlLeft.add(txtUsername, BorderLayout.NORTH);
final JTextArea txtInput = new JTextArea(5, 30);
txtInput.setEnabled(false);
pnlLeft.add(new JScrollPane(txtInput), BorderLayout.CENTER); final JTextArea txtOutput = new JTextArea(10, 30);
txtOutput.setFocusable(false);
pnlLeft.add(new JScrollPane(txtOutput), BorderLayout.SOUTH);
pnlLayout.add(pnlLeft, BorderLayout.WEST);
JPanel pnlRight = new JPanel();
pnlRight.setLayout(new BorderLayout());
final JTextArea txtUsers = new JTextArea(10, 10);
txtUsers.setFocusable(false);
Border border = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED);
txtUsers.setBorder(border);
pnlRight.add(txtUsers, BorderLayout.NORTH);
JPanel pnlButtons = new JPanel();
pnlButtons.setLayout(new GridLayout(3, 1));
final JButton btnConnect = new JButton("Connect");
ActionListener al;
al = new ActionListener()
{
@Override
public void actionPerformed(ActionEvent ae)
{
txtUsername.setFocusable(false);
String username = txtUsername.getText().trim();
try
{
socket = new Socket(SERVER_ADDR, SERVER_PORT);
btnConnect.setEnabled(false);
InputStreamReader isr;
isr = new InputStreamReader(socket.getInputStream());
br = new BufferedReader(isr);
pw = new PrintWriter(socket.getOutputStream(), true);
txtOutput.append(br.readLine()+"\n");
pw.println((!username.equals(""))?username:"unknown");
txtInput.setEnabled(true);
btnSend.setEnabled(true);
new Thread(new Runnable()
{
@Override
public void run()
{
String line;
try
{
while ((line = br.readLine()) != null)
{
if (line.charAt(0) != '!')
{
txtOutput.append(line+"\n");
continue;
}
txtUsers.setText("");
String[] users;
users = line.substring(1) .split(" ");
for (String user: users)
{
txtUsers.append(user);
txtUsers.append("\n");
}
}
}
catch (IOException ioe)
{
txtOutput.append("lost the link");
return;
}
}
}).start();
}
catch (Exception e)
{
txtOutput.append("unable to connect to server");
}
}
};
btnConnect.addActionListener(al);
pnlButtons.add(btnConnect);
btnSend = new JButton("Send");
btnSend.setEnabled(false);
al = new ActionListener()
{
@Override
public void actionPerformed(ActionEvent ae)
{
pw.println(txtInput.getText());
txtInput.setText("");
}
};
btnSend.addActionListener(al);
pnlButtons.add(btnSend);
JButton btnQuit = new JButton("Quit");
al = new ActionListener()
{
@Override
public void actionPerformed(ActionEvent ae)
{
try
{
if (socket != null)
socket.close();
}
catch (IOException ioe)
{
}
System.exit(0); }
};
btnQuit.addActionListener(al);
pnlButtons.add(btnQuit);
pnlRight.add(pnlButtons, BorderLayout.SOUTH);
pnlLayout.add(pnlRight, BorderLayout.EAST);
return pnlLayout;
}
public static void main(String[] args)
{
Runnable r = new Runnable()
{
@Override
public void run()
{
JFrame f = new JFrame("ChatClient");
f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
f.setContentPane(createGUI());
f.pack();
f.setResizable(false);
f.setVisible(true);
}
};
EventQueue.invokeLater(r);
}
}`
清单 9-3 的ChatClient类包含常量/非常量字段,一个用于创建该应用图形用户界面(GUI)的JPanel createGUI()类方法,以及一个用于创建 GUI 和运行该应用的main()方法。
GUI 创建代码呈现了几个我在第七章中没有讨论的项目(为了简洁):
java.awt.Component类声明了一个用于设置组件的focusable状态的void setFocusable(boolean focusable)方法。换句话说,它决定了用户是否可以跳转到或点击组件来给予该组件输入焦点(例如,让用户在文本字段中输入字符)。将false传递给这个方法可以防止组件接收输入焦点,为此我在各种 textfield/textarea 组件上这样做。虽然我可以调用setEnabled(false)来达到同样的效果,但是我没有这样做,因为一个禁用的 textfield/textarea 的文本看起来很模糊,很难阅读(至少在默认的 Metal 外观下)。相比之下,当组件不可聚焦时,文本会很清晰,易于阅读。java.awt.BorderLayout类被广泛用于布局 GUI。它允许您在关联容器的北、南、东、西和中心区域安排最多五个组件。组件根据它们的首选大小和容器大小的约束进行布局。南北分量可以水平拉伸;东分量和西分量可以垂直拉伸;中心组件可以水平和垂直拉伸,以填充任何剩余的空间。当添加一个组件到一个由边框布局管理的容器中时,java.awt.Container的void add(Component comp, Object constraints)方法被调用,其中一个BorderLayout的基于java.lang.String的约束常量(例如NORTH)作为第二个参数,告诉布局管理器在哪里放置组件。
连接到 Connect、Send 和 Quit 按钮的监听器向您展示了如何创建一个连接到聊天服务器的套接字,如何与聊天服务器通信,以及如何关闭套接字。
ChatServer和ChatClient通过相同的端口号(8010)进行通信。另外,ChatClient通过指定 localhost (127.0.0.1)假设ChatServer运行在同一台计算机上。如果ChatServer在不同的计算机上运行,你应该指定那台计算机的域名/IP 地址。
编译清单 9-3 ( javac ChatClient.java)。假设ChatServer正在运行,通过在两个不同的命令窗口中执行java ChatClient来启动一对ChatClient实例。
图 9-2 显示了用户杰克和吉尔通过他们的聊天客户端进行交流。
***图 9-2。*杰克正准备给吉尔发信息。
在顶部的文本框中输入一个名称,然后点击连接按钮连接到聊天服务器。如果未指定名称,聊天客户端将选择未知作为用户名,单击连接后您将无法更改用户名。用户名文本框右侧的文本区显示了参与聊天的所有用户。
继续在用户名文本字段下方的输入文本区域中输入文本,然后单击发送按钮将输入的文本发送给所有用户。该文本出现在输入文本区域下方的输出文本区域中。
最后,单击退出按钮终止聊天。
数据套接字和组播套接字
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)构造函数很方便。当构造函数无法创建数据报套接字或将数据报套接字绑定到本地端口时,它会抛出java.net.SocketException。
应用实例化DatagramSocket后,调用void send(DatagramPacket dgp)和void receive(DatagramPacket dgp)发送和接收数据报包。
清单 9-4 展示了服务器环境中的DatagramPacket和DatagramSocket。
***清单 9-4。*从客户端接收数据报数据包,并将它们回显给客户端
`import java.io.IOException;
import java.net.DatagramPacket; import java.net.DatagramSocket;
class DGServer { final static int PORT = 10000; public static void main(String[] args) throws IOException { System.out.println("server is starting"); try (DatagramSocket dgs = new DatagramSocket(PORT)) { 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()); } } }`
清单 9-4 的main()方法首先创建一个DatagramSocket对象,并将套接字绑定到本地主机上的端口 10000。然后它调用DatagramSocket的int getSendBufferSize()和int getReceiveBufferSize()方法来获取SO_SNDBUF和SO_RCVBUF套接字选项的值,然后输出这些值。
注意套接字与底层平台发送和接收缓冲区相关联,它们的大小通过调用
getSendBufferSize()和getReceiverBufferSize()来访问。同样,它们的大小可以通过调用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() next 实例化DatagramPacket,准备从客户端接收数据报分组,然后将分组回送到客户端。它假设数据包的大小不超过 100 字节。
最后,main()进入一个无限循环,该循环接收一个包,输出包内容,并将包发送回客户端—客户端的寻址信息存储在DatagramPacket中。
编译清单 9-4 ( javac DGServer.java)并运行应用(java DGClient)。您应该观察到与此处所示相同或相似的输出:
Server is starting
Send buffer size = 8192
Receive buffer size = 8192
清单 9-5 演示了客户端上下文中的DatagramPacket和DatagramSocket。
***清单 9-5。*向服务器发送数据包并从服务器接收数据包
`import java.io.IOException;
import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress;
class DGClient
{
final static int PORT = 10000;
final static String ADDR = "localhost";
public static void main(String[] args)
{
System.out.println("client is starting");
DatagramSocket s = null;
try (DatagramSocket dgs = new DatagramSocket())
{
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());
}
}
}`
清单 9-5 类似于清单 9-4 ,但是有一个很大的区别。我使用DatagramPacket(byte[] buf, int length, InetAddress address, int port)构造函数在数据报包中指定服务器的目的地,这个目的地恰好是本地主机上的端口 10000。send()方法调用将数据包路由到这个目的地。
编译清单 9-5 ( javac DGClient.java)并运行应用(java DGClient)。假设DGServer也在运行,您应该在DGClient的命令窗口中观察到以下输出(以及DGServer命令窗口中该输出的最后一行):
client is starting
Send me a datagram
描述基于 UDP 的多播会话的客户端或服务器端的套接字。两个常用的构造函数是MulticastSocket()(创建一个不绑定到端口的组播套接字)和MulticastSocket(int port)(创建一个绑定到指定port的组播套接字)。
什么是多播?
前面的例子已经演示了单播,它发生在服务器向单个客户端发送消息的时候。然而,也可以向多个客户端广播相同的消息(例如,向已经向在线程序注册以接收该通知的一组家长的所有成员发送“学校因恶劣天气关闭”通知);这个活动被称为多播。
服务器通过向一个特殊的 IP 地址(称为多播组地址)和一个特定的端口(由端口号指定)发送一系列数据报数据包来进行多播。想要接收这些数据报数据包的客户端创建一个使用该端口号的多播套接字。他们通过指定特殊 IP 地址的加入组操作请求加入组。此时,客户端可以接收发送到该组的数据报数据包,甚至可以向其他组成员发送数据报数据包。在客户端已经读取了它想要读取的所有数据报分组之后,它通过应用指定特殊 IP 地址的离开组操作来将自己从组中移除。
IPv4 地址 224.0.0.1 到 239.255.255.255(含)保留用作多播组地址。
清单 9-6 展示了一个组播服务器。
***清单 9-6。*组播数据报数据包
`import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket; 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());
}
}
}`
清单 9-6 的main()方法首先通过MulticastSocket()构造函数创建一个MulticastSocket实例。多播套接字不需要绑定到端口号,因为端口号是作为随后创建的DatagramPacket实例的一部分与多播组的 IP 地址(231.0.0.1)一起指定的。(dummy数组的存在是为了防止NullPointerException对象从DatagramPacket构造函数中抛出——这个数组不是用来存储要广播的数据的。)
此时,main()进入一个无限循环,首先从String实例创建一个字节数组,并使用平台的默认字符编码(参见第八章)将 Unicode 字符转换成字节。(尽管无关的StringBuilder和String对象是通过表达式"line "+i在每次循环迭代中创建的,但我并不担心它们对这个简短的一次性应用中的垃圾收集的影响。)
这个数据缓冲区随后通过调用它的void setData(byte[] buf)方法被分配给DatagramPacket实例,然后数据报包被广播给与端口 10000 和组播 IP 地址 231.0.0.1 相关联的组的所有成员。
编译清单 9-6 ( javac MCServer.java)并运行这个应用(java MCServer)。你不应该观察任何输出。
清单 9-7 展示了一个多播客户端。
***清单 9-7。*接收组播数据报数据包
`import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress; import java.net.MulticastSocket;
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()); } } }`
清单 9-7 的main()方法首先通过MulticastSocket(int port)构造函数创建一个绑定到端口 10000 的MulticastSocket实例。
然后,它获得一个包含多播组 IP 地址 231.0.0.1 的InetAddress对象,并通过调用MulticastSocket的void joinGroup(InetAddress mcastaddr)方法使用该对象加入该地址的组。
main() next 接收十个数据报包,打印它们的内容,并通过调用MulticastSocket的void leaveGroup(InetAddress mcastaddr)方法离开组,使用相同的组播 IP 地址作为它的参数。
注
joinGroup()和leaveGroup()当试图加入或离开组时发生 I/O 错误,或者 IP 地址不是组播 IP 地址时,抛出IOException。
因为客户端不知道字节数组到底有多长,所以它假设 256 个字节来确保数据缓冲区能够容纳整个数组。如果它试图打印出返回的数组,在实际数据被打印出来后,您会看到许多空白空间。为了消除这个空间,它调用DatagramPacket的int getLength()方法来获得数组的实际长度,用这个长度创建第二个字节数组(buffer2,并使用System.arraycopy()——在第四章中讨论过——将这么多字节复制到buffer2。在将这个字节数组转换成一个String对象后(通过String(byte[] bytes) 构造函数,它使用平台的默认字符集——参见第八章了解字符集),它将结果字符打印到标准输出设备。
编译清单 9-7 ( javac MCClient.java)并运行这个应用(java MCClient)。您应该观察到类似如下的输出:
line 521103
line 521104
line 521105
line 521106
line 521107
line 521108
line 521109
line 521110
line 521111
line 521112
通过 URL 交流
统一资源定位符(URL) 是指定资源(例如网页)在基于 TCP/IP 的网络(例如因特网)上的位置的字符串。此外,它还提供了检索该资源的方法。例如,[tutortutor.ca](http://tutortutor.ca)是一个定位我的网站主页的 URL。http://前缀指定必须使用超文本传输协议(HTTP) 来检索位于tutortutor.ca的网页,该协议是位于 TCP/IP 之上的用于定位 HTTP 资源(例如网页)的高级协议。
urn 和 uri
统一资源名(URN) 是一个字符串,它并不暗示资源的可用性。即使资源可用,URN 也不提供定位它的方法。例如,urn:isbn:9781430234135标识了一本名为 Android Recipes 的电子书,如此而已。
urn 和 URL 是统一资源标识符(URIs) 的例子,统一资源标识符是用于标识名称(urn)或资源(URL)的字符串。每个 URN 和 URL 也是一个 URI,我在后面的章节中通过指定 URI 而不是 URL 来利用这个事实。
java.net包提供了用于访问基于 URL 的资源的URL和URLConnection类。它还提供了用于编码和解码 URL 的URLEncoder和URLDecoder类,以及用于执行基于 URI 的操作(例如,相对化)并返回包含结果的URL实例的URI类。
URL 和 URLConnection
URL类表示 URL,并提供对它们所引用的资源的访问。每个URL实例明确地标识一个互联网资源。
URL声明了几个构造函数,其中URL(String s)是最简单的。这个构造函数从传递给s的String参数创建一个URL实例,演示如下:
try { URL url = new URL("http://tutortutor.ca"); } catch (MalformedURLException murle) { }
这个例子创建了一个使用 HTTP 在[tutortutor.ca](http://tutortutor.ca)访问网页的URL对象。如果我指定了一个非法的 URL(例如,foo),构造函数将抛出java.net.MalformedURLException(一个IOException子类)。
虽然您通常会指定http://作为协议前缀,但这不是您唯一的选择。例如,当资源位于本地主机上时,您也可以指定file:///。此外,当资源存储在 JAR 文件中时,您可以将jar:添加到http://或file:///的前面,如下所示:
jar:file:///C:./rt.jar!/com/sun/beans/TypeResolver.class
前缀jar:表示您想要访问一个 JAR 文件资源(例如,一个存储的类文件)。前缀file:///标识了本地主机的资源位置,在本例中是 Windows C:硬盘上当前目录中的rt.jar (Java 7 的运行时 JAR 文件)。
JAR 文件的路径后面跟一个感叹号(!)将 JAR 文件路径与 JAR 资源路径分开,JAR 资源路径恰好是这个 JAR 文件中的/com/sun/beans/TypeResolver.class classfile 条目(需要前导的/字符)。
注Oracle 的 Java 参考实现中的
URL类支持附加协议,包括ftp和mailto。
在创建了一个URL对象之后,您可以调用各种URL方法来访问 URL 的各个部分。例如,String getProtocol()返回 URL 的协议部分(例如,http)。您还可以通过调用InputStream openStream()方法来检索资源。
openStream()创建到资源的连接,并返回一个InputStream实例,用于从该连接读取资源数据,如下所示:
try (InputStream is = url.openStream())
{
int ch;
while ((ch = is.read()) != -1)
System.out.print((char) ch);
}
注意对于 HTTP 连接,会创建一个内部套接字,连接到通过 URL 的域名/IP 地址识别的服务器上的 HTTP 端口 80,除非您在域名/IP 地址后附加一个不同的端口号(例如
[tutortutor.ca:8080](http://tutortutor.ca:8080))。
我创建了一个应用,演示了如何定位和访问任意资源。清单 9-8 展示了它的源代码。
***清单 9-8。*输出通过 URL 命令行参数识别的资源内容
`import java.io.InputStream; import java.io.IOException;
import java.net.MalformedURLException; import java.net.URL;
class GetResource { public static void main(String[] args) { if (args.length != 1) { System.err.println("usage: java getresource url"); return; } try { URL url = new URL(args[0]); try (InputStream is = url.openStream()) { int ch; while ((ch = is.read()) != -1) System.out.print((char) ch); } } catch (MalformedURLException murle) { System.err.println("invalid url"); } catch (IOException ioe) { System.err.println("i/o error: "+ioe.getMessage()); } } }`
编译这个源代码(javac GetResource.java)并执行java GetResource [tutortutor.ca](http://tutortutor.ca)。以下输出显示了返回网页的短前缀:
<!DOCTYPE html PUBLIC "-//w3c//dtd html 4.01//en"
"http://www.w3.org/tr/html4/strict.dtd">
<html>
<head>
<title>
TutorTutor -- /main
</title>
openStream()是一个调用openConnection().getInputStream()的方便方法。URL的URLConnection openConnection()和URLConnection openConnection(Proxy proxy)方法都返回一个java.net.URLConnection类的实例,代表应用和 URL 之间的通信链接。
URLConnection为您提供对客户端/服务器通信的额外控制。例如,您可以使用此类将内容输出到接受内容的各种资源。相比之下,URL只允许你通过openStream()输入内容。
URLConnection声明了各种方法,包括:
InputStream getInputStream()返回从这个打开的连接中读取的输入流。OutputStream getOutputStream()返回写入这个打开的连接的输出流。void setDoInput(boolean doinput)指定这个URLConnection对象支持(传递true到doinput)或者不支持(传递false到doinput)输入。因为true是默认的,你只需要将true传递给这个方法来记录你执行输入的意图(正如我在第十一章中演示的)。void setDoOutput(boolean dooutput)指定这个URLConnection对象支持(通过true到dooutput)或者不支持(通过false到dooutput)输出。因为默认值为 false,所以在执行输出之前必须调用这个方法(如第十一章中的所示)。void setRequestProperty(String key, String value)设置请求属性(例如,HTTP 的accept属性)。当一个键已经存在时,它的值将被指定的值覆盖。
以下示例向您展示了如何从预创建变量url引用的URL对象中获取URLConnection对象,设置其dooutput属性,并获取用于写入资源的输出流:
URLConnection urlc = url.openConnection();
urlc.setDoOutput(true);
OutputStream os = urlc.getOutputStream();
URLConnection由java.net.HttpURLConnection和java.net.JarURLConnection子类化。这些类声明特定于使用 HTTP 协议或与基于 JAR 的资源交互的常量和/或方法。
注为了简洁,我让你参考关于
URLConnection、HttpURLConnection和JarURLConnection的 JDK 文档;以及第十一章的HttpURLConnection示例了解更多信息。
URLEncoder 和 URLDecoder
超文本标记语言(HTML)允许您将表单引入网页,向页面访问者请求信息。填写完表单的字段后,访问者单击表单的提交按钮(通常有不同的标签),表单内容(字段名称和值)被发送到某个服务器程序。
在将表单内容发送到服务器程序之前,web 浏览器通过替换空格和其他 URL 非法字符对数据进行编码,并将内容的多用途 Internet 邮件扩展(MIME)类型设置为application/x-www-form-urlencoded。
注意数据是为 HTTP POST 和 GET 操作编码的。与 POST 不同,GET 需要一个查询字符串 (a?-包含编码内容的前缀字符串)附加到服务器程序的 URL。
java.net包提供了URLEncoder和URLDecoder类来帮助你完成编码和解码表单内容的任务。
URLEncoder应用以下编码规则:
- 字母数字字符“A”到“Z”、“A”到“Z”以及“0”到“9”保持不变。
- 特殊字符。“、“-”、“*”和“_”保持不变。
- 空格字符“”在 Internet Explorer 上转换为加号“+”,在 Firefox 上转换为“%20”。
- 所有其他字符都是不安全的,首先使用某种编码方案转换成一个或多个字节。然后,每个字节由三个字符的字符串% xy 表示,其中 xy 是该字节的两位十六进制表示。推荐使用的编码方案是 UTF-8。然而,出于兼容性原因,当没有指定编码时,使用平台的默认编码。
例如,使用 UTF-8 作为编码方案,字符串"the string ü@foo-bar"被转换为"the+string+%c3%bc%40foo-bar"。在 UTF-8 中,字符ü被编码为两个字节 C3(十六进制)和 BC(十六进制),字符@被编码为一个字节 40(十六进制)。
URLEncoder声明了下面的类方法,用于对字符串进行编码:
String encode(String s, String enc)
这个方法使用编码方案enc将传递给s的String参数翻译成application/x-www-form-urlencoded格式。它使用提供的编码方案来获得不安全字符的字节,并在不支持enc的值时抛出java.io.UnsupportedEncodingException。
URLDecoder应用以下解码规则:
- 字母数字字符“A”到“Z”、“A”到“Z”以及“0”到“9”保持不变。
- 特殊字符。“、“-”、“*”和“_”保持不变。
- 加号“+”/“% 20”被转换为空格字符“”。
- 形式为% xy 的序列将被视为表示一个字节,其中 xy 是 8 位的两位十六进制表示。然后,所有连续包含一个或多个这些字节序列的子字符串将被其编码将产生这些连续字节的字符所替换。可以指定用于解码这些字符的编码方案;未指定时,使用平台的默认编码。
URLDecoder声明了以下用于解码编码字符串的类方法:
String decode(String s, String enc)
这个方法使用指定的编码方案解码一个application/x-www-form-urlencoded字符串。提供的编码用于确定% xy 形式的任何连续序列代表什么字符。当不支持enc的值时,抛出UnsupportedEncodingException。
解码器处理非法编码的字符串有两种可能的方式。它要么不处理非法字符,要么抛出IllegalArgumentException。解码器采用哪种方法由实现来决定。
注类似于 RFC 的万维网联盟推荐标准(
[www.w3.org/TR/html40/a…](http://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars))规定,UTF-8 应该被用作encode()和decode()的编码方案。不这样做可能会引入不兼容性。
我已经创建了一个应用,在前面的"the string ü@foo-bar"和"the+string+%c3%bc%40foo-bar"示例的上下文中演示了URLEncoder和URLDecoder。清单 9-9 展示了应用的源代码。
***清单 9-9。*对编码后的字符串进行编码和解码
`import java.io.UnsupportedEncodingException;
import java.net.URLDecoder; import java.net.URLEncoder;
class EncDec { public static void main(String[] args) throws UnsupportedEncodingException { String encodedData = URLEncoder.encode("the string ü@foo-bar", "utf-8"); System.out.println(encodedData); System.out.println(URLDecoder.decode(encodedData, "utf-8")); } }`
注意你可能想看看维基百科的“百分比编码”主题(
[en.wikipedia.org/wiki/Percent-encoding](http://en.wikipedia.org/wiki/Percent-encoding))来了解更多关于 URL 编码的知识(以及更准确的百分比编码术语)。
URI
URI类代表 URIs(例如,urn 和 URL)。当 URI 是 URL 时,它不提供对资源的访问。
一个URI实例在最高级别存储一个符合以下语法的字符串:
[*scheme*:]*scheme-specific-part*[#*fragment*]
这种语法揭示了每个 URI 可选地以一个 scheme 开头,后跟一个冒号字符,其中一个方案可以被认为是一个用于获取互联网资源的应用级协议。但是,这个定义太窄了,因为它暗示 URI 总是一个 URL。一个方案可以与资源位置无关。例如,urn 是用于识别 urn 的方案。
一个方案后跟一个 scheme-specific-part ,它提供了该方案的一个实例。例如,给定[tutortutor.ca](http://tutortutor.ca) URI,tutortutor.ca就是 http 方案的一个实例。特定于方案的部分符合其方案允许的语法和 URI 的整体语法结构(包括哪些字符可以按字面意思指定,哪些字符必须编码)。
方案以可选的#-前缀 fragment 结束,这是一个短字符串,表示从属于另一个主要资源的资源。主资源由 URI 标识;该片段指向从属资源。例如,[tutortutor.ca/document.txt#line=5,10](http://tutortutor.ca/document.txt#line=5,10)在我的网站上识别名为document.txt的文本文档的第5行到第10行。(这个例子只是说明性的;资源实际上并不存在。)
URIs 可以分为绝对的和相对的。一个绝对 URI 以一个方案开头,后面跟一个冒号字符。早期的 URI 是绝对 URI 的一个例子。其他例子包括mailto:jeff@tutortutor.ca和news:comp.lang.java.help。将绝对 URI 视为以独立于该标识符出现的上下文的方式引用资源。用文件系统来类比,绝对 URI 相当于从根目录开始的文件的路径名。
一个相对 URI 不是以一个 scheme(后跟一个冒号)开始的。比如tutorials/tutorials.html。将相对 URI 视为以依赖于标识符出现的上下文的方式引用资源。使用文件系统类比,相对 URI 就像从当前目录开始的文件的路径名。
URIs 也可以分为不透明和等级制。一个不透明的 URI 是一个绝对的 URI,其特定于方案的部分不以正斜杠(/)字符开头。例子有[tutortutor.ca](http://tutortutor.ca)和mailto:jeff@tutortutor.ca。不透明的 URIs 不会被解析(除了识别它们的模式),因为特定于模式的部分不需要被验证。
一个层次 URI 要么是一个绝对 URI,其特定于方案的部分以正斜杠字符开始,要么是一个相对 URI。
与不透明 URI 不同,分层 URI 的方案特定部分必须被解析成由以下语法标识的各种组件:
[//*authority*] [*path*] [?*query*] [#*fragment*]
authority 标识了 URI 命名空间的命名权限。如果存在,该组件以一对正斜杠字符开始,基于服务器或基于注册表,并以下一个正斜杠字符、问号字符或不再有字符(URI 的结尾)结束。基于注册中心的授权组件具有特定于方案的语法(因为不常用,所以不进行讨论),而基于服务器的授权组件通常采用以下语法:
[*userinfo*@] *host* [:*port*]
该语法指定基于服务器的授权组件可选地以用户信息(例如,用户名)和“at”(@)字符开始,然后以主机名继续,并且可选地以冒号字符和端口结束。例如,jeff@tutortutor.ca是一个基于服务器的权限组件,其中jeff表示用户信息,tutortutor.ca表示主机——没有端口。
path 根据权限组件(当存在时)或方案(当权限组件不存在时)标识资源的位置。一个路径分成一系列的路径段(路径的一部分),其中正斜杠字符用于分隔这些段。当第一个路径段以正斜杠字符开始时,路径是绝对的;否则,路径是相对的。例如,/a/b/c构成了一条包含三个路径段的路径— a、b和c。此外,路径是绝对的,因为第一个路径段前面有一个正斜杠字符(a)。
query 标识要传递给资源的数据。资源使用该数据来获取或生成其他数据,并将其传递回调用方。例如,在[tutortutor.ca/cgi-bin/makepage.cgi?/software/Aquarium](http://tutortutor.ca/cgi-bin/makepage.cgi?/software/Aquarium)中,/software/Aquarium代表一个查询。根据这个查询,/software/Aquarium是要传递给资源(makepage.cgi)的数据,这个数据恰好是一个目录的绝对路径,这个目录的同名文件通过 Perl 脚本与样板 HTML 合并,生成一个结果 web 页面。
最后一个组件是 fragment 。虽然它看起来是 URI 的一部分,但它不是。当在检索操作中使用 URI 时,执行该操作的主资源使用该片段来检索从属资源。例如,makepage.cgi是主要资源,/software/Aquarium是从属资源。
前面的讨论揭示了一个完整的 URI 由方案、权限、路径、查询和片段组成;或者它由方案、用户信息、主机、端口、路径、查询和片段组件组成。要在前一种情况下构造一个URI实例,调用URI(String scheme, String authority, String path, String query, String fragment)构造函数。在后一种情况下,叫URI(String scheme, String userInfo, String host, int port, String path, String query, String fragment)。
额外的构造函数可用于创建URI实例。例如,URI(String uri)通过解析uri创建一个URI。无论调用哪个构造函数,当产生的 URI 字符串有无效语法时,它都会抛出java.net.URISyntaxException。
提示
java.io.File类声明了一个URI toURI()方法,您可以调用该方法将File对象的抽象路径名转换为URI对象。内部 URI 的方案设定为file。
URI声明各种 getter 方法,让您检索URI组件。例如,String getScheme()让您检索方案,String getFragment()返回一个 URL 解码的片段。该类还声明了boolean isAbsolute()和boolean isOpaque()方法,当 URI 是绝对的和不透明的时,这些方法返回 true。
清单 9-10 展示了一个应用,让你了解 URI 组件以及绝对和不透明 URIs。
***清单 9-10。*了解 URI
`import java.net.URI; import java.net.URISyntaxException;
class URIComponents { public static void main(String[] args) throws URISyntaxException { if (args.length != 1) { System.err.println("usage: java uricomponents uri"); return; } URI uri = new URI(args[0]); System.out.println("authority = "+uri.getAuthority()); System.out.println("fragment = "+uri.getFragment()); System.out.println("host = "+uri.getHost()); System.out.println("path = "+uri.getPath()); System.out.println("port = "+uri.getPort()); System.out.println("query = "+uri.getQuery()); System.out.println("scheme = "+uri.getScheme()); System.out.println("scheme-specific part = "+uri.getSchemeSpecificPart()); System.out.println("user info = "+uri.getUserInfo()); System.out.println("uri is absolute: "+uri.isAbsolute()); System.out.println("uri is opaque: "+uri.isOpaque()); } }`
编译清单 9-10 ( javac URIComponents.java)并如下运行应用:java URIComponents [tutortutor.ca/cgi-bin/makepage.cgi?/software/Aquarium](http://tutortutor.ca/cgi-bin/makepage.cgi?/software/Aquarium)。您将观察到以下输出:
Authority = tutortutor.ca
Fragment = null
Host = tutortutor.ca
Path = /cgi-bin/makepage.cgi
Port = -1
Query = /software/Aquarium
Scheme = http
Scheme-specific part = //tutortutor.ca/cgi-bin/makepage.cgi?/software/Aquarium
User Info = null
URI is absolute: true
URI is opaque: false
在创建了一个URI实例之后,您可以对其包含的 URI 执行规范化、解析和相对化操作(稍后讨论)。虽然您不能通过这个实例进行通信,但是出于通信的目的,您可以通过调用它的URL toURL()方法,将它转换成一个URL实例(假设 URI 实际上是一个 URL,而不是一个 URN 或其他东西)。
当 URI 不代表绝对 URL 时,该方法抛出IllegalArgumentException,当找不到 URL 的协议处理程序时(即 URL 不是以支持的协议如http或file开始),或者当构造URL实例时发生其他错误时,该方法抛出MalformedURLException。
正常化
正常化就是去掉不必要的“.”的过程。还有“..”来自分层 URI 路径组件的路径段。每个“.”段被删除。一个“..”仅当段前面有非-" .. "时,才删除该段分段。标准化对不透明的 URIs 没有影响。
URI声明了一个用于规范化 URI 的URI normalize()方法。该方法返回一个新的URI对象,该对象包含其调用者的 URI 的规范化等价物。
清单 9-11 展示了一个让你用normalize()进行实验的应用。
***清单 9-11。*URIs 正常化
`import java.net.URI; import java.net.URISyntaxException;
class Normalize { public static void main(String[] args) throws URISyntaxException { if (args.length != 1) { System.err.println("usage: java normalize uri"); return; } URI uri = new URI(args[0]); System.out.println("normalized uri = "+uri.normalize()); } }`
编译清单 9-11 ( javac Normalize.java)并如下运行应用:java Normalize a/b/../c/./d。您应该观察到以下输出,它表明b不是规范化 URI 的一部分:
Normalized URI = a/c/d
分辨率
解决就是解决一个 URI 对另一个 URI 的过程,也就是所谓的基地。最终的 URI 按照 RFC 2396(见[tools.ietf.org/html/rfc2396](http://tools.ietf.org/html/rfc2396))规定的方式由两个 URIs 的部件建造而成,并从基地 URI 获得那些在*原 URI 中没有规定的部件。*对于分层 URIs,原始路径根据基础路径进行解析,然后进行规范化。
例如,解决原 URI docs/guide/collections/designfaq.html#28对基地 URI [java.sun.com/j2se/1.3/](http://java.sun.com/j2se/1.3/)的结果是结果 URI [java.sun.com/j2se/1.3/docs/guide/collections/designfaq.html#28](http://java.sun.com/j2se/1.3/docs/guide/collections/designfaq.html#28)。作为第二个例子,根据这个结果求解相对 URI ../../../demo/jfc/SwingSet2/src/SwingSet2.java得到[java.sun.com/j2se/1.3/demo/jfc/SwingSet2/src/SwingSet2.java](http://java.sun.com/j2se/1.3/demo/jfc/SwingSet2/src/SwingSet2.java)。
支持绝对和相对 URIs 的解析,以及分层 URIs 情况下的绝对和相对路径的解析。
URI声明了URI resolve(String str)和URI resolve(URI uri)方法,用于针对当前URI对象中包含的基本 URI,解析原始 URI 参数(传递给str或uri)(在该对象上调用了该方法)。当原始 URI 已经是绝对的或不透明的时,这些方法返回包含原始 URI 的新的URI对象或URI参数。否则,它们返回一个新的包含已解析 URI 的URI对象。当str或uri为null时,抛出NullPointerException。当str违反 RFC 2396 语法时,抛出IllegalArgumentException。
清单 9-12 展示了一个让你用resolve(String)进行实验的应用。
***清单 9-12。*解决 URIs
`import java.net.URI; import java.net.URISyntaxException;
class Resolve { public static void main(String[] args) throws URISyntaxException { if (args.length != 2) { System.err.println("usage: java resolve baseuri uri"); return; } URI uri = new URI(args[0]); System.out.println("resolved uri = "+uri.resolve(args[1])); } }`
编译清单 9-12 ( javac Resolve.java)并如下运行应用:java Resolve [java.sun.com/j2se/1.3/docs/guide/collections/designfaq.html#28](http://java.sun.com/j2se/1.3/docs/guide/collections/designfaq.html#28)。您应该观察到以下输出:
Resolved URI = http://java.sun.com/j2se/1.3/docs/guide/collections/designfaq.html#28
关系化
关系化是解析的逆。对于任何两个归一化的 URIs,相对化撤消了归结所做的工作,而归结撤消了相对化所做的工作。
URI声明了一个URI relativize(URI uri)方法,用于将它的uri参数与当前URI对象中的 URI 相对化(在该对象上调用了该方法)——uri为null时relativize()抛出NullPointerException。
注意对于任意两个规范化的
URI实例u和v,u.relativize(u.resolve(v)).equals(v)和u.resolve(u.relativize(v)).equals(v)评估为真。
relativize()对调用此方法的URI对象中的 URI 执行其URI参数的 URI 的相对化,如下所示:
- 如果这个 URI 或参数 URI 是不透明的,或者如果两个 URIs 的方案和授权组件不相同,或者如果这个 URI 的路径不是参数 URI 的路径的前缀,则返回参数 URI。
- 否则,用取自自变量 URI 的查询和片段组件,以及通过从自变量 URI 的路径的开头移除该 URI 的路径而计算的路径组件,来构造新的相对分层 URI。
清单 9-13 展示了一个让你用relativize()进行实验的应用。
***清单 9-13。*将 URIs 相对化
`import java.net.URI; import java.net.URISyntaxException;
class Relativize { public static void main(String[] args) throws URISyntaxException { if (args.length != 2) { System.err.println("usage: java relativize uri1 uri2"); return; } URI uri1 = new URI(args[0]); URI uri2 = new URI(args[1]); System.out.println("relativized uri = "+uri1.relativize(uri2)); } }`
编译清单 9-13 ( javac Relativize.java)并如下运行应用:java Relativize [java.sun.com/j2se/1.3/](http://java.sun.com/j2se/1.3/) [java.sun.com/j2se/1.3/docs/guide/collections/designfaq.html#28](http://java.sun.com/j2se/1.3/docs/guide/collections/designfaq.html#28)。您应该观察到以下输出:
Relativized URI = docs/guide/collections/designfaq.html#28
认证
RFC 1945:超文本传输协议—HTTP/1.0 ( [www.ietf.org/rfc/rfc1945…](http://www.ietf.org/rfc/rfc1945.txt))告诉您 HTTP 1.0 提供了一种简单的质询-响应机制,服务器可以使用这种机制来质询客户端访问某些资源的请求。此外,客户端可以使用这个机制来提供凭证(通常是用户名和密码)来认证(证明)客户端的身份。当提供的凭证满足服务器时,用户被授权(允许)访问资源。
为了质询客户端,原始服务器发出“401 未授权”消息。该消息包含一个WWW-Authenticate HTTP 头,它通过一个不区分大小写的令牌来标识一个认证方案(实现认证的方法)。令牌后面是逗号分隔的属性/值对序列,用于提供执行身份验证所需的特定于方案的参数。客户端回复一个提供凭证的Authorization头。
注意 HTTP 1.1 使得用代理认证客户端成为可能。为了质询客户端,代理服务器发出“407 需要代理认证”消息,该消息包括一个
Proxy-Authenticate报头。客户端通过Proxy-Authorization报头回复。
基本认证和认证者类
HTTP 1.0 引入了基本认证方案,客户端通过用户名和密码来识别自己。基本身份验证方案的工作方式如下:
WWW-Authenticate头指定Basic作为令牌和一个单独的realm="*quoted string*"对,该对标识由浏览器地址引用的领域(一个资源所属的受保护空间,例如一组特定的网页)。- 作为对该标题的响应,浏览器显示一个对话框,在其中输入用户名和密码。
- 输入之后,用户名和密码被连接成一个字符串(在用户名和密码之间插入一个冒号),该字符串是 base64 编码的,结果被放在一个发送回服务器的
Authorization头中。(要了解更多关于 base64 编码的信息,请访问[en.wikipedia.org/wiki/Base64](http://en.wikipedia.org/wiki/Base64)查看维基百科的 Base64 条目。) - 服务器对这些凭证进行 base64 解码,并将其与存储在其用户名/密码数据库中的值进行比较。当存在匹配时,应用被授权访问该资源(以及属于该领域的任何其他资源)。
Greg Stein 在[test.webdav.org/](http://test.webdav.org/)维护着一个测试服务器,可以用来测试基本的身份验证等等。例如,当你在浏览器中指定[test.webdav.org/auth-basic/](http://test.webdav.org/auth-basic/)时,你会得到一个 401 响应,如清单 9-14 中的应用所示。
***清单 9-14。*通过输出服务器的各种 HTTP 头来展示对基本认证的需求
`import java.io.IOException;
import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection;
import java.util.List; import java.util.Map;
class BasicAuthNeeded
{
public static void main(String[] args) throws IOException
{
String s = "test.webdav.org/auth-basic/… URL url = new URL(s); URLConnection urlc = url.openConnection(); Map<String,List<String>> hf = urlc.getHeaderFields(); for (String key: hf.keySet()) System.out.println(key+": "+urlc.getHeaderField(key)); System.out.println(((HttpURLConnection) urlc).getResponseCode()); } }
这个应用连接到测试地址,并输出所有服务器发送的头及其响应代码。编译源代码(javac BasicAuthNeeded.java)后,运行应用(java BasicAuthNeeded)。您应该会看到类似于以下内容的输出:
null: HTTP/1.1 401 Authorization Required
WWW-Authenticate: Basic realm="basic auth area"
Date: Mon, 19 Sep 2011 03:06:06 GMT
Content-Length: 401
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=iso-8859-1
Server: Apache/2.0.54 (Debian GNU/Linux) DAV/2 SVN/1.3.2
401
WWW-Authenticate标题的realm属性显示basic auth area为领域。虽然没有显示,但是从user1到user9的任何用户名和与用户名相同的密码都可以被指定进行认证。
为了将用户名和密码传回 HTTP 服务器,应用必须使用java.net.Authenticator类,如清单 9-15 所示。
***清单 9-15。*执行基本认证
`import java.io.IOException;
import java.net.Authenticator; import java.net.HttpURLConnection; import java.net.PasswordAuthentication; import java.net.URL; import java.net.URLConnection;
import java.util.List; import java.util.Map;
class BasicAuthGiven
{
final static String USERNAME = "user1";
final static String PASSWORD = "user1";
static class BasicAuthenticator extends Authenticator
{
@Override
public PasswordAuthentication getPasswordAuthentication()
{
System.out.println("Password requested from "+
getRequestingHost()+" for authentication "+ "scheme "+getRequestingScheme());
return new PasswordAuthentication(USERNAME, PASSWORD.toCharArray());
}
}
public static void main(String[] args) throws IOException
{
Authenticator.setDefault(new BasicAuthenticator());
String s = "test.webdav.org/auth-basic/";
URL url = new URL(s);
URLConnection urlc = url.openConnection();
Map<String,List> hf = urlc.getHeaderFields();
for (String key: hf.keySet())
System.out.println(key+": "+urlc.getHeaderField(key));
System.out.println(((HttpURLConnection) urlc).getResponseCode());
}
}`
因为Authenticator是抽象的,所以必须子类化。它的protected PasswordAuthentication getPasswordAuthentication()方法必须被覆盖以返回一个java.net.PasswordAuthentication对象中的用户名和密码。最后,必须调用void setDefault(Authenticator a)类方法来为整个 Java 虚拟机(JVM)安装Authenticator子类的一个实例。
安装了认证器后,当 HTTP 服务器需要基本认证时,JVM 将调用Authenticator的requestPasswordAuthentication()方法之一,这又调用覆盖的getPasswordAuthentication()方法。这可以在下面的输出中看到,它证明服务器已经授予了对资源的访问权限(某种程度上):
Password requested from test.webdav.org for authentication scheme basic
null: HTTP/1.1 404 Not Found
Date: Mon, 19 Sep 2011 03:09:11 GMT
Content-Length: 209
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=iso-8859-1
Server: Apache/2.0.54 (Debian GNU/Linux) DAV/2 SVN/1.3.2
404
该输出表明授权已经成功。但是,它也显示找不到该资源。(我猜一个人不可能拥有一切。)
摘要认证
因为基本认证方案假设客户端和服务器之间的连接是安全和可信的,所以它以明文形式传输凭证(没有加密【将信息转换为不可读的内容的过程,称为明文,通过称为密码的算法,),除非拥有特殊知识,通常称为密钥);base64 可以很容易地被解码),使得窃听者很容易访问这些信息。为此,在 RFC 2616:超文本传输协议—HTTP/1.1 ( [www.ietf.org/rfc/rfc2616…](http://www.ietf.org/rfc/rfc2616.txt))中描述的 HTTP 1.1 引入了摘要认证方案来处理基本认证方案缺乏安全性的问题。根据这个方案,WWW-Authenticate报头指定Digest作为令牌。它还指定了realm=" 引用字符串"属性对。
摘要认证方案使用 MD5 ,这是一种单向加密哈希算法,用于加密密码。它还使用服务器生成的一次性随机数(随时间变化的值,如时间戳和访问者计数器)来防止重放(也称为中间人)攻击。虽然密码是安全的,但其余的数据以明文形式传输,窃听者可以访问。此外,客户端没有办法确定它正在与适当的服务器通信(服务器没有办法验证自己)。
注有关摘要认证的更多信息,请查看维基百科的“摘要访问认证”条目(
[en.wikipedia.org/wiki/Digest_access_authentication](http://en.wikipedia.org/wiki/Digest_access_authentication))。
NTLM 和 Kerberos 认证
微软开发了一个专有的 NTLM 认证方案,它基于其 Windows NT 局域网(LAN)管理器认证协议,让客户通过他们的 Windows 凭证访问互联网信息服务器(IIS)资源。这种身份验证方案通常用于需要单点登录到内部网站点的企业环境中。WWW-Authenticate头指定NTLM为令牌;没有realm=" 引用字符串“属性对。与前两种面向请求的方案不同,NTLM 是面向连接的。
在 20 世纪 80 年代,MIT 开发了 Kerberos 来验证大型分布式网络上的用户。该协议比 NTLM 更加灵活和高效。此外,Kerberos 也被认为更安全。与 NTLM 相比,Kerberos 的一些优点是对服务器的身份验证更高效、相互身份验证以及将凭证委托给远程机器。
GSS API、SPNEGO 和协商认证方案
已经开发了各种安全服务来保护网络应用。服务包括多个版本的 Kerberos、NTLM 和 SESAME(Kerberos 的扩展)。因为很难重新设计一个应用来消除它对一个安全服务的依赖,并将其依赖于另一个安全服务,所以通用安全服务应用编程接口(GSS API)被开发为简化对这些服务的访问的标准 API。安全服务供应商通常将 GSS API 的实现作为一组库来提供,这些库随供应商的安全软件一起安装。GSS-API 实现的基础是实际的 Kerberos、NTLM 或其他用于提供凭证的机制。
注意微软提供了自己专有的 GSS API 变体,被称为安全服务提供商接口(SSPI),它高度特定于 Windows,并在一定程度上与 GSS API 互操作。
一对联网的对等体(可以是客户机或服务器的主机)可能有多个已安装的 GSS-API 实现可供选择。因此,这些对等体使用简单且受保护的 GSS-API 协商(SPNEGO)伪机制来识别共享的 GSS-API 机制,做出适当的选择,并基于该选择建立安全上下文。
微软的协商认证方案(在 Windows 2000 中引入)使用 SPNEGO 来选择用于 HTTP 认证的 GSS-API 机制。最初,这个方案只支持 Kerberos 和 NTLM。在集成 Windows 身份验证(以前称为 NTLM 身份验证,也称为 Windows NT 挑战/响应身份验证)下,当 Internet Explorer 试图从 IIS 访问受保护的资源时,IIS 会向该浏览器发送两个WWW-Authenticate标头。第一个报头有Negotiate作为令牌;第二个报头有NTLM作为令牌。因为Negotiate最先被列出,它最先被 ie 浏览器识别。识别后,浏览器会将 NTLM 和 Kerberos 信息返回给 IIS。当下列情况成立时,IIS 使用 Kerberos:
- 客户端是 Internet Explorer 5.0 或更高版本。
- 服务器是 IIS 5.0 或更高版本。
- 操作系统是 Windows 2000 或更高版本。
- 客户端和服务器都是同一个域或受信任域的成员。
否则,使用 NTLM。如果 Internet Explorer 没有识别出Negotiate,它会通过 NTLM 认证方案将 NTLM 信息返回给 IIS。
Java 客户端可以提供一个Authenticator子类,其getPasswordAuthentication()方法检查从protected final String getRequestingScheme()方法返回的方案名,以确定当前方案是否为"negotiate"。在这种情况下,该方法可以将用户名和密码传递给 HTTP SPNEGO 模块(假设需要它们—没有可用的凭据缓存),如以下代码片段所示:
class MyAuthenticator extends Authenticator
{
@Override
public PasswordAuthentication getPasswordAuthentication()
{
if (getRequestingScheme().equalsIgnoreCase("negotiate"))
{
String krb5user; // Assume Kerberos 5.
char[] krb5pass;
// get krb5user and krb5pass in your own way
...
return (new PasswordAuthentication(krb5user, krb5pass));
}
else
{
...
}
}
}
注意有关 Java 对 SPNEGO 和其他认证方案支持的更多信息,请查看位于
[download.oracle.com/javase/7/docs/technotes/guides/net/http-auth.html](http://download.oracle.com/javase/7/docs/technotes/guides/net/http-auth.html)的 JDK 7 文档的“Http 认证”页面。
Cookie 管理
服务器应用通常使用 HTTP cookies (状态对象)——cookies来保存客户端上的少量信息。例如,购物车中当前所选商品的标识符可以存储为 cookies。最好将 cookie 存储在客户端,而不是服务器上,因为可能会有数百万个 cookie(取决于网站的受欢迎程度)。在这种情况下,不仅服务器需要大量的存储空间来存储 cookie,而且搜索和维护 cookie 也非常耗时。
注意查看维基百科的“HTTP cookie”条目(
[en.wikipedia.org/wiki/HTTP_cookie](http://en.wikipedia.org/wiki/HTTP_cookie))快速复习一下 cookie。
服务器应用将 cookie 作为 HTTP 响应的一部分发送给客户端。客户端(例如,网络浏览器)将 cookie 作为 HTTP 请求的一部分发送给服务器。在 Java 5 之前,应用使用URLConnection类(及其HttpURLConnection子类)来获取 HTTP 响应的 cookie 并设置 HTTP 请求的 cookie。String getHeaderFieldKey(int n)和String getHeaderField(int n)方法用于访问响应的Set-Cookie头,而void setRequestProperty(String key, String value)方法用于创建请求的Cookie头。
注 RFC 2109: HTTP 状态管理机制(
[www.ietf.org/rfc/rfc2109…](http://www.ietf.org/rfc/rfc2109.txt))描述了Set-Cookie和Cookie头。
Java 5 引入了抽象的java.net.CookieHandler类作为回调机制,将 HTTP 状态管理连接到 HTTP 协议处理程序(想想具体的HttpURLConnection子类)。一个应用通过CookieHandler类的void setDefault(CookieHandler cHandler)类方法安装一个具体的CookieHandler子类作为系统范围的 cookie 处理程序。一个伴随的CookieHandler getDefault()类方法返回这个 cookie 处理程序,当系统范围的 cookie 处理程序还没有安装时,这个处理程序为空。
HTTP 协议处理程序访问响应和请求头。该处理程序调用系统范围的 cookie 处理程序的void put(URI uri, Map<String, List<String>> responseHeaders)方法将响应 cookie 存储在 cookie 缓存中,并调用Map<String, List<String>> get(URI uri, Map<String, List<String>> requestHeaders)方法从该缓存中获取请求 cookie。与 Java 5 不同,Java 6 引入了CookieHandler的具体实现,以便 HTTP 协议处理程序和应用可以使用 cookies。
具体的java.net.CookieManager类扩展了CookieHandler来管理 cookies。一个CookieManager对象被初始化如下:
- 用一个饼干店来储存饼干。cookie 存储基于
java.net.CookieStore接口。 - 使用 cookie 策略来确定接受哪些 cookie 进行存储。cookie 策略基于
java.net.CookiePolicy接口。
通过调用CookieManager()构造函数或CookieManager(CookieStore store, CookiePolicy policy)构造函数创建一个 cookie 管理器。CookieManager()构造函数调用带有null参数的后一个构造函数,使用默认的内存 cookie 存储和默认的从仅原始服务器接受 cookie 策略。除非您计划创建自己的CookieStore和CookiePolicy实现,否则您很可能会使用默认的构造函数。以下示例创建并建立一个新的CookieManager对象作为系统范围的 cookie 处理程序:
CookieHandler.setDefault(new CookieManager());
除了前面提到的构造函数,CookieManager还声明了以下方法:
Map<String, List<String>> get(URI uri, Map<String, List<String>> requestHeaders)返回从路径与uri的路径匹配的 cookie 存储中获得的 cookie 的Cookie和Cookie2请求头的不可变映射。虽然这个方法的默认实现不使用requestHeaders,但是子类可以使用它。发生 I/O 错误时抛出IOException。CookieStore getCookieStore()返回 cookie 管理器的 cookie 存储。void put(URI uri, Map<String, List<String>> responseHeaders)存储所有适用的 cookie,这些 cookie 的Set-Cookie和Set-Cookie2响应头是从指定的uri值中检索的,并(与所有其他响应头一起)放在 cookie 存储中不可变的responseHeaders映射中。发生 I/O 错误时抛出IOException。void setCookiePolicy(CookiePolicy cookiePolicy)将 cookie 管理器的 cookie 策略设置为CookiePolicy.ACCEPT_ALL(接受所有 cookie)、CookiePolicy.ACCEPT_NONE(不接受 cookie)或CookiePolicy.ACCEPT_ORIGINAL_SERVER(仅接受来自原始服务器的 cookie—这是默认设置)之一。将null传给这个方法对当前的政策没有影响。
与由 HTTP 协议处理程序调用的get()和put()方法相反,应用使用getCookieStore()和setCookiePolicy()方法。考虑清单 9-16 中的。
***清单 9-16。*列出特定域的所有 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;
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 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(); } } }`
清单 9-16 描述了一个命令行应用,它从它的单个域名参数中获取并列出所有的 cookies。
在创建 cookie 管理器并调用setCookiePolicy()来设置 cookie 管理器的策略以接受所有 cookie 之后,ListAllCookies安装 cookie 管理器作为系统范围的 cookie 处理程序。接下来,它连接到由命令行参数标识的域并读取内容(通过URL的Object getContent()方法)。
cookie 存储通过getCookieStore()获得,并通过它的List<HttpCookie> getCookies()方法检索所有未过期的 cookie。对于这些java.net.HttpCookie、String getName()、String getValue()和其他HttpCookie方法的每一个,都被调用来返回特定于 cookie 的信息。
调用java ListAllCookies [apress.com](http://apress.com)产生了以下输出:
Name = frontend
Value = tk95grc7tko42ghghu3qcep5l6
Lifetime (seconds) = 29985
Path = /
注意关于 cookie 管理的更多信息,包括展示如何创建自己的
CookiePolicy和CookieStore实现的例子,请查看Java 教程的“使用 cookie”课程([java.sun.com/docs/books/tutorial/networking/cookies/index.html](http://java.sun.com/docs/books/tutorial/networking/cookies/index.html))。
与数据库互动
一个数据库 ( [en.wikipedia.org/wiki/Database](http://en.wikipedia.org/wiki/Database))是一个有组织的数据集合。尽管数据库有很多种(例如,层次型、面向对象型和关系型),但是将数据组织成相互关联的表格的关系型数据库是很常见的。(每个表行存储一个单项,比如一个员工,每列存储一个单项属性,比如一个员工的姓名。)
除了最普通的数据库(如第八章的平面文件数据库),数据库都是通过数据库管理系统 (DBMS)创建和管理的。关系数据库系统(RDBMSes)支持结构化查询语言 (SQL)来处理表格等等。
注意为了简洁起见,我假设您熟悉 SQL。如果没有,你可能想看看维基百科的“SQL”条目(
[en.wikipedia.org/wiki/SQL](http://en.wikipedia.org/wiki/SQL))的介绍。
Java 通过其面向关系数据库的 JDBC (Java 数据库连接)API 支持数据库创建、访问等,本节将向您介绍 JDBC。在此之前,它向您介绍了 Java DB,我将用它来演示各种 JDBC 特性。
Java DB
最初由 Sun Microsystems 作为 JDK 6 的一部分(不包括在 JRE 中)引入,以给开发人员一个 RDBMS 来测试他们的 JDBC 代码, Java DB 是 Apache 的开源 Derby 产品的发行版,它基于 IBM 的 Cloudscape RDBMS 代码库。这种纯 Java RDBMS 也与 JDK 7 捆绑在一起(不在 JRE 中)。它是安全的,支持 JDBC 和 SQL(包括事务、存储过程和并发),并且占用空间小——其核心引擎和 JDBC 驱动程序占用 2MB。
注一个 JDBC 驱动是一个用于与数据库通信的 classfile 插件。当我在本章后面介绍 JDBC 的时候,我会对 JDBC 车手有更多的了解。
Java DB 能够在嵌入式环境或客户机/服务器环境中运行。在嵌入式环境中,应用通过 Java DB 的嵌入式驱动程序访问数据库引擎,数据库引擎与应用运行在同一个 JVM 中。图 9-3 展示了嵌入式环境架构,其中数据库引擎嵌入在应用中。
***图 9-3。*启动或关闭嵌入式数据库引擎不需要单独的进程。
在客户机/服务器环境中,客户机应用和数据库引擎在独立的 JVM 中运行。客户端应用通过 Java DB 的客户端驱动程序访问网络服务器。网络服务器与数据库引擎运行在同一个 JVM 中,它通过嵌入式驱动程序访问数据库引擎。图 9-4 展示了这种架构。
***图 9-4。*多个客户端通过网络服务器与同一个数据库引擎通信。
Java DB 将图 9-3 和图 9-4 中的数据库部分实现为与数据库同名的目录。在这个目录中,Java DB 创建一个log目录来存储事务日志,一个seg0目录来存储数据文件,一个service.properties文件来存储配置参数。
注意 Java DB 不提供删除(销毁)数据库的 SQL 命令。销毁数据库需要手动删除其目录结构。
Java DB 安装和配置
当使用默认设置安装 JDK 7 时,捆绑的 Java DB 会安装到 Windows 平台上的%JAVA_HOME%\db中,或者 Unix/Linux 平台上相同位置的db子目录中。(为了方便起见,我在表示环境变量路径时采用了 Windows 惯例。)
注意我在本章中关注 Java DB 10.8.1.2,因为它包含在 JDK 7 版本 1.7.0-b147 中,这是本书所基于的 Java 版本。
db目录包含五个文件和以下一对子目录:
bin目录包含用于设置嵌入式和客户机/服务器环境、运行命令行工具以及启动/停止网络服务器的脚本。您应该将这个目录添加到您的PATH环境变量中,以便您可以从文件系统中的任何地方方便地执行它的脚本。lib目录包含各种 JAR 文件,这些文件包含引擎库(derby.jar)、命令行工具库(derbytools.jar和derbyrun.jar)、网络服务器库(derbynet.jar)、网络客户端库(derbyclient.jar)和各种语言环境库。该目录还包含derby.war,用于在/derbynet相对路径注册网络服务器的 servlet——也可以通过 servlet 接口远程管理 Java DB 网络服务器(参见[db.apache.org/derby/docs/10.8/adminguide/cadminservlet98430.html](http://db.apache.org/derby/docs/10.8/adminguide/cadminservlet98430.html))。
此外,%JAVA_HOME%\demo\db目录包含各种 Java DB 演示。
在运行工具和演示以及启动/停止网络服务器之前,您必须设置DERBY_HOME环境变量。通过set DERBY_HOME=%JAVA_HOME%\db为 Windows 设置这个变量,通过export DERBY_HOME=$JAVA_HOME/db为 Unix (Korn shell)设置这个变量。
注意嵌入式和客户端/服务器环境设置脚本引用了一个
DERBY_INSTALL环境变量。根据“Re: DERBY_INSTALL 和 DERBY_HOME”邮件项([www.mail-archive.com/derby-dev@d…](http://www.mail-archive.com/derby-dev@db.apache.org/msg22098.html) ), DERBY_HOME等同于并取代DERBY_INSTALL,以与其他 Apache 项目保持一致。
您还必须设置CLASSPATH环境变量。设置该环境变量最简单的方法是运行 Java DB 附带的脚本文件。Windows 和 Unix/Linux 版本的各种“set*xxx*CP”脚本文件(扩展了当前的类路径)位于%JAVA_HOME%\db \ bin目录中。要运行的脚本文件将取决于您是使用嵌入式环境还是客户端/服务器环境:
- 对于嵌入式环境,调用
setEmbeddedCP将derby.jar和derbytools.jar添加到类路径中。 - 对于客户机/服务器环境,调用
setNetworkServerCP将derbynet.jar和derbytools.jar添加到类路径中。在单独的命令窗口中,调用setNetworkClientCP将derbyclient.jar和derbytools.jar添加到类路径中。
注意Windows 的
setEmbeddedCP.bat、setNetworkClientCP.bat和setNetworkServerCP.bat文件有问题。每个文件的@FOR %%X in ("%DERBY_HOME%") DO SET DERBY_HOME=%%~sX行搞乱了CLASSPATH环境变量——我认为问题与~sX有关。我发现注释掉这一行(通过在这一行前面加上rem和一个空格)可以解决这个问题。
Java DB 演示
%JAVA_HOME%\demo\db\programs目录包含描述 Java DB 中包含的演示的 HTML 文档;demo.html文件是这个文档的入口点。这些演示包括一个使用 Java DB 的简单 JDBC 应用、一个网络服务器示例程序,以及在使用 Derby 手册中介绍的示例程序。
注意使用 Derby 手册强调了 Java DB 的 Derby 传统。您可以从 Apache 的 Derby 项目网站(
[db.apache.org/derby/index.html](http://db.apache.org/derby/index.html))的文档部分([db.apache.org/derby/manuals/index.html](http://db.apache.org/derby/manuals/index.html))下载本手册和其他 Derby 手册。
为了简洁起见,我将只关注位于programs目录的simple子目录中的简单的 JDBC 应用。该应用运行在默认的嵌入式环境或客户机/服务器环境中。它创建并连接到一个derbyDB数据库,在这个数据库中引入一个表,在这个表上执行插入/更新/选择操作,丢弃(删除)这个表,并从数据库断开连接。
要在嵌入式环境中运行该应用,请打开一个命令窗口,并确保已经正确设置了DERBY_HOME和CLASSPATH环境变量;调用setEmbeddedCP来设置类路径。假设simple是当前目录,调用java SimpleApp或java SimpleApp embedded来运行这个应用。您应该观察到以下输出:
SimpleApp starting in embedded mode
Loaded the appropriate driver
Connected to and created database derbyDB
Created table location
Inserted 1956 Webster
Inserted 1910 Union
Updated 1956 Webster to 180 Grand
Updated 180 Grand to 300 Lakeshore
Verified the rows
Dropped table location
Committed the transaction
Derby shut down normally
SimpleApp finished
该输出表明,在嵌入式环境中运行的应用在退出之前关闭了数据库引擎。这样做是为了执行检查点并释放资源。当这个关闭没有发生时,Java DB 注意到检查点的缺失,假设崩溃,并导致恢复代码在下一个数据库连接之前运行(这需要更长的时间来完成)。
提示在嵌入式环境中运行
SimpleApp(或任何其他 Java DB 应用)时,可以通过设置derby.system.home属性来确定数据库目录将创建在哪里。例如,java -Dderby.system.home=c:\ SimpleApp导致在 Windows 平台上的 C:驱动器的根目录中创建derbyDB。
要在客户机/服务器环境中运行该应用,您需要启动网络服务器,并在单独的命令窗口中运行该应用。
在一个命令窗口中,设置DERBY_HOME。通过startNetworkServer脚本(位于%JAVA_HOME%\db \ bin)启动网络服务器,它负责设置类路径。您应该会看到类似如下的输出:
Mon Sep 19 21:23:14 CDT 2011 : Security manager installed using the Basic server security
policy.
Mon Sep 19 21:23:16 CDT 2011 : Apache Derby Network Server - 10.8.1.2 - (1095077) started
and ready to accept connections on port 1527
在另一个命令窗口中,设置DERBY_HOME,然后设置CLASSPATH(通过setNetworkClientCP)。假设simple是当前目录,调用java SimpleApp derbyClient来运行这个应用。这一次,您应该观察到以下输出:
SimpleApp starting in derbyclient mode
Loaded the appropriate driver
Connected to and created database derbyDB
Created table location
Inserted 1956 Webster
Inserted 1910 Union
Updated 1956 Webster to 180 Grand
Updated 180 Grand to 300 Lakeshore
Verified the rows
Dropped table location
Committed the transaction
SimpleApp finished
请注意,在客户机/服务器环境中,数据库引擎没有关闭。虽然在输出中没有指出,但是在嵌入式和客户机/服务器环境中运行SimpleApp还有第二个区别。在嵌入式环境中,derbyDB数据库目录是在simple目录中创建的。在客户机/服务器环境中,这个数据库目录是在执行startNetworkServer时的当前目录中创建的。
当您在客户机/服务器环境中使用完SimpleApp后,您应该关闭网络服务器和数据库引擎。通过调用stopNetworkServer脚本(位于%JAVA_HOME%\db \ bin)来完成这个任务。您还可以通过运行NetworkServerControl脚本(也位于%JAVA_HOME%\db \ bin)来关闭(或启动并控制)网络服务器。例如,NetworkServerControl shutdown关闭网络服务器和数据库引擎。
Java DB 命令行工具
%JAVA_HOME%\db \ bin目录包含用于启动命令行工具的sysinfo、ij和dblook Windows 和 Unix/Linux 脚本文件:
- 运行
sysinfo查看 Java 环境/Java DB 配置。 - 运行
ij来运行执行特定 SQL 命令和重复任务的脚本。 - 运行
dblook查看数据库的全部或部分数据定义语言(DDL)。
如果您在使用 Java DB 时遇到问题(例如,无法连接到数据库),您可以运行sysinfo来找出问题是否与配置有关。该工具在 Java 信息、Derby 信息和语言环境信息标题下报告各种设置——我在附录 c 中讨论了语言环境。
`------------------ Java Information ------------------
Java Version: 1.7.0
Java Vendor: Oracle Corporation
Java home: C:\Program Files\Java\jdk1.7.0\jre
Java classpath: C:\Program Files\Java\jdk1.7.0\db\lib\derby.jar;C:\Program
Files\Java\jdk1.7.0\db\lib\derbytools.jar;;C:\Program
Files\Java\jdk1.7.0\db/lib/derby.jar;C:\Program
Files\Java\jdk1.7.0\db/lib/derbynet.jar;C:\Program
Files\Java\jdk1.7.0\db/lib/derbyclient.jar;C:\Program
Files\Java\jdk1.7.0\db/lib/derbytools.jar
OS name: Windows XP
OS architecture: x86
OS version: 5.1
Java user name: Jeff Friesen
Java user home: C:\Documents and Settings\Jeff Friesen
Java user dir: C:\PROGRA1\Java\JDK171.0\db\bin
java.specification.name: Java Platform API Specification
java.specification.version: 1.7
java.runtime.version: 1.7.0-b147
--------- Derby Information --------
JRE - JDBC: Java SE 6 - JDBC 4.0
[C:\Program Files\Java\jdk1.7.0\db\lib\derby.jar] 10.8.1.2 - (1095077)
[C:\Program Files\Java\jdk1.7.0\db\lib\derbytools.jar] 10.8.1.2 - (1095077)
[C:\Program Files\Java\jdk1.7.0\db\lib\derbynet.jar] 10.8.1.2 - (1095077)
[C:\Program Files\Java\jdk1.7.0\db\lib\derbyclient.jar] 10.8.1.2 - (1095077)
----------------- Locale Information -----------------
Current Locale : [English/United States [en_US]]
Found support for locale: [cs]
version: 10.8.1.2 - (1095077)
Found support for locale: [de_DE]
version: 10.8.1.2 - (1095077)
Found support for locale: [es]
version: 10.8.1.2 - (1095077)
Found support for locale: [fr]
version: 10.8.1.2 - (1095077) Found support for locale: [hu]
version: 10.8.1.2 - (1095077)
Found support for locale: [it]
version: 10.8.1.2 - (1095077)
Found support for locale: [ja_JP]
version: 10.8.1.2 - (1095077)
Found support for locale: [ko_KR]
version: 10.8.1.2 - (1095077)
Found support for locale: [pl]
version: 10.8.1.2 - (1095077)
Found support for locale: [pt_BR]
version: 10.8.1.2 - (1095077)
Found support for locale: [ru]
version: 10.8.1.2 - (1095077)
Found support for locale: [zh_CN]
version: 10.8.1.2 - (1095077)
Found support for locale: [zh_TW]
version: 10.8.1.2 - (1095077)
------------------------------------------------------`
通过运行指定适当 DDL 语句的脚本文件,ij脚本对于创建数据库和初始化用户的模式(逻辑组织表和其他数据库对象的名称空间)非常有用。例如,您创建了一个包含NAME和PHOTO列的EMPLOYEES表,并在当前目录中创建了一个包含以下行的create_emp_schema.sql脚本文件:
CREATE TABLE EMPLOYEES(NAME VARCHAR(30), PHOTO BLOB);
下面的嵌入式ij脚本会话创建了employees数据库和EMPLOYEES表:
C:\db>ij
ij version 10.8
ij> connect 'jdbc:derby:employees;create=true';
ij> run 'create_emp_schema.sql';
ij> CREATE TABLE EMPLOYEES(NAME VARCHAR(30), PHOTO BLOB);
0 rows inserted/updated/deleted
ij> disconnect;
ij> exit;
C:>\db>
connect命令导致employees数据库被创建——当我在本章后面介绍 JDBC 时,我会对这个命令的语法说得更多。run命令导致create_emp_schema.sql执行,结果生成后续的一对线。
CREATE TABLE EMPLOYEES(NAME VARCHAR(30), PHOTO BLOB);行是一个 SQL 语句,用于创建一个名为EMPLOYEES的表,表中有NAME和PHOTO列。输入到NAME列中的数据项属于 SQL 类型VARCHAR(可变数量的字符—一个字符串),最多有30个字符,输入到PHOTO列中的数据项属于 SQL 类型BLOB(一个二进制大对象,如图像)。
注意我用大写指定 SQL 语句,但你也可以用小写或大小写混合指定。
在run 'create_emp_schema.sql'结束后,指定的EMPLOYEES表被添加到新创建的employees数据库中。要验证该表是否存在,请对employees目录运行dblook,如下面的会话所示。
C:\db>dblook -d jdbc:derby:employees
-- Timestamp: 2011-09-19 22:17:20.375
-- Source database is: employees
-- Connection URL is: jdbc:derby:employees
-- appendLogs: false
-- ----------------------------------------------
-- DDL Statements for tables
-- ----------------------------------------------
CREATE TABLE "APP"."EMPLOYEES" ("NAME" VARCHAR(30), "PHOTO" BLOB(2147483647));
C:\db>
所有数据库对象(例如,表和索引)都被分配给用户和系统模式,这些模式以包逻辑组织类的相同方式逻辑组织这些对象。当用户创建或访问数据库时,Java DB 使用指定的用户名作为新添加的数据库对象的名称空间名称。在没有用户名的情况下,Java DB 选择APP,如前面的会话输出所示。
JDBC
JDBC 是一个 API(与java.sql、javax.sql、javax.sql.rowset、javax.sql.rowset.serial和javax.sql.rowset.spi包相关联——本章我主要关注java.sql),用于以独立于 RDBMS 的方式与 RDBMS 通信。您可以使用 JDBC 来执行各种数据库操作,例如提交 SQL 语句,告诉 RDBMS 创建关系数据库或表,以及更新或查询表格数据。
注意 Java 7 支持 JDBC 4.1。关于 JDBC 4.1 特有的特性列表,请查看
[download.oracle.com/javase/7/docs/technotes/guides/jdbc/jdbc_41.html](http://download.oracle.com/javase/7/docs/technotes/guides/jdbc/jdbc_41.html)。
数据源、驱动程序和连接
尽管 JDBC 通常用于与 RDBMSes 通信,但它也可用于与平面文件数据库通信。出于这个原因,JDBC 使用术语数据源(一种由 RDBMS 管理的从简单文件到复杂关系数据库的数据存储设施)来抽象数据源。
因为访问数据源的方式不同(例如,第八章的平面文件数据库是通过java.io.RandomAccessFile类的方法访问的,而 Java DB 数据库是通过 SQL 语句访问的),JDBC 使用驱动程序(类文件插件)来抽象它们的实现。这种抽象使您可以编写一个应用,它可以适应任意数据源,而不必更改一行代码(在大多数情况下)。驱动程序是java.sql.Driver接口的实现。
JDBC 认可四种类型的司机:
- 类型 1 驱动程序将 JDBC 实现为到另一个数据访问 API 的映射(例如,开放式数据库连接,或 ODBC——参见
[en.wikipedia.org/wiki/ODBC](http://en.wikipedia.org/wiki/ODBC))。驱动程序将 JDBC 方法调用转换成另一个库的函数调用。JDBC-ODBC 桥驱动程序就是一个例子,它不受 Oracle 支持。在 JDBC 早期,当其他种类的司机不常见时,这种方法很常用。 - Type 2 驱动部分用 Java 编写,部分用原生代码编写(见附录 C)。它们与特定于数据源的本机客户端库进行交互,因此不可移植。Oracle 的 OCI (Oracle 调用接口)客户端驱动程序就是一个例子。
- Type 3 驱动不依赖于本地代码,通过 RDBMS 独立协议与中间件服务器(位于应用客户端和数据源之间的服务器)通信。然后,中间件服务器将客户机的请求传递给数据源。
- Type 4 驱动不依赖于本机代码,实现特定数据源的网络协议。客户端直接连接到数据源,而不是通过中间件服务器。
在与数据源通信之前,您需要建立连接。JDBC 为此提供了java.sql.DriverManager类和javax.sql.DataSource接口:
- 让应用通过指定 URL 连接到数据源。当该类首次尝试建立连接时,它会自动加载通过类路径找到的任何 JDBC 4.x 驱动程序。(JDBC 4.x 之前版本的驱动程序必须手动加载。)
DataSource对应用隐藏连接细节,以提高数据源的可移植性,因此比DriverManager更受欢迎。因为对DataSource的讨论有点复杂(并且通常在 Java EE 环境中使用),所以我在本章中重点讨论DriverManager。
在让您获得数据源连接之前,早期的 JDBC 版本要求您通过用实现Driver接口的类名指定Class.forName()来显式加载合适的驱动程序。例如,JDBC-ODBC 桥驱动程序是通过Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");加载的。后来的 JDBC 版本放宽了这一要求,允许您通过jdbc.drivers系统属性指定要加载的驱动程序列表。DriverManager会在初始化时尝试加载所有这些驱动程序。
在 Java 7 下,DriverManager首先加载由jdbc.drivers系统属性标识的所有驱动程序。然后,它使用基于java.util.ServiceLoader的服务提供者机制(在附录 C 中讨论)从可访问的驱动程序 JAR 文件中加载所有驱动程序,这样您就不必显式地加载驱动程序。这种机制需要将驱动程序打包到一个包含META-INF/services/java.sql.Driver的 JAR 文件中。java.sql.Driver文本文件必须包含一行来命名驱动程序对Driver接口的实现。
每个加载的驱动程序通过DriverManager的void registerDriver(Driver driver)类方法实例化并向DriverManager注册自己。当被调用时,getConnection()方法遍历注册的驱动程序,从识别getConnection()的 JDBC URL 的第一个驱动程序返回java.sql.Connection接口的实现。(你可能想看看DriverManager的源代码,看看这是怎么做到的。)
注意为了保持数据源的独立性,大部分 JDBC 由接口组成。每个驱动程序都提供了各种接口的实现。
要连接到数据源并获得一个Connection实例,调用DriverManager的Connection getConnection(String url)、Connection getConnection(String url, Properties info)或Connection getConnection(String url, String user, String password)方法之一。无论使用哪种方法,url参数都会指定一个基于字符串的 URL,该 URL 以前缀jdbc:开头,并以特定于数据源的语法结尾。
考虑 Java DB。URL 语法因驱动程序而异。对于嵌入式驱动程序(当您想要访问本地数据库时),语法如下:
jdbc:derby:*databaseName*;*URLAttributes*
对于客户端驱动程序(当您想要访问远程数据库时,尽管您也可以使用该驱动程序访问本地数据库),语法如下:
jdbc:derby://*host*:*port*/*databaseName*;*URLAttributes*
无论使用哪种语法, URLAttributes 都是分号分隔的 name =*value*对的可选序列。例如,create=true告诉 Java DB 创建一个新的数据库。
以下示例演示了第一种语法,告诉 JDBC 加载 Java DB 嵌入式驱动程序,并在本地主机上创建名为testdb的数据库:
Connection con = DriverManager.getConnection("jdbc:derby:testdb;create=true");
下面的例子演示了第二种语法,告诉 JDBC 加载 Java DB 客户机驱动程序,并在xyz主机的端口8500上创建名为testdb的数据库:
Connection con;
con = DriverManager.getConnection("jdbc:derby://xyz:8500/testdb;create=true");
注为了方便起见,本章的应用只使用嵌入式驱动程序连接语法。
例外情况
DriverManager的getConnection()方法(以及各种 JDBC 接口中的其他 JDBC 方法)在出错时抛出java.sql.SQLException或它的一个子类。除了从java.lang.Throwable(例如String getMessage())继承的方法之外,SQLException还声明了各种构造函数(为了简洁起见,不再讨论)和以下方法:
int getErrorCode()返回特定于供应商的整数错误代码。通常,该值将是基础数据源返回的实际错误代码。SQLException getNextException()返回链接到这个SQLException对象的SQLException实例(通过调用setNextException(SQLException ex)),或者当没有链接的异常时返回 null。String getSQLState()返回一个“SQLstate”字符串,该字符串提供了标识异常的 X/Open 或 SQL:2003 错误代码。Iterator<Throwable> iterator()以适当的顺序返回链式SQLException及其原因的迭代器。迭代器将用于迭代每个SQLException及其潜在原因(如果有的话)。你通常不会调用这个方法,但是当你需要迭代SQLException的链时,你会使用增强的 for 语句(在第五章中讨论过)来调用iterator()void setNextException(SQLException sqlex)将sqlex追加到链的末尾。
在处理请求时可能会出现一个或多个SQLException,抛出这些异常的代码可以通过调用setNextException()将它们添加到SQLException的链中。此外,一个SQLException实例可能作为一个不同异常(例如IOException)的结果被抛出,这被称为该异常的原因(参见第三章)。
SQL 状态错误代码由 ISO/ANSI 和开放组(X/Open) SQL 标准定义。错误代码是一个 5 个字符的字符串,由 2 个字符的类值和 3 个字符的子类值组成。类值“00”表示成功,类值“01”表示警告,其他类值通常表示异常。SQL 状态错误代码的示例有 00000(成功)和 08001(无法连接到数据源)。
以下示例向您展示了如何构建您的应用,以连接到 Java DB 数据源,执行一些工作,并响应抛出的SQLException实例:
String url = "jdbc:derby:employee;create=true";
try (Connection con = DriverManager.getConnection(url))
{
// Perform useful work. The following throw statement simulates a
// JDBC method throwing SQLException.
throw new SQLException("Unable to access database table",
new java.io.IOException("File I/O problem"));
}
catch (SQLException sqlex)
{
while (sqlex != null)
{
System.err.println("SQL error : "+sqlex.getMessage());
System.err.println("SQL state : "+sqlex.getSQLState());
System.err.println("Error code: "+sqlex.getErrorCode());
System.err.println("Cause: "+sqlex.getCause());
sqlex = sqlex.getNextException();
}
}
不再需要时,必须关闭连接;Connection为此声明了一个void close()方法。因为Connection实现了java.lang.AutoCloseable,你可以使用 try-with-resources 语句(参见第三章)让这个方法自动被调用,不管是否抛出异常。
假设还没有配置 Java DB(通过设置DERBY_HOME和CLASSPATH环境变量),您应该会看到下面的输出:
SQL error : No suitable driver found for jdbc:derby:employee;create=true SQL state : 08001 Error code: 0 Cause: null
如果您已经配置了 Java DB,您应该观察不到任何输出。
SQLException声明了几个子类(如java.sql.BatchUpdateException—批量更新操作过程中出现错误)。这些子类中的许多被归类在java.sql.SQLNonTransientException和java.sql.SQLTransientException根类层次结构下,其中SQLNonTransientException描述了在不改变应用源代码或数据源的某些方面的情况下不能重试的失败操作,而SQLTransientException描述了可以立即重试的失败操作。
报表
在获得到数据源的连接之后,应用通过发出 SQL 语句(例如,CREATE TABLE、INSERT、SELECT、UPDATE、DELETE和DROP TABLE)与数据源进行交互。JDBC 通过java.sql.Statement、java.sql.PreparedStatement和java.sql.CallableStatement接口支持 SQL 语句。此外,Connection声明了各种createStatement()、prepareStatement和prepareCall()方法,分别返回Statement、PreparedStatement或CallableStatement实现实例。
语句和结果集
Statement是最简单易用的接口,Connection的Statement createStatement()方法是最简单易用的获取Statement实例的方法。调用此方法后,您可以通过调用如下的Statement方法来执行各种 SQL 语句:
ResultSet executeQuery(String sql)执行一个SELECT语句,并(假设没有抛出异常)通过一个java.sql.ResultSet实例提供对其结果的访问。int executeUpdate(String sql)执行一个CREATE TABLE、INSERT、UPDATE、DELETE或DROP TABLE语句,并且(假设没有抛出异常)通常返回受该语句影响的表行数。
我创建了一个EmployeeDB应用来演示这些方法。清单 9-17 展示了它的源代码。
***清单 9-17。*创建、插入值、查询和删除EMPLOYEES表
`import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement;
class EmployeeDB
{
public static void main(String[] args)
{
String url = "jdbc:derby:employee;create=true"; try (Connection con = DriverManager.getConnection(url))
{
try (Statement stmt = con.createStatement())
{
String sql = "CREATE TABLE EMPLOYEES(ID INTEGER, NAME VARCHAR(30))";
stmt.executeUpdate(sql);
sql = "INSERT INTO EMPLOYEES VALUES(1, 'John Doe')";
stmt.executeUpdate(sql);
sql = "INSERT INTO EMPLOYEES VALUES(2, 'Sally Smith')";
stmt.executeUpdate(sql);
ResultSet rs = stmt.executeQuery("SELECT * FROM EMPLOYEES");
while (rs.next())
System.out.println(rs.getInt("ID")+" "+rs.getString("NAME"));
sql = "DROP TABLE EMPLOYEES";
stmt.executeUpdate(sql);
}
}
catch (SQLException sqlex)
{
while (sqlex != null)
{
System.err.println("SQL error : "+sqlex.getMessage());
System.err.println("SQL state : "+sqlex.getSQLState());
System.err.println("Error code: "+sqlex.getErrorCode());
System.err.println("Cause: "+sqlex.getCause());
sqlex = sqlex.getNextException();
}
}
}
}`
成功建立到employee数据源的连接后,main()创建一个语句,并使用它来执行 SQL 语句,以创建、插入值、查询和删除一个EMPLOYEES表。
executeQuery()方法返回一个ResultSet对象,该对象提供对查询的表格结果的访问。每个结果集都与一个光标相关联,该光标提供对特定数据行的访问。光标最初指向第一行之前;调用ResultSet的boolean next()方法将光标移到下一行。只要有下一行,这个方法就返回 true 当没有更多的行要检查时,它返回 false。
ResultSet还声明了基于类型返回当前行的列值的各种方法。例如,int getInt(String columnLabel)返回由columnLabel标识的基于INTEGER的列对应的整数值。类似地,String getString(String columnLabel)返回对应于由columnLabel标识的基于VARCHAR的列的字符串值。
提示如果没有列名但是有从零开始的列索引,那么调用
int getInt(int columnIndex)``String getString(int columnIndex)等ResultSet方法。然而,最佳实践是调用int getInt(String columnLabel)。
编译清单 9-17 ( javac EmployeeDB.java)并运行这个应用(java EmployeeDB)。您应该观察到以下输出:
1 John Doe
2 Sally Smith
SQL 的INTEGER和VARCHAR类型映射到 Java 的int和String类型。表 9-1 给出了一个更完整的类型映射列表。
准备报表
PreparedStatement是下一个最容易使用的接口,Connection的PreparedStatement prepareStatement()方法是获得PreparedStatement实例最容易使用的方法——PreparedStatement是Statement的子接口。
与常规语句不同,预处理语句代表预编译的 SQL 语句。SQL 语句被编译以提高性能并防止 SQL 注入(参见[en.wikipedia.org/wiki/SQL_injection](http://en.wikipedia.org/wiki/SQL_injection)),编译结果存储在PreparedStatement实现实例中。
当您希望多次执行同一个准备好的语句时(例如,您希望多次执行 SQL INSERT语句来填充数据库表),通常会获得此实例。考虑以下示例:
sql = "INSERT INTO EMPLOYEES VALUES(?, ?)";
try (PreparedStatement pstmt = con.prepareStatement(sql))
{
String[] empNames = {"John Doe", "Sally Smith"};
for (int i = 0; i < empNames.length; i++)
{
pstmt.setInt(1, i+1);
pstmt.setString(2, empNames[i]);
pstmt.executeUpdate();
}
}
这个例子首先创建一个指定 SQL INSERT语句的String对象。每个“?”字符都充当一个占位符,用于在执行语句之前指定一个值。
在获得了PreparedStatement实现实例之后,这个接口的void setInt(int parameterIndex, int x)和void setString(int parameterIndex, String x)方法在这个实例上被调用来提供这些值(传递给每个方法的第一个参数是一个从 1 开始的整数列索引,该列索引到与语句相关的表中——1 对应于最左边的列),然后PreparedStatement的int executeUpdate()方法被调用来执行这个 SQL 语句。最终结果是:包含John Doe、Sally Smith的一对行以及它们各自的标识符被添加到EMPLOYEES表中。
可调用语句
CallableStatement是最专业的语句接口;它延伸了PreparedStatement。您使用这个接口来执行 SQL 存储过程,其中存储过程是执行特定任务(例如,解雇一名员工)的 SQL 语句列表。Java DB 与其他 RDBMSes 的不同之处在于,存储过程的主体被实现为一个public static Java 方法。此外,声明该方法的类必须是public。
您可以通过执行一条 SQL 语句来创建一个存储过程,这条语句通常以CREATE PROCEDURE开始,然后以 RDBMS 特定的语法继续。例如,用于创建存储过程的 Java DB 语法,如在[db.apache.org/derby/docs/dev/ref/rrefcreateprocedurestatement.html](http://db.apache.org/derby/docs/dev/ref/rrefcreateprocedurestatement.html)的网页上所指定的,如下所示:
CREATE PROCEDURE *procedure-name* ([ *procedure-parameter* [, *procedure-parameter* ] ]*)
[ *procedure-element* ]*
procedure-name 表示为
[ *schemaName* .] *SQL92Identifier*
procedure-parameter 表示为
[{ IN | OUT | INOUT }] [ *parameter-Name* ] *DataType*
procedure-element 表示为
{
| [ DYNAMIC ] RESULT SETS INTEGER
| LANGUAGE { JAVA }
| *DeterministicCharacteristic*
| EXTERNAL NAME *string*
| PARAMETER STYLE JAVA
| EXTERNAL SECURITY { DEFINER | INVOKER }
| { NO SQL | MODIFIES SQL DATA | CONTAINS SQL | READS SQL DATA }
}
[]之间的任何内容都是可选的,[]右边的*表示这些元字符之间的任何内容可以出现零次或多次,{}元字符包围一个项目列表,|分隔可能的项目——只能指定其中一个项目。
例如,CREATE PROCEDURE FIRE(IN ID INTEGER) PARAMETER STYLE JAVA LANGUAGE JAVA DYNAMIC RESULT SETS 0 EXTERNAL NAME 'EmployeeDB.fire'创建了一个名为FIRE的存储过程。这个过程指定了一个名为ID的输入参数,并与名为EmployeeDB的public类中名为fire的public static方法相关联。
创建存储过程后,您需要获得一个CallableStatement实现实例来调用该过程,您可以通过调用Connection的prepareCall()方法之一来实现;比如CallableStatement prepareCall(String sql)。
传递给prepareCall()的字符串是一个转义子句(独立于 RDBMS 的语法),包括一个开始的{,后面是单词call,再后面是一个空格,再后面是存储过程的名称,再后面是一个参数列表,其中带有用于将要传递的参数的占位符?,再后面是一个结束的}。
注意 Escape 子句是 JDBC 消除不同 RDBMS 供应商实现 SQL 方式差异的方法。当 JDBC 驱动程序检测到转义语法时,它会将其转换成特定 RDBMS 能够理解的代码。这使得转义语法与 RDBMS 无关。
一旦有了一个CallableStatement引用,就可以像使用PreparedStatement一样将参数传递给这些参数。以下示例演示了:
try (CallableStatement cstmt = con.prepareCall("{ call FIRE(?)}"))
{
cstmt.setInt(1, 2);
cstmt.execute();
}
cstmt.setInt(1, 2)方法调用将 2 赋给最左边的存储过程参数——参数索引1对应于最左边的参数(或者当只有一个参数时对应于单个参数)。cstmt.execute()方法调用执行存储过程,这导致对应用的public static void fire(int id)方法的回调。
我已经创建了另一个版本的EmployeeDB应用来演示这个可调用语句。清单 9-18 展示了它的源代码。
***清单 9-18。*通过存储过程解雇员工
`import java.sql.CallableStatement; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement;
public class EmployeeDB
{
public static void main(String[] args)
{
String url = "jdbc:derby:employee;create=true";
try (Connection con = DriverManager.getConnection(url))
{
try (Statement stmt = con.createStatement())
{
String sql = "CREATE PROCEDURE FIRE(IN ID INTEGER)"+
" PARAMETER STYLE JAVA"+
" LANGUAGE JAVA"+
" DYNAMIC RESULT SETS 0"+
" EXTERNAL NAME 'EmployeeDB.fire'";
stmt.executeUpdate(sql);
sql = "CREATE TABLE EMPLOYEES(ID INTEGER, NAME VARCHAR(30), "+
"FIRED BOOLEAN)";
stmt.executeUpdate(sql);
sql = "INSERT INTO EMPLOYEES VALUES(1, 'John Doe', false)"; stmt.executeUpdate(sql);
sql = "INSERT INTO EMPLOYEES VALUES(2, 'Sally Smith', false)";
stmt.executeUpdate(sql);
dump(stmt.executeQuery("SELECT * FROM EMPLOYEES"));
try (CallableStatement cstmt = con.prepareCall("{ call FIRE(?)}"))
{
cstmt.setInt(1, 2);
cstmt.execute();
}
dump(stmt.executeQuery("SELECT * FROM EMPLOYEES"));
sql = "DROP TABLE EMPLOYEES";
stmt.executeUpdate(sql);
sql = "DROP PROCEDURE FIRE";
stmt.executeUpdate(sql);
}
}
catch (SQLException sqlex)
{
while (sqlex != null)
{
System.err.println("SQL error : "+sqlex.getMessage());
System.err.println("SQL state : "+sqlex.getSQLState());
System.err.println("Error code: "+sqlex.getErrorCode());
System.err.println("Cause: "+sqlex.getCause());
sqlex = sqlex.getNextException();
}
}
}
static void dump(ResultSet rs) throws SQLException
{
while (rs.next())
System.out.println(rs.getInt("ID")+" "+rs.getString("NAME")+
" "+rs.getBoolean("FIRED"));
System.out.println();
}
public static void fire(int id) throws SQLException
{
Connection con = DriverManager.getConnection("jdbc:default:connection");
String sql = "UPDATE EMPLOYEES SET FIRED=TRUE WHERE ID="+id;
try (Statement stmt = con.createStatement())
{
stmt.executeUpdate(sql);
}
}
}`
这个清单的大部分内容应该很容易理解,所以我将只讨论fire()方法。如前所述,这个方法是调用 callable 语句的结果。
用雇员的整数标识符调用fire()来触发。它首先通过使用jdbc.default:connection参数调用getConnection()来访问当前的Connection对象,这是由 Oracle JVMs 通过一个特殊的内部驱动程序支持的。
在创建一个 SQL UPDATE语句字符串以将EMPLOYEES表行中的FIRED列设置为 true(其 ID 字段等于id中的值)之后,fired()调用executeUpdate()来适当地更新该表。
编译清单 9-18 ( javac EmployeeDB.java)并运行这个应用(java EmployeeDB)。您应该观察到以下输出:
1 John Doe false
2 Sally Smith false
1 John Doe false
2 Sally Smith true
元数据
数据源通常与描述数据源的元数据(关于数据的数据)相关联。当数据源是 RDBMS 时,该数据通常存储在表的集合中。
元数据包括一列目录 (RDBMS 数据库,其表描述 RDBMS 对象,如基表【物理存在的表】、视图【虚拟表】、索引【提高数据检索操作速度的文件】)、模式(对数据库对象进行分区的名称空间),以及附加信息(如版本号、标识字符串和限制)。
要访问数据源的元数据,调用Connection的DatabaseMetaData getMetaData()方法。这个方法返回一个java.sql.DatabaseMetaData接口的实现实例。
我已经创建了一个演示了getMetaData()和各种DatabaseMetaData方法的MetaData应用。清单 9-19 展示了MetaData的源代码。
**清单 9-19。**从employee数据源获取元数据
`import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement;
class MetaData
{
public static void main(String[] args)
{
String url = "jdbc:derby:employee;create=true";
try (Connection con = DriverManager.getConnection(url))
{
try (Statement stmt = con.createStatement())
{
dump(con.getMetaData());
}
}
catch (SQLException sqlex)
{
while (sqlex != null)
{
System.err.println("SQL error : "+sqlex.getMessage()); System.err.println("SQL state : "+sqlex.getSQLState());
System.err.println("Error code: "+sqlex.getErrorCode());
System.err.println("Cause: "+sqlex.getCause());
sqlex = sqlex.getNextException();
}
}
}
static void dump(DatabaseMetaData dbmd) throws SQLException
{
System.out.println("DB Major Version = "+dbmd.getDatabaseMajorVersion());
System.out.println("DB Minor Version = "+dbmd.getDatabaseMinorVersion());
System.out.println("DB Product = "+dbmd.getDatabaseProductName());
System.out.println("Driver Name = "+dbmd.getDriverName());
System.out.println("Numeric function names for escape clause = "+
dbmd.getNumericFunctions());
System.out.println("String function names for escape clause = "+
dbmd.getStringFunctions());
System.out.println("System function names for escape clause = "+
dbmd.getSystemFunctions());
System.out.println("Time/date function names for escape clause = "+
dbmd.getTimeDateFunctions());
System.out.println("Catalog term: "+dbmd.getCatalogTerm());
System.out.println("Schema term: "+dbmd.getSchemaTerm());
System.out.println();
System.out.println("Catalogs");
System.out.println("--------");
ResultSet rsCat = dbmd.getCatalogs();
while (rsCat.next())
System.out.println(rsCat.getString("TABLE_CAT"));
System.out.println();
System.out.println("Schemas");
System.out.println("-------");
ResultSet rsSchem = dbmd.getSchemas();
while (rsSchem.next())
System.out.println(rsSchem.getString("TABLE_SCHEM"));
System.out.println();
System.out.println("Schema/Table");
System.out.println("------------");
rsSchem = dbmd.getSchemas();
while (rsSchem.next())
{
String schem = rsSchem.getString("TABLE_SCHEM");
ResultSet rsTab = dbmd.getTables(null, schem, "%", null);
while (rsTab.next())
System.out.println(schem+" "+rsTab.getString("TABLE_NAME"));
}
}
}`
清单 9-19 的dump()方法调用其dbmd参数上的各种方法来输出分类元数据。
int getDatabaseMajorVersion()和int getDatabaseMinorVersion()方法返回 Java DB 版本号的主要部分(例如 10)和次要部分(例如 8)。同样,String getDatabaseProductName()返回该产品的名称(如 Apache Derby),而String getDriverName()返回驱动程序的名称(如 Apache Derby 嵌入式 JDBC 驱动程序)。
SQL 定义了各种函数,这些函数可以作为SELECT和其他语句的一部分被调用。例如,您可以指定SELECT COUNT(*) AS TOTAL FROM EMPLOYEES来返回一行一列的结果集,其中列名为TOTAL,行值包含EMPLOYEES表中的行数。
因为不是所有的 RDM 都采用相同的语法来指定函数调用,所以 JDBC 使用了一个由{ fn *functionname*(*arguments*) }组成的函数转义子句,来抽象不同之处。例如,SELECT {fn UCASE(NAME)} FROM EMPLOYEES从EMPLOYEES中选择所有的NAME列值,并在结果集中大写它们的值。
String getNumericFunctions()、String getStringFunctions()、String getSystemFunctions()和String getTimeDateFunctions()方法返回可能出现在函数转义子句中的函数名列表。例如,getNumericFunctions()为 Java DB 10.8 返回ABS,ACOS,ASIN,ATAN,ATAN2,CEILING,COS,COT,DEGREES,EXP,FLOOR,LOG,LOG10,MOD,PI,RADIANS,RAND,SIGN,SIN,SQRT,TAN。
并非所有供应商都使用相同的术语来描述目录和模式。出于这个原因,String getCatalogTerm()和String getSchemaTerm()方法用于返回特定于供应商的术语,对于 Java DB 10.8 来说恰好是CATALOG和SCHEMA。
ResultSet getCatalogs()方法返回目录名的结果集,可通过结果集的TABLE_CAT列访问。对于 Java DB 10.8,这个结果集是空的,Java DB 10.8 将单个默认目录划分为不同的模式。
ResultSet getSchemas()方法返回模式名的结果集,可以通过结果集的TABLE_SCHEM列访问。该列包含 Java DB 10.8 的APP、NULLID、SQLJ、SYS、SYSCAT、SYSCS_DIAG、SYSCS_UTIL、SYSFUN、SYSIBM、SYSPROC和SYSSTAT值。APP是存储用户数据库对象的默认模式。
ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types)方法返回一个结果集,其中包含表名(在TABLE_NAME列中)和其他与指定的catalog、schemaPattern、tableNamePattern和types匹配的面向表的元数据。为了获得特定模式的所有表的结果集,将null传递给catalog和types,将模式名传递给schemaPattern,将%通配符传递给tableNamePattern。
例如,SYS模式存储了SYSALIASES、SYSCHECKS、SYSCOLPERMS、SYSCOLUMNS、SYSCONGLOMERATES、SYSCONSTRAINTS、SYSDEPENDS、SYSFILES、SYSFOREIGNKEYS、SYSKEYS、SYSPERMS、SYSROLES、SYSROUTINEPERMS、SYSSCHEMAS、SYSSEQUENCES、SYSSTATEMENTS、SYSSTATISTICS、SYSTABLEPERMS、SYSTABLES、SYSTRIGGERS和SYSVIEWS表。
清单 9-17 和 9-18 遇到了一个架构问题。在创建了EMPLOYEES表之后,假设在删除该表之前抛出了SQLException。下次运行EmployeeDB应用时,当应用试图重新创建EMPLOYEES时会抛出SQLException,因为这个表已经存在。您必须手动删除employee目录,然后才能重新运行EmployeeDB。
在创建EMPLOYEES之前调用某种isExist()方法会很好,但是那个方法不存在。然而,我们可以在getTables()的帮助下创建这个方法,清单 9-20 向你展示了如何完成这个任务。
**清单 9-20。**在创建该表之前确定EMPLOYEES的存在
import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.DriverManager; `import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
class EmployeeDB { public static void main(String[] args) { String url = "jdbc:derby:employee;create=true"; try (Connection con = DriverManager.getConnection(url)) { try (Statement stmt = con.createStatement()) { String sql; if (!isExist(con, "EMPLOYEES")) { System.out.println("EMPLOYEES doesn't exist"); sql = "CREATE TABLE EMPLOYEES(ID INTEGER, NAME VARCHAR(30))"; stmt.executeUpdate(sql); } else System.out.println("EMPLOYEES already exists"); sql = "INSERT INTO EMPLOYEES VALUES(1, 'John Doe')"; stmt.executeUpdate(sql); sql = "INSERT INTO EMPLOYEES VALUES(2, 'Sally Smith')"; stmt.executeUpdate(sql); ResultSet rs = stmt.executeQuery("SELECT * FROM EMPLOYEES"); while (rs.next()) System.out.println(rs.getInt("ID")+" "+rs.getString("NAME")); sql = "DROP TABLE EMPLOYEES"; stmt.executeUpdate(sql); } } catch (SQLException sqlex) { while (sqlex != null) { System.err.println("SQL error : "+sqlex.getMessage()); System.err.println("SQL state : "+sqlex.getSQLState()); System.err.println("Error code: "+sqlex.getErrorCode()); System.err.println("Cause: "+sqlex.getCause()); sqlex = sqlex.getNextException(); } } } static boolean isExist(Connection con, String tableName) throws SQLException { DatabaseMetaData dbmd = con.getMetaData(); ResultSet rs = dbmd.getTables(null, "APP", tableName, null); return rs.next(); } }`
清单 9-20 通过引入一个boolean isExist(Connection con, String tableName)类方法来重构清单 9-17 ,该方法在tableName存在时返回 true,并在创建该表之前使用该方法来确定EMPLOYEES的存在。
当指定的表存在时,返回包含一行的ResultSet对象,ResultSet的next()方法返回 true。否则,结果集不包含任何行,并且next()返回 false。
注意
isExist()采用默认的APP模式,当涉及用户名时可能不是这样(每个用户的数据库对象都存储在与用户名对应的模式中)。
行星
虽然很有帮助,但是以前的 JDBC 应用在揭示 JDBC 的威力方面有所欠缺,尤其是在与 Swing 结合使用时。为此,我创建了一个名为Planets的更广泛的应用,让您有机会在更有用的环境中探索这些 API。此外,您将发现每个 API 的一些新内容。
Planets应用通过展示八颗行星的图像以及它们的名称和直径(以千米为单位)、质量(以千克为单位)和距太阳的距离(以天文单位或 AUs 为单位,地球距太阳 1 AU)的统计数据,帮助用户了解太阳系的行星。
我设计了两种运行模式。当您执行java Planets initdb时,这个应用创建一个planets数据库,用八个条目填充它的PLANETS表(其中每个条目记录一个基于String的名称、double直径、double质量、double距离和一个存储行星图像的javax.swing.ImageIcon对象),然后终止。当您执行java Planets时,该表的内容被加载,然后您会看到如图图 9-5 所示的 GUI。
图 9-5。 Planets便于了解太阳系的行星。
我已经把Planets分成了Planets和SwingCanvas两个班级:
Planets分为names、diameters、masses、distances、iiPhotos、static字段,保存从数据库中读取的行星信息;创建 GUI 的JPanel createGUI()类方法;执行java Planets initdb时初始化数据库的void initDB()类方法;一个void loadDB()类方法,当您执行java Planets时,它从数据库的PLANETS表(在 GUI 显示之前)加载行星信息;和启动应用的main()入口点方法。SwingCanvas被组织成iiPhoto和d(尺寸)static字段,一个SwingCanvas(ImageIcon iiPhoto)构造器,将该组件的尺寸调整为每个图像的尺寸并保存初始图像图标以供显示,一个覆盖的Dimension getPreferredSize()方法,返回该组件的首选尺寸以便完全显示图像,一个覆盖的void paint(Graphics g)方法,将当前图像图标的图像绘制在组件的表面上,以及一个void setPhoto(ImageIcon iiPhoto)方法,将一个新的图像图标分配给画布组件并使其图像绘制在组件的绘图表面上。
简洁的需要限制了我展示完整的源代码,所以我将展示代码片段——您可以在本书附带的代码文件中找到完整的源代码(有关更多信息,请参见本书的介绍)。
考虑下面的initDB()源代码:
static void initDB() { String[] planets = { "mercury", "venus", "earth", "mars", "jupiter", "saturn", "uranus", "neptune" }; double[] diameters = { 4880, 12103.6, 12756.3, 6794, 142984, 120536, 51118, 49532 }; double[] masses = { 3.3e23, 4.869e24, 5.972e24, 6.4219e23, 1.9e27, 5.68e26, 8.683e25, 1.0247e26 }; double[] distances = { 0.38, 0.72, 1, 1.52, 5.2, 9.54, 19.218, 30.06 }; String url = "jdbc:derby:planets;create=true"; try (Connection con = DriverManager.getConnection(url)) { try (Statement stmt = con.createStatement()) { String sql = "create table planets(name varchar(30),"+ "diameter real,"+ "mass real,"+ "distance real,"+ "photo blob)"; stmt.executeUpdate(sql); sql = "insert into planets values(?, ?, ?, ?, ?)"; try (PreparedStatement pstmt = con.prepareStatement(sql)) { for (int i = 0; i < planets.length; i++) { pstmt.setString(1, planets[i]); pstmt.setDouble(2, diameters[i]); pstmt.setDouble(3, masses[i]); pstmt.setDouble(4, distances[i]); Blob blob = con.createBlob(); try (ObjectOutputStream oos = new ObjectOutputStream(blob.setBinaryStream(1))) { ImageIcon photo = new ImageIcon(planets[i]+".jpg"); oos.writeObject(photo); } catch (IOException ioe) { System.err.println("unable to write "+planets[i]+".jpg"); } pstmt.setBlob(5, blob); pstmt.executeUpdate(); blob.free(); // Free the blob and release any held resources. } } } } catch (SQLException sqlex) { while (sqlex != null) { System.err.println("sql error : "+sqlex.getMessage()); System.err.println("sql state : "+sqlex.getSQLState()); System.err.println("error code: "+sqlex.getErrorCode()); System.err.println("cause: "+sqlex.getCause()); sqlex = sqlex.getNextException(); } } }
这个方法最重要的是演示了如何将一个ImageIcon对象序列化为一个java.sql.Blob对象,然后将Blob对象存储在一个BLOB类型的表列中。
首先调用Connection的Blob createBlob()方法来创建一个实现Blob接口的对象。因为返回的对象最初不包含数据,所以您需要调用Blob的OutputStream setBinaryStream(long pos)方法(pos被传递到 blob 中从 1 开始的开始写入的位置)或它的一个重载setBytes()方法。
如果您选择setBinaryStream(),那么您将使用对象序列化(见第八章)来将对象序列化为 blob。完成后不要忘记关闭对象输出流——try-with-resources 语句很好地为您处理了这项任务。
在创建并填充了Blob对象之后,调用PreparedStatement的setBlob()方法之一(例如void setBlob(int parameterIndex, Blob x))在执行之前将 blob 传递给准备好的语句。在这个执行之后,blob 必须被释放,它的资源也必须被释放。
考虑下面的loadDB()源代码:
static boolean loadDB() { String url = "jdbc:derby:planets;create=false"; try (Connection con = DriverManager.getConnection(url)) { try (Statement stmt = con.createStatement()) { ResultSet rs = stmt.executeQuery("select count(*) from planets"); rs.next(); int size = rs.getInt(1); names = new String[size]; diameters = new double[size]; masses = new double[size]; distances = new double[size]; iiPhotos = new ImageIcon[size]; rs = stmt.executeQuery("select * from planets"); for (int i = 0; i < size; i++) { rs.next(); names[i] = rs.getString(1); diameters[i] = rs.getDouble(2); masses[i] = rs.getDouble(3); distances[i] = rs.getDouble(4); Blob blob = rs.getBlob(5); try (ObjectInputStream ois = new ObjectInputStream(blob.getBinaryStream())) { iiPhotos[i] = (ImageIcon) ois.readObject(); } catch (ClassNotFoundException|IOException cnfioe) { System.err.println("unable to read "+names[i]+".jpg"); } blob.free(); // Free the blob and release any held resources. } return true; } } catch (SQLException sqlex) { while (sqlex != null) { System.err.println("sql error : "+sqlex.getMessage()); System.err.println("sql state : "+sqlex.getSQLState()); System.err.println("error code: "+sqlex.getErrorCode()); System.err.println("cause: "+sqlex.getCause()); sqlex = sqlex.getNextException(); } return false; } }
这个方法是initDB()的逆方法,展示了如何通过执行 SQL 语句(如SELECT COUNT(*) FROM PLANETS)获得结果集的行数,以及如何反序列化 blob 包含的对象。
基于 Swing 的 GUI 由一个SwingCanvas组件(参见第七章)和一个javax.swing.JTable类的实例组成,后者用于显示和编辑单元格的常规二维表格,是显示表格数据的完美组件。
对JTable及其众多支持类型(在javax.swing和javax.swing.table包中)的完整讨论超出了本章的范围。相反,请考虑下面摘自createGUI()方法的一段话:
TableModel model = new AbstractTableModel() { @Override public int getColumnCount() { return 4; } @Override public String getColumnName(int column) { switch (column) { case 0: return "name"; case 1: return "diameter (km)"; case 2: return "mass (kg)"; default: return "distance (au)"; } } @Override public int getRowCount() { return names.length; } @Override public Object getValueAt(int row, int col) { switch (col) { case 0: return Character.toUpperCase(names[row].charAt(0))+ names[row].substring(1); case 1: return diameters[row]; case 2: return masses[row]; default: return distances[row]; } } }; final JTable table = new JTable(model); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setRowSelectionInterval(0, 0); ListSelectionListener lsl; lsl = new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent lse) { sc.setPhoto(iiPhotos[table.getSelectedRow()]); } }; table.getSelectionModel().addListSelectionListener(lsl);
每个JTable实例从一个表模型中获取数据,这个表模型是实现javax.swing.table.TableModel接口的类的一个实例。我发现子类化javax.swing.table.AbstractTableModel类很方便,它实现了很多TableModel的方法。
AbstractTableModel没有实现int getColumnCount()(表中的列数)、int getRowCount()(表中的行数)和Object getValueAt(int row, int col)(指定行和列的值),因此覆盖这些方法以返回合适的值的任务落在了表模型实现上。
虽然AbstractTableModel实现了String getColumnName(int column),但是这个实现只返回使用电子表格约定的列的默认名称:A、B、C、...z,AA,AB 等等。若要返回有意义的名称,还必须重写此方法。
表格组件将在必要时调用这些方法。当它这样做时,任何传递的列和/或行值都相对于 0。
创建模型后,它被传递给JTable的JTable(TableModel dm)构造函数,后者创建表格组件。除了指定的表模型之外,构造函数还安装了一个默认的列模型(用于选择、添加、删除列以及对列执行其他操作)和一个默认的列表选择模型(用于选择一行或多行)。
JTable(TableModel)的默认列表选择模型允许用户选择一行或多行。因为用户应该一次只能选择一行(应用如何同时显示多个行星图像?),JTable的void setSelectionMode(int selectionMode)方法被调用,参数javax.swing.ListSelectionModel.SINGLE_SELECTION被传递给selectionMode。
当应用开始运行时,第一个表格行(Mercury)应该突出显示(与显示的 Mercury 图像相对应)。这个任务是通过调用JTable的void setRowSelectionInterval(int index0, int index1)方法完成的。因为只需要选择第 0 行(第一行或最上面的行),所以这个值被传递给index0和index1。(setRowSelectionInterval()允许您选择多行,但仅当选择模式不是SINGLE_SELECTION时。)
SwingCanvas组件最初显示水星的图像。当用户选择另一个表行时,必须显示该行的行星图像。这个任务是通过向表组件的ListSelectionModel实现实例注册一个javax.swing.event.ListSelectionListener实现实例来完成的,该实例由JTable的ListSelectionModel getSelectionModel()方法返回。
ListSelectionListener声明了一个void valueChanged(ListSelectionEvent lse)方法,每当用户选择一行时都会调用这个方法。选中的行是通过调用JTable的int getSelectedRow()方法获得的,该方法用于索引到iiPhotos,其ImageIcon实例被传递给SwingCanvas的void setPhoto(ImageIcon iiPhoto)方法,从而显示新照片。
我为Planets应用选择的架构风格适合完全放在内存中的小型数据库表。但是,您可能会遇到这样的情况:需要从具有数百万(或更多)行的数据库表中获取数据,并用所有这些数据填充一个表组件。因为没有足够的内存来实现这个,你会怎么做?
解决方案是只将少量的行读入缓存(也许在参考 API 的帮助下——参见第四章)并跟踪当前位置。例如,假设每一行都有一个唯一的从 1 开始的整数标识符,您可以指定一个 SQL 语句,如SELECT * FROM EMPLOYEE WHERE ID >= 20 && ID <= 30来返回那些其ID列包含从 20 到 30 的整数值之一的行。另外,查看“Java:将大数据加载到 JTable 或 JList”([www.snippetit.com/2009/11/jav…](http://www.snippetit.com/2009/11/java-loading-large-data-into-jtable-or-jlist/))来学习如何创建一个合适的表模型用于这种情况。
注意要了解更多关于 JDBC 的信息,请查看 Java 教程的“JDBC:数据库访问”部分。
演习
以下练习旨在测试您对网络 API 和 JDBC 的理解:
1.创建一个网络版的 21 点——这个游戏要实现的版本将在本练习后描述。实现一个用作庄家的BJDealer应用和一个用作玩家的BJPlayer应用。BJDealer等待玩家连接指示,然后创建一个后台线程为玩家服务——这使得庄家可以和多个玩家玩独立游戏。当BJDealer接受来自玩家的套接字时,它创建java.io.ObjectInputStream和java.io.ObjectOutputStream对象用于与玩家通信。类似地,当BJPlayer创建一个用于与经销商通信的套接字时,它会创建ObjectInputStream和ObjectOutputStream对象。因为ObjectInputStream(InputStream in)构造函数会阻塞,直到相应的ObjectOutputStream实例已经写入并刷新了序列化流头,所以让BJDealer和BJPlayer中的每一个立即调用所创建的ObjectOutputStream实例上的flush()方法。BJDealer将Card对象和基于String的状态消息序列化到BJPlayer;BJPlayer将Card对象和基于String的命令序列化到BJDealer。BJDealer不显示用户界面,而BJPlayer显示图 9-6 所示的用户界面。
图 9-6。 BJPlayer的 GUI 由一个渲染扑克牌的组件(玩家的牌在上半部分,庄家的牌在下半部分)和一个显示状态消息和按钮的面板组成。
玩家点击发牌按钮,让庄家发一手新牌。该按钮随后被禁用,直到玩家输或赢。玩家单击“击中”按钮向庄家请求另一张牌,并在庄家站着时单击“站立”按钮—这些按钮在发牌启用时被禁用。最后,玩家点击退出按钮终止游戏。
为了节省您的工作,清单 9-21 展示了BJDealer和BJPlayer各自使用的Card类。
***清单 9-21。*根据花色和等级描述扑克牌
`import java.io.Serializable;
import java.util.ArrayList; import java.util.List;
class Card implements Serializable
{
enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES }
enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN,
JACK, QUEEN, KING;
int getValue()
{
return ordinal()+1;
}
}
private Suit suit;
private Rank rank;
private static final List initialDeck = new ArrayList();
Card(Suit suit, Rank rank)
{
this.suit = suit;
this.rank = rank;
}
Rank getRank()
{
return rank;
}
Suit getSuit()
{
return suit;
}
int getValue()
{
return rank.ordinal()+1;
}
static
{
for (Suit suit: Suit.values())
for (Rank rank: Rank.values())
initialDeck.add(new Card(suit, rank));
}
static List newDeck() // Return a new unshuffled deck. {
// Copy initial deck to new deck.
List deck = new ArrayList(initialDeck);
return deck;
}
}`
注 二十一点是一种纸牌游戏,玩家与发牌者竞争,看谁能最接近 21 而不被超过。第一个达到 21 分的玩家获胜并结束本轮游戏。庄家开始一轮游戏时,先给玩家发两张牌,自己也发两张牌。玩家看到了她的两张牌,并且只看到了庄家手中的第一张牌。庄家检查她的手牌,看是否有21 点(正好 21 点)。在这种情况下,玩家输了,除非玩家也有 21 点。在这种情况下,结果被称为推,没有人赢或输。当庄家的手牌不是 21 点时,玩家检查她的牌是否有 21 点,如果是这样,玩家获胜。否则,由于庄家和玩家最初都没有 21 点,游戏进行如下:玩家可以请求击中(从庄家处获得额外的牌——每次击中一张),直到玩家的牌总数超过 21(玩家输了)或者玩家决定站在(玩家对她的牌满意,并将等待看庄家的牌如何进展)。玩家通常在他们认为自己的牌不错和/或另一击可能导致他们超过 21 时才会站起来。在玩家站起来之后,发牌者继续向玩家展示她的第二张牌。当庄家的牌数少于 17 时,他总是受到打击,当他的牌数等于或大于 17 时,他总是站着不动。当评估庄家的临时得分以确定是否需要击中时,ACE 总是计为 11,但在最终决定中可能计为 1。当庄家完成她的击球时,她的手与玩家的手进行比较。当它更高时,庄家赢;当它更低时,玩家获胜(除非玩家超过 21);当它们相同时,这是一种推动,没有人会赢。从 2 到 10 的牌都有其面值,杰克是 10,皇后是 10,国王是 10,王牌是 1 或 11(直到手牌被评估)。
2.用新的统计数据扩展Planets应用(例如,卫星数量、成分和内部温度)。此外,提供关于这个星球的附加注释(通过标签显示)。将所有这些额外信息保存在数据库中,并在应用开始运行时检索它们。你会在[nineplanets.org/](http://nineplanets.org/)找到有用的信息。
总结
网络是可以在用户之间共享硬件和软件的互连节点的集合。主机节点之间的通信通过套接字进行,其中套接字是两个进程之间的通信链路中的端点。端点由标识主机的 IP 地址和标识该网络节点上运行的进程的端口号组成。
一个进程将消息写入套接字,套接字将该消息分解成一系列数据包,并将这些数据包转发给另一个进程的套接字,套接字将这些数据包重新组合成原始消息供该进程使用。
TCP 用于通过来回发送消息来创建两台主机之间的持续对话。在此对话发生之前,必须在这些主机之间建立连接。建立连接后,TCP 进入一种模式,发送一个信息包并等待信息包正确到达的应答(或者当由于网络问题应答没有到达时等待超时)。这种发送/回复循环保证了可靠的连接。
因为建立连接需要时间,而且因为需要接收应答确认(或超时)而发送数据包也需要时间,所以 TCP 相当慢。相比之下,UDP 不需要连接和数据包确认,比 TCP 快得多。然而,UDP 不像 TCP 那样可靠(不能保证数据包会正确到达,甚至到达),因为没有确认。此外,UDP 仅限于单包单向会话。
java.net包提供了用于执行基于 TCP 的通信的Socket和ServerSocket类。它还提供了用于执行 UDP 通信的DatagramSocket、DatagramPacket和MulticastSocket类。
URL 是指定资源(例如网页)在基于 TCP/IP 的网络(例如因特网)上的位置的字符串。此外,它还提供了检索该资源的方法。
java.net包提供了用于访问基于 URL 的资源的URL和URLConnection类。它还提供了用于编码和解码 URL 的URLEncoder和URLDecoder类,以及用于执行基于 URI 的操作(例如,相对化)并返回包含结果的URL实例的URI类。
HTTP 支持认证,由此客户端(例如,浏览器用户)必须证明他们的真实性。已经提出了各种认证方案来处理这个任务;例如,基本和摘要。Java 提供了Authenticator和相关类型,以便联网的 Java 应用可以与这些认证方案交互。
服务器应用通常使用HTTP cookie(状态对象)——cookie来保存客户端上的少量信息。Java 通过CookieManager、CookieHandler和相关类型支持 cookie 管理。
数据库是有组织的数据集合。虽然数据库有很多种(例如,层次型、面向对象型和关系型),但关系型数据库很常见,它们将数据组织成表,每行存储一个项目,如雇员,每列存储一个项目属性,如雇员姓名,这些表可以相互关联。
除了最普通的数据库(例如平面文件数据库),数据库都是通过 DBMS 创建和管理的。RDBMSes 支持 SQL 来处理表等等。
Java 通过其面向关系数据库的 JDBC (Java 数据库连接)API 支持数据库创建、访问等。JDK 还提供了 Java DB,这是一个 RDBMS,可以用来测试支持 JDBC 的应用。
JDBC 提供了许多功能,包括连接到数据源的驱动程序、数据源连接、存储有关数据源问题的各种信息的异常、执行 SQL 的语句(常规、准备好的和可调用的)、存储 SQL 查询结果的结果集以及了解有关数据源的更多信息的元数据。预处理语句是预编译语句,可调用语句用于执行存储过程。
第十章向您介绍 XML,以及 Java 的 SAX、DOM、StAX、XPath 和 XSLT APIs。您甚至简要了解了它的验证 API。