Java NIO简单实例教程

来源:互联网 发布:ipad装旧版软件 编辑:程序博客网 时间:2024/06/05 22:45

相较于传统的IO基于字节流和字符流的阻塞式操作,NIO则是基于通道(channel)和缓冲区(buffer)的非阻塞式操作。数据总是从通道读取到缓冲区或者从缓冲区写入到通道。NIO采用内存映射文件的方式来处理输入/输出,NIO将文件或文件的一段区域映射到内存中(map()方法),这样就可以像访问内存一样来访问文件了,也可以采用“用竹筒多次重复取水”的方式,创建一个固定大小的ByteBuff,每次从Channel中读取、写入的也都是固定大小的数据。


ChannelChannel类似流,程序不能直接访问Channel中的数据,Channel只能与Buffer交互,也就是Channel中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。Java NIO中最重要的Channel的实现:

  • FileChannel从文件中读写数据。
  • DatagramChannel能通过UDP读写网络中的数据。
  • SocketChannel能通过TCP读写网络中的数据。
  • ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样对每一个新进来的连接都会创建一个SocketChannel


BufferBuffer的三个属性capacity、position、limit;position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。

  • capacity:作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
  • position:当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向移动到下一个可插入数据的Buffer单元。position最大可为 capacity – 1. 当读取数据时,也是从某个特定位置读,当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向移动到下一个可读的位置。
  • limit:在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据方法flip()Buffer从写模式切换到读模式


Selector:选择器是JavaNIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。所有希望采用非阻塞方式通信的Channel都应该注册到Selector上,通过SelectableChannel.register()方法来实现register()方法的第二个参数是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣,如果对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来。可以监听四种不同类型的事件:

  1. Read(1)
  2. Write(4)
  3. Connect(8)
  4. Accept(16)


一个Selector实例有三个SelectionKey集合:

  1. 所有的SelectionKey集合:代表了注册在该Selector上的Channel,通过keys()方法返回
  2. 被选择的SelectionKey集合:代表了所有可通过select()方法获取的、需要进行IO处理的Channel,通过selectedKeys()方法返回
  3. 被取消的SelectionKey集合:代表了所有被取消注册关系的Channel,在下一次执行select()方法时,这些Channel对应的SelectionKeys会被彻底删除
