NIO的原理介绍和Java示例代码

NIO简介

nio 是New IO 的简称,在jdk1.4 里提供的新api 。Sun 官方标榜的特性如下: 为所有的原始类型提供(Buffer)缓存支持。字符集编码解码解决方案。 Channel :一个新的原始I/O 抽象。 支持锁和内存映射文件的文件访问接口。 提供多路(non-bloking) 非阻塞式的高伸缩性网络I/O 。

传统IO与NIO的区别

这里写图片描述

传统的I/O

使用传统的I/O程序读取文件内容, 并写入到另一个文件(或Socket), 如下程序:

File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);

会有较大的性能开销, 主要表现在一下两方面:
1. 上下文切换(context switch), 此处有4次用户态和内核态的切换
2. Buffer内存开销, 一个是应用程序buffer, 另一个是系统读取buffer以及socket buffer
其运行示意图如下
这里写图片描述
1) 先将文件内容从磁盘中拷贝到操作系统buffer
2) 再从操作系统buffer拷贝到程序应用buffer
3) 从程序buffer拷贝到socket buffer
4) 从socket buffer拷贝到协议引擎.

NIO

NIO技术省去了将操作系统的read buffer拷贝到程序的buffer, 以及从程序buffer拷贝到socket buffer的步骤, 直接将 read buffer 拷贝到 socket buffer。java 的 FileChannel.transferTo() 方法就是这样的实现, 这个实现是依赖于操作系统底层的sendFile()实现的.

public void transferTo(long position, long count, WritableByteChannel target);

他的底层调用的是系统调用sendFile()方法

sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

如下图
这里写图片描述
nio的优势不在于数据传送的速度,而是性能开销更小。

Socket/NIO原理

阻塞与非阻塞

阻塞与非阻塞:阻塞与非阻塞是进程在访问数据的时候,数据内是否准备就绪的一种处理方式。
当数据没有准备好的时候会出现阻塞和非阻塞两种情况。
阻塞:往往需要等待缓冲区中的数据准备好之后才能处理其他的事情,否则一直等待在哪里
非阻塞:当我们的进程访问我们的数据缓冲区的时候,数据没有准备好的时候,直接返回,不需要等待,数据有的时候也直接返回。

同步和异步

同步和异步都是基于应用程序和操作系统处理IO事件所采用的方式:
同步:应用程序要直接参与IO事件的操作;
异步:所有的IO读写事件交给操作系统去处理;

同步的方式在处理IO事件的时候,必须阻塞在某个方法上面等待我们的IO事件完成(阻塞在IO事件或者通过轮询IO事件的方式)了对于异步来说,所有的IO读写操作都交给操作系统,这个时间,我们可以去做其他的时间,并不需要去完成真正的IO操作。当操作系统完成IO之后,给我们的应用程序一个通知就可以了。

同步有两种实现模式:
1.阻塞到IO事件,阻塞到read或者write方法上,这个时候我们就完全不能做自己的事情(在这种情况下,我们只能把读写方法放置到线程中,然后阻塞线程的方式来实现并发服务,对线程的性能开销比较大)

2.IO事件的轮询 ——在linux C语言编程中叫做多路复用技术(select模式)
读写事件交给一个专门的线程去处理,这个线程完成IO事件的注册功能,还有就是不断地去轮询我们的读写缓冲区(操作系统),看是否有数据准备好,然后通知我们的相应的业务去处理线程。这样的话,我们的业务线程就可以去做其他的事情。在这种模式下,阻塞的不是所有的IO线程,而是阻塞的只是select线程。
这里写图片描述

NIO的三大核心

NIO三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)
Buffer:容器对象, 包含一些要写入或者读出的数据。在NIO库,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是写入缓冲区中。任何时候访问NIO的数据,都是通过缓冲区信息操作。
Channel:通道对象,对数据的读取和写入要通过Channel,它就像水管一样。通道不同于流的地方就是通道是双向的,可以同于读、写和同时读写操作。Channel不会直接处理字节数据,而是通过Buffer对象来处理数据。
Selector:多路复用器,选择器。提供选择已经就绪的任务的能力。Selector会不断轮询注册在其上的Channel,如果某个Channel上面发送读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,进行后续的I/O操作。这样服务器只需要一两个线程就可以进行多客户端通信。
这里写图片描述

NIO的Java代码实现

服务端:

package cn.itcast.bigdata.nio;

import java.io.IOException;

public class TimeServer {

    /**
     * @param args
     * @throws IOException
     * @author blackcoder
     */
    public static void main(String[] args) throws IOException {
    int port = 8080;
    if (args != null && args.length<0) {
        try {
        port = Integer.valueOf(args[0]);
        } catch (NumberFormatException e) {
        // 采用默认值
        }
    }
    MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
    new Thread(timeServer, "NIO-MultiplexerTimeServer-001").start();
    }
}
package cn.itcast.bigdata.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;
import java.util.Set;

public class MultiplexerTimeServer implements Runnable {

    private Selector selector;

    private ServerSocketChannel servChannel;

    private volatile boolean stop;

