NIO入门

  1. NIO入门
    1. Netty底层-NIO
      1. Channel & Buffer
      2. Selector
    2. 引入Netty依赖
      1. ByteBuffer结构
      2. 字符串转换为ByteBuffer
      3. Channel之间的数据传输
    3. 文件编程
      1. Path
      2. Files

NIO入门

Netty网络框架致力于开发高性能的服务器程序、高性能的客户端程序。

Netty底层-NIO

NIO: none-blocking io 非阻塞IO

非阻塞体现在当线程需要去进行IO操作而尚未获取到数据时,并不让线程阻塞,而是让线程继续去执行其它事件

Channel & Buffer

Channel:数据传输通道

Channel是一个对象,作用是用于源节点和目标节点的连接,在javaNIO中负责缓冲区数据的传递,Channel本身不存储数据,因此需要配合缓冲区进行传输

Buffer: 缓冲区,暂存数据的区域

NIO的Buffer(缓冲区)本质上是一个内存块,既可以写入数据,也可以从中读取数据

Java NIO中代表缓冲区的Buffer类是一个抽象类,位于java.nio包中

Selector

Selector中能够检测到一到多个 NIO 通道,并能够知道通道是否为读写

事件做好准备

这样,一个单独的线程可以管理多个 Channel,从而管理多个网络连接

原来的设计一个线程只管理一个socket,这样子会造成线程开销的内存浪费,线程上下文切换成本高,只适合连接数少的场景

因此我们要让一个线程管理多个连接就需要使用Selector,适合连接数多,但是流量低的场景

由于阻塞模式下,线程只能处理一个socket连接,导致了线程利用率不高

引入selector可以实现单线程的非阻塞式多信道管理,实现了多路复用

引入Netty依赖

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.0.23.Final</version>
</dependency>

使用FileChannel

FileChannel只能工作在阻塞模式下,无法和Selector一起用

必须通过FileInputStream,FileOutputStream或者RandomAccessFile来获取FileChannel,它们都有getChannel的方法

  • RandomAccessFile可以指定读写模式,”rw”即可以读写
//fileChannel
        //获取输入流
        try {
            FileChannel channel = new FileInputStream("data.txt").getChannel();

            //准备缓冲区 10bytes Buffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(10);

            while (true){
                //读取channel数据,即写入Buffer
                int read = channel.read(byteBuffer);

                if (read == -1){
                    break;//无内容读取则退出循环
                }

                //打印buffer内容
                //切换到缓冲区读模式
                byteBuffer.flip();

                //检查是否还有数据
                while (byteBuffer.hasRemaining()){
                    //每次读一个字节
                    byte b = byteBuffer.get();
                    System.out.println((char) b);
                }
                
                //切换回写模式
                byteBuffer.clear();
            }

        } catch (IOException e) {

        }

缓冲区使用步骤

1.向Buffer写入数据,如调用channel.read(buffer)

2.调用flip()切换至读模式

3.从buffer读取数据,如调用buffer.get()

4.调用clear()或compact()切换至写模式

5.重复以上步骤,直到读空为止

ByteBuffer结构

属性

  • Capacity 数据容量
  • position 读写指针
  • limit 读写限制

开始Buffer为空

Position处于0,Limit = 容量大小

当写入4个字节后,调用flip,position从4回到0,Limit指向4,Buffer处于读模式

clear调用后,容器回到Buffer为空的写模式状态

compact调用后,则是把未读完的部分向前压缩,然后切换至写模式

常用方法:

  • 分配空间
Bytebuffer buf = ByteBuffer.allocate(16);
  • 读取channel数据到buffer中
int read = channel.read(buf);

buf.put((byte)127);
  • 向channel写入buffer中的数据
int writeBytes = channel.write(buf);
//get方法会使得position指针向后走
//get(int i)会获取索引i的内容,指针不移动
byte b = buf.get();

字符串转换为ByteBuffer

public class StringToByteBuffer {

    public static void main(String[] args) {

        //方法一
        //创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(16);

        //变为字节数组写入数据
        buffer.put("hello".getBytes());

        //方法二,使用标准字符集来编码字符串为byte数组
        ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode("hello");

        //方法三,包装
        ByteBuffer byteBuffer1 = ByteBuffer.wrap("hello".getBytes());

    }

}

使用charset方法后buffer会自动切换到读模式

Scattering Reads

分散读取

channel.read(new ByteBuffer[]{b1,b2,b3});

GatheringWrites

整合写入

channel.write(new ByteBuffer[]{b1,b2,b3});

不需要合并多个buffer,这样子可以减少数据在ByteBuffer之间的拷贝复制,一次性写入

粘包和半包问题

粘包发送数据是因为这样能一次性发送多个包,提高了网络传输的效率

半包现象是由于服务器缓冲区存在大小限制因此必须拆分发送

