缓冲区(buffer)
- 定义: 一个用于特定基本数据类型的容器
- 作用:用于与channel进行数据的交互,数据都是从通道读入缓冲区的,从缓冲区写入通道中去
子类:
Buffer 就像一个数组,可以保存多个相同类型的数据。根 据数据类型不同 ,有以下 Buffer 常用子类:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
创建方法 : ByteBuffer . allocate(int capacity) 里面可以指定自定义的字段类型
缓冲区的基本属性
- 容量 (capacity) :作为一个内存块,Buffer具有一定的固定大小,也称为"容量",缓冲区容量不能为负,并且创建后不能更改。
- 限制 (limit):表示缓冲区中可以操作数据的大小(limit 后数据不能进行读写)。缓冲区的限制不能为负,并且不能大于其容量。 写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量。
- 位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为 负,并且不能大于其限制
- 标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法 指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这 个 position.
标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity
ByteBuffer 结构
- capacity
- position
- limit
初始化状态
切换写模式。写入四个字节
调用filp()方法切换可读模式
全部读取完毕的时候,position发生了变化
执行clear()操作
compact 方法,是把未读完的部分向前压缩,然后切换至写模式
相关操作
- Buffer clear() 清空缓冲区并返回对缓冲区的引用
- Buffer flip() 为 将缓冲区的界限设置为当前位置,并将当前位置充值为 0
- int capacity() 返回 Buffer 的 capacity 大小
- boolean hasRemaining() 判断缓冲区中是否还有元素
- int limit() 返回 Buffer 的界限(limit)的位置 Buffer limit(int n) 将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象
- Buffer mark() 对缓冲区设置标记
- int position() 返回缓冲区的当前位置 position
- Buffer position(int n) 将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象
- int remaining() 返回 position 和 limit 之间的元素个数
- Buffer reset() 将位置 position 转到以前设置的 mark 所在的位置
- Buffer rewind() 将位置设为为 0, 取消设置的 mark
实例化操作演示
public class BufferTest {
@Test
public void test03() {
// 1、创建一个直接内存的缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
System.out.println(buffer.isDirect());
}
// 遍历输出buffer内容
@Test
public void tst() {
ByteBuffer buffer = ByteBuffer.allocate(10);
String a = "asd";
// String 类型转换为 byte
buffer.put(a.getBytes());
buffer.flip();
// System.out.println(buffer.get(a.getBytes(),0,buffer.limit()));
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println();
int i = 0;
while (i < a.length()) {
System.out.print((char) buffer.get(i));
i++;
}
System.out.println();
System.out.println("String ");
System.out.println(new String(a.getBytes(),0,buffer.limit()));
buffer.clear();
System.out.println(buffer.limit());
}
}
@Test
public void test02() {
// 1、分配一个缓冲区,容量设置成10
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println(buffer.position()); // 0
System.out.println(buffer.limit()); // 10
System.out.println(buffer.capacity()); // 10
System.out.println("----------------------------");
String name = "itheima";
buffer.put(name.getBytes());
System.out.println(buffer.position()); // 7
System.out.println(buffer.limit()); // 10
System.out.println(buffer.capacity()); // 10
System.out.println("--------------------------");
// 2、clear清除缓冲区中的数据
buffer.clear();
System.out.println(buffer.position()); // 0
System.out.println(buffer.limit()); // 10
System.out.println(buffer.capacity()); // 10
System.out.println((char) buffer.get());
// 3、定义一个缓冲区
ByteBuffer buf = ByteBuffer.allocate(10);
String n = "=================";
buf.put(n.getBytes());
buf.flip();
// 读取数据
byte[] b = new byte[2];
buf.get(b);
String rs = new String(b);
System.out.println(rs);
System.out.println(buf.position()); // 2
System.out.println(buf.limit()); // 7
System.out.println(buf.capacity()); // 10
System.out.println("---------------------");
buf.mark(); // 标记此刻这个位置! 2
byte[] b2 = new byte[3];
buf.get(b2);
System.out.println(new String(b2));
System.out.println(buf.position()); // 5
System.out.println(buf.limit()); // 7
System.out.println(buf.capacity()); // 10
buf.reset(); // 回到标记位置
if (buf.hasRemaining()) {
System.out.println(buf.remaining());
}
}
Channel通道
1、 定义:Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。
- NIO 的通道类似于流,但有些区别如下:
-
通道可以同时进行读写,而流只能读或者只能写
-
通道可以实现异步读写数据
-
通道可以从缓冲读数据,也可以写数据到缓冲:
2、BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)
是双向的,可以读操作,也可以写操作。
3、Channel 在 NIO 中是一个接口
常用的Channel实现类
- FileChannel:用于读取、写入、映射和操作文件的通道。
- DatagramChannel:通过 UDP 读写网络中的数据通道。
- SocketChannel:通过 TCP 读写网络中的数据。
- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket】
FileChannel 类
-
获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下:
* FileInputStream * FileOutputStream * RandomAccessFile * DatagramSocket * Socket * ServerSocket 获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道
-
常用方法
- int read(ByteBuffer dst) 从 从 Channel 到 中读取数据到 ByteBuffer
- long read(ByteBuffer[] dsts) 将 将 Channel 到 中的数据“分散”到 ByteBuffer[]
- int write(ByteBuffer src) 将 将 ByteBuffer 到 中的数据写入到 Channel
- long write(ByteBuffer[] srcs) 将 将 ByteBuffer[] 到 中的数据“聚集”到 Channel
- long position() 返回此通道的文件位置
- FileChannel position(long p) 设置此通道的文件位置
- long size() 返回此通道的文件的当前大小
- FileChannel truncate(long s) 将此通道的文件截取为给定大小
- void force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中
实例
写入操作
@Test
public void write(){
try {
// 1、字节输出流通向目标文件
FileOutputStream fos = new FileOutputStream("data01.txt");
// 2、得到字节输出流对应的通道Channel
FileChannel channel = fos.getChannel();
// 3、分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello,黑马Java程序员!".getBytes());
// 4、把缓冲区切换成写出模式
buffer.flip();
channel.write(buffer);
channel.close();
System.out.println("写数据到文件中!");
} catch (Exception e) {
e.printStackTrace();
}
}
读取操作
@Test
public void read() throws Exception {
// 1、定义一个文件字节输入流与源文件接通
FileInputStream is = new FileInputStream("data01.txt");
// 2、需要得到文件字节输入流的文件通道
FileChannel channel = is.getChannel();
// 3、定义一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4、读取数据到缓冲区
channel.read(buffer);
buffer.flip();
// 5、读取出缓冲区中的数据并输出即可
String rs = new String(buffer.array(),0,buffer.remaining());
System.out.println(rs);
}
使用Buffer完成文件复制
@Test
public void copy() throws Exception {
// 源文件
File srcFile = new File("C:\\Users\\a.jpg");
File destFile = new File("C:\\Users\\new.jpg");
// 得到一个字节字节输入流
FileInputStream fis = new FileInputStream(srcFile);
// 得到一个字节输出流
FileOutputStream fos = new FileOutputStream(destFile);
// 得到的是文件通道
FileChannel isChannel = fis.getChannel();
FileChannel osChannel = fos.getChannel();
// 分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(true){
// 必须先清空缓冲然后再写入数据到缓冲区
buffer.clear();
// 开始读取一次数据
int flag = isChannel.read(buffer);
if(flag == -1){
break;
}
// 已经读取了数据 ,把缓冲区的模式切换成可读模式
buffer.flip();
// 把数据写出到
osChannel.write(buffer);
}
isChannel.close();
osChannel.close();
System.out.println("复制完成!");
}
分散 (Scatter) 和聚集 (Gather)
分散读取(Scatter ):是指把Channel通道的数据读入到多个缓冲区中去
聚集写入(Gathering )是指将多个 Buffer 中的数据“聚集”到 Channel。
@Test
public void test() throws Exception {
// 1、字节输入管道
FileInputStream is = new FileInputStream("data01.txt");
FileChannel isChannel = is.getChannel();
// 2、字节输出流管道
FileOutputStream fos = new FileOutputStream("data02.txt");
FileChannel osChannel = fos.getChannel();
// 3、定义多个缓冲区做数据分散
ByteBuffer buffer1 = ByteBuffer.allocate(4);
ByteBuffer buffer2 = ByteBuffer.allocate(1024);
ByteBuffer[] buffers = {buffer1 , buffer2};
// 4、从通道中读取数据分散到各个缓冲区
isChannel.read(buffers);
// 5、从每个缓冲区中查询是否有数据读取到了
for(ByteBuffer buffer : buffers){
buffer.flip();// 切换到读数据模式
System.out.println(new String(buffer.array() , 0 , buffer.remaining()));
}
// 6、聚集写入到通道
osChannel.write(buffers);
isChannel.close();
osChannel.close();
System.out.println("文件复制~~");
}
transferFrom()
指定目标文件的复制
@Test
public void test02() throws Exception {
// 1、字节输入管道
FileInputStream is = new FileInputStream("data01.txt");
FileChannel isChannel = is.getChannel();
// 2、字节输出流管道
FileOutputStream fos = new FileOutputStream("data04.txt");
FileChannel osChannel = fos.getChannel();
// 3、复制数据
// osChannel.transferFrom(isChannel , isChannel.position() , isChannel.size());
isChannel.transferTo(isChannel.position() , isChannel.size() , osChannel);
isChannel.close();
osChannel.close();
System.out.println("完成复制!");
}
transferTo()
@Test
public void test02() throws Exception {
// 1、字节输入管道
FileInputStream is = new FileInputStream("data01.txt");
FileChannel isChannel = is.getChannel();
// 2、字节输出流管道
FileOutputStream fos = new FileOutputStream("data04.txt");
FileChannel osChannel = fos.getChannel();
// 3、复制
isChannel.transferTo(isChannel.position() , isChannel.size() , osChannel);
isChannel.close();
osChannel.close();
}
selector选择器
- 概述:选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心
- Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)
- Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个
Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管
理多个通道,也就是管理多个连接和请求。 - 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都
创建一个线程,不用去维护多个线程 - 避免了多线程之间的上下文切换导致的开销
//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数
ops 指定。可以监听的事件类型(用 可使用 SelectionKey 的四个常量 表示):
- 读 : SelectionKey.OP_READ (1)
- 写 : SelectionKey.OP_WRITE (4)
- 连接 : SelectionKey.OP_CONNECT (8)
- 接收 : SelectionKey.OP_ACCEPT (16)
- 若注册时不止监听一个事件,则可以使用“位或”操作符连接。