Java NIO ---------Channel,Buffer,Selector

一.前言

java nio 是java new io 的意思,但是因为它是增加了非阻塞的特性,很多人又把它称作java non-blocking io。

在这阵子学习nio的过程中,主要是理解了nio工作的整体流程,三个核心API:Channel(通道),Buffer(缓冲区),Selector(多路复用器/选择器)的用法,以及FileChannel,SocketChannel,ServerSocketChannel,DatagramChannel等具体子类的用法。

学习了上面这些api的基本用法是远远不够的,https://github.com/jjenkov/java-nio-server里面有一个nio实现非阻塞服务器的源码,值得看一下。

注:本文是我看完http://tutorials.jenkov.com/java-nio/index.html系列教程后的总结。

二.Overview

Typically, all IO in NIO starts with a Channel. A Channel is a bit like a stream. From the Channel data can be read into a Buffer. Data can also be written from a Buffer into a Channel. Here is an illustration of that:
Java NIO ---------Channel,Buffer,Selector_第1张图片
Then you call it’s select() method. This method will block until there is an event ready for one of the registered channels. Once the method returns, the thread can then process these events. Examples of events are incoming connection, data received etc.
Java NIO ---------Channel,Buffer,Selector_第2张图片

三.Channel

Java NIO Channels are similar to streams with a few differences:

You can both read and write to a Channels. Streams are typically one-way (read or write).
Channels can be read and written asynchronously.
Channels always read to, or write from, a Buffer.

implementations:

FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
The FileChannel reads data from and to files.

The DatagramChannel can read and write data over the network via UDP.

The SocketChannel can read and write data over the network via TCP.

The ServerSocketChannel allows you to listen for incoming TCP connections, like a web server does. For each incoming connection a SocketChannel is created.

Demo

