【Netty】第一章 NIO 三大组件、ByteBuffer 和文件编程
创始人
2024-05-29 13:26:37
0

【Netty】第一章 NIO 三大组件、ByteBuffer 和文件编程

文章目录

  • 【Netty】第一章 NIO 三大组件、ByteBuffer 和文件编程
  • 一、Channel & Buffer
  • 二、Selector
  • 三、ByteBuffer
    • 1.ByteBuffer 使用方式
    • 2.ByteBuffer 结构
    • 3.ByteBuffer 常用方法
    • 4.Scattering Reads
    • 4.Gathering Write
    • 5.黏包半包
  • 四、文件编程
    • 1.FileChannel
    • 2.两个 Channel 传输数据
    • 3.Path
    • 4.Files

一、Channel & Buffer

channel 类似于 stream,它是读写数据的双向通道,可以从 channel 将数据读入 buffer,也可以将 buffer 的数据写入 channel,而之前的 stream 要么是输入,要么是输出,channel 比 stream 更为底层

常见的 Channel 有:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

常见的 buffer 有:

  • ByteBuffer
    • MappedByteBuffer
    • DirectByteBuffer
    • HeapByteBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • CharBuffer

二、Selector

多线程
此前,服务器每接收到一个客户端的请求就需要开启一个线程建立 socket 连接,这样会有很多弊端:

  • 内存占用高(Windows 下默认一个线程占用 1MB)
  • 线程上下文切换成本高
  • 只适合连接数少的场景

线程池
固定线程池,线程数量是固定的,当请求太多时,那么这些请求会被阻塞,直到有 socket 断开线程空闲下来

  • 阻塞模式下,线程仅能处理一个 socket 连接
  • 仅适合短连接场景

selector
selector 的作用就是配合一个线程来管理多个 channel,获取这些 channel 上发生的事件,这些 channel 工作在非阻塞模式下,不会让线程吊死在一个 channel 上。适合连接数特别多,但流量低的场景。

调用 selector 的 select() 方法会阻塞知道 channel 发生了读写就绪事件,这些事件发生,select() 方法就会返回这些事件交给 thread 来处理

三、ByteBuffer

1.ByteBuffer 使用方式

  1. 向 buffer 写入数据,例如调用 channel.read(buffer)
  2. 调用 flip() 切换至读模式
  3. 从 buffer 读取数据,例如调用 buffer.get()
  4. 调用 clear() 或 compact() 切换至写模式
  5. 重复 1~4 步骤

2.ByteBuffer 结构

ByteBuffer 有以下重要属性:

  • capacity
  • position
  • limit

初始状态下
在这里插入图片描述
写模式下
在这里插入图片描述
调用 flip() 后,position 切换为读取位置,limit 切换为为读取限制
在这里插入图片描述

3.ByteBuffer 常用方法

分配空间
可以使用 allocate() 方法为 ByteBuffer 分配空间,其他 buffer 类也有该方法

ByteBuffer buf = ByteBuffer.allocate(16);

向 buffer 写入数据
有两种方法

  • 调用 channel 的 read 方法
  • 调用 buffer 自己的 put 方法
int readBytes = channel.read(buf);

buf.put((byte)127);

从 buffer 读取数据
同样有两种方法

  • 调用 channel 的 write 方法
  • 调用 buffer 自己的 get 方法
int writeBytes = channel.write(buf);

byte b = buf.get();

get 方法会让 position 读指针向后移动,如果想重复读取数据

  • 可以调用 rewind() 方法将 position 重新置为 0
  • 或者调用 get(int i) 方法获取索引 i 的内容,它不会移动指针

4.Scattering Reads

分散读取,有一个文本文件 3parts.txt

onetwothree

使用如下方式读取,可以将数据填充至多个 buffer

try(RandomAccessFile file = new RandomAccessFile("helloworld/3parts.txt", "rw")){FileChannel channel = file.getChannel();ByteBuffer a = ByteBuffer.allocate(3);ByteBuffer b = ByteBuffer.allocate(3);ByteBuffer c = ByteBuffer.allocate(5);channel.read(new ByteBuffer[]{a, b, c});
}catch(IOException e){e.printStackTrace();
}

4.Gathering Write

集中写入

ByteBuffer b1 = StandardCharasets.UTF_8.encode("hello");
ByteBuffer b2 = StandardCharasets.UTF_8.encode("world");
ByteBuffer b3 = StandardCharasets.UTF_8.encode("你好");try(FileChannel channel = new RandomAccessFile("words2.txt", "rw")){channel.write(new ByteBuffer[]{b1, b2, b3})
}catch(IOException e){
}

5.黏包半包

网络上有多条数据发送给服务器,数据之间使用 \n 进行分隔
但由于某种原因这些数据在接收时被重新组合,例如原始数据有3条:

Hello,world\n
I'm zhangsan\n
How are you?\n

变成了下面的两个 byteBuffer

Hello,world\nI'm zhangsan\nHo
w are you?\n

编写程序,将错乱的数据恢复成原始的按 \n 分隔的数据

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 + 1 - source.position();//把这条完整消息存入新的 ByteBufferByteBuffer target = ByteBuffer.allocate(256);//从 source 读,向 target 写for(int j = 0; j < length; j++){target.put(source.get());}debugAll(target);}}
source.compact();
}

四、文件编程

1.FileChannel

FileChannel 只能工作在阻塞模式下

