今天发表一篇一年前已经总结好的关于NIO的知识点,希望对新学的朋友有帮助,当时是写在Doc文档上面,最近有写博文的时间和心情,所以发表出来。由于在DOC copy出来,所以格式有点乱,希望大家见谅。如果写得不好,请大家给点建议。今天这篇是常识篇01,接下来还会有02。
(一)、为什么要使用java nio而不是流I/O呢?
使用某样技术前,先要对比一下,它的优点和缺点。在JDK1.4之前,我们通过流I/O的方式来进行输入输出操作。而在程序中,I/O操作是最耗时的。Java NIO与流I/O的作用是一样的,但是他们的使用方式和运行的效率不同。Java NIO的效率更快,主要是java nio是通过管道和缓冲区来完成的。下图对比一下流IO和NIO读取数据的方式。
(二)、何为缓冲区(Buffer)?
在上面已经提到了两个概念:管道和缓冲区,那何为缓冲区?缓冲区是一块内存块,其实就是一个数组。NIO中,读取数据时,直接读到缓冲区,写数据时,直接写到缓冲区,也就是说访问所有的数据都是通过缓冲区来进行的。操作缓冲区主要是通过Buffer类下的子类来进行。在JDK文档中可以看到Buffer类是抽象的,它的子类有:ByteBuffer、MappedByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。而ByteBuffer是经常用到的。下面以图片的方式来对ByteBuffer的属性方法进行讲解。
- ByteBuffer的几个属性:
- capacity:缓冲区能够容纳的数据元素的最大数量。比如说:ByteBuffer buffer = ByteBuffer.allocate(12); 那么capacity就等于12.
- Limit:下一个要被写或读的元素。从字面上理解为允许,当写入数据时,limit通常和capacity相等,当读数据时,limit代表buffer中的有效数据的长度(下面有图)
- Position:缓冲区的读写当前的下标。
- Mark:临时存放的位置下标。调用reset()前,要确保已经调用过mark(),因为调用reset()相当于position=mark。
- 例子:仔细观察position、limit、capacity、remaining的变化(在此之前要从http://www.javanio.org/上下载了一个基于图形界面的buffer模拟器),不过为了方便大家已经上传到了附件中
-
- 通过以上5步我们可以总结出,如果我们要读出缓冲区的数据时,先要调用flip()[其实调用flip()后相当于limit=position,position=0];如果我们要写数据到缓冲区时,先要clear()[其实调用clear(),相当于position=0,limit=capacity=12]。
在这里我想缓冲区的概念应该了解得差不多了。
(三)、何为通道(Channel)?
- 从字面上可以理解为传输数据的通道。其实通道就是一种途径,连接到外设或者文件等等的一种途径。通道和流的几个不同点:1、通道可以同时读写而流要不就是读要不就是写 ;2、通道可以进行异步读写;3、通道总是通过缓冲区来进行读写;通道与流最大的区别在于,通道可以是双向的,而流只能是单向。Channel是一个接口,从JDK文档中可以看出,它只有两个方法:isOpen()、close()。读写是输入输出中最基本的过程,从一个通道中读取数据只能通过缓冲区,写数据进通道也只能通过缓冲区,所以在nio中,通道和缓冲区是人和自己影子的关系,永不分离。Channel的几个常用的比较重要的实现类有:
FileChannel:主要是应用于文件的读写
SocketChannel:主要对基于TCP协议的网络数据的读写
ServerSocketChannel:主要是监听到新的连接创建一个SocketChannel
DatagramChannel:主要对基于UDP协议的网络数据的读写
(1)、NIO对文件的操作:文件通道(FileChannel)
- 读取三步走:
- ①、从FileInputStream获取Channel
- ②、创建Buffer
- ③将数据读取到buffer中 写入三步走和读取三步走差不多。
- 先看一个例子吧:(这个例子很简单,主要是加深了解上面的ByteBuffer和Channel)
public class NIOFileOperate { public static void main(String[] args) { try { FileInputStream input = new FileInputStream(new File( "D:\\soft\\java\\netbeans-6.9-ml-windows.exe")); //得到channel FileChannel fc1 = input.getChannel(); FileOutputStream out = new FileOutputStream(new File( "e://netbeans-6.9-ml-windows.exe")); FileChannel fc2 = out.getChannel(); //直接创建缓冲区 ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 100); while (fc1.read(buffer) != -1) { //翻转缓冲区 buffer.flip(); //position与capacity之间是否有元素,有则代表有数据可读 if (buffer.hasRemaining()) { fc2.write(buffer); } //重设缓冲区 buffer.clear(); } fc1.close(); fc2.close(); } catch (Exception e) { e.printStackTrace(); } } }
(2)、NIO对网络套接字的操作:
socket通道(SocketChannel、DatagramChannel、ServerSocketChannel)DatagramChannel和SocketChannel都实现了读和写的功能,而ServerSocketChannel只是负责监听网络中传入的连接和创建新的SocketChannel对象,它是不负责传送数据的。
①、 ServerSocketChannel:它只是一个基于通道的socket监听器,它和ServerSocket的作用差不多,只是它多了一个channel,很明显它增加了通道的功能。在JDK文档中我们可以看到它是抽象的,那么怎么创建他的一个对象呢,它有一个open()方法。我们在服务器端常用到下面的一段代码:
//创建一个未绑定的与ServerSocket有关联的服务器通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ServerSocket serverSocket = ssChannel.socket();
//绑定IP地址和端口
serverSocket.bind(new InetSocketAddress(8080)); );//设置非阻塞通道,默认为阻塞通道
ssChannel.configureBlocking(false)
②、 SocketChannel:经常用到。在JDK文档中我们可以看到创建SocketChannel的对象的方法有两个:open()、open(InetSocketAddress)。调用finishConnect()方法来完成连接过程。创建一个非阻塞的SocketChannel
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(“localhost”,8080));
while(!socketChannel.finishConnect()){
//一直等,直到连接成功
}
(四)、选择器(selector)?
选择器是一个可以检查一个或多个通道,并确定那些通道是准备好的(读或写)的NIO组件。正因为选择器的存在,才使得一个线程可以管理多个通道。下面用图的方式来说明一下selector的作用:
(1)、创建一个Selector
Selector selector = Selector.open();
(2)、把通道注册到选择器
channel.configureBlocking(false);
SelectorKey key = channel.register(selector,SelectionKey.OP_READ)
上面的SelectionKey.OP_READ ,是一个“兴趣集”,其实就是一个事件集。他把你感兴趣的事件注册到选择器。当你感兴趣的事件发生在通道中,Selector会捕获到。SelectKey的四个主要的常量:
1. SelectionKey.OP_CONNECT
2. SelectionKey.OP_ACCEPT
3. SelectionKey.OP_READ
4. SelectionKey.OP_WRITE
如果多于一个事件要被注册呢,那就用‘|’号把两个事件连在一起。
如: SelectionKey.OP_READ | SelectionKey.OP_WRITE
下面是常用到的代码段:
Set<SelectionKey> selectedkeys = selector.selectorKeys(); Iterator<SelectorKey> it = selectedKeys.iterator(); While(it.hasNext()){ SelectionKey key = it.next(); if(key.isAcceptable()){ //代表是一个新连接 }else if(key.isReadable()){ //通道的读事件 }else if(key.isWritable()){ //通道的写事件 } it.remove();//删除处理过的事件 }
下面以一个完整的例子来总结上面的缓冲区+通道+选择器:
Selector管理多管道 (当客户端发来数据时,服务器端把收到的数据发回客户端)
public class ServerTest { private final static int PORT = 1111; private ServerSocketChannel serverChannel; private ServerSocket socket; private Selector selector; private InetSocketAddress socketAddr; private ByteBuffer buffer = ByteBuffer.allocateDirect(1024); public ServerTest() { try { //创建一个新的选择器 selector = Selector.open(); //分配一个未绑定的服务器套接字通道 serverChannel = ServerSocketChannel.open(); //得到与通道相关的socket对象 socket = serverChannel.socket(); socketAddr = new InetSocketAddress(PORT); //将scoket绑定到特定的端口上 socket.bind(socketAddr); //配置通道使用非阻塞模式,在非阻塞模式下,可以编写多道程序同时避免使用复杂的多线程 serverChannel.configureBlocking(false); //把serversocket注册到selector serverChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("服务器已经启动!"); while (true) { int num = -1; try { //监控注册在selector上的SelectableChannel num = selector.select(); } catch (IOException e) { e.printStackTrace(); } if (num == 0) { continue; } //selectedKey返回一个SelectionKey的集合,其中每一个SelectionKey代表一个进行IO的channel Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); //是否是一个新的连接 if (key.isAcceptable()) { ServerSocketChannel channel = (ServerSocketChannel) key .channel(); SocketChannel socketChannel = null; try { socketChannel = channel.accept(); } catch (IOException e) { e.printStackTrace(); } registerChannel(selector, socketChannel, SelectionKey.OP_READ); } //通道是否有数据要读 if (key.isReadable()) { try { readDataFromSocket(key); } catch (Exception e) { e.printStackTrace(); } } } it.remove(); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { new ServerTest(); } protected void readDataFromSocket(SelectionKey key) throws Exception { SocketChannel socketChannel = (SocketChannel) key.channel(); int count; buffer.clear(); while ((count = socketChannel.read(buffer)) > 0) { buffer.flip(); //判断是否有新的数据,有数据就把它写回通道中 while (buffer.hasRemaining()) { socketChannel.write(buffer); } buffer.clear(); } if (count < 0) { System.out.println("Close!"); socketChannel.close(); } System.out.println("read end"); } protected void registerChannel(Selector selector, SelectableChannel channel, int ops) { if (channel == null) { return; } try { channel.configureBlocking(false); channel.register(selector, ops); } catch (Exception e) { e.printStackTrace(); } } }