public class ChannelDemo {
    public static void main(String[] args) {
        Charset charset = Charset.forName("GBK");
        CharsetDecoder decoder = charset.newDecoder();
        try {
            RandomAccessFile radom = new RandomAccessFile("C:\\t.txt", "rw");
            FileChannel channel = radom.getChannel();         //文件中有获取通道的方法
            ByteBuffer byteBuffer = ByteBuffer.allocate(200); //用allocate()方法实例化一个字节缓冲块
            CharBuffer charBuffer = CharBuffer.allocate(200); //read方法无法直接使用CharBuffer,所以自行转码
            int readBytes = channel.read(byteBuffer);
            System.out.println((char) 65);
            while (readBytes != -1) {
                System.out.println("读到" + readBytes + "个字节");
                byteBuffer.flip();                           //将byteBuffer由写模式转换为读模式
                decoder.decode(byteBuffer, charBuffer, false);
                charBuffer.flip();                           //charBuffer也要转为读模式,不然position=0,limit=200,是不会读出来的
                System.out.println(charBuffer);
                byteBuffer.clear();                          //清空缓冲区
                charBuffer.clear();
                readBytes = channel.read(byteBuffer);          //再次读
            }
            radom.close();
            channel.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

四.Buffer

Using a Buffer to read and write data typically follows this little 4-step process:

Write data into the Buffer
Call buffer.flip()
Read data out of the Buffer
Call buffer.clear() or buffer.compact()

Java NIO comes with the following Buffer types:

ByteBuffer
MappedByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
As you can see, these Buffer types represent different data types. In other words, they let you work with the bytes in the buffer as char, short, int, long, float or double instead.

The MappedByteBuffer is a bit special, and will be covered in its own text.

五.Scatter/Gather

http://ifeve.com/java-nio-scattergather/

六.SocketChannel,ServerSocketChannel,Selector简单时间服务器

TimeServer:

public class TimeServer {
    public static void main(String[] args) {
        int port = 8383;
        MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
        new Thread(timeServer,"NIO-MultiplexerTimeServer-001").start();
    }
}

MultiplexerTimeServer:

public class MultiplexerTimeServer implements Runnable {
    private Selector selector;    //多路复用器,Selector(选择器)是Java
    // NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
    private ServerSocketChannel serverChannel; //A selectable channel for stream-oriented listening sockets.
    //volatile是一个类型修饰符(type specifier),就像大家更熟悉的const一样,它是被设计用来修饰被不同线程访问和修改的变量。
    // volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
    // volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。
    private volatile boolean stop;

    /**
     * 初始化多路复用器,绑定监听端口
     *
     * @param port
     */
    public MultiplexerTimeServer(int port) {
        try {
            selector = Selector.open();
            serverChannel = ServerSocketChannel.open();
            serverChannel.configureBlocking(false);     //设置连接为非阻塞模式
            serverChannel.socket().bind(new InetSocketAddress(port), 1024);//设置监听端口
            //@param backlog  requested maximum length of the queue of incoming connections.
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);//将ServerSocketChannel注册到多路复用器Selector上,监听ACCEPT事件
            System.out.println("The server start in port:" + port);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);//exit(0)是正常退出,exit(1)或者exit(非零)为非正常退出
        }
    }

    /**
     * 停止
     */
    public void stop() {
        this.stop = true;
    }

    @Override
    public void run() {
        //一直轮询
        while (!stop) {
            try {
                selector.select(1000);//无论是否有读写事件发生,selector每隔秒就巡查一遍
                Set selectionKeys = selector.selectedKeys();//Set集合,里面的元素独一无二,获取key集合
                Iterator it = selectionKeys.iterator();     //迭代器
                SelectionKey key = null;
                //遍历选择器,有元素就取出来,进行操作,并移除它
                while (it.hasNext()) {         //如果没有元素会返回false,不会经过下面的循环
                    key = it.next();
                    System.out.println("选择到一个元素");
                    it.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null) {
                                key.channel().close();
                            }
                        }
                    }
                }

            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
        //多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
        if (selector != null) {
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 操作多路复用器里面的key
     *
     * @param key
     */
    private void handleInput(SelectionKey key) throws IOException {
        //key是否有效,有效才能进行操作
        if (key.isValid()) {
            //处理新接入的请求信息
            System.out.println("key 有效");
            //key的通道是否已准备好接受新的套接字连接。
            if (key.isAcceptable()) {
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();//将key的channel强转为ServerSocketChannel类型
                SocketChannel sc = ssc.accept();    //从ServerSocketChannel中接收连接
                System.out.println("接收到连接" + sc.getRemoteAddress().toString());
                sc.configureBlocking(false);        //把连接设置为非阻塞模式
                sc.register(selector, SelectionKey.OP_READ);   //把连接注册到多路复用器中,并让其监听读操作,因为要读取客户端发来的命令
            }
            if (key.isReadable()) { //key的通道是否准备好进行读操作
                //Read the data
                SocketChannel sc = (SocketChannel) key.channel();
                System.out.println(sc.socket().getInetAddress() + "连接通道可读");
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                if (readBytes > 0) {
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("The time server recieve order:" + body);
                    String currentTime = "QUERY".equalsIgnoreCase(body) ? new java.util.Date(System.currentTimeMillis())
                            .toString
                                    () : "BAD ORDER";
                    doWrite(sc, currentTime);
                } else if (readBytes < 0) {
                    //对端链路关闭
                    key.channel();
                    sc.close();
                } else {
                    ;//读到0字节,忽略
                }
            }
        }
    }

    private void doWrite(SocketChannel channel, String response) throws IOException {
        if (response != null && response.trim().length() > 0) {
            byte[] bytes = response.getBytes();
            ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            channel.write(writeBuffer);
        }
    }

}

TimeClient:

public class TimeClient {
    private static SocketChannel socketChannel;

    public static void main(String[] args) {
        int port = 8383;
        new Thread(new TimeClientHandle("127.0.0.1", port), "TimeClient--001").start();
    }
}

TimeClientHandle:

public class TimeClientHandle implements Runnable {

    private String host;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;
    private volatile boolean stop;

    /**
     * 初始化
     *
     * @param host
     * @param port
     */
    public TimeClientHandle(String host, int port) {
        this.host = host == null ? "127.0.0.1" : host;
        this.port = port;
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    @Override
    public void run() {
        try {
            doConnect();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
        while (!stop) {
            try {
                selector.select(1000);
                Set selectionKeys = selector.selectedKeys();
                Iterator iterator = selectionKeys.iterator();
                SelectionKey key = null;
                while (iterator.hasNext()) {
                    key = iterator.next();
                    iterator.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        System.out.println("出错了" + e.getMessage());
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null) {
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
                System.exit(1);
            }
        }
        //多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册关闭,所以不需要重复释放资源
        if (selector != null) {
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void handleInput(SelectionKey key) throws IOException {
        if (key.isValid()) {
            //判断连接是否成功
            SocketChannel sc = (SocketChannel) key.channel();

            if (key.isConnectable()) {
                sc.register(selector, SelectionKey.OP_READ);
                doWrite(sc);
            } else {
                System.exit(1);//连接失败,进程退出
            }

            if (key.isReadable()) {
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                if (readBytes > 0) {
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("Now is:" + body);
                    this.stop = true;
                } else if (readBytes < 0) {
                    //对端链路关闭
                    key.cancel();
                    sc.close();
                } else {
                    //0字节,忽略
                }
            }
        }
    }

    private void doConnect() throws IOException {
        //如果直接连接成功,则注册到多路复用器上,发送请求信息,读应答
        socketChannel.connect(new InetSocketAddress(host, port));
        while (true){
            if(socketChannel.finishConnect()){
                System.out.println("连接服务器成功");
                socketChannel.register(selector, SelectionKey.OP_READ);
                doWrite(socketChannel);
                break;
            }else{
                socketChannel.connect(new InetSocketAddress(host, port));
            }
        }
    }

    private void doWrite(SocketChannel sc) throws IOException {
        byte[] req = "QUERY".getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
        writeBuffer.put(req);
        writeBuffer.flip();
        System.out.println("通道是否开启:" + sc.isOpen());
        sc.write(writeBuffer);
        if (!writeBuffer.hasRemaining()) {
            System.out.println("Send order to server succeed.");
        }
    }
}

六.总结

java nio比同步阻塞bio要复杂的多,但它的应用越来越广泛,因为它具有以下优点:

(1)客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像之前的客户端一样被同步阻塞。

(2)SocketChannel的读写操作是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他的链路,不需要同步等待这个链路可用。

(3)线程模型的优化:由于JDK的Selector在Linux等主流操作系统上通过epoll()实现,它没有连接句柄数的限制(只受限于操作系统的最大句柄数或者单个进程的句柄限制),这意味着一个Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降。因此,它非常适合做高性能,高负载的网络服务器。

你可能感兴趣的:(java,JAVA语言基础)