首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaNIO快速入门

JavaNIO快速入门

作者头像
用户1216676
发布2018-01-24 16:50:39
1.8K0
发布2018-01-24 16:50:39
举报
文章被收录于专栏:熊二哥熊二哥

NIO是Jdk中非常重要的一个组成部分,基于它的Netty开源框架可以很方便的开发高性能、高可靠性的网络服务器和客户端程序。本文将就其核心基础类型Channel, Buffer, Selector进行详细介绍,之后将介绍内存映射文件,Scatter/Gatter等扩展知识,最后将对Linux的5种IO模型进行剖析。

基础概念

Java NIO(non-blocking IO非阻塞IO)是jdk1.4后提供的新IO API,为所有基础类型都提供类缓存支持。其基础类型Channel定义了一个新的I/O接口,支持锁和访问内存映射文件,提供多路非阻塞式的高伸缩性网络I/O,其与传统IO的区别如下所示。

  1. Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
  2. Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
  3. Selectors(选择器):Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。

核心理解 NIO中的SocketChannel等网络Channel充分利用了操作系统内核对象的调度,只使用少量的几个线程来处理和客户端的所有通信,消除了无谓的线程上下文切换,最大限度的提高了网络通信的性能。可以看到,NIO主要关注文件IO和基于传输层的网络传输IO。对于.NET程序员来说,Windows的完成端口模型和NIO有一些相似之处,具体的IO模型分析请见本文最后部分的五种IO模型使用场景 对于比较时髦的物联网公司,需要收集设备的数据,通常需要自定义相关协议并基于传输层通信。

Channel

Channel通道和传统IO中的Stream流类似,但它是双向的,而流式单向的,如InputStreamOutputStream等,Channel的常见实现如下所示。 FileChannel: 从文件中读写数据。 DatagramChannel: 能通过UDP读写网络中的数据。 SocketChannel: 能通过TCP读写网络中的数据。 ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。 接下里通过一个TCP客户端的NIO实现来熟悉Channel的应用。

public class TCPClient {
    public void start() {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        try (SocketChannel channel = SocketChannel.open()) {
            channel.configureBlocking(false);// 在非阻塞式信道上调用一个方法总是会立即返回
            channel.connect(new InetSocketAddress("127.0.0.1", 8080));

            if (channel.finishConnect()) {
                int i = 0;
                while (true) {
                    TimeUnit.SECONDS.sleep(1);
                    String info = "I'm  " + (i++) + "-th information from client";
                    buffer.clear();
                    buffer.put(info.getBytes());
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        // 问题,这样一个个直接的输出,速度快么?
                        System.out.println(buffer);
                        channel.write(buffer);
                    }
                }
            }
        } catch (Throwable ex) {
            ex.printStackTrace();
        }
    }
}

比对一下传统的IO

try (RandomAccessFile aFile = new RandomAccessFile("src/test/resources/test.xml", "r")) {
    // 1.读数据
    byte[] buffer = new byte[1024];
    StringBuilder requestString = new StringBuilder();
    int bytesRead = 0;
    while (-1 != (bytesRead = aFile.read(buffer))) {
        requestString.append(new String(buffer), 0, bytesRead);
    }
}

Buffer

NIO中主要的Buffer缓存实现包括ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer等,分别对应相应的基础类型,Buffer的主要作用请见下图。

Buffer的数据结构包括一个连续数组,表示缓冲区数组的总长度的capacity,记录下一个要操作的数据元素的位置position,以及limit、mark等字段,接下来请见一个最简单的NIO示例(其中使用到了文件锁和CharBuffer)。

