仓库源文站点原文


title: NIO相关基础篇之JDK toc: true date: 2019-09-25 15:52:54 cover: http://ckimg.baidu.com/course/2017-02/04/fe7122fba4ca2e6071d7e2571373d419.jpg categories: IO基础 tags: [NIO]

description: 本篇讲述了Java NIO的相关知识.

最近在看《Netty In Action》, 发现里面好多东西看不懂, 实际上是Java IO相关的知识太少了! 尤其是Java 1.4之后推出的NIO. 所以在网上搜集了资料, 在这里整理一下关于Java NIO的相关知识.

代码实例: https://github.com/JasonkayZK/Java_Samples/tree/java-nio

<br/>

<!--more-->

NIO相关基础篇之JDK

零. 前言

现在使用NIO的场景越来越多,很多网上的技术框架或多或少的使用NIO技术,譬如Tomcat,Jetty。学习和掌握NIO技术已经不是一个JAVA攻城狮的加分技能,而是一个必备技能!

<br/>

1. Java IO体系

先来自顶向下看一下整个Java的IO体系:

<br/>

2. IO相关主题

<br/>

需要注意的是: <font color="#ff0000">`java.nio.*`包的引入是为了提高速度, 并且旧的IO包已经用nio重新实现过,所以即使你不用nio,也已经收益了!</font>

<br/>


一. 什么是NIO

Java NIO( New IO) 是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同, NIO支持面向缓冲区的、基于通道的IO操作。 NIO将以更加高效的方式进行文件的读写操作。

<br/>

NIO与OIO的主要区别

IO NIO
面向流(Stream Oriented) 面向缓冲区(Buffer Oriented)
阻塞IO(Blocking IO) 非阻塞IO(Non Blocking IO)
(无) 选择器(Selectors)

<br/>


二. 缓冲区: Buffer

通过上面NIO与普通IO的主要区别也可以看到<font color="#ff0000">在基本的IO操作中所有的操作都是基于流进行操作的,而在NIO中所有的操作都是*基于缓冲区*操作的!</font>

