资讯专栏INFORMATION COLUMN

【J2SE】java NIO 基础学习

yexiaobai / 860人阅读

摘要:标记,表示记录当前的位置。直接缓冲通过方法分配的缓冲区,此缓冲区建立在物理内存中。直接在两个空间中开辟内存空间,创建映射文件,去除了在内核地址空间和用户地址空间中的操作,使得直接通过物理内存传输数据。

NIO与IO的区别
IO NIO
阻塞式 非阻塞式、选择器selectors
面向流:单向流动,直接将数据从一方流向另一方 面向缓存:将数据放到缓存区中进行存取,经通道进行数据的传输
缓冲Buffer

根据数据类型的不同,提供了对应的类型缓冲区(boolean类型除外),每一个Buffer类都是Buffer接口的一个实例。通过Buffer类.allocate()方法获取缓冲区;对缓冲区的数据进行操作可以使用put方法和get方法。

四个核心属性

// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;

capacity:容量,表示缓冲区中最大存储容量,一旦声明不可更改。

limit:界限,表示限制可对缓冲区操作数据的范围,范围外的数据不可被操作。

position:位置,表示当前操作的数据位于缓冲区中的位置。

mark:标记,表示记录当前position的位置。

常用方法(以ByteBuffer为例)

public static ByteBuffer allocateDirect(int capacity):分配一个直接缓冲区

public static ByteBuffer allocate(int capacity):分配一个间接缓冲区

当分配一个缓冲区时,capacity=capacity,mark=-1, position=0, limit=capacity,源码分析如下:

public static ByteBuffer allocate(int capacity) {
    ...
    return new HeapByteBuffer(capacity, capacity);
}

// class HeapByteBuffer extends ByteBuffer
HeapByteBuffer(int cap, int lim) {
    // 调用ByteBuffer的构造函数传入默认参数:mark=-1, position=0, limit=capacity
    super(-1, 0, lim, cap, new byte[cap], 0);
};

// public abstract class ByteBuffer extends Buffer
ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset) {
    super(mark, pos, lim, cap);
    this.hb = hb;            // final byte[] hb; 
    this.offset = offset;    // final int offset;
}

Buffer(int mark, int pos, int lim, int cap) { 
    ...
    this.capacity = cap;
    limit(lim);        //    设置limit
    position(pos);    //    设置position
    if (mark >= 0) {
        ...
        this.mark = mark;
    }
}
public final ByteBuffer put(byte[] src):将一个字节数组放入缓冲区。

每当放置一个字节时,position将会+1,保证position的值就是下一个可插入数据的buffer单元位置。源码分析如下:

public final ByteBuffer put(byte[] src) {
    return put(src, 0, src.length);    
}

// 由allocate方法调用分配缓冲区可知,返回的是Buffer的实现类HeapByteBuffer对象
public ByteBuffer put(byte[] src, int offset, int length) {
    checkBounds(offset, length, src.length);    // 检查是否下标越界
    if (length > remaining())                    // 检查是否超出了可操作的数据范围= limit-position
        throw new BufferOverflowException();
    System.arraycopy(src, offset, hb, ix(position()), length);
    position(position() + length);                // 重设position
    return this;
}
public ByteBuffer get(byte[] dst):从缓冲区中读取数据到 dst中。应在 flip() 方法后调用。

获取数据,是在缓冲区字节数组中的position位置处开始,读取一次完毕后,并会记录当前读取的位置,即position,以便于下一次调用get方法继续读取。

public ByteBuffer get(byte[] dst) {
    return get(dst, 0, dst.length);
}

// 调用HeapByteBuffer对象的get方法
public ByteBuffer get(byte[] dst, int offset, int length) {
    ...
    // 从缓冲区的字节数组final byte[] hb中,拷贝从 hb的 offset+position(注:offset=0) 处的长度为length的数据到 dst中
    System.arraycopy(hb, ix(position()), dst, offset, length);
    position(position() + length);    // 设置position
    return this;
}

通过源码分析可知,当put操作后,position记录的是下一个可用的buffer单元,而get会从position位置处开始获取数据,这显然是无法获得的,因此需要重新设置 position, 即 flip()方法。

public final Buffer flip() :翻转缓冲区,在一个通道读取或PUT操作序列之后,调用此方法以准备一个通道写入或相对获取操作的序列

将此通道的缓冲区的界限设置为当前position,保证了有可操作的数据。

public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}
public final Buffer mark():标记当前position

可用于在put操作转get操作时标记当前的position位置,以便于调用reset方法从该位置继续操作

public final Buffer mark() {
    mark = position;
    return this;
}
public final Buffer reset():回到mark标记的位置
public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}
public final Buffer clear():清除缓冲,重置初始化原始状态
public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}
public final Buffer rewind():倒回,用于重新读取数据
public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}

