JAVA NIO:NIO与OIO的对比以及Channel通道、Selector选择器、Buffer缓冲区的介绍 //高并发

博文所在专栏里有更多相关内容,如IO模型概述、Reactor反应器模式等,欢迎阅读与交流。
文字来源于读书笔记及个人心得,可能有引用其他博文,若引用了你的文字请联系我,我会加上来源,或者删除相关内容。

二 Java NIO

在1.4之前,Java IO类库是阻塞IO,从1.4开始,才引入了新的异步IO类库,即Java New IO,简称Java NIO。相对的,1.4之前称为Old IO,OIO。

(一)NIO对比OIO

NIOOIO
面向缓冲区:只要从channel读取数据到buffer(读取),就可以读取buffer的任意位置。(写入:从buffer写到channel)面向流:面向字节流或字符流,按顺序读取字节
非阻塞:内核缓冲区准备数据时,用户read操作都不会阻塞用户线程阻塞:整个IO过程都会阻塞用户线程
有选择器概念:基于底层操作系统的选择器的系统调用,系统开销更小,因为不必为每个网络连接创建线程无选择器概念

(二)概述三个核心组件

Channel通道

在Java NIO中,同一个网络连接(文件描述符)用一个通道表示,所有Java NIO的IO操作都从通道开始,即可从通道读取,也可向通道写入。

对比OIO,同个网络连接需要关联输入流和输出流共两个流。

Selector选择器

用于NIO通道的注册,以及查询就绪状态的IO事件。一个选择器可监控/管理多个通道,比起OIO系统开销更小,因为不必为每个网络连接创建线程。

Buffer缓冲区

本质是一个内存块(数组),用于应用程序和通道的交互,如通道的读取:将数据从通道读取到缓冲区;通道的写入:将数据从缓冲区写入到通道。

(三)Buffer详解

1 Buffer类

Buffer类是个抽象类,有8个子类:ByteBuffer、ShortBuffer、CharBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、MappedByteBuffer,前7种覆盖了所有能在IO传输的Java基本数据类型,MappedByteBuffer专门用于内存映射。

2 四个属性

(1)capacity:容量,buffer内部byte[]数据内存块的容量,该容量不是该数组可写入的字节容量,而是可写入的对象数量,如DoubleBuffer写入的对象时double类型,capacity为10时表示可写入10个double数据。

(2)position:位置,缓冲区中下一个要被读/写的元素的索引。不同模式的position最大值不同:

写模式(通道-缓冲区):初始值0,最大可写值为limit-1,position=limit表示缓冲区已无位置可写;

读模式:初始值0,最大可读值为limit-1,position=limit表示缓冲区已无可读数(我看的书说的是,“读模式下最大可读值是limit“,但position=limit时已经没有数据可读了,所以我认为正确的说法是,读模式limit最大可取值是limit,但最大可读值是limit-1)

(3)limit:上限,缓冲区中当前的数据量

(4)mark:用于临时存储当前position

3 重要方法

(1)allocate()创建缓冲区:

IntBuffer ib=IntBuffer.allocate(5);
System.out.println("position:"+ib.position());
System.out.println("limit:"+ib.limit());
System.out.println("capacity:"+ib.capacity());
System.out.println("------");

缓冲区初创建时为写入模式。

(2)put()写入到缓冲区:

for(int i=0;i<5;i++){
    ib.put(i);
}

(3)flip()切换写模式为读模式:

设置limit为当前position、重置position为0、清除之前的mark标记

ib.flip();

(4)get()从缓冲区获取:

System.out.println(ib.get());

如果position=limit,即缓冲区数据读完了,此时若想进行写入,需调用clear()或compact()

(5)rewind()倒带重读:

重置position为0、清除之前的mark标记——与flip只差了对limit的处理,rewind不修改limit

ib.rewind();
System.out.println("-----after rewind-----");
System.out.println("position:"+ib.position());
System.out.println("limit:"+ib.limit());
System.out.println("capacity:"+ib.capacity());

(6)mark()和reset()

mark()表示暂存当前position值至mark属性,reset()将mark值恢复到position。