获取
不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法

  • 通过 FileInputStream 获取的 channel 只能读
  • 通过 FileOutputStream 获取的 channel 只能写
  • 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定

读取
会从 channel 读取数据填充 ByteBuffer,返回值表示读到了多少字节,-1 表示到达了文件的末尾

int readBytes = channel.read(buffer);

写入
写入的正确方式如下,SocketChannel

ByteBuffer buffer = ...;
buffer.put(...);	//存入数据
buffer.flip();		//切换读模式while(buffer.hasRemaining()){channel.write(buffer);
}

在 while 中调用 channel.write 是因为 write 方法并不能保证一次性将 buffer 中的内容全部写入 channel

关闭
channel 必须关闭,不过调用了 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 close() 方法会间接地调用 channel 的 close 方法

位置
获取当前位置

long pos = channel.position();

设置当前位置

long newPos = ...;
channel.position(newPos);

设置当前位置时,如果设置为文件的末尾

  • 这时读取会返回 -1
  • 这时写入,会追加内容,但要注意如果 position 超过了文件末尾,再写入时在新内容和原末尾之间会有空洞(00)

大小
使用 size 方法获取文件的大小

强制写入
操作系统处于性能的考虑,会将数据缓存,不是立刻写入磁盘。可以调用 force(true) 方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘

2.两个 Channel 传输数据

String FROM = "helloworld/data.txt";
String TO = "helloworld/to.txt";
long start = System.nanoTime();
try(FileChannel from = new FileInputStream(FROM).getChannel();FileChannel to = new FileOutputStream(TO).getChannel();){//效率高,底层会利用操作系统的零拷贝进行优化from.transferTo(0, from.size(), to);
}catch(IOException){e.printStackTrace();
}
long end = System.nanaTime();
System.out.println("transferTo 用时:" + (end - start) / 1000_000.0);

输出

transferTo 用时:8.2001

3.Path

jdk7 引入了 Path 和 Paths 类

  • Path 用来表示文件路径
  • Paths 是工具类,用来获取 Path 实例
Path source = Paths.get("1.txt");		//相对路径 使用 user.dir 环境变量来定位 1.txtPath source = Paths.get("d:\\1.txt");	//绝对路径 代表了 d:\1.txtPath source = Paths.get("d:/1.txt");	//绝对路径 代表了 d:\1.txtPath projects = Paths.get("d\\data", "projects");		//代表了 d:\data\projects

4.Files

检查文件是否存在

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

创建一级目录

Path path = Paths.get("helloworld/d1");
Files.createDirectory(path);
  • 如果目录已存在,会抛异常 FileAlreadyExistsException
  • 不能一次创建多级目录,否则会抛异常 NoSuchFileException

创建多级目录

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

拷贝文件

Path source = Paths.get("helloworld/data.txt");
Path target = Paths.get("helloworld/target.txt");Files.copy(source, target);
  • 如果文件已存在,会抛异常 FileAlreadyExistsException

如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来空值

Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

移动文件

Path source = Paths.get("helloworld/data.txt");
Path target = Paths.get("helloworld/data.txt");Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
  • StandardCopyOption.ATOMIC_MOVE保证文件移动的完整性

删除文件

Path target = Paths.get("helloworld/target.txt");
Files.delete(target);
  • 如果文件不存在,会抛异常 NoSuchFileException

删除目录

Path target = Paths.get("hellowworld/d1");
Files.delete(target);
  • 如果目录还有内容,会抛异常 DirectoryNoEmptyException

遍历目录文件

public static void main(String[] args) throws IOException{//计数器,需要使用原子类,匿名类如果要使用外部变量,则该变量的地址值不能发生改变AtomicInteger dirCount = new AtomicInteger();AtomicInteger fileCount = new AtomicInteger();Files.walkFileTree(Paths.get("C:\\Program Files\\Java\\jdk1.8.0_91"), new SimpleFileVistor(){@Overridepublic FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException{System.out.println("====="+dir);dirCount.incrementAndGet();return super.preVisitDirectory(dir, attrs);}@Overridepublic 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{//计数器,需要使用原子类,匿名类如果要使用外部变量,则该变量的地址值不能发生改变AtomicInteger jarCount = new AtomicInteger();Files.walkFileTree(Paths.get("C:\\Program Files\\Java\\jdk1.8.0_91"), new SimpleFileVistor(){@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException{if(file.toString().endWith(".jar")){System.out.println(file);}return super.visitFile(file, attrs);}});System.out.println("jar count:" + jarCount);
}

删除多级目录
(友情提示:不要直接执行!删掉后不会进回收站!)

public static void main(String[] args) throws IOException{Files.walkFileTree(Paths.get("C:\\Program Files\\Java\\jdk1.8.0_91"), new SimpleFileVistor(){@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException{Files.delete(file)return super.visitFile(file, attrs);}@Overridepublic FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException{Files.delete(dir);return super.postVisitDirectory(file, exc);}});
}

拷贝多级目录

public static void main(String[] args) throws IOException{String source = "D:\\sourceDir";String target = "D:\\targetDir";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();}});
}

相关内容

热门资讯

【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
AsusVivobook无法开... 首先,我们可以尝试重置BIOS(Basic Input/Output System)来解决这个问题。...
ASM贪吃蛇游戏-解决错误的问... 要解决ASM贪吃蛇游戏中的错误问题,你可以按照以下步骤进行:首先,确定错误的具体表现和问题所在。在贪...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...