前些天碰到比尔,你看他皱着眉头,一定又碰到了什么难题。一问,还真让我猜中了。原来,比尔对Java中的一些新概念还不太理解,不知道该怎么用。其实,碰到什么难题,可以去找人问,和别人讨论嘛。于是我把比尔领到了“开发者联盟”,相信他会在那得到“前辈”的帮助.现在,就让我们来看看,比尔究竟碰到了什么难题。
了解Java的人一定对于Java中的I/O APIs很熟悉,这里不想对此多费口舌,而是希望向大家介绍一下JDK1.4中新的I/O APIs,让大家都能了解这些新特性,尽早用最先进的装备来武装自己,以免豪华的法拉利跑车总是跑在痛苦的泥泞路上。
当然阅读本文之前,你最好先了解一下原有的Java I/O APIs。
新在何处
任何新事物的出现,都是对旧事物的一种修正和改进,从而使其更加方便可用,在J2SE v1.4中出现的新I/O(NIO) APIs亦是如此,主要是针对那些旧的I/O APIs不能解决或者解决起来很麻烦的问题。这些新特性主要体现在以下几个方面:
· 更加灵活的可伸缩的I/O接口(scalable I/O),包括I/O抽象Channels的出现以及新的多元的(multiplexed),非阻塞(non-blocking)的I/O机制。这使得构建产品级的应用服务更加方便灵活,使你能够轻松应付成千上万个开放的连接,并且可以有效地利用多个处理器。
· 快速缓存(fast buffered)的二进制和字符I/O接口。快速缓存的二进制I/O API使得你可以很轻易地编写出操作文件流或者二进制数据流的高性能代码。而快速缓存的字符I/O API使得你可以更加高效地处理字符流和文件,此外它还将正则表达式引入到Java平台中来格式化你的输入输出。
· 字符集的编码器和解码器(Character-set encoders and decoders)。这些字符集转换API使得我们可以直接访问操作系统内置的字符集转换器,同时还支持那些外来的转化器。
· 基于Perl风格正则表达式的模式匹配机制(A pattern-matching facility based on Perl-style regular eXPressions)。
· 改良的文件系统接口,支持锁定和内存映射(locks and memory mapping)。该特性使得你可以更加轻易地处理各种文件系统操作中出现的问题,同时使得你可以更加高效地访问大量的文件属性集。此外假如你确实需要,你还可以访问与平台相关的一些特性。最后,它还提供对非本地文件系统的支持,比如网络文件系统(network filesystems)。
· 新的I/O违例类可以使你更加有针对性地来处理各种I/O错误,让你能够在各种平台上一致地来对待这些错误。
· 增加了对并发的支持,NIO类中的大部分方法都支持多个并发的线程。
新的包(packages),类(classes)和接口(interfaces)
为了实现上面提到的那些功能,在Java JDK1.4中新增加以下这些部分来提供支持:
· java.nio包:主要是和Buffers有关的一些类
· java.nio.channels包:主要包括Channels和selectors
· java.nio.charset包:和字符集有关的类
· java.nio.channels.spi包:提供channels服务的类
· java.nio.charset.spi包:提供charsets服务的类
· java.util.regex包:主要是利用正则表达式进行模式匹配的类
· java.lang.CharSequence接口:主要是为java.util.regex包中的一些方法提供一个统一的接口。类String,StringBuffer,java.nio.CharBuffer都重新实现了该接口。
除了这些新增加的类以外,许多原有的类和接口也做了相应的改变。比如FileInputStream和FileOutputStream类中的getChannel、close方法,RandomAccessFile中的getChannel方法等。(1)
几点说明
这些新的I/O APIs的推出,并不意味着原有的I/O APIs的废弃,尽管我们提倡以后尽量使用NIO APIs中的特性。另外,虽然这些NIO APIs都希望做到完全的平台无关性,但是由于I/O工作的非凡性,有些特性还是对操作系统和硬件平台有很大的依靠性,比如可升级的I/O API(scalable I/O API),二进制I/O API(binary I/O API)和新的文件系统接口(new filesystem interface)。所以我们以后在利用这些NIO APIs的时候,应当尽量减少本地代码的部分,做到最大限度的可移植性。
一些例子
介绍完这些新特性以后,我想大家都迫切希望能够通过具体的实例来看看它们具体的用法,可不能光说不练。但是,要知道整个NIO APIs涵盖太大的范围,要一个个完整地讲解它们可能需要一本厚厚的书。所以我们今天只想通过一个典型的例子来做一个引导性的讲解,更多的工作需要大家以后在具体的实际编程过程中慢慢学习。
为了方便,我将直接采用Sun Java的例子程序,这个例子包括两个文件:TimeQuery.java和TimeServer.java。前者可以向一系列主机查询时间,后者监听连接并且告诉呼叫者确切的时间。这是个演示NIO socket channels,缓存治理(buffer handling),字符集和正则表达式的很好的例子。(2)
首先,让我们来看看TimeServer.java(具体代码见清单一)。该程序首先检查参数是否是一个数字串,注重这里模式匹配的用法。
if ((args.length == 1) && Pattern.matches("[0-9]+", args[0]))
port = Integer.parseInt(args[0]);
接着在方法setup()中,调用类ServerSocketChannel的静态方法open()建立一个server-socket channel,此时它还并没有和具体的主机和端口绑定起来,此时我们需要利用到相关联的server socket的bind()方法,server socket可以用类ServerSocketChannel的socket()方法得到。
ServerSocketChannel ssc = ServerSocketChannel.open();
InetSocketAddress isa
= new InetSocketAddress(InetAddress.getLocalHost(), port);
ssc.socket().bind(isa);
最后,监听服务请求的任务在方法serve()中实现。首先,调用类ServerSocketChannel的方法accept()接受连接并返回一个SocketChannel对象,接着调用该对象的write()方法向channel中写入数据。注重在数据写入之前对它的处理过程。
SocketChannel sc = ssc.accept();
String now = new Date().toString();
sc.write(encoder.encode(CharBuffer.wrap(now + "\r\n")));
/********************清单一:TimeServer.java完整的程序清单********************/
/*
* @(#)TimeServer.java1.3 01/12/13
* Listen for connections and tell callers what time it is.
* Demonstrates NIO socket channels (accepting and writing),
* buffer handling, charsets, and regular expressions.
*
* Copyright 2001-2002 Sun Microsystems, Inc. All Rights Reserved.
*/
import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.*;
import java.util.regex.*;
public class TimeServer
{
// We can't use the normal daytime port (unless we're running as root,
// which is unlikely), so we use this one instead
private static int PORT = 8013;
// The port we'll actually use
private static int port = PORT;
// Charset and encoder for US-ASCII
private static Charset charset = Charset.forName("US-ASCII");
private static CharsetEncoder encoder = charset.newEncoder();
// Direct byte buffer for writing
private static ByteBuffer dbuf = ByteBuffer.allocateDirect(1024);
// Open and bind the server-socket channel
//
private static ServerSocketChannel setup() throws IOException
{
ServerSocketChannel ssc = ServerSocketChannel.open();
InetSocketAddress isa
= new InetSocketAddress(InetAddress.getLocalHost(), port);
ssc.socket().bind(isa);
return ssc;
}
// Service the next request to come in on the given channel
//
private static void serve(ServerSocketChannel ssc) throws IOException
{
SocketChannel sc = ssc.accept();
try
{
String now = new Date().toString();
sc.write(encoder.encode(CharBuffer.wrap(now + "\r\n")));
System.out.println(sc.socket().getInetAddress() + " : " + now);
sc.close();
}
finally
{
// Make sure we close the channel (and hence the socket)
sc.close();
}
}
public static void main(String[] args) throws IOException
{
if (args.length 1)
{
System.err.println("Usage: java TimeServer [port]");
return;
}
// If the first argument is a string of digits then we take that
// to be the port number
if ((args.length == 1) && Pattern.matches("[0-9]+", args[0]))
port = Integer.parseInt(args[0]);
ServerSocketCh