本文开始讲解Java NIO 的三个核心组件,Channel,Buffer,Selector。先从Channel开始,Channel指的是通道。Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。通道是一种途径,借助该途径,可以用最小的总开销来访问操作系统本身的I/O服务。
通道基础
首先,看一下基本的Channel接口,下面是Channel接口的完整源码:
public interface Channel extends Closeable {
/**
* Tells whether or not this channel is open. </p>
*
* @return <tt>true</tt> if, and only if, this channel is open
*/
public boolean isOpen();
/**
* Closes this channel.
*
* <p> After a channel is closed, any further attempt to invoke I/O
* operations upon it will cause a {@link ClosedChannelException} to be
* thrown.
*
* <p> If this channel is already closed then invoking this method has no
* effect.
*
* <p> This method may be invoked at any time. If some other thread has
* already invoked it, however, then another invocation will block until
* the first invocation is complete, after which it will return without
* effect. </p>
*
* @throws IOException If an I/O error occurs
*/
public void close() throws IOException;
}
可以从底层的Channel接口看到,对所有通道来说只有两种共同的操作:检查一个通道是否打开isOpen()和关闭一个打开的通道close(),其余所有的东西都是那些实现Channel接口以及它的子接口的类。
从Channel接口引申出的其他接口都是面向字节的子接口:
通道可以是单向的也可以是双向的。一个Channel类可能实现定义read()方法的ReadableByteChannel接口,而另一个Channel类也许实现WritableByteChannel接口以提供write()方法。实现这两种接口其中之一的类都是单向的,只能在一个方向上传输数据。如果一个类同时实现这两个接口,那么它是双向的,可以双向传输数据,就像下面的ByteChannel。
public interface ReadableByteChannel extends Channel {
public int read(ByteBuffer dst) throws IOException;
}
****************************************************************
public interface WritableByteChannel extends Channel{
public int write(ByteBuffer src) throws IOException;
}
****************************************************************
public interface ByteChannel
extends ReadableByteChannel, WritableByteChannel
{
}
通道可以以阻塞(blocking)或非阻塞(nonblocking)模式运行,非阻塞模式的通道永远不会让调用的线程休眠,请求的操作要么立即完成,要么返回一个结果表明未进行任何操作。只有面向流的(stream-oriented)的通道,如sockets和pipes才能使用非阻塞模式。
比方说非阻塞的通道SocketChannel。可以看出,socket通道类从SelectableChannel类引申而来,从SelectableChannel引申而来的类可以和支持有条件的选择的选择器(Selectors)一起使用。将非阻塞I/O和选择器组合起来可以使开发者的程序利用多路复用I/O
public abstract class SocketChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel
{
...
}
public abstract class AbstractSelectableChannel
extends SelectableChannel
{
...
}
文件通道
通道是访问I/O服务的导管,I/O可以分为广义的两大类:File I/O和Stream I/O。那么相应的,通道也有两种类型,它们是文件(File)通道和套接字(Socket)通道。文件通道指的是FileChannel,套接字通道则有三个,分别是SocketChannel、ServerSocketChannel和DatagramChannel。
通道可以以多种方式创建。Socket通道可以有直接创建Socket通道的工厂方法,但是一个FileChannel对象却只能通过在一个打开的RandomAccessFile、FileInputStream或FileOutputStream对象上调用getChannel()方法来获取,开发者不能直接创建一个FileChannel。并且文件通道总是阻塞式的,因此不能被置于非阻塞模式下。
public static void main(String[] args) throws Exception
{
//读文件
File file = new File("D:/files/readchannel.txt");
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteBuffer bb = ByteBuffer.allocate(35);
fc.read(bb);
bb.flip();
while (bb.hasRemaining())
{
System.out.print((char)bb.get());
}
bb.clear();
fc.close();
//写文件
File file = new File("D:/files/writechannel.txt");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fc = raf.getChannel();
ByteBuffer bb = ByteBuffer.allocate(10);
String str = "abcdefghij";
bb.put(str.getBytes());
bb.flip();
fc.write(bb);
bb.clear();
fc.close();
}
Socket通道
Socket通道与文件通道有着不一样的特征:
1、NIO的Socket通道类可以运行于非阻塞模式并且是可选择的,因此,再也没有为每个Socket连接使用一个线程的必要了。这一特性避免了管理大量线程所需的上下文交换总开销,借助NIO类,一个或几个线程就可以管理成百上千的活动Socket连接了并且只有很少甚至没有性能损失
2、全部Socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)在被实例化时都会创建一个对应的Socket对象,就是我们所熟悉的来自java.net的类(Socket、ServerSocket和DatagramSocket),这些Socket可以通过调用socket()方法从通道类获取,此外,这三个java.net类现在都有getChannel()方法
3、每个Socket通道(在java.nio.channels包中)都有一个关联的java.net.socket对象,反之却不是如此,如果使用传统方式(直接实例化)创建了一个Socket对象,它就不会有关联的SocketChannel并且它的getChannel()方法将总是返回null。
现在简单写一下Socket服务端代码和客户端代码:
public class NonBlockingSocketServer
2 {
3 public static void main(String[] args) throws Exception
4 {
5 int port = 1234;
6 if (args != null && args.length > 0)
7 {
8 port = Integer.parseInt(args[0]);
9 }
10 ServerSocketChannel ssc = ServerSocketChannel.open();
11 ssc.configureBlocking(false);
12 ServerSocket ss = ssc.socket();
13 ss.bind(new InetSocketAddress(port));
14 System.out.println("开始等待客户端的数据!时间为" + System.currentTimeMillis());
15 while (true)
16 {
17 SocketChannel sc = ssc.accept();
18 if (sc == null)
19 {
20 // 如果当前没有数据,等待1秒钟再次轮询是否有数据,在学习了Selector之后此处可以使用Selector
21 Thread.sleep(1000);
22 }
23 else
24 {
25 System.out.println("客户端已有数据到来,客户端ip为:" + sc.socket().getRemoteSocketAddress()
26 + ", 时间为" + System.currentTimeMillis()) ;
27 ByteBuffer bb = ByteBuffer.allocate(100);
28 sc.read(bb);
29 bb.flip();
30 while (bb.hasRemaining())
31 {
32 System.out.print((char)bb.get());
33 }
34 sc.close();
35 System.exit(0);
36 }
37 }
38 }
39 }
和BIO的区别可以理解为,在ServerSocket做了一层wrapper,并且数据的读写要通过ByteBuffer。
1 public class NonBlockingSocketClient
2 {
3 private static final String STR = "Hello World!";
4 private static final String REMOTE_IP= "127.0.0.1";
5
6 public static void main(String[] args) throws Exception
7 {
8 int port = 1234;
9 if (args != null && args.length > 0)
10 {
11 port = Integer.parseInt(args[0]);
12 }
13 SocketChannel sc = SocketChannel.open();
14 sc.configureBlocking(false);
15 sc.connect(new InetSocketAddress(REMOTE_IP, port));
16 while (!sc.finishConnect())
17 {
18 System.out.println("同" + REMOTE_IP+ "的连接正在建立,请稍等!");
19 Thread.sleep(10);
20 }
21 System.out.println("连接已建立,待写入内容至指定ip+端口!时间为" + System.currentTimeMillis());
22 ByteBuffer bb = ByteBuffer.allocate(STR.length());
23 bb.put(STR.getBytes());
24 bb.flip(); // 写缓冲区的数据之前一定要先反转(flip)
25 sc.write(bb);
26 bb.clear();
27 sc.close();
28 }
29 }
网友评论