    /**
     * 初始化多路复用器、绑定监听端口
     * 
     * @param port
     */
    public MultiplexerTimeServer(int port) {
    try {
        //打开一个多路复用器
        selector = Selector.open();
        //创建一个socket channel:channel是nio中对通信通道的抽象,部分入站出站方向
        servChannel = ServerSocketChannel.open();
        //设置通道为非阻塞
        servChannel.configureBlocking(false);
        //将通道绑定在服务器的ip地址和某个端口上
        servChannel.socket().bind(new InetSocketAddress(port), 1024);
        //将上面创建好的socket channel注册到selector多路复用器上
        //对于服务端来说,一定要先注册一个OP_ACCEPT事件用来响应客户端的请求连接
        servChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("The time server is start in port : " + port);
    } catch (IOException e) {
        e.printStackTrace();
        System.exit(1);
    }
    }

    public void stop() {
    this.stop = true;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Runnable#run()
     */
    @Override
    public void run() {
    while (!stop) {
        try {
        //去询问一次selector选择器
        selector.select(1000);
        Set<SelectionKey> selectedKeys = selector.selectedKeys();
        Iterator<SelectionKey> it = selectedKeys.iterator();
        SelectionKey key = null;
        while (it.hasNext()) {
            //遍历到一个事件key
            key = it.next();
            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();
        }
    }

    private void handleInput(SelectionKey key) throws IOException {

    if (key.isValid()) {
        // 处理新接入的请求消息
        if (key.isAcceptable()) {
        // Accept the new connection
        ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
        SocketChannel sc = ssc.accept();//接受连接请求
        sc.configureBlocking(false);
        // Add the new connection to the selector
        sc.register(selector, SelectionKey.OP_READ);
        }
        if (key.isReadable()) {
        // Read the data
        SocketChannel sc = (SocketChannel) key.channel();
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        //当客户端channel关闭之后,会不断受到read事件,但没有消息,即read方法返回-1
        //所以这时服务器也需要关闭channel,避免无限处理
        int readBytes = sc.read(readBuffer);
        if (readBytes > 0) {
            //一定需要调用flip函数,否则读取错误数据
            //简单来说,flip操作就是让读写指针、limit指针复位到正确的位置
            readBuffer.flip();
            byte[] bytes = new byte[readBuffer.remaining()];
            readBuffer.get(bytes);
            String body = new String(bytes, "UTF-8");
            System.out.println("The time server receive order : "
                + body);
            String currentTime = "QUERY TIME ORDER"
                .equalsIgnoreCase(body) ? new java.util.Date(
                System.currentTimeMillis()).toString()
                : "BAD ORDER";
            doWrite(sc, currentTime);
        } else if (readBytes < 0) {
            // 对端链路关闭
            key.cancel();
            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);
    }
    }
}

客户端:

package cn.itcast.bigdata.nio;

public class TimeClient {

    /**
     * @param args
     */
    public static void main(String[] args) {

    int port = 8080;
    if (args != null && args.length > 0) {
        try {
        port = Integer.valueOf(args[0]);
        } catch (NumberFormatException e) {
        // 采用默认值
        }
    }
    new Thread(new TimeClientHandle("127.0.0.1", port), "TimeClient-001")
        .start();
    }
}

package cn.itcast.bigdata.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;
import java.util.Set;

public class TimeClientHandle implements Runnable {
    private String host;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;
    private volatile boolean stop;

    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);
    }
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Runnable#run()
     */
    @Override
    public void run() {
    try {
        doConnect();
    } catch (IOException e) {
        e.printStackTrace();
        System.exit(1);
    }
    while (!stop) {
        try {
        selector.select(1000);
        Set<SelectionKey> selectedKeys = selector.selectedKeys();
        Iterator<SelectionKey> it = selectedKeys.iterator();
        SelectionKey key = null;
        while (it.hasNext()) {
            key = it.next();
            it.remove();
            try {
            handleInput(key);
            } catch (Exception e) {
            if (key != null) {
                key.cancel();
                if (key.channel() != null)
                key.channel().close();
            }
            }
        }
        } catch (Exception e) {
        e.printStackTrace();
        System.exit(1);
        }
    }

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


    private void doConnect() throws IOException {
    // 如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答
    if (socketChannel.connect(new InetSocketAddress(host, port))) {
        socketChannel.register(selector, SelectionKey.OP_READ);
        doWrite(socketChannel);
    } else
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
    }

    private void doWrite(SocketChannel sc) throws IOException {
    byte[] req = "QUERY TIME ORDER".getBytes();
    ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
    writeBuffer.put(req);
    writeBuffer.flip();
    sc.write(writeBuffer);
    if (!writeBuffer.hasRemaining())
        System.out.println("Send order 2 server succeed.");
    }


    private void handleInput(SelectionKey key) throws IOException {

    if (key.isValid()) {
        // 判断是否连接成功
        SocketChannel sc = (SocketChannel) key.channel();
        if (key.isConnectable()) {
        if (sc.finishConnect()) {
            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字节,忽略
        }
    }

    }


}

测试:
这里写图片描述

这里写图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值