NIOChannel(通道)

举报
xcc-2022 发表于 2022/07/21 22:17:13 2022/07/21
【摘要】 一、Channel 基本介绍(1)NIO 的通道类似于流,但有些区别如下:  ① 通道可以同时进行读写,而流只能读或者只能写;  ② 通道可以实现异步读写数据;  ③ 通道可以从缓冲读数据,也可以写数据到缓存;(2)BIO 中的 stream 是单向的, 例如 FileInputStream 对象只能进行读取数据的操作, 而 NIO 中的通道(Channel)是双向的, 可以读操作, 也可以...

一、Channel 基本介绍

(1)NIO 的通道类似于流,但有些区别如下:

  ① 通道可以同时进行读写,而流只能读或者只能写;

  ② 通道可以实现异步读写数据;

  ③ 通道可以从缓冲读数据,也可以写数据到缓存;

(2)BIO 中的 stream 是单向的, 例如 FileInputStream 对象只能进行读取数据的操作, 而 NIO 中的通道(Channel)是双向的, 可以读操作, 也可以写操作。

(3)Channel在NIO中是一个接口

 

(4)常用的 Channel 类有: FileChannel、DatagramChannel、 ServerSocketChannel  SocketChannel

【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket】如下示意图:

(5)FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP的数据读写。

二、FileChannel 类

FileChannel主要用来对本地文件进行 IO 操作, 常见的方法有

(1)public int read(ByteBuffer dst)从通道读取数据并放到缓冲区中

  (2)public int write(ByteBuffer src)把缓冲区的数据写到通道中

  (3)public long transferFrom(ReadableByteChannel src, long position, long count)从目标通道中复制数据到当前通道

  (4)public long transferTo(long position, long count, WritableByteChannel target), 把数据从当前通道复制给目标通道

三、Channel 案例

1、案例一:本地文件写数据

(1)使用前面学习后的ByteBuffer(缓冲) 和 FileChannel(通道), 将 "hello,中国" 写入到file01.txt 中

(2)文件不存在就创建

    实现:

public class NIOFileChannel01 {
    public static void main(String[] args) throws Exception {

        String str = "Hello,中国";

        //创建一个输出流->channel
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");

        //通过 fileOutputStream 获取 对应的 FileChannel
        //这个 fileChannel 真实 类型是  FileChannelImpl
        FileChannel fileChannel = fileOutputStream.getChannel();

        //创建一个缓冲区 ByteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //将 str 放入 byteBuffer
        byteBuffer.put(str.getBytes());

        //对byteBuffer 进行flip
        byteBuffer.flip();

        //将byteBuffer 数据写入到 fileChannel
        fileChannel.write(byteBuffer);

        //关闭资源
        fileOutputStream.close();
    }
}

2、案例二:本地文件读数据

(1)使用前面学习后的ByteBuffer(缓冲) 和 FileChannel(通道), 将 file01.txt 中的数据读入到程序, 并显示在控制台屏幕

(2)假定文件已经存在

实现:

public class NIOFileChannel02 {
    public static void main(String[] args) throws Exception{

        //创建文件的输入流
        File file = new File("d:\\file01.txt");
        FileInputStream fileInputStream = new FileInputStream(file);

        //通过fileInputStream 获取对应的FileChannel -> 实际类型  FileChannelImpl
        FileChannel fileChannel = fileInputStream.getChannel();

        //创建缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(512);

        //将通道中的数据读到 Buffer
        fileChannel.read(byteBuffer);

        //将byteBuffer 的 字节数据 转成String
        System.out.println(new String(byteBuffer.array()));

        //关闭资源
        fileInputStream.close();
    }
}

3、案例三:使用一个Buffer完成文件读取

(1)使用 FileChannel(通道) 和 方法 read , write, 完成文件的拷贝

(2)拷贝一个文本文件 1.txt , 放在项目下即可

实现:

public class NIOFileChannel03 {

    public static void main(String[] args) throws Exception {

        FileInputStream fileInputStream = new FileInputStream("1.txt");
        FileChannel fileChannel01 = fileInputStream.getChannel();

        FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
        FileChannel fileChannel02 = fileOutputStream.getChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate(512);

        //循环读取
        while (true) {

            //这里有一个重要的操作,一定不要忘了!!!
            /*
             public final Buffer clear() {
                position = 0;
                limit = capacity;
                mark = -1;
                return this;
            }
             */
            //清空buffer,重置操作位,并没有删除数组内容
            byteBuffer.clear();

            int read = fileChannel01.read(byteBuffer);
            System.out.println("read =" + read);
            //表示读完
            if (read == -1) {
                break;
            }

            //将buffer 中的数据写入到 fileChannel02 -- 2.txt
            byteBuffer.flip();
            fileChannel02.write(byteBuffer);
        }

        //关闭资源
        fileInputStream.close();
        fileOutputStream.close();
    }
}

  注意:这里一定要注意 clear 操作,因为使用的是一个 buffer来进行读取的,第一次读取完毕后 limit = position,这时已经无法进行写操作了。

4、案例四:拷贝文件 transferFrom 方法

(1)使用 FileChannel(通道) 和 方法 transferFrom , 完成文件的拷贝

(2)拷贝一张图片

实现:

public class NIOFileChannel04 {