for(int i=0;ib.position()<ib.limit();i++){
    if(i==2) {
        ib.mark();
        System.out.println("mark");
    }
    if(i==4) {
        ib.reset();
        System.out.println("reset");
    }
    System.out.println(ib.get());
    System.out.println("position:"+ib.position());
    System.out.println("limit:"+ib.limit());
    System.out.println("capacity:"+ib.capacity());
    System.out.println("------");
}

(7)clear()清空缓冲区/compact()压缩缓冲区

在缓冲区处于读模式时调用clear或compact,缓冲区会切换为写模式,不同在于clear会重置position为0,而compact会将所有未读元素复制到缓冲区起始处,并将position设置为未读元素的后一位,如有一个维度元素,则compact后position为1。

(四)Channel详解

Java NIO中一个连接就是一个channel,也就是一个文件描述符。而对于不同的网络传输协议类型在java中有不同的NIO channel实现。这里着重聚焦于以下四种:

  • FileChannel文件通道:用于文件的数据读写
  • SocketChannel套接字通道:用于Socket套接字TCP连接
  • ServerSocketChannel服务器嵌套字通道(或服务器监听通道):允许我们监听TCP请求并为监听到的请求创建一个SocketChannel套接字通道
  • DatagramChannel数据报通道:用于UDP协议的数据读写

1 FileChannel文件通道

用于从文件读取数据或者向文件写入数据。只有阻塞模式,无法设置为非阻塞

(1)获取FileChannel通道:通过文件流获取,或通过文件随机访问类获取。文件流、文件随机访问类、通道,都需要手动关闭

RandomAccessFile infile=new RandomAccessFile("C:\\Desktop\\消息累加器.png","rw");
FileChannel inChannel=infile.getChannel();
RandomAccessFile outfile=new RandomAccessFile("C:\\Desktop\\消息累加器-copy.png","rw");
FileChannel outChannel=outfile.getChannel();

(2)read(buf)读取通道(对通道来说是读取,对缓冲区来说是写入)

ByteBuffer buffer=ByteBuffer.allocate(1024);
int i=-1;
while((i=inChannel.read(buffer))!=-1){
    //......
}

(3)write(buf)写入通道(对缓冲区来说是读取,需要切换为读模式)

while((i=inChannel.read(buffer))!=-1){
    //第一次切换:翻转buf,切换为读模式
    buffer.flip();
    int outLength=0;
    //将buf写入到输出通道
    while((outLength= outChannel.write(buffer))!=0);
    //第二次切换:清除buf,切换为写模式
    buffer.clear();
}

(4)force()强制刷新到磁盘

在调用该方法之前,出于性能原因,操作系统可能会将数据缓存在内存中,而该方法能将所有未写入的数据从通道刷新到磁盘中。

outChannel.force(true);

(5)close关闭通道

channel.close();

(6)使用FileChannel完成文件复制的实例

public void FileChannelTest(){
    FileInputStream fis=null;
    FileChannel inChannel=null;
    FileOutputStream fos=null;
    FileChannel outChannel=null;
    try {
        fis=new FileInputStream("C:\\Users\\Simon\\Desktop\\消息累加器.png");
        inChannel=fis.getChannel();
        fos=new FileOutputStream("C:\\Users\\Simon\\Desktop\\消息累加器-copy.png");
        outChannel=fos.getChannel();
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        int i=-1;
        while((i=inChannel.read(buffer))!=-1){
    		//第一次切换:翻转buf,切换为读模式
            buffer.flip();
            int outLength=0;
   			//将buf写入到输出通道
            while((outLength= outChannel.write(buffer))!=0);
    		//第二次切换:清除buf,切换为写模式
            buffer.clear();
        }
        //强制刷新到磁盘
        outChannel.force(true);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }finally{
        IOUtils.close(outChannel);
        IOUtils.close(fos);
        IOUtils.close(inChannel);
        IOUtils.close(fis);
    }
}

2 SocketChannel套接字通道

