文章目录
1. 前言
笔记基于黑马的Netty教学,视频地址:黑马Netty
2. 学习
1. 概念
non-blocking io:非阻塞IO
2. 三大组件
2.1 Channel和Buffer
Channel:数据的传输通道
Buffer:数据的缓冲区
channel有一点类似于stream,它就是读写数据的双向通道,可以从channel将数据读入buffer,也可以将buffer的数据写入channel,而之前的stream要么是输入,要么是输出,channel比stream更加底层。在读入数据或者读出数据的时候都要通过中间的buffer,读入数据的时候先把IO设备的数据读到buffer中,再从buffer中读入,读出也一样。
常见的channel有:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
Buffer有以下几种,用于缓冲数据,其中使用较多的是ByteBuffer
- ByteBuffer
1、MappedByteBuffer
2、DirectByteBuffer
3、HeapByteBuffer - ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
- CharBuffer
2.1 Selector
selector:选择器,结合服务器的设计演化来理解用途。
在NIO出现之前,如何开发一个服务器端的程序?
思路1:多线程
对于每一个socket,用一个单独的线程进行处理,但是如果需要处理大批量的socket,就不可以了,需要用到的资源随着连接数的增加就会提高,一旦资源超出服务器的负载能力,就会直接瘫痪。
多线程版缺点:
- 内存占用高
- 线程上下文切换成本高
- 只适合连接少的场景
其次,线程的数量应该是根据CPU的核数决定的,所以线程也不是越多越好,再多的线程,如果没有CPU分配时间线来指向,那么大量的线程只能等待。
思路2:线程池版
这一版的的优点就是根据线程池的大小分配线程,不会出现说线程越来越多,空闲线程堆积的情况。但是这也有缺点
多线程版缺点:
- 阻塞模式下,线程同一时间内只能处理一个socket
- 线程获取了一个任务的时候只有完成当前任务之后其他线程才可以继续执行。就比如点单的时候服务员必须一直陪着,只有客人点了单才能离开。
- 线程无法得到充分利用,如果有一个socket连接了什么都不做,就很耗费资源。
- 仅适合短连接场景
思路3:selector版
selector的作用是配合一个线程来管理多个channel,获取这些channel上发生的事件,这些channel工作在非阻塞模式下,不会让线程吊死在一个channel上,连接数特别多,但是流量低的场景(low traffic),因为如果数据量大,那么线程就光执行一个channel的请求了。
调用selector的select()会阻塞直到channel发生了读写就绪事件,这些事件发生。select方法就会返回这些事件交给thread处理。比如channel需要传输数据,selector接受到请求之后,会通知thread去处理。相当于一个监听的作用,selector监听channel的变化,并且让thred去处理。
2.2 ByteBuffer
1、Buffer的一个小示例:读取文件
有一段普通文本文件data.txt,使用ByteBuffer读取文件
@Slf4j
public class TestByteBuffer {
public static void main(String[] args) {
//FileChannel,获取方式:
//1. 输入输出流间接获取
//2. RandomAccessFile
//文件data.txt:放在工程下
try (FileChannel channel = new FileInputStream("data.txt").getChannel()){
//准备缓冲区暂存数据:ByteBuffer,参数:缓冲区大小/字节
ByteBuffer buffer = ByteBuffer.allocate(10);
//使用while循环是为了防止读不全,因为只读一次的话,buffer的容量是多大就只能读出多大的,不能全部读出
while(true){
//可以从channel读取数据了,意味着写入缓冲区中
int read = channel.read(buffer);
log.debug("读取到的字节数{}", read);
if(read == -1){
//没有内容了
break;
}
//打印buffer内容
buffer.flip();//切换到buffer的读模式
//无参get():一次读一个字节、
//hasRemaining(),检测Buffer还有没有数据
while(buffer.hasRemaining()){
byte b = buffer.get();
log.debug("实际的字节{}", (char)b);
}
//读完一次要切换成写模式
buffer.clear();
}
} catch (IOException e) {
}
}
}
ByteBuffer正确的使用姿势
2、ByteBuffer内部结构
ByteBuffer有以下重要属性
- capacity:容量
- position:指针,指向读到的地方
- limit:读写字节的限制
1、一开始情况:
2、写模式下,position是写入的量,limit等于容量,读入4个字节后的状态:
3、flip动作完成后,position指针切换为读取的位置,limit切换为读取限制:
4、读取4个字节后的状态:
5、clear动作发生后,重新清0:
6、compact方法,是把未读完的部分向前压缩,然后切换至写模式。保留没有读的部分,适合用在读了一些就要立马写的情况。
2.3 使用读写工具类来打印内部结构:
1、导入Netty依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.51.Final</version>
</dependency>
2、工具类
package com.jianglianghao;
import io.netty.util.internal.MathUtil;
import io.netty.util.internal.StringUtil;
import java.nio.ByteBuffer;
public class ByteBufferUtil {
private static final char[] BYTE2CHAR = new char[256];
private static final char[] HEXDUMP_TABLE = new char[256 * 4];
private static final String[] HEXPADDING = new String[16];
private static final String[] HEXDUMP_ROWPREFIXES = new String[65536 >>> 4];
private static final String[] BYTE2HEX = new String[256];
private static final String[] BYTEPADDING = new String[16];
static {
final char[] DIGITS = "0123456789abcdef".toCharArray();
for (int i = 0; i < 256; i++) {
HEXDUMP_TABLE[i << 1] = DIGITS[i >>> 4 & 0x0F];
HEXDUMP_TABLE[(i << 1) + 1] = DIGITS[i & 0x0F];
}
int i;
// Generate the lookup table for hex dump paddings
for (i = 0; i < HEXPADDING.length; i++) {
int padding = HEXPADDING.length - i;
StringBuilder buf = new StringBuilder(padding * 3);
for (int j = 0; j < padding; j++) {
buf.append(" ");
}
HEXPADDING[i] = buf.toString();
}
// Generate the lookup table for the start-offset header in each row (up to 64KiB).
for (i = 0; i < HEXDUMP_ROWPREFIXES.length; i++) {
StringBuilder buf = new StringBuilder(12);
buf.append(StringUtil.NEWLINE);
buf.append(Long.toHexString(i << 4 & 0xFFFFFFFFL | 0x100000000L));
buf.setCharAt(buf.length() - 9, '|');
buf.append('|');
HEXDUMP_ROWPREFIXES[i] = buf.toString();
}
// Generate the lookup table for byte-to-hex-dump conversion
for (i = 0; i < BYTE2HEX.length; i++) {
BYTE2HEX[i] = ' ' + StringUtil.byteToHexStringPadded(i);
}
// Generate the lookup table for byte dump paddings
for (i = 0; i < BYTEPADDING.length; i++) {
int padding = BYTEPADDING.length - i;
StringBuilder buf = new StringBuilder(padding);
for (int j = 0; j < padding; j++) {
buf.append(' ');
}
BYTEPADDING[i] = buf.toString();
}
// Generate the lookup table for byte-to-char conversion
for (i = 0; i < BYTE2CHAR.length; i++) {
if (i <= 0x1f || i >= 0x7f) {
BYTE2CHAR[i] = '.';
} else {
BYTE2CHAR[i] = (char) i;
}
}
}
/**
* 打印所有内容
* @param buffer
*/
public static void debugAll(ByteBuffer buffer) {
int oldlimit = buffer.limit();
buffer.limit(buffer.capacity());
StringBuilder origin = new StringBuilder(256);
appendPrettyHexDump(origin, buffer, 0, buffer.capacity());
System.out.println("+--------+-------------------- all ------------------------+----------------+");
System.out.printf("position: [%d], limit: [%d]\n", buffer.position(), oldlimit);
System.out.println(origin);
buffer.limit(oldlimit);
}
/**
* 打印可读取内容
* @param buffer
*/
public static void debugRead(ByteBuffer buffer) {
StringBuilder builder = new StringBuilder(256);
appendPrettyHexDump(builder, buffer, buffer.position(), buffer.limit() - buffer.position());
System.out.println("+--------+-------------------- read -----------------------+----------------+");
System.out.printf("position: [%d], limit: [%d]\n", buffer.position(), buffer.limit());
System.out.println(builder);
}
private static void appendPrettyHexDump(StringBuilder dump, ByteBuffer buf, int offset, int length) {
if (MathUtil.isOutOfBounds(offset, length, buf.capacity())) {
throw new IndexOutOfBoundsException(
"expected: " + "0 <= offset(" + offset + ") <= offset + length(" + length
+ ") <= " + "buf.capacity(" + buf.capacity() + ')');
}
if (length == 0) {
return;
}
dump.append(
" +-------------------------------------------------+" +
StringUtil.NEWLINE + " | 0 1 2 3 4 5 6 7 8 9 a b c d e f |" +
StringUtil.NEWLINE + "+--------+-------------------------------------------------+----------------+");
final int startIndex = offset;
final int fullRows = length >>> 4;
final int remainder = length & 0xF;
// Dump the rows which have 16 bytes.
for (int row = 0; row < fullRows; row++) {
int rowStartIndex = (row << 4) + startIndex;
// Per-row prefix.
appendHexDumpRowPrefix(dump, row, rowStartIndex);
// Hex dump
int rowEndIndex = rowStartIndex + 16;
for (int j = rowStartIndex; j < rowEndIndex; j++) {
dump.append(BYTE2HEX[getUnsignedByte(buf, j)]);
}
dump.append(" |");
// ASCII dump
for (int j = rowStartIndex; j < rowEndIndex; j++) {
dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]);
}
dump.append('|');
}
// Dump the last row which has less than 16 bytes.
if (remainder != 0) {
int rowStartIndex = (fullRows << 4) + startIndex;
appendHexDumpRowPrefix(dump, fullRows, rowStartIndex);
// Hex dump
int rowEndIndex = rowStartIndex + remainder;
for (int j = rowStartIndex; j < rowEndIndex; j++) {
dump.append(BYTE2HEX[getUnsignedByte(buf, j)]);
}
dump.append(HEXPADDING[remainder]);
dump.append(" |");
// Ascii dump
for (int j = rowStartIndex; j < rowEndIndex; j++) {
dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]);
}
dump.append(BYTEPADDING[remainder]);
dump.append('|');
}
dump.append(StringUtil.NEWLINE +
"+--------+-------------------------------------------------+----------------+");
}
private static void appendHexDumpRowPrefix(StringBuilder dump, int row, int rowStartIndex) {
if (row < HEXDUMP_ROWPREFIXES.length) {
dump.append(HEXDUMP_ROWPREFIXES[row]);
} else {
dump.append(StringUtil.NEWLINE);
dump.append(Long.toHexString(rowStartIndex & 0xFFFFFFFFL | 0x100000000L));
dump.setCharAt(dump.length() - 9, '|');
dump.append('|');
}
}
public static short getUnsignedByte(ByteBuffer buffer, int index) {
return (short) (buffer.get(index) & 0xFF);
}
}
3、演示
public class TestByteBufferReadWrite {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put((byte)0x61);//字符'a'
ByteBufferUtil.debugAll(buffer);
/**
+--------+-------------------- all ------------------------+----------------+
position: [1], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 00 00 00 00 00 00 00 00 00 |a......... |
+--------+-------------------------------------------------+----------------+
*/
buffer.put(new byte[]{0x61,0x62,0x63});//字符a,b,c
ByteBufferUtil.debugAll(buffer);
/**
+--------+-------------------- all ------------------------+----------------+
position: [4], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 61 62 63 00 00 00 00 00 00 |aabc...... |
+--------+-------------------------------------------------+----------------+
*/
//从position开始读
//buffer.get();
buffer.flip();
System.out.println(buffer.get());
ByteBufferUtil.debugAll(buffer);
//切换成读模式,读取数据之后,limit设置为存入的数据的个数,position为1证明0已经被读出了
/**
+--------+-------------------- all ------------------------+----------------+
position: [1], limit: [4]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 61 62 63 00 00 00 00 00 00 |aabc...... |
+--------+-------------------------------------------------+----------------+
*/
buffer.compact();
ByteBufferUtil.debugAll(buffer);
//compact切换写模式,从上面的1到4,给赋值到0,1,2的位置上
/**
+--------+-------------------- all ------------------------+----------------+
position: [3], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 63 00 00 00 00 00 00 |abcc...... |
+--------+-------------------------------------------------+----------------+
*/
buffer.put(new byte[]{0x65,0x66});
ByteBufferUtil.debugAll(buffer);
//写数据从position开始写,不用担心上面的63没有清除完全
/**
+--------+-------------------- all ------------------------+----------------+
position: [5], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 65 66 00 00 00 00 00 |abcef..... |
+--------+-------------------------------------------------+----------------+
*/
}
}
2.4 ByteBuffer常见的方法
1、分配空间
可以使用allocate方法分配空间,其他的buffer也有这个方法,注意这里的分配容量是固定的。
ByteBuffer allocate = ByteBuffer.allocate(16);
public class TestByteBufferAllocate {
public static void main(String[] args) {
Class<? extends ByteBuffer> allocate = ByteBuffer.allocate(16).getClass();
Class<? extends ByteBuffer> aClass = ByteBuffer.allocateDirect(16).getClass();
System.out.println(allocate);
System.out.println(aClass);
//class java.nio.HeapByteBuffer --java堆内存,读写效率较低,收到垃圾回收GC的影响
//class java.nio.DirectByteBuffer --直接内存,读写效率高(少一次数据的拷贝),使用系统内存,不受GC影响
/**
* 堆内存会在垃圾回收时收到影响,当空间不足的时候,使用标记清理、拷贝、整理算法让空间更加紧凑,牵扯到数据重新赋值移动
* 直接内存不受GC的影响,数据是固定的,所以效率更高,但是分配内存效率比较慢,因为要调用系统的内存函数,而且如果释放不到位
* 就有可能造成内存泄漏,类似c语言指针这种分配释放的方式,人工释放。
*/
}
}
2、写入数据
有2种方法
- 调用channel的read方法
int readBytes = channel.read(buf);
- 调用buffer自己的put方法
buf.put((byte)127);
3、读出数据
同样2种方法
- 调用channel的write方法
int write= channel.write(buf);
- 调用buffer的get方法
byte b = buf.get();
get方法会让position读指针向后走,如果想重复读取数据
- 可以调用rewind方法将position重新设置为0
- 或者调用get(int i)方法获取索引i的内容,它不会移动指针,position不变
public class TestBufferRead {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put(new byte[]{'a', 'b', 'c', 'd'});
//读模式
buffer.flip();
//rewind从头开始读,一次读4个字节
buffer.get(new byte[4]);
debugAll(buffer);
/**
+--------+-------------------- all ------------------------+----------------+
position: [4], limit: [4]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 00 00 00 00 00 00 |abcd...... |
+--------+-------------------------------------------------+----------------+
*/
buffer.rewind();//把position设置为开头
System.out.println((char)buffer.get());
debugAll(buffer);
/**
a
+--------+-------------------- all ------------------------+----------------+
position: [1], limit: [4]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 00 00 00 00 00 00 |abcd...... |
+--------+-------------------------------------------------+----------------+
*/
buffer.rewind();//把position设置为开头
//mark & reset
//mark做一个标记,记录position位置,reset是将position重置到mark标记的位置
System.out.println((char)buffer.get()); //a
System.out.println((char)buffer.get()); //b
//此时position = 2;
buffer.mark();//此时position为2
System.out.println((char)buffer.get()); //c
System.out.println((char)buffer.get()); //d
buffer.reset();//将position的位置重置到2的位置
System.out.println((char)buffer.get()); //c
System.out.println((char)buffer.get()); //d
//get(i)
buffer.rewind();//把position设置为开头
System.out.println((char)buffer.get(3));//不会改变position
debugAll(buffer);
/**
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [4]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 00 00 00 00 00 00 |abcd...... |
+--------+-------------------------------------------------+----------------+
*/
}
}
4、字符串和ByteBuffer互相转换
1、字符串转化成ByteBuffer
- buffer.put:维持在写模式
- CharSet:会自动切换成读模式(position置0)
- wrap:也会自动切换成读模式(position置0)
public class TestByteBufferString {
public static void main(String[] args) {
//1. 字符串转化为ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(16);
buffer.put("hello".getBytes());
debugAll(buffer);
/**
+--------+-------------------- all ------------------------+----------------+
position: [5], limit: [16]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f 00 00 00 00 00 00 00 00 00 00 00 |hello...........|
+--------+-------------------------------------------------+----------------+
*/
//2. charset
//encode方法会自动切换到读模式
ByteBuffer hello = StandardCharsets.UTF_8.encode("hello");
debugAll(hello);
/**
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [5]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f |hello |
+--------+-------------------------------------------------+----------------+
*/
//3. wrap,同样的也是会自动切换成读模式
ByteBuffer wrap = ByteBuffer.wrap("hello".getBytes());
debugAll(wrap);
/**
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [5]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f |hello |
+--------+-------------------------------------------------+----------------+
*/
}
}
2、ByteBuffer转化成String
- 重点介绍decode方法,decode方法转化读模式下的byteBuffer可以,但是不能转化写模式下的,因为decode是从当前position往后找的,所以必须切换成读模式才可以。
//decode不能直接转写模式的
String s1 = StandardCharsets.UTF_8.decode(hello).toString();
System.out.println(s1);//hello
//decode转化读模式的
buffer.flip();
String s2 = StandardCharsets.UTF_8.decode(buffer).toString();
System.out.println(s2);//hello
2.5 Scattering Reads:分散读取
读取文件,分散读取,在读取已知长度的文本的时候可以使用
如文本3parts.txt
onetwothree
public class TestScatteringReads {
public static void main(String[] args) {
try( FileChannel channel = new RandomAccessFile("3parts.txt", "r").getChannel()){
ByteBuffer b1 = ByteBuffer.allocateDirect(3);
ByteBuffer b2 = ByteBuffer.allocateDirect(3);
ByteBuffer b3 = ByteBuffer.allocateDirect(5);
channel.read(new ByteBuffer[]{b1, b2, b3});
//切换读模式
b1.flip();
b2.flip();
b3.flip();
debugAll(b1);
/**
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [3]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 6f 6e 65 |one |
+--------+-------------------------------------------------+----------------+
*/
debugAll(b2);
/**
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [3]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 74 77 6f |two |
+--------+-------------------------------------------------+----------------+
*/
debugAll(b3);
/**
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [5]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 74 68 72 65 65 |three |
+--------+-------------------------------------------------+----------------+
*/
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.6 GatheringWrites:集中写入
public class TestGatheringWrites {
public static void main(String[] args) {
ByteBuffer b1 = StandardCharsets.UTF_8.encode("hello");
ByteBuffer b2 = StandardCharsets.UTF_8.encode("world");
//UTF8中中文3个字节
ByteBuffer b3 = StandardCharsets.UTF_8.encode("你好");
//rw:读写模式
try (FileChannel channel = new RandomAccessFile("words2.txt", "rw").getChannel()) {
channel.write(new ByteBuffer[]{b1, b2, b3});
} catch (IOException e) {
}
}
}
分散读和集中都是以一个整体来运行的,这样的好处就是减少了数据拷贝的过程,因为如果用一个大的buffer来运算,那么结果就是会多出一个大的buffer,并且还要把原来的buffer内容拷贝到大的buffer中去,耗费时间和空间都要多。
2.7 粘包半包分析
网络上有多条数据发送给服务端,数据之间使用 \n 进行分隔
但由于某种原因这些数据在接收时,被进行了重新组合,例如原始数据有3条为
- Hello,world\n
- I’m Nyima\n
- How are you?\n
变成了下面的两个 byteBuffer (粘包,半包)
Hello,world\nI’m Nyima\nHo
w are you?\n
粘包: 发送数据的时候数据被组合组合起来一起发送。多条信息被放在一个缓冲区中被一起发送出去
半包: 接受信息的时候由于缓冲区大小限制导致接受到的信息会被分开接受,那么出现的问题就有可能是接受的信息拼接后发现出现了消息截断的现象,也就是消息不完整。
现在要求编写程序,将错乱的数据恢复成原始的按\n分散的数据!!!
public class TestByteBufferExam {
public static void main(String[] args) {
ByteBuffer source = ByteBuffer.allocate(32);
source.put("Hello workd\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();
//把这条完整消息存入一个新的byteBuffer
ByteBuffer target = ByteBuffer.allocate(length);
for(int j = 0; j < length; j++ ){
target.put(source.get());
}
debugAll(target);
}
}
//不用clear,防止数据丢失,比如第一条消息
//最后面的Ho由于没有找到换行符就留到下一次处理
source.compact();
}
/**
+--------+-------------------- all ------------------------+----------------+
position: [12], limit: [12]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 20 77 6f 72 6b 64 0a |Hello workd. |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [13], limit: [13]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 49 27 6d 20 7a 68 61 6e 67 73 61 6e 0a |I'm zhangsan. |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [12], limit: [12]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 6f 77 20 61 72 65 20 79 6f 75 0a |How are you. |
+--------+-------------------------------------------------+----------------+
*/
}
3. 文件编程
3.1 FileChannel
注意:FileChannel只工作在阻塞模式下,不能和selector一起用
获取:
不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法
- 通过 FileInputStream 获取的 channel 只能读
- 通过 FileOutputStream 获取的 channel 只能写
- 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定
读取:
通过 FileInputStream 获取channel,通过read方法将数据写入到ByteBuffer中,返回值表示读到了多少字节,若读到了文件末尾则返回-1。
int readBytes = channel.read(buffer);
写入:
写入的正确姿势
ByteBuffer buffer = ...;
buffer.put(...);//存入数据
buffer.flip();//切换读模式
while(buffer.hasRemaining()){
channel.write(buffer);
}
在while中调用channel.write是因为weite方法并不能保证一次将buffer中的内容一次全部写到channel中。channel的写入能力是有上限的。
关闭:
通道需要close,不过调用了FileInputStream,FileOutputStream或者RamdomAccessFile的close方法会间接地调用channel的close方法。
位置:
获取当前位置:
long pos = channel.position();
设置当前位置:
long newPos = ...;
channel.position(newPos);
设置当前位置时,如果设置为文件的末尾
- 这时读取会返回 -1
- 这时写入,会追加内容,但要注意如果 position 超过了文件末尾,再写入时在新内容和原末尾之间会有空洞(00)
大小:
使用size方法获取文件的大小
强制写入:
操作系统出于性能的考虑,会将数据缓存,不是立刻写入磁盘,而是等到缓存满了以后将所有数据一次性的写入磁盘。可以调用 force(true) 方法将文件内容和元数据**(文件的权限等信息)**立刻写入磁盘
3.2 两个Channel传输数据
利用channel来传送数据比文件流传输数据要快,因为底层调用了计算机操作系统的零拷贝。传输数据的上限是2g数据,改进可以使用多次传输。
public class TestFileChannelTransferTo {
public static void main(String[] args) {
try (FileChannel from = new FileInputStream("data.txt").getChannel();
FileChannel to = new FileOutputStream("to.txt").getChannel();
) {
//参数:从哪开始传,传的大小,传到哪
//from.transferTo(0, from.size(), to);
long size = from.size();
//效率高,底层会利用操作系统的零拷贝进行优化,传输数据的上限是2g数据,改进可以使用多此传输
for(long left = from.size(); left > 0; ){
//left代表还剩余多少字节没有被创输
left -= from.transferTo((size-left), left, to);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.3 Path
jdk7引入了Path和Paths类
- Path 用来表示文件路径
- Paths 是工具类,用来获取 Path 实例
Path source = Paths.get("1.txt"); // 相对路径 不带盘符 使用 user.dir 环境变量来定位 1.txt
Path source = Paths.get("d:\\1.txt"); // 绝对路径 代表了 d:\1.txt 反斜杠需要转义
Path source = Paths.get("d:/1.txt"); // 绝对路径 同样代表了 d:\1.txt
Path projects = Paths.get("d:\\data", "projects"); // 代表了 d:\data\projects
.
代表了当前路径
..
代表了上层路径
例如目录结构如下:
d:
|- data
|- projects
|- a
|- b
代码:
Path path = Paths.get("d:\\data\\projects\\a\\..\\b");
System.out.println(path);
System.out.println(path.normalize()); // 正常化路径,去除了..
输出结果:
d:\data\projects\a\..\b
d:\data\projects\b
3.4 Files
1、检查文件是否存在:
Path path = Paths.get("helloword/data.txt");
System.out.println(Files.exists(path));
2、创建一级目录:
Path path = Paths.get("helloword/d1");
Files.createDirectory(path);
- 如果目录已存在,会抛异常 FileAlreadyExistsException
- 不能一次创建多级目录,否则会抛异常 NoSuchFileException
3、创建多级目录:
Path path = Paths.get("helloword/d1/d2");
Files.createDirectories(path);
4、拷贝文件:
copy方法底层也是调用操作系统的一个实现,效率其实都差不多,各有好处。
Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/target.txt");
Files.copy(source, target);
- 如果文件已存在,会抛异常 FileAlreadyExistsException
如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来控制
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
5、移动文件:
Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/data.txt");
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
StandardCopyOption.ATOMIC_MOVE 保证文件移动的原子性
6、删除文件:
Path target = Paths.get("helloword/target.txt");
Files.delete(target);
- 如果文件不存在,会抛异常 NoSuchFileException
7、删除目录:
Path target = Paths.get("helloword/d1");
Files.delete(target);
- 如果目录还有内容,会抛异常 DirectoryNotEmptyException
8、遍历目录:
Files.walkFileTree是从某个文件夹路径开始遍历其中的文件夹和文件。
public class TestFilesWalkFileTree {
public static void m1() throws IOException {
AtomicInteger dirCount = new AtomicInteger();
AtomicInteger fileCount = new AtomicInteger();
//匿名内部类只能访问外部的final变量,而final变量不能++,所以这里用原子类
//典型的visitor模式的应用
Files.walkFileTree(Paths.get("D:\\学习资料\\电工"), new SimpleFileVisitor<Path>(){
//遍历到文件夹之前要做什么
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
dirCount.incrementAndGet();
System.out.println("==>" + dir);
//返回值就不要动了,避免走不下去
return super.preVisitDirectory(dir, attrs);
}
//遍历文件的时候要做什么
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
fileCount.incrementAndGet();
System.out.println(file);
//避免走不下去,返回值不要动
return super.visitFile(file, attrs);
}
});
System.out.println("文件夹Count =" + dirCount + ", 文件名Count = " + fileCount );
}
public static void main(String[] args) throws IOException {
//小例子:统计文件夹中jar包个数
AtomicInteger jar = new AtomicInteger();
Files.walkFileTree(Paths.get("D:\\学习资料\\电工"), new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if(file.toString().endsWith(".jar")){
System.out.println(file);
jar.incrementAndGet();
}
return super.visitFile(file, attrs);
}
});
}
}
9、删除多级目录:
Files.walkFileTree中重写方法,在遍历到文件的时候进行删除,在最后退出的时候就可以删目录了。不用我们再自己写那些递归代码。
参数:
- 路径
- 文件访问器(访问者模式),实现类使用SimpleFileVisitor
public class TestFilesWalkFileTreeDelete {
public static void main(String[] args) throws IOException {
//Files.delete()方法当一个文件夹里面有文件的时候是删不掉的。
Files.walkFileTree(Paths.get("D:\\学习资料\\电工"), 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);
}
});
}
public static void m2(String[] args) {
}
public static void m3(String[] args) {
}
}
10、拷贝多级目录:
public class TestFilesCopy {
public static void main(String[] args) throws IOException {
String source = "D:\\Snipaste-1.16.2-x64";
String target = "D:\\Snipaste-1.16.2";
Files.walk(Paths.get(source)).forEach(
path->{
try {
//比如文件夹的path=D:\Snipaste-1.16.2-x64\aaa
//那么替换后就是path=D:\Snipaste-1.16.2\aaa,只是改变了文件夹的名字,创建文件的时候其他都是一模一样的
String targetName = path.toString().replace(source, target);
//是目录
if(Files.isDirectory(path)){
//我们就创建一个相同的文件夹的名字
Files.createDirectories(Paths.get(targetName));
}
//是文件
else if(Files.isRegularFile(path)){
//把原始文件拷贝到目的文件夹去
Files.copy(path, Paths.get(targetName));
}
} catch (IOException e) {
e.printStackTrace();
}
}
);
}
}
如有错误,欢迎指出