    public static void main(String[] args) throws Exception{

        //创建相关流
        FileInputStream fileInputStream = new FileInputStream("d:\\a.jpg");
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\b.jpg");

        //获取各个流对应的filechannel
        FileChannel sourceCh = fileInputStream.getChannel();
        FileChannel destCh = fileOutputStream.getChannel();

        //使用transferForm完成拷贝
        destCh.transferFrom(sourceCh, 0,sourceCh.size());

        //关闭资源
        sourceCh.close();
        sourceCh.close();
        fileInputStream.close();
        fileOutputStream.close();

    }
}

四、关于Buffer 和 Channel 的注意事项和细节

1、ByteBuffer 支持类型化的put 和 get, put 放入的是什么数据类型, get就应该使用相应的数据类型来取出, 否则可能有 BufferUnderflowException 异常。

示例:

public class NIOByteBufferPutGet {

    public static void main(String[] args) {

        //创建一个Buffer
        ByteBuffer buffer = ByteBuffer.allocate(64);

        //类型化方式放入数据
        buffer.putInt(100);
        buffer.putLong(9);
        buffer.putChar('尚');
        buffer.putShort((short) 4);

        //取出
        buffer.flip();

        System.out.println();

        System.out.println(buffer.getInt());
        System.out.println(buffer.getLong());
        System.out.println(buffer.getChar());
        System.out.println(buffer.getShort());

    }
}

  如果把最后一个 getShort 替换成 getLong 就会抛出异常。

2、可以将一个普通Buffer 转成只读Buffer

示例:

public class ReadOnlyByteBuffer {

    public static void main(String[] args) {
        //创建一个buffer
        ByteBuffer buffer = ByteBuffer.allocate(64);

        for(int i = 0; i < 64; i++) {
            buffer.put((byte)i);
        }

        //准备读取
        buffer.flip();

        //得到一个只读的Buffer
        ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
        System.out.println(readOnlyBuffer.getClass());

        while (readOnlyBuffer.hasRemaining()) {
            System.out.println(readOnlyBuffer.get());
        }

        //ReadOnlyBufferException
        readOnlyBuffer.put((byte)100);
    }
}

如果对 readOnlyBuffer进行 put 操作就会抛出异常。

3、NIO 还提供了 MappedByteBuffer, 可以让文件直接在内存(堆外的内存) 中进行修改, 而如何同步到文件由NIO 来完成。

示例:

/*
说明
1. MappedByteBuffer 可让文件直接在内存(堆外内存)修改, 操作系统不需要拷贝一次
 */
public class MappedByteBufferTest {

    public static void main(String[] args) throws Exception {
        RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
        //获取对应的通道
        FileChannel channel = randomAccessFile.getChannel();

        /**
         * 参数1: FileChannel.MapMode.READ_WRITE 使用的读写模式
         * 参数2: 0 : 可以直接修改的起始位置
         * 参数3:  5: 是映射到内存的大小(不是索引位置) ,即将 1.txt 的多少个字节映射到内存
         * 可以直接修改的范围就是 0-5
         * 实际类型 DirectByteBuffer
         */
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);

        mappedByteBuffer.put(0, (byte) 'H');
        mappedByteBuffer.put(3, (byte) '9');
        mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException

        randomAccessFile.close();
        System.out.println("修改成功~~");
    }
}

这里的5是映射到内存的大小,不是索引位置,如果在操作第5个位置就会抛出异常:

4、前面我们讲的读写操作, 都是通过一个Buffer 完成的, NIO 还支持 通过多个Buffer (即 Buffer 数组) 完成读写操作, 即 Scattering 和 Gathering

示例:Buffer 的分散和聚集

/**
 * Scattering:将数据写入到buffer时,可以采用buffer数组,依次写入  [分散]
 * Gathering: 从buffer读取数据时,可以采用buffer数组,依次读
 */
public class ScatteringAndGatheringTest {
    public static void main(String[] args) throws Exception{

        //使用 ServerSocketChannel 和 SocketChannel 网络
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);

        //绑定端口到socket ,并启动
        serverSocketChannel.socket().bind(inetSocketAddress);

        //创建buffer数组
        ByteBuffer[] byteBuffers = new ByteBuffer[2];
        byteBuffers[0] = ByteBuffer.allocate(5);
        byteBuffers[1] = ByteBuffer.allocate(3);

        //等待客户端连接(telnet)
        SocketChannel socketChannel = serverSocketChannel.accept();

        //假定从客户端接受8个字节
        int messageLength = 8;

        //循环的读取
        while (true) {

            int byteRead = 0;

            while (byteRead < messageLength) {
                long r = socketChannel.read(byteBuffers);
                System.out.println("r = " + r);
                //累计读取的字节数
                byteRead += 1;
                System.out.println("byteRead = " + byteRead);
                //使用流打印, 看看当前的这个buffer的position 和 limit
                Arrays.asList(byteBuffers)
                        .stream()
                        .map(byteBuffer -> "position=" + byteBuffer.position() + ", limit=" + byteBuffer.limit())
                        .forEach(System.out::println);
            }

            //将所有的buffer进行flip
            Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.flip());

            //将数据读出显示到客户端
            int byteWrite = 0;
            while (byteWrite < messageLength) {
                long w = socketChannel.write(byteBuffers);
                byteWrite += 1;
                System.out.println("byteWrite = " + byteWrite);
            }

            //将所有的buffer 进行clear
            Arrays.asList(byteBuffers).forEach(buffer -> {
                buffer.clear();
            });

            System.out.println("byteRead:=" + byteRead + " byteWrite=" + byteWrite + ", messagelength=" + messageLength);
        }
    }

}
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。