直接缓冲区与间接缓冲区

间接缓冲:通过allocate方法分配的缓冲区。当程序发起read请求获取磁盘文件时,该文件首先被OS读取到内核地址空间中,并copy一份原始数据传入JVM用户地址空间,再传给应用程序。增加了一个copy操作,导致效率降低。

直接缓冲:通过allocateDirecr方法分配的缓冲区,此缓冲区建立在物理内存中。直接在两个空间中开辟内存空间,创建映射文件,去除了在内核地址空间和用户地址空间中的copy操作,使得直接通过物理内存传输数据。虽然有效提高了效率,但是分配和销毁该缓冲区的成本高于间接缓冲,且对于缓冲区中的数据将交付给OS管理,程序员无法控制。

通道Channel

用于源节点与目标节点之间的连接,负责对缓冲区中的数据提供传输服务。

常用类

​ FileChannel:用于读取、写入、映射和操作文件的通道。

​ SocketChannel:通过 TCP 读写网络中的数据。

​ ServerSocketChannerl:通过 UDP 读写网络中的数据通道。

​ DatagramChannel:通过 UDP 读写网络中的数据通道。

​ 本地IO:FileInputStream、FileOutputStream、RandomAccessFile

​ 网络IO:Socket、ServerSocket、DatagramSocket

获取Channel方式(以FileChannel为例)

​ 1. Files.newByteChannel工具类静态方法

​ 2. getChannel方法:通过对象动态获取,使用间接缓冲区。

FileInputStream fis = new FileInputStream(ORIGINAL_FILE);
FileOutputStream fos = new FileOutputStream(OUTPUT_FILE);

// 获取通道
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();

// 提供缓冲区(间接缓冲区)
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (inChannel.read(buffer) != -1) {
    buffer.flip();
    outChannel.write(buffer);
    buffer.clear();
}

​ 3. 静态open方法:使用open获取到的Channel通道,使用直接缓冲区。

FileChannel inChannel = 
    FileChannel.open(Paths.get(ORIGINAL_FILE), StandardOpenOption.READ);
FileChannel outChannel = 
    FileChannel.open(Paths.get(OUTPUT_FILE), StandardOpenOption.READ,
    StandardOpenOption.CREATE, StandardOpenOption.WRITE);
    
// 使用物理内存 内存映射文件 
MappedByteBuffer inBuffer = 
    inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outBuffer = 
    outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
    
byte[] dst = new byte[inBuffer.limit()];
inBuffer.get(dst);
outBuffer.put(dst);
// 使用DMA 直接存储器存储
inChannel.transferTo(0, inChannel.size(), outChannel);
outChannel.transferFrom(inChannel, 0, inChannel.size());
public static FileChannel open(Path path, OpenOption... options):从path路径中以某种方式获取文件的Channel
StandardOpenOption 描述
CREATE 创建一个新的文件,如果存在,则覆盖。
CREATE_NEW 创建一个新的文件,如果该文件已经存在则失败。
DELETE_ON_CLOSE 关闭时删除。
DSYNC 要求将文件内容的每次更新都与底层存储设备同步写入。
READ 读方式
SPARSE 稀疏文件
SYNC 要求将文件内容或元数据的每次更新都同步写入底层存储设备。
TRUNCATE_EXISTING 如果文件已经存在,并且打开 wirte访问,则其长度将截断为0。
WRITE 写方式
APPEND 如果文件以wirte访问打开,则字节将被写入文件的末尾而不是开头。
public abstract MappedByteBuffer map(MapMode mode, long position, long size):将通道的文件区域映射到内从中。当操作较大的文件时,将数据映射到物理内存中才是值得的,因为映射到内存是需要开销的。
FileChannel.MapMode 描述
PRIVATE 专用映射模式(写入时拷贝)
READ_ONLY 只读模式
READ_WRIT 读写模式
public abstract long transferFrom(ReadableByteChannel src, long position, long count):从给定的可读取通道src,传输到本通道中。直接使用直接存储器(DMA)对数据进行存储。

public abstract long transferTo(long position, long count, WritableByteChannel target):将本通道的文件传输到可写入的target通道中。

分散(Scatter)与聚集(Gather)

​ 分散读取:将通道中的数据分散到多个缓冲区中。 public final long read(ByteBuffer[] dsts)

​ 聚集写入:将多个缓冲区中的数据聚集到一个Channel通道中。public final long write(ByteBuffer[] srcs)

字符集(Charset)

public final ByteBuffer encode(CharBuffer cb):编码

public final CharBuffer decode(ByteBuffer bb):解码