public static void readFile() {
    long timeBegin = System.currentTimeMillis();
    try (RandomAccessFile aFile = new RandomAccessFile("src/nio.txt", "rw")) {
        FileChannel fileChannel = aFile.getChannel();
        ByteBuffer buf = ByteBuffer.allocate(1024);
        Charset charset = Charset.forName("UTF-8");

        int position = 0;
        while (true) {
            // 文件锁,锁一段区域,shard为true表示共享锁
            FileLock fileLock = fileChannel.lock(position, 1024, true);
            int bytesRead = fileChannel.read(buf);
            fileLock.release();
            if (-1 == bytesRead)
                break;
            position = position + bytesRead;
            buf.flip();
            //将ByteBuffer转化为CharBuffer
            System.out.println(charset.decode(buf));
            buf.clear();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    long timeEnd = System.currentTimeMillis();
    System.out.println("Read time: " + (timeEnd - timeBegin) + "ms");
}

Selector

Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。例如,在一个聊天服务器中,要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,如新连接进来,数据接收等。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好,接下来通过TCP服务端的NIO实现来熟悉Selector的应用。

public class TCPServer {
    private static final int BUF_SIZE = 1024;
    private static final int PORT = 8080;
    private static final int TIMEOUT = 3000;
    private TCPServer() {
    }
    public static final TCPServer instance = new TCPServer();
    public static TCPServer getInstance() {
        return instance;
    }

    /*
     * 关键类
     */
    public void selector() {
        try (Selector selector = Selector.open(); ServerSocketChannel ssChannel = ServerSocketChannel.open()) {
            ssChannel.bind(new InetSocketAddress(PORT));
            ssChannel.configureBlocking(false);
            ssChannel.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {
                if (0 == selector.select(TIMEOUT)) {
                    System.out.println("==");
                    continue;
                }
                // 使用 for (SelectionKey key : selector.selectedKeys()) 方式无法移除对象
                // Iterator对象的remove方法是迭代过程中删除元素的唯一方法
                Iterator<SelectionKey> selectKeys = selector.selectedKeys().iterator();
                while (selectKeys.hasNext()) {
                    SelectionKey key = selectKeys.next();
                    //分别处理接受、连接、读、写4中状态
                    if (key.isAcceptable()) {
                        handleAccept(key);
                    }
                    if (key.isReadable()) {
                        handleRead(key);
                    }
                    if (key.isWritable() && key.isValid()) {
                        handleWrite(key);
                    }
                    if (key.isConnectable()) {
                        System.out.println("isConnectable = true");
                    }
                    selectKeys.remove();
                }
            }
        } catch (Throwable ex) {
            ex.printStackTrace();
        }
    }

    public void handleAccept(SelectionKey key) throws IOException {
        ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();// 获取ServerSocket的Channel
        SocketChannel sc = ssChannel.accept();// 监听新进来的链接
        sc.configureBlocking(false);// 设置client链接为非阻塞
        sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(BUF_SIZE));// 将channel注册Selector
    }

    public void handleRead(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();// 获取链接client的channel
        ByteBuffer buf = (ByteBuffer) key.attachment();// 获取附加在key上的数据
        long bytesRead = channel.read(buf);// 读channel上数据到buf?这部分表达的是否正确?
        while (bytesRead > 0) {
            buf.flip();
            while (buf.hasRemaining()) {
                System.out.println(buf.getChar());
            }
            System.out.println();
            buf.clear();
            bytesRead = channel.read(buf);
        }
        if (-1 == bytesRead) {
            channel.close();
        }
    }

    public void handleWrite(SelectionKey key) throws IOException {
        ByteBuffer buf = (ByteBuffer) key.attachment();// 获取buffer对象
        buf.flip();
        SocketChannel channel = (SocketChannel) key.channel();
        while (buf.hasRemaining()) {
            channel.write(buf);
        }
        buf.compact();
    }
}

进阶概念

Java IO与NIO的区别主要包括如下的3个方面。 面向流和面向缓冲:Java IO是面向流的,其意味着在读取所有字节前,数据未被缓存到任何地方,其不能前后移动流中的数据。而NIO是面向缓存的,可以方便在缓存区的移动数据。 阻塞和非阻塞IO:java IO是完全阻塞的,在数据读取或写入完成前,该线程一直被占用。而NIO的非阻塞模式,当线程获取不到数据时,可以被释放用于其他工作。 选择器Selector:其允许一个单独的线程来监视多个出入通道,利于线程的高效利用。

内存映射文件

对于内存映射文件,大家应该不会陌生,大学里学习windows网络编程时就终点介绍过该概念。在JAVA中,一般用BufferedReaderBufferedInputStream的来处理大文件。而当文件超大时推荐使用MappedByteBuffer,其是NIO引入的文件内存映射方案,读写性能极高。一般操作系统的内存包括物理内存和虚拟内存。其中虚拟内存一般使用的是页面映像文件,即硬盘中的某个特殊的文件。操作系统负责该页面文件内容的读写,这个过程叫”页面中断/切换”MappedByteBuffer与其类似,可以看做一个超级大的ByteBuffer,示例如下所示。

public class MappedByteBufferDemo {
    public static void commonReadBigFile() {
        try (RandomAccessFile aFile = new RandomAccessFile("D:/svn.rar", "rw");
                FileChannel fileChannel = aFile.getChannel()) {

            long timeBegin = System.currentTimeMillis();
            ByteBuffer buff = ByteBuffer.allocate((int) aFile.length());
            buff.clear();
            fileChannel.read(buff);
            long timeEnd = System.currentTimeMillis();
            System.out.println("Read time: " + (timeEnd - timeBegin) + "ms");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void highPerformanceReadBigFile() {
        try (RandomAccessFile aFile = new RandomAccessFile("D:/svn.rar", "rw");
                FileChannel fileChannel = aFile.getChannel()) {

            long timeBegin = System.currentTimeMillis();
            MappedByteBuffer mappedBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, aFile.length());
            long timeEnd = System.currentTimeMillis();
            System.out.println("Read time: " + (timeEnd - timeBegin) + "ms");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

通过一个1G的文件做单元测试测试,普通的Buffer读需要1373ms,而内存映射文件只需要1ms,这个测试可能不太准确,但差异已经无比明显。当然使用内存映射文件也存在问题,就是它只要GC时才能被回收,会占用大量内存资源,我所了解的一个使用场景涉及复杂的推荐算法因素和规则的加载(国际机票组合选择),之后进行运算。

其他

Pipe:之前介绍的Channel虽然既可以用作读,也可以用作写,但它每次只能支持一种方式的通讯,即单工/半双工的。而Pipe是全双工的,其内部包含两个Channel,一个是SinkChannel用于写入数据,一个Source通道用于读取数据。 Scatter/Gatter:前者在读操作时将数据从一个Channel中读入到多个Buffer,而后者用于将多个Buffer写入一个Channel,常见的使用场景为将消息头和消息体一起写入到Channel中。 TransferFrom & TransferTo:传输方法用于通道之间的通信,比如FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中,而transferTo()方法将数据从FileChannel传输到其他的channel中。 Java的Path接口是Java NIO2 的一部分,是对Java6 和Java7的 NIO的更新。Java的Path接口在Java7 中被添加到Java NIO,位于java.nio.file包中, 其全路径是java.nio.file.Path。一个Path实例代表了一个文件系统中的路径。一个路径可以指向一个文件或者一个文件夹。一个路径可以是绝对路径或者是相对路径。绝对路径是从根路径开始的全路径,相对路径是一个相对其他路径的文件或文件夹路径。相对路径可能会造成一点混乱,但是不要担心,在本文章中,我会详细解释相对路径的。 DatagramChannel:用于基于UDP协议,用于发送和接受数据包,常用于QQ语音、视频等通讯质量要求不高的场景。

NIO2

Jdk7对NIO做了一些改进,包括对AIO的支持,以及增强型的文件操作类。过去的java.io.File访问文件系统时,无法利用特定文件系统的特性,且性能不高,因此引入了Path,Paths,Files等。对于AIO,其提供了AsynchronousChannelAsynchronousFileChannel等与NIO响应的类型,大大简化了异步IO操作,比如在过去我们需要自己管理线程池来进行Callable的调用返回Future<?>,而现在省去了该步骤,请见下面的示例(该场景下直接使用NIO更合适,AIO适合更加复杂的场景)。

public static void readFile() {
    Path filePath = Paths.get("src/nio.txt");

    long timeBegin = System.currentTimeMillis();
    try (AsynchronousFileChannel afc = AsynchronousFileChannel.open(filePath)) {
        ByteBuffer buf = ByteBuffer.allocate(1024);
        Charset charset = Charset.forName("UTF-8");
        
        int position = 0;
        for (;;) {
            Future<Integer> futureResult = afc.read(buf, position);
            int result = futureResult.get();

            if (-1 == result)
                break;

            position = position + result;
            buf.flip();
            System.out.println(charset.decode(buf));
            buf.clear();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    long timeEnd = System.currentTimeMillis();
    System.out.println("Read time: " + (timeEnd - timeBegin) + "ms");
}

Linux的五种IO模型

通常来说,网络IO模型大致包括如下2大类,其中同步模型包括4种。 1. 同步模型 a.阻塞IO:简称bio,linux中默认所有的socket都是blocking,其特点是进程会一直阻塞,直到数据拷贝完成。 b.非阻塞IO:简称nio,与本文的NIO(New IO)不是一个概念,其特点是进程会反复调用IO函数,并马上返回,但在数据拷贝的过程中,进行仍然是阻塞的。 c.多路复用IO:由于NIO需要大量的轮训会消耗大量CPU时间,而如果这件事有操作内核来通知就好了,这是就会用到Linux的selectpollepoll函数。比如select,其可以对多个IO端口进行监控。 d.信号驱动IO:允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。 2. 异步模型(aio):相对于同步IO,异步IO不是顺序执行。其特点是IO的两个阶段,进程都是非阻塞的。

小结 以上对Linux的IO模型进行了介绍,对应到java程序,那么io包中的操作其实就是阻塞IO的方式,而nio包中的Channel的类型就是非阻塞IO的方式,Selector提供了多路复用的方式,而AsynchronousChannel提供了异步IO的方式。此外,这几种IO模型并没有谁优谁劣的说法,需要结合具体场景进行分析,通常来说,高并发的程序会使用同步非阻塞的方式。如果感觉解释的不是很完善,请见博文聊聊Linux 五种IO模型,该博主通过和女友等餐的过程对5种IO的过程做了很好的诠释,大赞。 Tip: 之后的文章中将对Netty,mina框架进行介绍,其是NIO的封装库。

参考资料 netty官网 Java NIO系列教程 攻破JAVA NIO技术壁垒 完成端口(Completion Port)详解 java nio框架netty 与tomcat的关系 Tomcat7中NIO处理分析(一) NIO文档 高性能IO模型浅析 聊聊同步、异步、阻塞与非阻塞

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-08-09 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基础概念
    • Channel
      • Buffer
        • Selector
          • 内存映射文件
          • 其他
          • NIO2
      • 进阶概念
      • Linux的五种IO模型
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档