1、阻塞IO与非阻塞IO:
阻塞IO:传统的socket每个请求都要创建一个线程Thread,在进行IO读写的时候,会阻塞直到有文件或者数据的读写才会继续进行,面向流,阻塞的
非阻塞IO: 基于Reactor(响应式),创建Selector,将通道Channal注册到Selector,当真正 发生IO文件或数据读写时才去发生相应的处理,面向缓冲Buffer的,非阻塞的
2、3个核心组件:Channal,Buffer,Selector(还有其他组件,Pipe,FileLock)
Channel通道(接口),通过Channel进行数据操作,是全双工的,可以读,也可以写(IO是单向的,比如InputStream输入流),Channel是一个接口,有4种实现(这4种实现基本上涵盖了文件IO、基于UDP和TCP的IO操作)
Channel的实现:FileChannel(文件IO)
案例一:从一个文件Channel中读取数据
步骤:1、获取文件通道,有2种方式,可以直接通过文件IO获取,也可以通过RandomAccessFile获取
//获取FileChannel的实例,打开一个channel(通过new FileInputStream的方式获取)
FileChannel fileChannel1 = new FileInputStream("D:\\data\\nio\\test01.txt").getChannel();
//通过随机文件读取方式获取FileChannel实例,通过游标指针随机获取文件内容
//mode的取值:r(readOnly),rw(readAndWrite),rws(文件的读写内容和元数据每次都会同步到底层设备,也就是直接磁盘),rwd(文件的读写内容每次都会同步到底层设备)
FileChannel fileChannel = new RandomAccessFile(filePath, "rw").getChannel();
2、创建Buffer并指定缓冲区的大小,一般是获取ByteBuffer
//创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
3、读取数据
StringBuilder stringBuilder = new StringBuilder();
//通道被缓冲区读取数据(被read),直到读到的数据==-1
int read;
while ((read = channel.read(buffer)) != -1) {
System.out.println("read:" + read);
//转换方向?写到stringBuilder
buffer.flip();
while (buffer.hasRemaining()) {
stringBuilder.append((char) buffer.get());
}
buffer.clear();
}
4、关闭资源:
//关闭通道
channel.close();
案例二:将内容写入一个文件Channel中
1、获取文件通道
//打开一个channel
FileChannel fileChannel = new RandomAccessFile(filePath, "rw").getChannel();
2、创建Buffer,clear并且put(byte)
//创建一个bytebuffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//清理一下
buffer.clear();
3、只要Buffer里面有数据就channel.write(buffer)
//将内容put到buffer
buffer.put(context.getBytes(StandardCharsets.UTF_8));
//转换方向?由读转为写
buffer.flip();
//如果buffer里面有内容,就写入通道
while (buffer.hasRemaining()) {
fileChannel.write(buffer);
}
4、关闭资源:
fileChannel.close();
案例三:FileChannel之间的数据读写转换(不通过buffer)
通道之间的数据传输(不通过buffer,实现通道之间的零拷贝技术)transferTo和transferFrom
FileChannel fileChannel1 = new RandomAccessFile(path1, "rw").getChannel();
FileChannel fileChannel2 = new RandomAccessFile(path2, "rw").getChannel();
long position = fileChannel2.position();
long size = fileChannel2.size();
//transferFrom数据从哪里转移过来的,channel1的数据是从channel2转移过来的,也就是说数据从channel2往channel1写入
fileChannel1.transferFrom(fileChannel2, position, size);
fileChannel1.close();
fileChannel2.close();
FileChannel的其他重要方法:
force()是否强制写磁盘,
position()获取通道读取的位置,
size()获取文件大小,
truncate(int)截取对应字节的内容
Channel的另一种实现:Socket通道:
相关特点:
- 功能更强大,可以通过selector实现通道选择,一个线程可以管理多个socket,节约线程资源开销以及线程上下文切换
- DatagramChannel和SocketChannel实现了读和写的功能,但是ServerSocketChannel没有实现读写功能,也就是说ServerSocketChannel不能读写数据,主要负责监听socket的连接和创建新的SocketChannel,相当于一个监听器的功能,而SocketChannel实现了ByteChannel接口 继承了ReadableByteChannel, WritableByteChannel接口,具有读写功能(新版本ServerSocketChannel也可以读写)
- Channel与socket的关系,socket不能使用多种协议,而Channel可以
- Channel可以设置阻塞和非阻塞模式(一般服务端),而socket不能
实现ServerSocketChannel(基于TCP的服务端Channel)
//静态方法创建Channel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(888), 3000);
//设置非阻塞
serverSocketChannel.configureBlocking(false);
ByteBuffer buffer=ByteBuffer.wrap("test".getBytes(StandardCharsets.UTF_8),0,1024);
while (true) {
System.out.println("等待socket客户端链接中。。。。");
SocketChannel accept = serverSocketChannel.accept();
//没有链接时打印null并sleep
if (accept == null) {
System.out.println("没有设备链接:" + null);
Thread.sleep(2000);
} else {
//有链接时,打印远程客户端地址,写入buffer
//获取socket
Socket socket = accept.socket();
//获取socket的地址
SocketAddress remoteAddress = accept.getRemoteAddress();
System.out.println("remoteAddress:" + remoteAddress);
buffer.rewind();
accept.write(buffer);
serverSocketChannel.close();
}
}
实现SocketChannel(基于TCP的客户端Channel)
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));
System.out.println(socketChannel.isConnected());
System.out.println(socketChannel.isOpen());
socketChannel.configureBlocking(false);
System.out.println(socketChannel.isBlocking());
System.out.println(socketChannel.isRegistered());
socketChannel.configureBlocking(true);
2.2 Buffer:通道中的数据从一个Buffer(缓存区)中读取,用于与Channel进行交互,实现数据的写入和读出,本质上,Buffer是在NIO的读写的时候开辟的一块内存区域,从数据结构的角度来看,就是一个数组:
Buffer是一个抽象类,既然是数据类型中的数组,包含 capacity、limit和position,关系为:0 <= mark <= position <= limit <= capacity,缓冲区是非线程安全的,可以使用链式调用:buffer.flip().position(23).limit(42);有些缓冲区是只读的,有些是可读也可写的,如果只读的缓冲区,数据只能读,不能改变值,但可以mark,position,可以调用isReadOnly来判断缓冲区是否可读;几个重要方法:
Buffer在读模式下,position=0,limit=当前buffer内可读的数据的位置,capacity表示容量,固定的
Buffer在写模式下,position=下一个可写入的位置,limit=容量
clear,让buffer为新的序列的读取或put操作做好准备,并设置position = 0; limit = capacity; mark = -1;
flip,让buffer为新的序列的写入或get操作做好准备,并设置 limit = position; position = 0; mark = -1;读写模式转换
rewind:准备好重新读,倒带的意思,并且数据仍然在容器,limit不变,position=0;fileChannel.read(buffer);//从channel读取数据到buffer区,根据buffer的容量
buffer.flip();//转换读写模式,实际上是转换postion和limit的位置
buffer.remaining() > 0 //返回limit-position的值,>0说明buffer里面还有数据
hasRemaining() //返回position < limit的布尔值,用来判断buffer里面是否还有数据 buffer.get(); //获取数据
buffer.clear(); //清空数据,将position,limit恢复到初始状态,返回当前buffer,切换为写模式,实际上并未清除buffer的数据
buffer.compact(); //ByteBuffer才有该方法,Buffer没有,已读数据清空,未读完数据向前压缩,返回新缓冲区
buffer.get() //从缓冲区读取数据,实际上是获取缓冲区数组中某个index的值
buffer.rewind() //position置为0,从头开始读的意思
缓冲区分片:在缓冲区切一片作为一个子缓冲区,现有缓冲区和子缓冲区数据是共享的
//创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
//缓冲区写入内容
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put((byte) i);
}
//创建子缓冲区,还是当前缓冲区,因为position方法返回的是this,只是把position置为新的position
buffer.position(3);
buffer.limit(7);
//从缓冲区切出一个分片(子缓冲区)
ByteBuffer sliceBuffer = buffer.slice();
//改变子缓冲区的值
for (int i = 0; i < sliceBuffer.capacity(); i++) {
byte b = sliceBuffer.get();
b *= 10;
sliceBuffer.put(i, b);
}
//切换子缓冲区的读写模式(改为读模式)
sliceBuffer.flip();
//读取子缓冲区的数据
while (sliceBuffer.hasRemaining()) {
System.out.println(sliceBuffer.get());
}
//原缓冲区重新设置position和limit,不能使用flip()切换读写模式,因为分片时设置limit=limit-position=4
buffer.position(0);
//设置limit=原来的capacity
buffer.limit(buffer.capacity());
//读取原buffer的内容,实际上是原缓冲区+子缓冲区+原缓冲区的内容
while (buffer.hasRemaining()) {
System.out.println(buffer.get());
}
只读缓冲区:只能读取数据内容,不能写入内容
//设置为只读缓冲区,返回一个新缓冲区,与原缓冲区一直,只是readOnly=true
buffer.asReadOnlyBuffer();
直接缓冲区:
/** * 基本使用步骤: * 1、创建Buffer,clear一下 * 2、往Buffer里面put数据 * 3、flip一下,切换为读模式 * 4、读取数据 * 5、clear或compact */
Selector:选择器,也可以理解为多路复用器
1、与Channel的关系,用于检查一个或多个(NIO)Channel的状态是否处于可读、可写,可以实现一个线程管理多个通道(Channel),也就是多个网络连接,降低资源消耗
2、SelectableChannel,可选择的选择器,只有实现了SelectableChannel这个抽象类的的Channel,才能注册到选择器,
如SocketChannel继承了,可以注册到选择器,但FileChannel没继承,不能注册到选择器
3、一个通道可以注册到多个选择器,但是在每一个选择器上,只能注册一次,通道(Channel) -->继承SelectableChannel---->注册Selector
4、如何注册到选择器?
Channel.register(),并指定Selector感兴趣的事(可以理解为在Channel发生哪类事件时触发选择器的哪类Handler方法)
2、3个重要组件和概念:
3、管道,文件锁,