网络通信的阻塞与非阻塞

阻塞是相对网络传输而言的。传统的IO流都是阻塞的,在网络通信中,由于 IO 阻塞,需要为每一个客户端创建一个独立的线程来进行数据传输,性能大大降低;而NIO是非阻塞的,当存在空闲线程时,可以转去操作其他通道,因此不必非要创建一个独立的线程来服务每一个客户端请求。

选择器(Selector)

SelectableChannle对象的多路复用器,可同时对多个SelectableChannle对象的 IO 状态监听,每当创建一个Channel时,就向Selector进行注册,交由Selector进行管理,只有Channel准备就绪时,Selector可会将任务分配给一个或多个线程去执行。Selector可以同时管理多个Channel,是非阻塞 IO 的核心。

NIO 阻塞式

服务器Server不断监听客户端Client的请求,当建立了一个Channel时,服务器进行read操作,接收客户端发送的数据,只有当客户端断开连接close,或者执行shutdownOutput操作时,服务器才知晓没有数据了,否则会一直进行read操作;当客户端在read操作获取服务器的反馈时,若服务器没有关闭连接或者shutdownInput时也会一直阻塞。示例代码如下:

 static final String ORIGINAL_FILE = "F:/1.png";
 static final String OUTPUT_FILE = "F:/2.jpg";
public void server() throws Exception {
    // 打开TCP通道,绑定端口监听
    ServerSocketChannel serverChannel = ServerSocketChannel.open();
    serverChannel.bind(new InetSocketAddress(9988));    
    
    ByteBuffer buf = ByteBuffer.allocate(1024);
    // 获取连接
    SocketChannel accept = null;
    while ((accept= serverChannel.accept()) != null) {
        FileChannel fileChannel = FileChannel.open(
            Paths.get(OUTPUT_FILE), StandardOpenOption.CREATE,
            StandardOpenOption.WRITE);
        
        // 读取客户端的请求数据
        while (accept.read(buf) != -1) {
            buf.flip();
            fileChannel.write(buf);
            buf.clear();
        }
        
        // 发送执行结果
        buf.put("成功接收".getBytes());
        buf.flip();
        accept.write(buf);
        buf.clear();
        
        fileChannel.close();
        // 关闭连接,否则客户端会一直等待读取导致阻塞,可使用shutdownInput,但任务已结束,该close
        accept.close();
    }
    serverChannel.close();
}    
public void client() throws Exception {
    // 打开一个socket通道
    SocketChannel clientChannel = SocketChannel.open(
        new InetSocketAddress("127.0.0.1", 9988));
    // 创建缓冲区和文件传输通道
    FileChannel fileChannel = FileChannel.open(Paths.get(ORIGINAL_FILE),
            StandardOpenOption.READ);
    ByteBuffer buf = ByteBuffer.allocate(1024);
    
    while ( fileChannel.read(buf) != -1) {
        buf.flip();
        clientChannel.write(buf);
        buf.clear();
    }
    
    // 关闭输出(不关闭通道),告知服务器已经发送完毕,去掉下面一行代码服务区将一直读取导致阻塞
    clientChannel.shutdownOutput();
    
    int len = 0;
    while ((len = clientChannel.read(buf)) != -1) {
        buf.flip();
        System.out.println(new String(buf.array(), 0, len));
        buf.clear();
    }
    
    fileChannel.close();
    clientChannel.close();
}

NIO 非阻塞式

通过在通道Channel中调用configureBlocking将blocking设置为false,让Channel可以进行异步 I/O 操作。

