Java梳理之理解NIO(二)

由上篇文章中知道,通道Channel和缓冲器Buffer两者需要共同作用,但是选择器Selector只会作用在继承了抽象类SelectableChannel的网络IO中,下面由简单的FileChannel开始了解nio包的使用。

FileChannel

之前已经说过,在java 1.4的时候,改写了传统IO包下的三个类用以生成通道类FileChannel,这里使用缓冲器类ByteBuffer来对文件进行操作,如下所示:

/**
**FileChannel示例 文件拷贝
**/
public static void main(String[] arg0) throws IOException{
        FileChannel inputChanel = new FileInputStream("/test/test.text").getChannel();
        FileChannel outputChanel = new FileOutputStream("/test/test1.text").getChannel();   
        ByteBuffer buf = ByteBuffer.allocate(1024);
        int len = 0;
        while((len = inputChanel.read(buf))!= -1){
            buf.flip();
            outputChanel.write(buf);
            buf.clear();
        }       
    }

如上所示,代码会将test.text中的数据拷贝到test1.text中,其中将通道中的数据读取到ByteBuffer后,经过三个步骤,即flip()方法调用,调用写通道outputChannelwrite()方法,最后调用ByteBufferclear()方法。虽说完成了功能,但是这种方法的效率并不是很高的,因为每一次读的时候都需要在while循环中调用了几步。在FileChannel类中还有两个方法transferFrom(ReadableByteChannel src,long position, long count)transferTo(long position, long count,WritableByteChannel target),可以通过这两个方法来做到文件拷贝,如下所示:

/**
**FileChannel示例 文件拷贝 transferTo() & transferFrom()
**/
public static void main(String[] arg0) throws IOException{
        FileChannel inputChanel = new FileInputStream("/test/nio1.text").getChannel();
        FileChannel outputChanel = new FileOutputStream("/test/test.text").getChannel();    
//      inputChanel.transferTo(0, inputChanel.size(), outputChanel);
        outputChanel.transferFrom(inputChanel, 0, inputChanel.size());
    }

上面的例子运行完之后,通过将两个通道连接也能正确的拷贝文件。需要注意的是文中的代码为了简便全部是直接抛出异常也没有关闭流,如果实际书写请酌情处理。看了FileChannel操作普通文件,那么可以看一下怎么操作大文件的,记得之前说过这样一个类MappedByteBuffer,使用它即可快速操作,如下:

/**
**MappedByteBuffer示例 操作文件
**/
public static void main(String[] arg0) throws IOException{
        FileChannel randomAccessChannel = new RandomAccessFile("/test/nio1.text", "rw").getChannel();
        FileChannel outputChannel = new FileOutputStream("/test/test.text").getChannel();
        long size = randomAccessChannel.size();
        MappedByteBuffer mapBuffer = randomAccessChannel.map(MapMode.READ_ONLY, 0, size);
        int len = 1024;
        byte[] buf = new byte[len];
        long cycle = size/len;
        while(mapBuffer.hasRemaining()&&cycle>=0){
            if(cycle==0){
                len = (int)size % len;
            }
            mapBuffer.get(buf,0,len);
            System.out.println("--"+new String(buf,0,len));
            cycle--;
        }
    }

如上通过类FileChannel映射文件,会打印出文件中的所有数据。在这里对于速度的提升可能无法看出,但是换一个大文件对比一下普通的ByteBuffer就可以很容易的看出来,如下所示:

/**
**MappedByteBuffer对比普通ByteBuffer
**/
public static void main(String[] arg0) throws IOException{
        FileChannel randomAccessChannel = new RandomAccessFile("/test/cpicgxwx.war", "r").getChannel();
        long size = randomAccessChannel.size();
        long cur = System.currentTimeMillis();
        MappedByteBuffer mapBuffer = randomAccessChannel.map(MapMode.READ_ONLY, 0, size);
        System.out.println("MappedByteBuffer spend "+(System.currentTimeMillis()-cur));
        ByteBuffer buf = ByteBuffer.allocate(1024*1024*150);
        cur = System.currentTimeMillis();
        randomAccessChannel.read(buf);
        System.out.println("ByteBuffer spend "+(System.currentTimeMillis()-cur));
}
输出:
MappedByteBuffer spend 4
ByteBuffer spend 259

