探索Java NIO

什么是NIO?

  java.nio全称java non-blocking IO,是指jdk1.4 及以上版本里提供的新api(New IO),NIO提供了与标准IO不同的IO工作方式。

核心部分:

    • Channels(通道)
    • Buffers(缓冲区)
    • Selectors
    • 除此之外还有组件,像Pipe、FileLock,但这些都是建立在以上三个核心基础之上的。

与传统IO的区别:

  IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

  传统IO的流是阻塞的,这就意味着当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。NIO的非阻塞模式,不是保持线程阻塞,在数据可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

Channel

  在标准的IO当中,都是基于字节流/字符流进行操作的,而在NIO中则是是基于Channel和Buffer进行操作,其中的Channel的虽然模拟了流的概念,类似与流,但并不相同。

  

区别StreamChannel
支持异步 不支持 支持
是否可双向传输数据 不能,只能单向 可以,既可以从通道读取数据,也可以向通道写入数据
是否结合Buffer使用 必须结合Buffer使用
性能 较低 较高

  Channel必须结合着Buffer使用,不能直接往通道里面读写数据

Channel的实现类

    • FileChannel(从文件中读写数据)
    • DatagramChannel(通过UDP读写网络传输的数据)
    • SocketChannel(通过TCP读写网络传输的数据)
    • ServerSocketChannel(可以监听CP连接,对每一个新的连接都会创建一个SocketChannel)

    简单的一个读写文件的实例:

    

RandomAccessFile aFile = new RandomAccessFile("filePath/inFileName", "rw");

        RandomAccessFile outFile = new RandomAccessFile("filePath/outFileName", "rw");
        FileChannel inChannel = aFile.getChannel();
        FileChannel outChannel = outFile.getChannel();
     
        ByteBuffer buf = ByteBuffer.allocate(1024);

        int bytesRead = inChannel.read(buf);
        while (bytesRead != -1) {

            buf.flip();

            while(buf.hasRemaining()){
                outChannel.write(buf);
            }

            buf.clear();
            bytesRead = inChannel.read(buf);
        }

        aFile.close();
    }

    在读取数据中遇到中文乱码问题

Charset charset = Charset.forName("UTF-8");// 创建UTF-8字符集,或者 Charset.forName("GBK");
System.out.print(charset.decode(buf));

数据传输

假如有两个Channel,我们可以直接把Channel1的数据传输给Channel2

主要的方法:

transferFrom(ReadableByteChannel src, long position, long count)

transferTo(long position, long count, WritableByteChannel target)
// 从position为0的位置把大小为Channel1.size()的数据写到Channel2中。
Channel2.transferFrom(Channel1, 0, Channel1.size());

Channel1.transferTo(0, Channel2.size(), Channel2);

 

 

 

Buffer

     缓冲区(Buffer)就是在内存中预留指定字节数的存储空间用来对输入/输出(I/O)的数据作临时存储,这部分预留的内存空间就叫做缓冲区;在Java NIO中,缓冲区的作用也是用来临时存储数据,可以理解为是I/O操作中数据的中转站。缓冲区直接为通道(Channel)服务,写入数据到通道或从通道读取数据,这样的操利用缓冲区数据来传递就可以达到对数据高效处理的目的。在NIO中主要有八种缓冲区类(其中MappedByteBuffer是专门用于内存映射的一种ByteBuffer)

Buffer的基本用法

实际上在Channel的例子中已经体现了Buffer的使用方法:

  1. 创建一个特定长度的Buffer
  2. 读取数据到Buffer
  3. 调用flip()方法,使向Buffer写数据转换为向Buffer读数据
  4. 从Buffer中读取数据
  5. 调用clear()或者compat()方法对缓冲区进行清空

clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

Buffer重要的三个属性

  • 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值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)

Buffer的类型

 

这些Buffer类型代表了不同的数据类型。换句话说,就是可以通过char,short,int,long,float 或 double类型来操作缓冲区中的字节。

MappedByteBuffer

在FileChannel上调用map方法 返回一个MappedByteBuffer对象 

MapMode 有三种Mode:READ_ONLY 、READ_WRITE 、PRIVATE(通过put方法对MappedByteBuffer的修改   不会修改到磁盘文件  只是虚拟内存的修改)

MappedByteBuffer继承与Buffer,在父类的基础上增加了三个方法:

  1. force缓冲区在READ_WRITE模式下,此方法对缓冲区所做的内容更改强制写入文件
  2. load:将缓冲区的内容载入物理内存,并返回该缓冲区的引用
  3. isLoaded:判断缓冲区的内容是否在物理内存,如果在则返回true,否则返回false

MappedByteBuffer的用法和ByteBuffer的用法类似,只是创建有所差别

// 创建一个MappedByteBuffer
MappedByteBuffer mappedByteBuffer = inChannel.map(MapMode.READ_ONLY, 0, fileChannel.size()); 

如果要把数据写入磁盘中需调用force()方法,如果不调用只会更新内存中的值。

 Scatter/Gather

 

分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。
聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。

scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体。

 

scatter样例

// 定义一个ByteBuffer的数组
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = { header, body };
// Buffer从Channel中读取数据
channel.read(bufferArray);

数据在读入到buffer中的时候,优先填满第一个buffer,如果在消息传输中,header的字节小于第一个buffer的(capacity)容量大小,那么就会把body的一部分数据写到header中,破坏数据。因此scatter不适合做动态消息。

gather样例

// 定义一个Byte Buffer数组
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

// 把Buffer中的数据写到Channel中

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);