public void client() throws Exception {
    // 打开一个socket通道
    SocketChannel clientChannel = SocketChannel.open(
        new InetSocketAddress("127.0.0.1", 9988));
    ByteBuffer buf = ByteBuffer.allocate(1024);

    // 告知服务器,已经发送完毕
    //    clientChannel.shutdownOutput();
    //    设置非阻塞
    clientChannel.configureBlocking(Boolean.FALSE);

    buf.put("哈哈".getBytes());
    buf.flip();
    clientChannel.write(buf);

    clientChannel.close();
}
public void server() throws Exception {
    // 打开TCP通道,绑定端口监听
    ServerSocketChannel serverChannel = ServerSocketChannel.open();
    serverChannel.configureBlocking(Boolean.FALSE);
    serverChannel.bind(new InetSocketAddress(9988));

    //    创建一个Selector用于管理Channel
    Selector selector = Selector.open();
    //    将服务器的Channel注册到selector中,并添加 OP_ACCEPT 事件,让selector监听通道的请求
    serverChannel.register(selector, SelectionKey.OP_ACCEPT);

    // 一直判断是否有已经准备就绪的Channel
    while (selector.select() > 0) {
          // 存在一个已经准备就绪的Channel,获取SelectionKey集合中获取触发该事件的所有key
        Iterator keys = selector.selectedKeys().iterator();
        while (keys.hasNext()) {
            SelectionKey sk = keys.next();
            SocketChannel accept = null;
            ByteBuffer buffer = null;
            // 针对不同的状态进行操作
            if (sk.isAcceptable()) {
                // 可被连接,设置非阻塞并注册到selector中
                accept = serverChannel.accept();
                accept.configureBlocking(Boolean.FALSE);
                accept.register(selector, SelectionKey.OP_READ);
            } else if (sk.isReadable()) {
                // 可读,获取该选择器上的 Channel进行读操作
                accept = (SocketChannel) sk.channel();
                buffer = ByteBuffer.allocate(1024);
                int len = 0;
                while ((len = accept.read(buffer)) != -1) {
                    buffer.flip();
                    System.out.println(new String(buffer.array(), 0, len));
                    buffer.clear();
                }
            }
        }
        // 移除本次操作的SelectionKey
        keys.remove();
    }
    serverChannel.close();
}    

方法使用说明

ServerSocketChannel对象只能注册accept 事件。

设置configureBlocking为false,才能使套接字通道中进行异步 I/O 操作。

调用selectedKeys方法,返回发生了SelectionKey对象的集合。

调用remove方法,用于从SelectionKey集合中移除已经被处理的key,若不处理,那么它将继续以当前的激活事件状态继续存在。

Pipe管道

Channel都是双向通道传输,而Pipe就是为了实现单向管道传送的通道对,有一个source通道(Pipe.SourceChannel)和一个sink通道(Pipe.SinkChannel)。sink用于写数据,source用于读数据。直接使用Pipe.open()获取Pipe对象,操作和FileChannel一样。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73720.html

相关文章

  • Java版本之前世今生

    摘要:原文链接版本之前世今生最全篇语言语言是博士在创建年,被命名为提出了愿景公开版本个包文件,的类文件第一个版本发布在定义为代表技术虚拟机版本发布时间代表技术文件格式内部类反射版本发布时间从开始以后的版本定义为扩展到个包个类版本名称为区分企业平 原文链接:Java版本之前世今生-最全篇 1.Oak 语言 Oak 语言是James Gosling 博士在1991创建 2.JDK Beta 1...

    Neilyo 评论0 收藏0
  • Java语言十五讲(前言)

    摘要:发布史年月日,公司正式发布语言,这一天是的生日。年月日,发布,成为语言发展史上的又一里程碑。年月,发布,三个版本分别改为,,,。年月日,以亿美元收购公司,并取得了的版权。年月日,发布,并改用的命名方式。 特此声明:本文为本人公司郭总原创书籍的前言,该书还未出版,作为该书籍的初版在接下来的时间里,将免费在本人微信公众号内不间断更新与大家一起学习阅读。喜欢学习的小伙伴可以搜索微信公众号:程...

    endless_road 评论0 收藏0
  • Java新手的一些建议——Java知识点归纳(Java基础部分)

    摘要:中很多特性或者说知识点都是和面向对象编程概念相关的。在多线程中内容有很多,只是简单说明一下中初步使用多线程需要掌握的知识点,以后有机会单独再详细介绍一些高级特性的使用场景。   写这篇文章的目的是想总结一下自己这么多年来使用java的一些心得体会,主要是和一些java基础知识点相关的,所以也希望能分享给刚刚入门的Java程序员和打算入Java开发这个行当的准新手们,希望可以给大家一些经...

    lykops 评论0 收藏0
  • java学习(一) —— java概述

    摘要:编译完成后,如果没有报错,那么通过命令对字节码文件进行解释运行,执行时不需要添加后缀总结说白了,整个程序对编写运行有三步编写为后缀对程序文件通过程序文件进行编译生成文件文件名解释运行写代码编译解释运行 前言 最近开始学习下java,毕竟web开发还是java比较完善功能也较php更加强大。学习资料参考:https://github.com/DuGuQiuBai... 此章主要记录下...

    edgardeng 评论0 收藏0
  • 慕课网_《Java生成二维码》学习总结

    摘要:时间年月日星期五说明本文部分内容均来自慕课网。线性堆叠式二维码示意图矩阵式二维码在一个矩形空间通过黑白像素在矩阵中的不同分布进行编码。 时间:2017年06月23日星期五说明:本文部分内容均来自慕课网。@慕课网:http://www.imooc.com教学示例源码:无个人学习源码:https://github.com/zccodere/s... 第一章:二维码的概念 1-1 二维码概述...

    QLQ 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<