这样很明显就看的出来两者的效率,对于一个大于100M的文件,MappedByteBufferByteBuffer效率高了60多倍,更不用说几个G大小的文件了。
这里会不会有疑惑,为什么类MappedByteBuffer可以这么快?其实是因为它并不是读取文件到内存中而是像类名Mapped一样映射文件。当然这个类其实也存在一些问题,如内存占用和文件关闭,被类MappedByteBuffer打开的文件只有在垃圾收集的时候才关闭,而这个垃圾收集的点是不确定的,在网上有人提供了这样一个方法来关闭这个文件映射,如下所示:

/**
**MappedByteBuffer 文件映射 Clear()
**/
public static void main(String[] arg0) throws Exception{
        File file = new File("/test/nio.text");
        RandomAccessFile randomAccessFile = new RandomAccessFile(file,"r");
        FileChannel channel = randomAccessFile.getChannel();
        long size = channel.size();
        long cur = System.currentTimeMillis();
        MappedByteBuffer mapBuffer = channel.map(MapMode.READ_ONLY, 0, size);
        System.out.println("MappedByteBuffer spend "+(System.currentTimeMillis()-cur));
        ByteBuffer buf = ByteBuffer.allocate(1024*1024*150);
        cur = System.currentTimeMillis();
        channel.read(buf);
        System.out.println("ByteBuffer spend "+(System.currentTimeMillis()-cur));
        channel.close();
        randomAccessFile.close();
//      clean(mapBuffer);
        System.out.println(file.delete());
    }
public static void clean(final Object buffer) throws Exception {
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
            try {
               Method getCleanerMethod = buffer.getClass().getMethod("cleaner",new Class[0]);
               getCleanerMethod.setAccessible(true);
               sun.misc.Cleaner cleaner =(sun.misc.Cleaner)getCleanerMethod.invoke(buffer,new Object[0]);
               cleaner.clean();
            } catch(Exception e) {
               e.printStackTrace();
            }
               return null;}});
        
    }
输出:
MappedByteBuffer spend 0
ByteBuffer spend 157
false

如上所示:在注销掉clean(mapBuffer);时,由于系统还持有这个文件的句柄,无法删除文件,导致打印出false,加上这个方法调用后就可以了。除了反射调用sun.misc.Cleaner cleaner外,还可以直接调用,如下:

/**
**sun.misc.Cleaner调用
**/
public static void unmap(MappedByteBuffer buffer){
       sun.misc.Cleaner cleaner = ((DirectBuffer) buffer).cleaner();
       cleaner.clean(); 
    }

这样调用也是可以删除文件的,这样就补足了上面所说的问题。

在java 1.4还加入了文件加锁机制FileLock,它允许我们同步访问某个作为共享资源的文件,这个文件锁直接通过映射本地操作系统的加锁工具,所以对其他操作系统的进程也是可见的。这个文件锁可以通过FileChanneltryLock()lock()方法获取,当然也提供了有参数的方法来加锁文件的一部分,如lock(long position, long size, boolean shared)tryLock(long position, long size, boolean shared),这里参数boolean shared指是否共享锁。这个就不放实例了,感兴趣可以试试。
下面看看其他的网络Channel

SocketChannel

在上一篇就说道NIO的强大功能部分来自它的非阻塞特性,这一点在网络IO中效果表现的更加明显,如对ServerSocketaccept()方法会等待某一个客户端连接而导致阻塞,或者InputStreamread方法阻塞到数据完全读完。一般来说,我们在调用一个方法之前并不知道它是不是会阻塞,但是NIO提供了这样的方法来配置它的阻塞行为,以实现非阻塞信道。

/**
**nio非阻塞信道配置
**/
channel.configureBlocking(false);

非阻塞信道的优势在于它调用的方法都会有一个即时返回,用来指示所请求的操作的完成程度。下面用一个例子来演示,客户端使用NIO非阻塞信道,服务器使用IO实现,如下:

/**
**SocketChannel示例
**/
public static void main(String[] arg0) throws InterruptedException{
        new Thread(){
            public void run() {
                    server();
            }
        }.start();
        new Thread(){
            public void run() {
                try {
                    client();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }.start();
        
        
    }
    public static void client() throws InterruptedException{
        SocketChannel client = null;
        ByteBuffer buf = ByteBuffer.allocate(1024);
        try {
            client = SocketChannel.open();
            client.configureBlocking(false);
            client.connect(new InetSocketAddress("192.168.191.5",8080));
            if(client.finishConnect()){
                int i = 0;
                while(true){
                    Thread.sleep(3000);
                    String test = "test "+i+" from client";
                    i++;
                    buf.clear();
                    buf.put(test.getBytes());
                    buf.flip();
                    while(buf.hasRemaining()){
                        System.out.println(buf);
                        client.write(buf);
                    }
                }
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    
    public static void server(){
        ServerSocket server = null;
        InputStream in = null;
        
        try {
            server = new ServerSocket(8080);
             int recvMsgSize = 0;
             byte[] recvBuf = new byte[1024];
             while(true){
                  System.out.println("mark in server 1");
                  Socket clntSocket = server.accept();
                  System.out.println("mark in server 2");
                  SocketAddress clientAddress = clntSocket.getRemoteSocketAddress();
                  System.out.println("Handling client at "+clientAddress);
                  in = clntSocket.getInputStream();
                  while((recvMsgSize=in.read(recvBuf))!=-1){
                      byte[] temp = new byte[recvMsgSize];
                      System.arraycopy(recvBuf, 0, temp, 0, recvMsgSize);
                      System.out.println(new String(temp));
                  }
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
输出:
mark in server 1
mark in server 2
Handling client at /192.168.191.5:57982
java.nio.HeapByteBuffer[pos=0 lim=18 cap=1024]
test 0 from client
java.nio.HeapByteBuffer[pos=0 lim=18 cap=1024]
test 1 from client
...

在上面的例子中,如果将client方法先启动就会出现只打印mark in server 1,后面就不会打印了,造成这样的情况是因为client.finishConnect()方法返回false直接往程序后面跑了,并不会继续阻塞直到连上服务端,但是换过个顺序让server先启动时,返回true就能连接成功,因为会在accept()方法阻塞,直到有客户端client()连接。这里就可以看到这个非阻塞的特点。

当然不仅仅只有客户端client有这也的非阻塞特性,服务器端也是存在的。

ServerSocketChannel

类似于类SocketChannel,网络IO服务器端的非阻塞特性是通过类ServerSocketChannel来实现的。可以从下面这个例子看出:

/**
**ServerSocketChannel示例
**/
public static void server(){
        ServerSocketChannel server = null;
        try {
            server = ServerSocketChannel.open();
            server.socket().bind(new InetSocketAddress(8080));
            server.configureBlocking(false);
            ByteBuffer buf = ByteBuffer.allocate(1024);
            byte[] bytes = new byte[512];
            System.out.println("--服务器启动-- ");
            while(true){
                  SocketChannel socket = server.accept();
                 while(socket!=null&&socket.isConnected()){
                     buf.clear();
                     int len = socket.read(buf);
                     if(len == -1){
                         socket.close();
                         System.out.println("连接断开");
                     }
                     buf.flip();
                     while(buf.hasRemaining()){
                         buf.get(bytes,0,buf.remaining());
                         System.out.println(new String(bytes));
                     }
                 }
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
输出:
--服务器启动-- 
test 0 from client
test 1 from client
...

这个例子简单的将上一个例子改换了一下(其中的各种异常和资源的操作都没有完善,实际开发请勿使用),服务器如果将 while(socket!=null)换成if(socket!=null),那么这个程序只会打印前面一次从客户端发送过来的数据,连接后只读取了一次。在这里通过int len = socket.read(buf);中的len判断后面是否还会有传来数据,如果数据长度是-1,则关闭连接。这样改过后就是非阻塞的网络IO。

直到这里,都只是使用了前面两个重要的概念BufferChannel,现在可以了解选择器Selector并配合使用。

Selector & SelectionKey

选择器这部分,不仅仅只有类Selector,它还有一个特别重要的类SelectionKey。在之前的文章中简单的了解过这两个类,这里可以回顾一下:要注册Selector则需要这个Channel继承类SelectableChannel,这里只有通道类FileChannel没有继承;注册的Selector实体可以返回一个SelectionKey集合,通过这个集合可以对不同的通道做出相应的操作,这样就避免了传统的网络IO为每一个连接创建一个线程而花费大量的资源,只用一个线程就可以解决问题。Selector管理多个Channel的结构图如下所示:

Java梳理之理解NIO(二)_第1张图片
Selector结构图.png

下面可以看一下结合了选择器类Selector后构建的简单服务器代码:

/**
**Selector 示例,简单服务器
**/
public static void server(){
        System.out.println("--开始启动服务器");
        Selector selector = null;
        ServerSocketChannel server = null;
        try {
            server = ServerSocketChannel.open();            
            server.configureBlocking(false);
            server.socket().bind(new InetSocketAddress(8080));
            System.out.println("--监听8080端口");
            selector = Selector.open();
            server.register(selector,SelectionKey.OP_ACCEPT);
            System.out.println("--服务器已启动成功");
            while(true){
                 int num = selector.select();
                 if(num == 0){
                     continue;
                 }
                 Iterator selectionKeys = selector.selectedKeys().iterator();
                 while(selectionKeys.hasNext()){
                     SelectionKey selectionKey = selectionKeys.next();
                     selectionKeys.remove();
                     if(selectionKey.isAcceptable()){
                         System.out.println("-连接请求:"); 
                         ServerSocketChannel serverSocket = (ServerSocketChannel)selectionKey.channel();
                         SocketChannel socket = serverSocket.accept();
                         socket.configureBlocking(false);
                         socket.register(selector, SelectionKey.OP_READ);
                     }else if(selectionKey.isReadable() && selectionKey.isValid()){
                         System.out.println("-读取:"); 
                         SocketChannel channel = (SocketChannel) selectionKey.channel();
                         ByteBuffer buf = ByteBuffer.allocate(1024);
                         byte[] bytes = new byte[1024];
                         while(channel.isConnected()){
                             buf.clear();
                             int len = channel.read(buf);
                             if(len == -1){
                                 channel.close();
                                 selector.selectNow();
                                 System.out.println("-连接关闭:"+channel.isConnected());
                             }
                             if(len > 0){
                                 channel.write(ByteBuffer.wrap("收到消息啦".getBytes()));
                                 System.out.println(buf+"-buf-length -"+len);
                                 buf.flip();
                                 while(buf.hasRemaining()){
                                     buf.get(bytes, 0, len);
                                     System.out.println(new String(bytes));
                                 }
                             }
                             
                        }
                         
                     }
                 }
                 
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
输出:
--开始启动服务器
--监听8080端口
--服务器已启动成功
-连接请求:
-读取:
java.nio.HeapByteBuffer[pos=18 lim=1024 cap=1024]-buf-length -18
test 0 from client
...

上面的例子中,通过selector注册相应的通道,通过selector获取的selectionkey来做对应的动作。在这个例子一直遇到了异常如ClosedChannelException或者提示远程关闭了一个连接,导致一直很疑惑,因为我明明调用了SelectableChannel.close()方法的。查过资料才知道,关闭一个已经注册的SelectableChannel需要两个步骤:

1.取消注册的key,这个可以通过SelectionKey.cancel方法,也可以通过SelectableChannel.close方法,或者中断阻塞在该channel上的IO操作的线程来做到。
2.后续的Selector.selectXXX()方法的调用才真正地关闭 本地Socket

因此,如果调用了close()方法后没有调用selectXXX()方法,那么本地socket将进入CLOSE-WAIT 状态。就是这个原因造成在buf.read(bytes)时发生CloseChannelException,因为在上面这个例子中我使用了while(channel.isConnected())来进行条件循环,如果转换一下思路,不用while循环,而是把多次传递的信息分成多个Channel来发送,是不是就会好一点。每一次接收的都是新SocketChannel实例,而不在一个实例中循环,造成上面那样的不调用Selector.selectXXX()无法真正关闭连接的问题。
这里的SelectorSelectorKey还有很多细节的地方需要再细细研磨,操作。当然现在也可以选择成熟的NIO框架如Netty使用,以免进入一些不了解的坑中。

DatagramChannel

类似于之前的类SocketChannel,类DatagramChannel处理的也是网络IO,但是它对应的是UDP连接,因为UDP是无连接数据包的网络协议,所以它并不能像其他通道一样读取和写入数据,但是提供了receive()方法和send()方法来使用。如下所示:

/**
**DatagramChannel示例
**/
服务器端
public static void testDatagramChannel(){
        DatagramChannel datagramChannel = null;
        Selector selector = null;
        try {
            selector = Selector.open();
            datagramChannel = DatagramChannel.open();
            datagramChannel.configureBlocking(false);
            datagramChannel.socket().bind(new InetSocketAddress(8080));
            datagramChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("---服务器启动");
            while(true){
                int num = selector.select();
                if(num == 0)continue;
                Iterator selectionKeys = selector.selectedKeys().iterator();
                while(selectionKeys.hasNext()){
                    SelectionKey selectionKey = selectionKeys.next();
                    selectionKeys.remove();
                    if(selectionKey.isReadable()){
                        DatagramChannel channel = (DatagramChannel)selectionKey.channel();
                        ByteBuffer buf = ByteBuffer.allocate(1024);
                        channel.receive(buf);
                        buf.flip();
                        byte[] bytes = new byte[1024];
                        int len = buf.remaining();
                        buf.get(bytes,0,len);
                        String receive = new String(bytes,0,len);
                        System.out.println(receive);
                    }
                    if(selectionKey.isWritable()){
                        System.out.println("--write");
                    }
                }
            }
            
            
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
客户端:
public static void testClient(){
        DatagramChannel datagramChannel = null;
        try {
            System.out.println("--客户端开始");
            datagramChannel = DatagramChannel.open();
            datagramChannel.configureBlocking(false);
            ByteBuffer buf = ByteBuffer.allocate(1024);
            buf.put("这是个Demo".getBytes());
            buf.flip();
            datagramChannel.send(buf, new InetSocketAddress("192.168.191.3", 8080));
            datagramChannel.close();
            System.out.println(buf);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }
输出:
--客户端开始
java.nio.HeapByteBuffer[pos=10 lim=10 cap=1024]
---服务器启动
这是个Demo

在上面这个demo中有几点可以说一下,类DatagramChannel是个注册到Selector中后,这个selector.select()方法是个阻塞方法,只有等新连接进入时才会继续向下执行,并不会因为之前对DatagramChannel设置了非阻塞而使这个方法非阻塞。相对于SocketChannel类来说,变化并不大。

Pipe

看到这个名字,就已经很眼熟了,就像之前的PipedInputStream/PipedOutputStreamPipedReader/PipedWriter,这个类也是实现线程间通信的功能。在上篇文章中也有提到,在这个类中是使用两个静态内部类SourceChannelSinkChannel来实现功能的,代码如下所示:

/**
**Pipe 管道示例
**/
public static void testPipe() throws IOException{
        final Pipe pipe = Pipe.open();
        ExecutorService executor = Executors.newScheduledThreadPool(2);
        executor.submit(new Runnable() {
            
            @Override
            public void run() {
                // TODO Auto-generated method stub
                Pipe.SinkChannel sinkChannel = pipe.sink();
                ByteBuffer buf = ByteBuffer.allocate(1024);
                int i = 0;
                try {
                    while(i<4){

                        Thread.sleep(1000);
                        buf.clear();
                        String text = "Pipe test "+ i;
                        buf.put(text.getBytes());
                        buf.flip();
                        sinkChannel.write(buf);
                        i++;
                    }
                    sinkChannel.close();
                } catch (InterruptedException e1) {
                    // TODO Auto-generated catch block
                    e1.printStackTrace();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        });
        executor.submit(new Runnable() {
            
            @Override
            public void run() {
                // TODO Auto-generated method stub
                Pipe.SourceChannel sourceChannel = pipe.source();
                ByteBuffer buf = ByteBuffer.allocate(1024);
                while(sourceChannel.isOpen()){
                    try {
                        buf.clear();
                        int len = sourceChannel.read(buf);
                        if(len == -1)sourceChannel.close();
                        if(len>0){
                            byte[] bytes = new byte[1024];
                            buf.flip();
                            buf.get(bytes, 0, len);
                            System.out.println(new String(bytes,0,len));
                        }
                        
                    } catch (IOException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
                
            }
        });
        
    }
输出:
Pipe test 0
Pipe test 1
Pipe test 2
Pipe test 3

如上所示,管道Pipe的操作和之前的几个Channel的操作并没有太大的变化,这个类完成线程间的通信靠的是它的两个静态内部类,把握住着一点,其余就需要研究书写细节了。

总的来说,这部分其实很重要,这里也只是先打一点基础,如果想学的更深入的话,可以找找相关的框架进行学习,如Netty。有错误疑惑的地方还请麻烦指出一起学习,对于这部分我也是看了相关内容并没有在实际的工作中用到,很多地方可能并不深入或细节并不完善,还请指出,之后会一一完善。

本文参考
Java NIO 系列教程
攻破JAVA NIO技术壁垒
通俗编程——白话NIO之Selector
NIO的SelectableChannel关闭的一个问题
TCP和UDP的区别(转)

你可能感兴趣的:(Java梳理之理解NIO(二))