public static void main(String[] args) {

    ByteBuffer source = ByteBuffer.allocate(32);

    source.put("Hello,world\nI'm zhangsan\nHo".getBytes());
    split(source);
    source.put("w are you?\n".getBytes());
    split(source);
}

private static void split(ByteBuffer source) {
    //切换为写模式
    source.flip();

    //这种解决方案每次都需要匹配换行符,存在效率低下的问题
    for (int i = 0;i < source.limit() ; i++) {
        if (source.get(i) == '\n'){
            int length = i - source.position() + 1;
            ByteBuffer target = ByteBuffer.allocate(length);
            for (int j = 0; j < length; j++) {
                target.put(source.get());
            }
        }
    }

    source.compact();
}

compact():使得未读完的数据前移,切换为写模式

limit():buffer存储写入数据的大小,即limit指针索引

position():当前读取的索引位置,即position指针索引

Channel之间的数据传输

try {
    FileChannel from = new FileInputStream("data.txt").getChannel();
    FileChannel to = new FileOutputStream("to.txt").getChannel();

    //这种方法效率高,底层会利用操作系统的零拷贝进行优化
    from.transferTo(0,from.size(),to);

} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

改进后

try {
    FileChannel from = new FileInputStream("data.txt").getChannel();
    FileChannel to = new FileOutputStream("to.txt").getChannel();

    //这种方法效率高,底层会利用操作系统的零拷贝进行优化,上限为2G数据
    long size = from.size();
    for (long left = size; left > 0;){
        left -= from.transferTo(size - left, left, to);
    }

} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

这种可以支持更大数据的传输

文件编程

Path

jdk7引入了Path和Paths类

  • Path用来表示文件路径
  • Paths是工具类,用来获取Path实例
Path source = Paths.get("1.txt"); //相对路径
Path source = Paths.get("d:\\1.txt"); //绝对路径
Path source = Paths.get("d:/1.txt");

Files

检查文件是否存在

Path path = Paths.get("helloword/data.txt");
System.out.println(Files.exists(path));

创建一级目录

//目录已存在会抛出异常
//只能创建一级目录
Path path = Paths.get("helloword/d1");
Files.createDirectory(path);

创建多级目录

Path path = Paths.get("helloword/d1/d2");
Files.createDirectories(path);

拷贝文件

Path source = Path.get("helloword/data.txt");
Path target = Path.get("helloword/target.txt");

//高效率,使用的是操作系统的底层实现
//文件已存在则抛出异常
Files.copy(source,target);
//覆盖已存在
Files.copy(source,target,StandardCopyOption.REPLACE_EXISTING);

移动文件

//保证文件移动原子性
Files.move(source,target,StandardCopyOption.ATOMIC_MOVE);

删除文件

Path target = Path.get("helloword/target.txt");
//文件不存在则会报出异常
Files.delete(target);

删除目录(空目录)

Path target = Path.get("helloword/d1");

Files.delete(target);

更加方便的删除非空目录方法

walkFileTree访问多级目录

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

        //由于匿名内部类对外部引用都需要复制一份拷贝,为了保持一致性
        //默认为final,因此不能用int类型变量作为计数器
        AtomicInteger dirCount = new AtomicInteger();
        AtomicInteger fileCount = new AtomicInteger();

         //参数一为起点文件位置,参数二为设定的遍历规则,需要重写内部类方法
        Path path = Files.walkFileTree(Paths.get("D://xxx/xxx"), new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                //此处添加逻辑即可
                System.out.println("====>"+dir);
                dirCount.incrementAndGet();
                return super.preVisitDirectory(dir, attrs);
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                //此处添加逻辑即可
                System.out.println(file);

                fileCount.incrementAndGet();

                return super.visitFile(file, attrs);
            }
        });

        System.out.println("dir count:" + dirCount);
        System.out.println("file count:"+ fileCount);

    }

使用了访问者设计模式

相比于我们自己写递归访问更加的方便

删除多级目录

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

        Path path = Files.walkFileTree(Paths.get("D://xxx/xxx"), new SimpleFileVisitor<Path>() {

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.delete(file);
                return super.visitFile(file, attrs);
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                Files.delete(dir);
                return super.postVisitDirectory(dir, exc);
            }

        });

    }

拷贝文件

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

    String source = "D:\\xxx1";
    String target = "D:\\xxx2";

    Files.walk(Paths.get(source)).forEach(path -> {
        try {
            String targetName = path.toString().replace(source,target);

            //是目录
            if (Files.isDirectory(path)){
                Files.createDirectory(Paths.get(targetName));
            }
            //是普通文件
            else if (Files.isRegularFile(path)){
                Files.copy(path,Paths.get(targetName));
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    });

}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以邮件至 1300452403@qq.com

文章标题:NIO入门

字数:2.1k

本文作者:Os467

发布时间:2023-03-17, 21:25:10

最后更新:2023-03-17, 21:26:53

原始链接:https://os467.github.io/2023/03/17/NIO%E5%85%A5%E9%97%A8/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

×

喜欢就点赞,疼爱就打赏