Java NIO中涉及网络连接的两个通道:

  • SocketChannel负责连接的数据传输,对应OIO的Socket类,应用于服务端和客户端
  • ServerSocketChannel负责连接的监听,对应OIO的ServerSocket类,应用于服务端

这两种通道都支持阻塞和非阻塞两种模式,调用configureBlocking(false)设置为非阻塞模式,传参true设置为阻塞模式。

示例

//该示例为运行过
@Test
public void socketChannelTest(){
    try {
        //1.获得套接字传输通道
        SocketChannel socketChannel=SocketChannel.open();
        //设置为非阻塞模式
        socketChannel.configureBlocking(false);
        //连接目标服务端的ip和端口
        socketChannel.connect(new InetSocketAddress("127.0.0.1",80));
        //不停自旋,直到连接真正建立起来
        while (!socketChannel.finishConnect()){

        }

        //2.从套接字通道读取数据(需通道可读才行,通过Selector选择器判断是否可读)
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
        //read方法是异步的。返回读取到的字节数,若返回-1表示对方已输出结束
        int bytesRead=socketChannel.read(byteBuffer);

        //3.写入套接字通道
        //写入通道需读取缓冲区,因此现将缓冲区置为读模式
        byteBuffer.flip();
        socketChannel.write(byteBuffer);

        //4.关闭套接字通道
        //如果当前套接字通道是用来写入数据给对方服务器的,建议调用以下方法终止输出,向对方发送一个输出结束标志(-1)
        socketChannel.shutdownOutput();
        //调用close()关闭连接
        IOUtils.close(socketChannel);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

3 DatagramChannel数据报通道

用于处理UDP的数据传输,使用UDP时,只需要知道服务器的IP和端口就可以传输数据

//未运行过
public void datagramChannelTest(){
    try {
        //1.获取数据报通道
        DatagramChannel channel=DatagramChannel.open();
        //设置为非阻塞模式
        channel.configureBlocking(false);
        //如果要接收数据,需绑定一个数据报监听接口
        channel.bind(new InetSocketAddress(18080));

        //2.从数据报中读取数据,通过Selector判断通道是否可读
        //创建缓冲区
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
        //从数据报通道读入,并写入缓冲区,返回数据发送端的连接地址(包括IP和端口)
        SocketAddress clientAddr=channel.receive(byteBuffer);

        //3.写入数据报通道
        byteBuffer.flip();
        channel.send(byteBuffer,new InetSocketAddress("127.0.0.1",18899));
        byteBuffer.clear();

        //4.关闭数据报通道
        IOUtils.close(channel);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

(五)Selector选择器

简单说,选择器的使命是完成IO多路复用。

一般一个单线程处理一个选择器,一个选择器可监控多个通道的IO状况。

只有继承了抽象类SelectableChannel才能被选择/监控,例如FileChannel就不行。

选择器使用流程:

1、获取选择器示例

Selector selector=Selector.open();

2.将通道注册到选择器

channel.register(selector,SelectionKey.OP_ACCEPT);

channel.register(selector,SelectionKey.OP_ACCEPT|SelectionKey.OP_WRITE)

通道对象调用register方法完成在选择器上的注册,其中参数一是选择器对象,参数二是监听的IO事件类型(状态)。

监控同个通道的多个事件,使用“按位或”运算符“|”。

注册到选择器的通道必须是非阻塞模式,否则会抛出异常

四种IO事件类型:
  • SelectionKey.OP_ACCEPT:接收就绪,某个Channel成功连接到另一个服务器
  • SelectionKey.OP_CONNECT:连接就绪,某个ServerSocketChannel准备好接收新进入的连接
  • SelectionKey.OP_READ,读就绪,一个有数据可读的通道
  • SelectionKey.OP_WRITE:写就绪,等待写数据的通道
SelectableChannel可选择通道

只有继承了SelectableChannel抽象类的通道才能被选择器监控/选择

validOps()

一个通道不一定支持全部四种IO事件

如ServerSocketChannel只支持OP_ACCEPT,而SocketChannel不支持OP_ACCEPT,

可通过channel.validOps()获取该通道支持的IO事件集合

SelectionKey选择键

即被选择器选中的指定状态下的通道。

3.选出感兴趣的IO就绪事件(选择键集合)
Select()

选择器的Select()方法可选出已注册且已就绪的IO事件,存储到selectedKeys中,并返回被选中的通道的数量(通道数,不是事件数,准确说是上次select到这次select之间发生了就绪IO事件的通道数)。该方法为阻塞调用,会阻塞直到有已注册且已就绪的事件。

Select(long timeout)

与Select()类似,但指定了最长阻塞时间(毫秒数)

SelectNow()

与Select()类似,但非阻塞,不管有无就绪IO事件都立即返回

selectedKeys()

返回已注册且已就绪的选择键集合。

每个Selector内部都有两个SelectionKey集合(Set),一个是keys,存储该选择器的所有选择键,一个是selectedKeys,存储该选择器已注册且已就绪的选择键,通过选择键可以获得通道、通道的事件类型、选出该通道的选择键实例等。

这两个SelectionKey集合都是线程不安全的,都不可以直接手动添加元素进去,selectedKeys可以执行remove操作,keys不行。

4.判断选择键IO就绪类型

key.isAcceptable()、key.isConnectable()、key.isReadable()、key.isWritable()

其他
selector的wakeup()

用于唤醒阻塞的select()方法。

其他线程如果因为调用了selector.select()或者selector.select(long)这两个方法而阻塞,调用了selector.wakeup()之后,就会立即返回结果,并且返回的值!=0;
如果当前Selector没有阻塞在select方法上,那么本次 wakeup调用会在下一次select阻塞的时候生效。

SelectionKey的interestOps(int)

修改指定选择键的监听IO事件类型

示例

public void selectorTest(){
    try {
        //创建一个监听通道
        ServerSocketChannel channel=ServerSocketChannel.open();
        channel.configureBlocking(false);
        channel.bind(new InetSocketAddress("127.0.0.1",8091));

        //1.获取选择器实例
        Selector selector=Selector.open();
        //2.将通道注册到选择器
        //注册到选择器的通道必须是非阻塞模式,否则会抛出异常
        //一个通道不一定支持全部四种IO事件,如ServerSocketChannel只支持OP_ACCEPT,而SocketChannel不支持OP_ACCEPT
        //可通过channel.validOps()获取该通道支持的IO事件集合
        //监控同个通道的多个事件,使用“按位或”运算符“|”,如register(selector,SelectionKey.OP_ACCEPT|SelectionKey.OP_WRITE)
        channel.register(selector,SelectionKey.OP_ACCEPT);
        //3.选出感兴趣的IO就绪事件(选择键集合)
        while(selector.select()>0){
            //选择器集合不可添加元素,否则会报错
            Set<SelectionKey> selectionKeys=selector.selectedKeys();
            Iterator<SelectionKey> keyIterator=selectionKeys.iterator();
            while(keyIterator.hasNext()){
                SelectionKey key=keyIterator.next();
                //根据具体的IO事件类型,执行对应的业务操作
                if(key.isAcceptable()){
                    //IO事件:ServerSocketChannel服务器监听通道有新连接
                    //获取这个新连接(通道)
                    SocketChannel socketChannel=channel.accept();
                    socketChannel.configureBlocking(false);
                    //若该通道为数据输入通道,可使用可读事件注册到选择器
                    socketChannel.register(selector,SelectionKey.OP_READ);
                }else if(key.isConnectable()){
                    //IO事件:传输通道连接成功
                }else if(key.isReadable()){
                    //IO事件:传输通道可读
                    //获取这个新连接(通道)
                    SocketChannel socketChannel= (SocketChannel) key.channel();
                    ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
                    int length=0;
                    while((length=socketChannel.read(byteBuffer))>0){
                        byteBuffer.flip();
                        System.out.println(new String(byteBuffer.array(),0,length));
                        byteBuffer.clear();
                    }
                    socketChannel.close();
                }else if(key.isWritable()){
                    //IO事件:传输通道可写
                }
                //处理完成后,移除选择键,以免被下次循环重复处理
                keyIterator.remove();
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值