下面是一个读取文件的简单例子:
public class NIODemo {public static void main(String[] args) {try {RandomAccessFile aFile = new RandomAccessFile("D:\\test.txt", "rw");FileChannel inChannel = aFile.getChannel();//创建一个capacity为48 bytes 的BufferByteBuffer buf = ByteBuffer.allocate(48);// 将字节序列从channel写到给定buffer,buffer的大小即每次能写入的最大值,这里则是上面定义的48b。while(inChannel.read(buf) != -1) {buf.flip(); //反转Buffer,接着再从Buffer中读取数据,将Buffer从写模式切换到读模式Charset charset = Charset.forName("UTF-8");CharsetDecoder decoder = charset.newDecoder();//这里注意:当输入字节序列对于给定 charset 来说是不合法的,或者输入字符序列不是合法的 16 位 Unicode 序列时,//这里会抛出java.nio.charset.MalformedInputException//例如:当在这块ByteBuffer空间中末尾的字符不符合UTF-8编码。 //可以选择初始化一块很大的ByteBuffer空间或者使用map()方法将文件全部映射到内存;或者以指定字符读取文件CharBuffer cbuff = decoder.decode(buf);System.out.println(cbuff);buf.clear(); //clear()方法,position将被设回0,limit被设置成 capacity的值。换句话说,Buffer 被清空了。//Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。}aFile.close();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}}

下面模拟一个客户端和服务器通过Socket连接使用NIO实现的过程:
//服务端public class Server {public static void main(String[] args) {try {Selector selector = Selector.open(); //创建一个selectorServerSocketChannel channel = ServerSocketChannel.open();channel.bind(new InetSocketAddress("127.0.0.01",30000));channel.configureBlocking(false); //与Selector一起使用时,Channel必须处于非阻塞模式下。//将ServerSocketChannel注册到selector上,返回一个SelectionKey对象            //这里ServerSocketChannel只能注册SelectionKey.OP_ACCEPTSelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT);while (true) {// select方法会一直阻塞,直到IO事件到达或者设置超时返回;//当需要处理IO操作,对应的SelectionKey加入被选择的SelectionKey集合int readyChannels = selector.select();//返回的是Channel的个数if (readyChannels == 0) {continue;}Set<SelectionKey> selectedKeys = selector.selectedKeys();//返回此Selector的已选择键集。System.out.println("被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = "+selectedKeys.size());Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();// 检测channel中什么事件或操作已经就绪if (key.isAcceptable()) {     //1.对应的Channel包含客户端的连接ServerSocketChannel sscTemp = (ServerSocketChannel) key.channel();                       //得到一个连接好的SocketChannel                        SocketChannel socketChannel = sscTemp.accept();                        socketChannel.configureBlocking(false);                                                //将得到的SocketChannel注册到Selector上,并对该SocketChannel添加兴趣                        //SocketChannel中可以注册SelectionKey.OP_READ、SelectionKey.OP_WRITE、SelectionKey.OP_CONNECT                        socketChannel.register(selector, SelectionKey.OP_READ);// ......(1)                                              System.out.println("注册在该Selector上的Channel个数:" + selector.keys().size());} else if (key.isReadable()) { //2.读取数据,满足Readable条件,则此Channel已准备好进行读取//读取通道中的数据                        SocketChannel readchannel = (SocketChannel) key.channel();                        ByteBuffer buffer = ByteBuffer.allocate(128);                        buffer.clear();try {while (readchannel.read(buffer) > 0) {buffer.flip();byte[] bytes = new byte[buffer.remaining()];buffer.get(bytes);System.out.println("server收到的数据 ..."+ new String(bytes));}} catch (IOException e) {e.printStackTrace();}//......(2)//if(readchannel.read(buffer) < 0) {//key.cancel();//}} else if (key.isConnectable()) {  //3.连接System.out.println("connect ...");} else if (key.isWritable()) {     //4.写System.out.println("write .. ");}keyIterator.remove();}}} catch (IOException e) {e.printStackTrace();}}}
客户端代码如下:
public class Client extends Thread{String threadname;public Client(String threadname){this.threadname = threadname;}@Overridepublic void run(){SocketChannel socketChannel;Selector selector;try {selector = Selector.open();InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 30000);socketChannel = SocketChannel.open(isa);socketChannel.configureBlocking(false);//将SocketChannel对象注册到指定Selector//......(3)socketChannel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);String data = "这是内容,发给服务端,来自 -- " + threadname;ByteBuffer bd = ByteBuffer.allocate(64);bd.clear();//开始往Buffer里写数据。bd.put(data.getBytes());//写入Bufferbd.flip();//写模式切换为读模式  //Write()方法无法保证能写多少字节到SocketChannel。所以,我们重复调用write()直到Buffer没有要写的字节为止while(bd.hasRemaining()){ //当且仅当此缓冲区中至少还有一个元素时返回 truesocketChannel.write(bd);}socketChannel.close();} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {ExecutorService es = Executors.newCachedThreadPool();es.submit(new Client("线程1"));es.submit(new Client("线程2"));es.shutdown();}}

debug运行客户端和服务端输出结果如下:
被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 1注册在该Selector上的Channel个数:2被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2注册在该Selector上的Channel个数:3server收到的数据 ...这是内容,发给服务端,来自 -- 线程2被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2server收到的数据 ...这是内容,发给服务端,来自 -- 线程1被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2...

结果解析:一开始服务端的Selector注册了一个ServerSocketChannel监听新进来的TCP连接,因此只有一个Channel,当连接进来后,对新进来的连接都会创建一个SocketChannel,并注册新的事件(read事件 (1)处),这时注册在该Selector上的Channel个数就 +1,而任何对 Key所关联的兴趣操作集的改变,都只在下次调用了 select()方法后才会生效,所以read事件在下次select()方法之后进行,而接下来需要进行IO处理的Channel +1

总共有两个线程创建了两个SocketChannel,加上ServerSocketChannel,因此服务端Selector上共注册了3Channel。处理完数据之后,发现一直在输出外层循环中的“被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2”,且循环会一直进入key.isReadable(),可是client中的Channel已经关闭了,这是为什么呢?当客户端主动切断连接时,read仍然起作用,也就是说,状态仍然是有东西可读,不过读出来的字节是0,所以需要进一步判断一下读取的字节的数目,把(2)处的注释打开即可删除该Channel的注册关系,当读完Channel中的数据之后,客户端的两个Channel就没有了,就只剩下了ServerSocketChannel这一个Channel了。可以运行新的ClientServer仍可以正常工作。


还有一个坑!:

  • NIO中通过Selector的轮询当前是否有IO事件,根据JDK NIO api描述,Selectorselect方法会一直阻塞,直到IO事件达到或超时,但是在Linux平台上这里有时会出现问题,在某些场景下select()方法会直接返回,即使没有超时并且也没有IO事件到达,这就是著名的epoll bug,这是一个比较严重的bug,它会导致线程陷入死循环,会让CPU飙到100%,极大地影响系统的可靠性,到目前为止,JDK都没有完全解决这个问题,而Netty则对此做了很好的处理,请见Netty为啥可靠(二)。为什么我会知道这玩意儿,一把辛酸泪。。。


参考文章:
Java NIO 系列教程
基于 NIO 的 TCP 通信
Java NIO浅析
Netty为啥可靠(二)
Java NIO编程实例之三Selector
Java NIO 读数据处理过程
我的Java开发学习之旅------>Java NIO 报java.nio.charset.MalformedInputException: Input length = 1异常
《疯狂Java讲义》
原创粉丝点击