标签:
javaNIO是非阻塞的IO。可以用于替代IO操作,但用于对文件的操作时它并不能设置为非阻塞,它的优势体现在网络通信上。从上一篇文章java网络-Socket来看,即使使用多线程来处理Socket,但一个线程只能处理一个客户端的请求,单个线程在read的时候还是会阻塞,开销还是很大。如果使用NIO来处理,当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Channels、Buffers、Selectors是NIO的核心组成部分。
Channel:
Channel类似于IO中的流。主要实现有:FileChannel(用于处理文件)DatagramChannel(处理UDP)SocketChannel(处理TCP连接)、ServerSocketChannel(可以监听TCP连接,和ServerSocket能创建一样它可以创建SocketChannel),但流是单向读写的,要实现对流的读写需要分别使用input/ouput流,Channel是双向的。而且通道可以异步读写。数据在通道中需要先写入到Buffer中,也只能从Buffer中读取。
Selector:
Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。
File读取文件的一个例子
public class FileChannelTest { public static void main(String args[]) throws IOException { RandomAccessFile aFile = new RandomAccessFile("nio-data.txt", "rw"); FileChannel inChannel = aFile.getChannel(); //定义一个固定大小的buffer ByteBuffer buf = ByteBuffer.allocate(48); //将数据从channel写入buffer 返回channel字节数 int bytesRead = inChannel.read(buf); while (bytesRead != -1) { System.out.println("Read " + bytesRead); buf.flip(); while(buf.hasRemaining()){ System.out.print((char) buf.get()); } //将buffer清空 并将空的channel写入buf 控制循环结束 buf.clear(); bytesRead = inChannel.read(buf); } aFile.close(); } }Buffer:
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存Buffer读写数据一般经过下面四个步骤:
1,写入数据到Buffer
2,调用flip()方法:将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。
3,从Buffer中读取数据
4,调用clear()方法或者compact()方法:
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。
clear()方法会清空整个缓冲区。
compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
Buffer有三个重要的属性:capacity、position、limit
capacity指Buffer创建时的容量大小,position和limit取决于当前是读还是写模式。
在写模式下:
position初始为0,当写入一个数据到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1。
limit表示可写的最大位置,和capacity相同。
在读模式下:
position会在切换到读模式时重置为0,表示可从最开始位置读取,当读取完一个buffer单元后会后移一个单元到下一次读取的位置。
limit为所有数据占据的最大buffer单元处,也就是在切换前写模式下的position位置。
向Buffer中写数据:
1,从Channel写到Buffer:inChannel.read(buf);
2,使用Buffer的put()方法:buf.put(123);
从Buffer中读取数据:
1,从Buffer读取数据到Channel:inChannel.write(buf);
2,使用get()方法从Buffer中读取数据:byte aByte = buf.get();
Buffer常用方法:
1,flip()方法:将Buffer从写模式切换到读模式,将position设置为0,写模式下的position设置为limit
2,rewind()方法:将position设回为0,所以可以重读Buffer中的所有数据
3,clear()、compact()方法:上面已介绍,实际上clear也不会真正清除数据,只是将position设置为0,可以从0开始写了。如果存在未读数据使用compact()可将未读数据置前。
4,mark()、reset()方法:可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。
通道之间的数据传输:
在Java NIO中,如果两个通道中有一个是FileChannel,那你可以直接将数据从一个channel传输到另外一个channel。
FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中。
transferTo()方法将数据从FileChannel传输到其他的channel中
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw"); FileChannel fromChannel = fromFile.getChannel(); RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); toChannel.transferFrom(position, count, fromChannel); //fromChannel.transferTo(position, count, toChannel);方法的输入参数position表示从position处开始向目标文件写入数据,count表示最多传输的字节数。
Selector:
Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。单个线程处理多个Channels的好处是可以减少多线程切换的开销。
Selector的创建
Selector selector = Selector.open();
向Selector注册通道
SelectableChannel.register()方法来实现,如下:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。
Selectionkey.OP_READ是一个interest集合,selector监听的事件类型,有Connect/Accept/Read/Write等类型。可以同时监听多个。返回的类型也是SelectionKey类型,包含了interest集合、ready集合、Channel、Selector
interest集合:
注册时监听的事件类型
ready集合:
ready 集合是通道已经准备就绪的操作的集合,可以通过下面四个方法来检测目前就绪的事件是什么事件,从而进行相应的处理
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
获取Channel和Selector:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
SelectionKey还可以携带附加的对象。
//获取selector对象 Selector selector = Selector.open(); //设置Channel为非阻塞 channel.configureBlocking(false); //向channel注册选择器 非监听read事件的就绪状态 SelectionKey key = channel.register(selector, SelectionKey.OP_READ); while(true) { //阻塞方法 有read事件就绪时会返回就绪channel的个数 int readyChannels = selector.select(); if(readyChannels == 0) continue; //访问“已选择键集”中的就绪通道SelectionKey对象 Set selectedKeys = selector.selectedKeys(); Iterator keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading } else if (key.isWritable()) { // a channel is ready for writing } //Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。 keyIterator.remove(); } }
下面是一个完整的例子,Selector监听Accept事件,监听到新进来的连接创建SocketChannel后又让原来的Selector监听这个通道上的read事件,当read就绪后打印客户端传输过来的数据。
public class NIOServer { // 通道管理器 private Selector selector; /** * 获得一个ServerSocket通道,并对该通道做一些初始化的工作 * @param port 绑定的端口号 * @throws IOException */ public void initServer(int port) throws IOException { // 获得一个ServerSocket通道 ServerSocketChannel serverChannel = ServerSocketChannel.open(); // 设置通道为非阻塞 serverChannel.configureBlocking(false); // 将该通道对应的ServerSocket绑定到port端口 serverChannel.socket().bind(new InetSocketAddress(port)); // 获得一个通道管理器 this.selector = Selector.open(); // 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后, // 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。 serverChannel.register(selector, SelectionKey.OP_ACCEPT); } /** * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理 * @throws IOException */ public void listen() throws IOException { System.out.println("服务端启动成功!"); // 轮询访问selector while (true) { // 当注册的事件到达时,方法返回;否则,该方法会一直阻塞 selector.select(); // 获得selector中选中的项的迭代器,选中的项为注册的事件 Iterator<?> ite = this.selector.selectedKeys().iterator(); while (ite.hasNext()) { SelectionKey key = (SelectionKey) ite.next(); // 删除已选的key,以防重复处理 ite.remove(); handler(key); } } } /** * 处理请求 * @param key * @throws IOException */ public void handler(SelectionKey key) throws IOException { // 客户端请求连接事件 if (key.isAcceptable()) { handlerAccept(key); // 获得了可读的事件 } else if (key.isReadable()) { handelerRead(key); } } /** * 处理连接请求 * @param key * @throws IOException */ public void handlerAccept(SelectionKey key) throws IOException { ServerSocketChannel server = (ServerSocketChannel) key.channel(); // 获得和客户端连接的通道 SocketChannel channel = server.accept(); // 设置成非阻塞 channel.configureBlocking(false); // 在这里可以给客户端发送信息哦 System.out.println("新的客户端连接"); // 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。 channel.register(this.selector, SelectionKey.OP_READ); } /** * 处理读的事件 * @param key * @throws IOException */ public void handelerRead(SelectionKey key) throws IOException { // 服务器可读取消息:得到事件发生的Socket通道 SocketChannel channel = (SocketChannel) key.channel(); // 创建读取的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); int read = channel.read(buffer); if(read > 0){ byte[] data = buffer.array(); String msg = new String(data).trim(); System.out.println("服务端收到信息:" + msg); //回写数据 ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes()); channel.write(outBuffer);// 将消息回送给客户端 }else{ System.out.println("客户端关闭"); key.cancel(); } } /** * 启动服务端测试 * @throws IOException */ public static void main(String[] args) throws IOException { NIOServer server = new NIOServer(); server.initServer(8000); server.listen(); } }
标签:
原文地址:http://blog.csdn.net/zhang19910814/article/details/51492130