而这种方式会依次把数组中的ByteBuffer写道Channel中,因此不会打破原始数据。因此gather可用作动态消息的场景中。

Selector(此处内容部分来自于与小菜fly基于NIO的Socket通信

选择器提供选择执行已经就绪的任务的能力.从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Selector 允许单线程处理多个Channel。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销。

选择器执行过程

  1. 创建一个或者多个可选择的通道
  2. 将这些创建的通道注册到选择器对象中
  3. 选择键会记住开发者关心的通道,它们也会追踪对应的通道是否已经就绪
  4. 开发者调用一个选择器对象的select()方法时,相关的键会被更新,用来检查所有被注册到该选择器的通道
  5. 获取一个键的集合,从而找到当时已经就绪的通道,通过遍历这些键,开发者可以选择出每个从上次调用select()开始直到现在已经就绪的通道

一个选择器可以被注册多个Channel(通道),选择器可以轮番从迭代器SelectedKeys获取注册的事件。

服务端和客户端各自维护一个通道调度器(Selector)对象,该对象能检测一个或多个通道 (channel) 上的事件。我们以服务端为例,如果服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。

服务端和客户端的实例

package cn.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

/**
 *
 * @author*/
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
     */
    @SuppressWarnings("unchecked")
    public void listen() throws IOException {
        System.out.println("server start success");
        // 轮询访问selector
        while (true) {
            //当注册的事件到达时,方法返回;否则,该方法会一直阻塞
            selector.select();
            // 获得selector中选中的项的迭代器,选中的项为注册的事件
            Iterator ite = this.selector.selectedKeys().iterator();
            while (ite.hasNext()) {
                SelectionKey key = (SelectionKey) ite.next();
                // 删除已选的key,以防重复处理
                ite.remove();
                // 客户端请求连接事件
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key
                            .channel();
                    // 获得和客户端连接的通道
                    SocketChannel channel = server.accept();
                    // 设置成非阻塞
                    channel.configureBlocking(false);

                    //在这里可以给客户端发送信息哦
                    channel.write(ByteBuffer.wrap(new String("send a message to client").getBytes()));
                    //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
                    channel.register(this.selector, SelectionKey.OP_READ);
                    
                    // 获得了可读的事件
                } else if (key.isReadable()) {
                        read(key);
                }

            }

        }
    }
    /**
     * 处理读取客户端发来的信息 的事件
     * @param key
     * @throws IOException 
     */
    public void read(SelectionKey key) throws IOException{
        // 服务器可读取消息:得到事件发生的Socket通道
        SocketChannel channel = (SocketChannel) key.channel();
        // 创建读取的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(10);
        channel.read(buffer);
        byte[] data = buffer.array();
        String msg = new String(data).trim();
        System.out.println("received message from client:"+msg);
        ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
        channel.write(outBuffer);// 将消息回送给客户端
    }
    
    /**
     * 启动服务端测试
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        NIOServer server = new NIOServer();
        server.initServer(8000);
        server.listen();
    }

}
package com.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

/**
 *
 * @author 14  */
public class NIOClient {
    //通道调度器
    private Selector selector;

    /**
     * 获得一个Socket通道,并对该通道做一些初始化的工作
     * @param ip 连接的服务器的ip
     * @param port  连接的服务器的端口号         
     * @throws IOException
     */
    public void initClient(String ip,int port) throws IOException {
        // 获得一个Socket通道
        SocketChannel channel = SocketChannel.open();
        // 设置通道为非阻塞
        channel.configureBlocking(false);
        // 获得一个通道调度器
        this.selector = Selector.open();
        
        // 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
        //用channel.finishConnect();才能完成连接
        channel.connect(new InetSocketAddress(ip,port));
        //将通道调度器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
        channel.register(selector, SelectionKey.OP_CONNECT);
    }

    /**
     * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
     * @throws IOException
     */
    @SuppressWarnings("unchecked")
    public void listen() throws IOException {
        // 轮询访问selector
        while (true) {
            selector.select();
            // 获得selector中选中的项的迭代器
            Iterator ite = this.selector.selectedKeys().iterator();
            while (ite.hasNext()) {
                SelectionKey key = (SelectionKey) ite.next();
                // 删除已选的key,以防重复处理
                ite.remove();
                // 连接事件发生
                if (key.isConnectable()) {
                    SocketChannel channel = (SocketChannel) key
                            .channel();
                    // 如果正在连接,则完成连接
                    if(channel.isConnectionPending()){
                        channel.finishConnect();
                        
                    }
                    // 设置成非阻塞
                    channel.configureBlocking(false);

                    //在这里可以给服务端发送信息哦
                    channel.write(ByteBuffer.wrap(new String("send a  message to server").getBytes()));
                    //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
                    channel.register(this.selector, SelectionKey.OP_READ);
                    
                    // 获得了可读的事件
                } else if (key.isReadable()) {
                        read(key);
                }

            }

        }
    }
    /**
     * 处理读取服务端发来的信息 的事件
     * @param key
     * @throws IOException 
     */
    public void read(SelectionKey key) throws IOException{
        //和服务端的read方法一样
    }
    
    
    /**
     * 启动客户端测试
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        NIOClient client = new NIOClient();
        client.initClient("localhost",8000);
        client.listen();
    }

}

非阻塞式服务器

参考外国友人搭建的非阻塞式服务器,我把GitHub上的项目fork到我自己的仓库里可供大家学习

地址:https://github.com/yanfzhang/java-nio-server

posted @ 2017-09-23 22:52  bug-zhang  阅读(185)  评论(0编辑  收藏  举报