NIO 基础
- 字符串与ByteBuffer 互转
// 1. 字符串转为 ByteBuff
// 首先分配一个16字节大小的byteBuff
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
// 然后将“你好”的字节数组填充到 byteBuff 中
byteBuffer.put("你好".getBytes(StandardCharsets.UTF_8));
// 2. 或者通过Charset 的 encode方法获取 _byteBuff
ByteBuffer _byteBuffer = StandardCharsets.UTF_8.encode("你好");
ByteBuffer __byteBuffer = Charset.forName("UTF-8").encode("你好");
// 3. 通过 NIO的API wrap方法
ByteBuffer ___byteBuffer = ByteBuffer.wrap("你好".getBytes(StandardCharsets.UTF_8));
// 将 ByteBuffer 转为 String
String str = StandardCharsets.UTF_8.decode(__byteBuffer).toString();
- Scattering Reads
分散读取,有一个文本文件 3parts.txt
onetwothree
使用如下方式读取,可以将数据填充至多个 buffer
try (FileChannel channel = new RandomAccessFile("3parts.txt", "r").getChannel()) {
ByteBuffer byteBuffer = ByteBuffer.allocate(3);
ByteBuffer _byteBuffer = ByteBuffer.allocate(3);
ByteBuffer __byteBuffer = ByteBuffer.allocate(5);
channel.read(new ByteBuffer[]{byteBuffer, _byteBuffer, __byteBuffer});
byteBuffer.flip();
_byteBuffer.flip();
__byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
System.out.print((char)byteBuffer.get());
}
System.out.println();
while (_byteBuffer.hasRemaining()) {
System.out.print((char)_byteBuffer.get());
}
System.out.println();
while (__byteBuffer.hasRemaining()) {
System.out.print((char)__byteBuffer.get());
}
System.out.println();
} catch (IOException e) {
System.out.println(e);
}
- Gathering Writes
集中写入,将三个bytebuffer 的 内容写入 words.txt
ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode("hello");
ByteBuffer _byteBuffer = StandardCharsets.UTF_8.encode("world");
// 6 bytes
ByteBuffer __byteBuffer = StandardCharsets.UTF_8.encode("你好");
try (FileChannel channel = new RandomAccessFile("word.txt", "rw").getChannel()) {
// 将三个bytebuff 集中写入到 fileChannel中
channel.write(new ByteBuffer[]{byteBuffer, _byteBuffer, __byteBuffer});
} catch (IOException e) {
System.out.println(e);
}
思考:使用 分散读集中写,将减少数据在 ByteBuffer中的复制
- 粘包半包分析
网络上有很多条数据发送给服务器,数据之间通过 \n 的格式进行分隔,但由于某种原因这些数据在接收时,被进行了重新组合,例如原始数据有三条为
Hello,world\n
I’m zhangsan\n
How are you?\n
变成了下面的两个 ByteBuffer (粘包–发送一次发多条数据、半包–服务器的缓冲区大小决定的)
Hello,world\nI’m zhangsan\nH
ow are you?\n
现在要求编写程序,将错乱的数据恢复成原始数据按照\n分隔的数据
public static void main(String[] args) {
ByteBuffer byteBuffer = ByteBuffer.allocate(32);
byteBuffer.put("Hello,World\nI'm zhangsan\nHo".getBytes(StandardCharsets.UTF_8));
split(byteBuffer);
byteBuffer.put("w are you?\n".getBytes(StandardCharsets.UTF_8));
split(byteBuffer);
}
private static void split(ByteBuffer byteBuffer) {
// 切换成读模式
byteBuffer.flip();
// limit 上限
for (int i = 0; i < byteBuffer.limit(); i++) {
// 找到一条完整的消息
if(byteBuffer.get(i) == '\n') {
// 把完整的消息存储到 新的byteBuffer中
int length = i + 1 - byteBuffer.position();
ByteBuffer target = ByteBuffer.allocate(length);
// 从 byteBuffer中读,向 target 中写
for (int i1 = 0; i1 < length; i1++) {
target.put(byteBuffer.get());
}
for (int j = 0; j < target.limit(); j++) {
System.out.print((char)target.get(j));
}
}
}
// 把未读的移到首部
byteBuffer.compact();
}
- 文件编程
5.1 FileChannel
FileChannel 只能工作在阻塞模式下
获取
不能直接打开FileChannel,必须通过FileInputStream、FileOutputStream或者RandomAccessFile来获取FileChannel,它们都有getChannel方法
- 通过 FileInputStream 获取的 channel 只能读
- 通过 FileOutputStream 获取的 channel 只能写
- 通过 RandomAccessFile 是否读写由 RandomAccessFile的读写模式决定
读取
会从 channel 读取数据填充 ByteBuffer, 返回值表示读取到了多少字节,-1表示到达文件的末尾
int readBytes = channel.read(buffer);
写入
写入的正确姿势如下,
ByteBuffer buffer = ...;
buffer.put(...); // 存入数据
buffer.flip(); // 切换读模式
while(buffer.hasRemaining()) {
channel.write(buffer);
}
在while中调用channel.write是因为 write方法并不能保证一次将缓冲区的数据全部写入到channel中
关闭
channel必须关闭,不过调用了FileInputStream、FileOutputStream或者RandomAccessFile的close方法会简介地调用channel的close方法
位置
获取当前位置
long pos = channel.position();
设置当前位置
long newPos = ...;
channel.position(newPos);
设置当前位置时,如果设置为文件的末尾
- 这时读取会返回 -1
- 这时写入,会追加内容,但要注意如果positon超过了文件末尾,再写入时会在新内容和原来末尾之间存在空洞(00)
大小
FileChannel可以通过size方法获取文件大小
强制写入
操作系统出于性能考虑,会将数据缓存,不是立刻写入磁盘,可以调用force(true)方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘
5.2 两个Channel 传输数据
try (FileChannel from = new FileInputStream("data.txt").getChannel();
FileChannel to = new FileOutputStream("data2.txt").getChannel()) {
// 从起始位置读到文件最后 效率高,底层会利用操作系统的零拷贝进行优化
// 传输文件有上限,最大2G
from.transferTo(0, from.size(), to);
// 如果需要 传输大于2G的数据
// for(long left = from.size(); left > 0;) {
// left -= from.transferTo(from.size() - left, left, to);
// }
} catch (IOException e) {
}
5.3 Files(对文件操作的)的 API
5.3.1 walkFileTree 通过 树形文件访问,提供操作,例如删除某个文件夹下的文件
Files.walkFileTree(Paths.get("D:/Program Files/Java/jdk-11.0.9"), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("===>" + dir);
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println(file);
// Files.delete(file);
return super.visitFile(file, attrs);
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
System.out.println("退出文件 ===>" + dir);
// Files.delete(dir);
return super.postVisitDirectory(dir, exc);
}
});
5.3.2 Files的walk 方法