<font color="#ff0000">NIO所有的读写操作都是通过缓存区来进行完成,缓冲区(Buffer)是一个线性的、有序的数据集,只能容纳特定的数据类型(基本就是基本数据类型对应的Buffer或者其子类;</font>

<br/>

1. 各数据类型的视图缓存区类

缓存区类 相关描述
ByteBuffer 存储字节的Buffer
CharBuffer 存储字符的Buffer
ShortBuffer 存储短整型的Buffer
IntBuffer 存储整型的Buffer
LongBuffer 存储长整型的Buffer
FloatBuffer 存储单精度浮点型Buffer
DoubleBuffer 存储双精度浮点型Buffer

<br/>

备注:看到上面这几类是不是想起了JAVA的8种基本数据类型,唯一缺少boolean对于的类型。

为什么boolean不需要缓存呢?

根据描述规范中数字的内部表示和存储,boolean所占位数1bit(取值只有true或者false),由于字节(byte)是操作系统和所有I/O设备使用的基本数据类型,所以基本都是以字节或者连续的一段字节来存储表示,所以就没有boolean,感觉也没有必要boolean类型的缓存操作(像RocketMQ源码里面可能把一个Int里面的某位来表示boolean,其他位继续来存储数据.

<font color="#ff0000">ByteBuffer是很底层的类,直接存储未加工的字节</font>

ByteBuffer系列的类继承关系挺有意思,可以研究研究

ByteArrayBuffer是其最通用子类,一般操作的都是ByteArrayBuffer

ByteBuffer.asLongBuffer(), asIntBuffer(), asDoubleBuffer()等一系列

<br/>

字符流:CharBuffer和Charset(byte[]和编码问题)

ByteBuffer是最原始的,其实就是字节流,适用于二进制数据的读写,图片文件等.

但我们更常用的,其实是字符串

例: 获得编码相关的一些信息

package nio.basic.charset;

import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.SortedMap;

public class ShowCharSetDemo {

    public static void main(String[] args) {
        SortedMap<String, Charset> charsets = Charset.availableCharsets();
        Iterator<String> iterator = charsets.keySet().iterator();
        while (iterator.hasNext()) {
            String csName = iterator.next();
            System.out.print(csName);
            Iterator aliases = charsets.get(csName).aliases().iterator();
            if (aliases.hasNext())
                System.out.print(": ");
            while (aliases.hasNext()) {
                System.out.print(aliases.next());
                if (aliases.hasNext())
                    System.out.print(", ");
            }
            System.out.println();
        }
    }
}

输出:

Big5: csBig5
Big5-HKSCS: big5-hkscs, big5hk, Big5_HKSCS, big5hkscs
CESU-8: CESU8, csCESU-8
EUC-JP: csEUCPkdFmtjapanese, x-euc-jp, eucjis, Extended_UNIX_Code_Packed_Format_for_Japanese, euc_jp, eucjp, x-eucjp
EUC-KR: ksc5601-1987, csEUCKR, ksc5601_1987, ksc5601, 5601, euc_kr, ksc_5601, ks_c_5601-1987, euckr
GB18030: gb18030-2000
GB2312: gb2312, euc-cn, x-EUC-CN, euccn, EUC_CN, gb2312-80, gb2312-1980
GBK: CP936, windows-936
IBM-Thai: ibm-838, ibm838, 838, cp838
......

<font color="#ff0000">ByteBuffer.asCharBuffer()的局限:没指定编码,容易乱码</font>

<font color="#ff0000">asCharBuffer()一般情况下不能用: 会把ByteBuffer转为CharBuffer,但用的是系统默认编码</font>

<br/>

字节序

简介:

<br/>

2. Buffer的使用

分配空间

<br/>

读数据

<br/>

写数据

例: 交换相邻的两个字符串

/**
 * 给一个字符串,交换相邻的两个字符
 */
private static void symmetricScramble(CharBuffer buffer) {
    while (buffer.hasRemaining()) {
        buffer.mark();
        char c1 = buffer.get();
        char c2 = buffer.get();
        buffer.reset();
        buffer.put(c2).put(c1);
    }
}

/*
思考:如果没有mark和reset功能,你怎么做?用postion方法记录和恢复刚才位置
*/
private static void symmetricScramble2(CharBuffer buffer) {
    while (buffer.hasRemaining()) {
        int position = buffer.position();
        char c1 = buffer.get();
        char c2 = buffer.get();
        buffer.position(position);
        buffer.put(c2).put(c1);
    }
}

<br/>

3. 缓冲区的基本属性

如图所示为缓冲区的细节图:

Buffer_Structure

<br/>

备注:标记、 位置、 限制、 容量遵守以下不变式: 0 <= position <= limit <= capacity。

<br/>

对应的方法为:

buffer_method

<br/>

为了更形象解释上面重要属性,准备配上简单代码以及图来进行说明就容易懂了。

package nio.basic.buffer;

import java.nio.IntBuffer;

public class BufferModelDemo {

    public static void main(String[] args) {
        //第一步,获取IntBuffer,通过IntBuffer.allocate操作
        IntBuffer buf = IntBuffer.allocate(10) ;    // 准备出10个大小的缓冲区

        //第二步未操作前输出属性值
        System.out.println("position = " + buf.position() + ",limit = " + buf.limit() + ",capacty = " + buf.capacity()) ;

        //第三步进行设置数据
        buf.put(6) ;    // 设置一个数据
        buf.put(16) ;    // 设置二个数据

        //第四步操作后输出属性值
        System.out.println("position = " + buf.position() + ",limit = " + buf.limit() + ",capacty = " + buf.capacity()) ;

        //第五步将Buffer从写模式切换到读模式 postion = 0 ,limit = 原本position
        buf.flip() ;

        //第六步操作后输出属性值
        System.out.println("position = " + buf.position() + ",limit = " + buf.limit() + ",capacty = " + buf.capacity()) ;
    }

}

程序输出结果:

    position = 0,limit = 10,capacty = 10
    position = 2,limit = 10,capacty = 10
    position = 0,limit = 2,capacty = 10

查看下图来进行说明:

buffer_1

buffer_2

buffer_3

buffer_4

<br/>


三. 通道: Channel

通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。Channel 负责传输, Buffer 负责存储。通道是由 java.nio.channels 包定义的。 Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel本身不能直接访问数据, Channel 只能与Buffer 进行交互。

<font color="#ff0000">通道都是依靠操作缓存区完成全部的功能的。</font>

<br/>

1. Java中所有已知 Channel 实现类

常用的有入下几个:

<br/>

2. 获取通道

获取通道的一种方式是<font color="#ff0000">对支持通道的对象调用getChannel() 方法.</font> 支持通道的类如下:

获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道。

<br/>

3. FileChannel实例

为了更形象解释说明的Channel,下面准备以FileChannel的一些简单代码进行说明就容易懂了。准备以FileOutputStream类为准,这两个类都是支持通道操作的。

package nio.basic.channel.fileChannel;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelDemo {

    public static void main(String[] args) {
        String[] info = {"Hello ", "world", " by ", "Java", " NIO"};
        File file = new File("src/testFileChannel.txt");
        FileOutputStream output = null;
        FileChannel fout = null;

        try {
            output = new FileOutputStream(file);
            fout = output.getChannel(); // 得到输出的通道
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            for (String s : info) {
                buffer.put(s.getBytes()); // 字符串变为字节数组放进缓冲区之中
            }
            buffer.flip(); // 切换模式准备输出
            fout.write(buffer); // 输出缓冲区的内容
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fout != null) {
                try {
                    fout.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (output != null) {
                try {
                    output.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

程序最终在src文件夹下输出文件:

Hello world by Java NIO

<br/>

4. 连接通道: TransferTo

nio通过大块数据的移动来加快读写速度,而这个大小都由ByteBuffer来控制, 其实还有方法可以直接将读写两个Channel相连.

这也是<font color="#ff0000">实现文件复制的更好的方法</font>

public class TransferTo {
    public static void main(String[] args) throws Exception {
        if (args.length != 2) {
            System.out.println("arguments: sourcefile destfile");
            System.exit(1);
        }
        FileChannel in = new FileInputStream(args[0]).getChannel(), out = new FileOutputStream(
                args[1]).getChannel();
        in.transferTo(0, in.size(), out);
        // 或者:
        // out.transferFrom(in, 0, in.size());
    }
}

<br/>


四. 文件锁: FileLock

文件锁和其他我们了解并发里面的锁很多概念类似,<font color="#ff0000">当多个人同时操作一个文件的时候,只有第一个人可以进行编辑,其他要么关闭(等第一个人操作完成之后可以操作),要么以只读的方式进行打开。</font>

在java nio中提供了新的锁文件功能,<font color="#ff0000">当一个线程将文件锁定之后,其他线程无法操作此文件,文件的锁操作是使用FileLock类来进行完成的,此类对象需要依赖FileChannel进行实例化。</font>

1. 文件锁的两种方式

<font color="#ff0000">文件锁定以整个 Java 虚拟机来保持。但它们**不适用于**控制同一虚拟机内多个线程对文件的访问。多个并发线程可安全地使用文件锁定对象。</font>

<br/>

2. Java文件依赖FileChannel的主要涉及的方法

方法 说明
lock() 获取对此通道的文件的<font color="#0000ff">独占锁定</font>
lock(long position, long size, boolean shared) 获取此通道的文件<font color="#0000ff">给定区域上的锁定</font>
tryLock() throws IOException 试图获取对此通道的文件的独占锁定
tryLock(long position, long size, boolean shared) throws IOException 试图获取对此通道的文件给定区域的锁定

<br/>

3. lock()和tryLock()的区别

例如:

package nio.basic.filelock;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

public class FileLockDemo {

    public static void main(String[] args) {
        File file = new File("src/testFileChannel.txt");
        FileOutputStream output = null;
        FileChannel fout = null;

        try {
            output = new FileOutputStream(file, true);
            fout = output.getChannel(); // 得到通道

            FileLock lock = fout.tryLock(); // 进行独占锁的操作
            if (lock != null) {
                System.out.println(file.getName() + "文件锁定") ;
                Thread.sleep(5000) ;
                lock.release() ;    // 释放
                System.out.println(file.getName() + "文件解除锁定。") ;
            }
        } catch (InterruptedException | IOException e) {
            e.printStackTrace();
        } finally {
            if(fout!=null){
                try {
                    fout.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(output!=null) {
                try {
                    output.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

结果:

testFileChannel.txt文件锁定
testFileChannel.txt文件解除锁定。

<br/>


五. 选择器: Selector

说明:

以前的Socket程序是阻塞的,服务器必须始终等待客户端的连接,而NIO可以通过Selector完成非阻塞操作。

<font color="#ff0000">其实NIO主要的功能是解决服务端的通讯性能。</font>

1. Selector一些主要方法

方法 说明
open() 打开一个选择器。
select() 选择一组键,其相应的通道已为 I/O 操作准备就绪。
selectedKeys() 返回此选择器的已选择键集。

<br/>

2. SelectionKey的四个重要常量

字段 说明
OP_ACCEPT 用于套接字接受操作的操作集位。
OP_CONNECT 用于套接字连接操作的操作集位。
OP_READ 用于读取操作的操作集位。
OP_WRITE 用于写入操作的操作集位。

说明:<font color="#ff0000">其实四个常量就是Selector监听SocketChannel四种不同类型的事件。</font>

如果你对不止一种事件感兴趣,那么可以用"位或"操作符将常量连接起来,如下: int interestSet = SelectionKey.OPREAD | SelectionKey.OPWRITE;

NIO简单实例

服务端:

package nio.basic.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
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.Date;
import java.util.Iterator;
import java.util.Set;

public class Server {

    public static void main(String[] args) throws IOException {
        int port = 8848;
        // 通过open()方法找到Selector
        Selector selector = Selector.open();
        // 打开服务器的通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 服务器配置为非阻塞
        serverSocketChannel.configureBlocking(false);
        ServerSocket serverSocket = serverSocketChannel.socket();
        // 进行服务的绑定
        serverSocket.bind(new InetSocketAddress(port));
        // 注册到selector,等待连接
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务器运行,端口:" + port);

        // 数据缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        while (true) {
            if (selector.select() > 0) { // 选择一组键,并且相应的通道已经准备就绪
                Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 取出全部生成的key
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next(); // 取出每一个key
                    if (key.isAcceptable()) {
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        // 接收新连接 和BIO写法类是都是accept
                        SocketChannel client = server.accept();
                        // 配置为非阻塞
                        client.configureBlocking(false);
                        byteBuffer.clear();
                        // 向缓冲区中设置内容
                        byteBuffer.put(("当前的时间为:" + new Date()).getBytes());
                        byteBuffer.flip();
                        // 输出内容
                        client.write(byteBuffer);
                        client.register(selector, SelectionKey.OP_READ);
                    } else if (key.isReadable() && key.isValid()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        byteBuffer.clear();
                        // 读取内容到缓冲区中
                        int readSize = client.read(byteBuffer);
                        if (readSize > 0) {
                            System.out.println("服务器端接受客户端数据:" + new String(byteBuffer.array(), 0, readSize));
                            client.register(selector, SelectionKey.OP_WRITE);
                        }
                    } else if (key.isWritable() && key.isValid()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        byteBuffer.clear();
                        // 向缓冲区中设置内容
                        byteBuffer.put(("向客户端发送消息").getBytes());
                        byteBuffer.flip();
                        // 输出内容
                        client.write(byteBuffer);
                        client.register(selector, SelectionKey.OP_READ);
                    }
                }
                selectionKeys.clear(); // 清除全部的key
            }
        }
    }
}

客户端:

package nio.basic.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.Date;
import java.util.Iterator;
import java.util.Set;

public class Client {

    public static void main(String[] args) throws IOException {
        // 打开socket通道
        SocketChannel socketChannel = SocketChannel.open();
        // 设置为非阻塞方式
        socketChannel.configureBlocking(false);
        // 通过open()方法找到Selector
        Selector selector = Selector.open();
        // 注册连接服务端socket动作
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        // 连接
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8848));

        /* 数据缓冲区 */
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        while (true) {
            if ((selector.select()) > 0) { // 选择一组键,并且相应的通道已经准备就绪
                Set<SelectionKey> selectedKeys = selector.selectedKeys();// 取出全部生成的key
                Iterator<SelectionKey> iter = selectedKeys.iterator();
                while (iter.hasNext()) {
                    SelectionKey key = iter.next(); // 取出每一个key
                    if (key.isConnectable()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        if (client.isConnectionPending()) {
                            client.finishConnect();
                            byteBuffer.clear();
                            // 向缓冲区中设置内容
                            byteBuffer.put(("isConnect,当前的时间为:" + new Date()).getBytes());
                            byteBuffer.flip();
                            // 输出内容
                            client.write(byteBuffer);
                        }
                        client.register(selector, SelectionKey.OP_READ);
                    } else if (key.isReadable() && key.isValid()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        byteBuffer.clear();
                        // 读取内容到缓冲区中
                        int readSize = client.read(byteBuffer);
                        if (readSize > 0) {
                            System.out.println("客户端接受服务器端数据:" + new String(byteBuffer.array(), 0, readSize));
                            client.register(selector, SelectionKey.OP_WRITE);
                        }
                    } else if (key.isWritable() && key.isValid()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        byteBuffer.clear();
                        // 向缓冲区中设置内容
                        byteBuffer.put(("nio文章学习很多!").getBytes());
                        byteBuffer.flip();
                        // 输出内容
                        client.write(byteBuffer);
                        client.register(selector, SelectionKey.OP_READ);
                    }
                }
                selectedKeys.clear(); // 清楚全部的key
            }

        }

    }
}

运行结果:

# Server
服务器运行,端口:8848
服务器端接受客户端数据:isConnect,当前的时间为:Thu Sep 26 10:21:06 CST 2019
服务器端接受客户端数据:nio文章学习很多!
服务器端接受客户端数据:nio文章学习很多!
服务器端接受客户端数据:nio文章学习很多!
服务器端接受客户端数据:nio文章学习很多!
服务器端接受客户端数据:nio文章学习很多!
.......

# Client
客户端接受服务器端数据:当前的时间为:Thu Sep 26 10:21:06 CST 2019
客户端接受服务器端数据:向客户端发送消息
客户端接受服务器端数据:向客户端发送消息
客户端接受服务器端数据:向客户端发送消息
客户端接受服务器端数据:向客户端发送消息
客户端接受服务器端数据:向客户端发送消息
客户端接受服务器端数据:向客户端发送消息
客户端接受服务器端数据:向客户端发送消息
客户端接受服务器端数据:向客户端发送消息
.......

上面仅仅是一个demo,其实使用nio开发复杂度很高的,需要考虑:

这些都是很复杂,那里搞不好就容易出错,而很多问题netty已经帮我们解决了,所以有必要好好看看netty了!

<br/>


六. 简单聊几句AIO

虽然NIO在网络操作中提供了非阻塞方法,但是NIO的IO行为还是同步的,对于NIO来说,我们的业务线程是在IO操作准备好时,才得到通知,接着就有这个线程自行完成IO操作,但是IO操作的本身其实还是同步的。

AIO是异步IO的缩写,相对与NIO来说又进了一步,它不是在IO准备好时再通知线程,而是在IO操作完成后在通知线程,所以AIO是完全不阻塞的,我们的业务逻辑看起来就像一个回调函数了。

<br/>


七. 总结

本文从Java IO体系开始, 自顶向下讲述了与IO相关的主题, 之后主要讲述了:

<br/>


附录

文章参考:

代码实例: https://github.com/JasonkayZK/Java_Samples/tree/java-nio