highlight: arduino-light
通道Channe概述
通道(Channel):由 java.nio.channels 包定义 的。Channel 表示 IO 源与目标打开的连接。 Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。
1、 NIO 的通道类似于流,但有些区别如下:
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读数据,也可以写数据到缓冲:
2、BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel) 是双向的,可以读操作,也可以写操作。
3、Channel 在 NIO 中是一个接口
public interface Channel extends Closeable{}
常用的Channel实现类
- FileChannel:用于读取、写入、映射和操作文件的通道。
- DatagramChannel:通过 UDP 读写网络中的数据通道。
- SocketChannel:通过 TCP 读写网络中的数据。
- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
ServerSocketChannel 类似 ServerSocket , SocketChannel 类似 Socket
netty又在ServerSocketChanne 和SocketChannel 做了进一步的封装,分别是NioServerSocketChannel、NioSocketChannel
ServerSocketChannel相关Api
java public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel{ //得到一个 ServerSocketChannel 通道 public static ServerSocketChannel open() //设置服务器端端口号 public final ServerSocketChannel bind(SocketAddress local) //设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式 public final SelectableChannel configureBlocking(boolean block) //接受一个连接,返回代表这个连接的通道对象 public SocketChannel accept() //注册一个选择器并设置监听事件 public final SelectionKey register(Selector sel, int ops) } //绑定一个端口号, 在服务器的6666监听 2个方式有什么区别 public final ServerSocketChannel bind(SocketAddress local)throws IOException{ return bind(local, 0); } //调用方法参数不一样,第二个参数backlog是0,调用serverSocketChannelImpl的bind方法 serverSocketChannel.bind(new InetSocketAddress(6666)); public void bind(SocketAddress var1) throws IOException { this.bind(var1, 50); } //调用方法参数不一样,第二个参数backlog是50 //通过ServerSocketAdaptor调用serverSocketChannelImpl的bind方法 serverSocketChannel.socket().bind(new InetSocketAddress(6666));
SocketChannel相关Api
java public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel{ public static SocketChannel open();//得到一个 SocketChannel 通道 设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式 public final SelectableChannel configureBlocking(boolean block); public boolean connect(SocketAddress remote);//连接服务器 public boolean finishConnect();//如果上面的方法连接失败,接下来就要通过该方法完成连接操作 public int write(ByteBuffer src);//往通道里写数据 public int read(ByteBuffer dst);//从通道里读数据 注册一个选择器并设置监听事件,最后一个参数可以设置共享数据 public final SelectionKey register(Selector sel, int ops, Object att); public final void close();//关闭通道 }
FileChannel 类
获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下:
- FileInputStream
- FileOutputStream
- RandomAccessFile
- DatagramSocket
- Socket
- ServerSocket 获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道
FileChannel的常用方法
java 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) 强制将所有对此通道的文件更新写入到存储设备中
案例1-本地文件写数据
需求:使用前面学习后的 ByteBuffer(缓冲) 和 FileChannel(通道), 将 "hello,黑马Java程序员!" 写入到 data.txt 中.
java package com.itheima; import org.junit.Test; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class ChannelTest { @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()); //此时 position=28 limit=1024 capacity=1024 // 4、把缓冲区切换成写出模式即 将position赋值给limit, position还原为0 buffer.flip(); //将缓存区的内容写入文件 channel.write(buffer); channel.close(); System.out.println("写数据到文件中!"); } catch (Exception e) { e.printStackTrace(); } } }
案例2-本地文件读数据
需求:使用前面学习后的 ByteBuffer(缓冲) 和 FileChannel(通道), 将 data01.txt 中的数据读入到程序,并显示在控制台屏幕
java public class ChannelTest { @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、读取数据到缓冲区 //此时 position=28 limit=1024 capacity=1024 channel.read(buffer); // 翻转即 将position赋值给limit, position还原为0 buffer.flip(); // 5、读取出缓冲区中的数据并输出即可 String rs = new String(buffer.array(),0,buffer.remaining()); System.out.println(rs); }
案例3-使用Buffer完成文件复制
使用 FileChannel(通道) ,完成文件的拷贝。
java @Test public void copy() throws Exception { // 源文件 File srcFile = new File("C:\Users\dlei\Desktop\BIO,NIO,AIO\文件\壁纸.jpg"); File destFile = new File("C:\Users\dlei\Desktop\BIO,NIO,AIO\文件\壁纸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){ // 必须先清空缓冲然后再写入数据到缓冲区 // position = 0; // limit = capacity; buffer.clear();//position=0 limit = 1024 capacity=1024 // 开始读取一次数据 int flag = isChannel.read(buffer);//position=1024 limit = 1024 capacity=1024 if(flag == -1){ break; } // 已经读取了数据 ,把缓冲区的模式切换成可读模式 // limit = position; // position = 0; buffer.flip();//position=0 limit = 1024 capacity=1024 // 把数据写出到缓冲区 osChannel.write(buffer);//position=1024 limit = 1024 capacity=1024 } isChannel.close(); osChannel.close(); System.out.println("复制完成!"); }
案例4-分散 (Scatter) 和聚集 (Gather)
分散读取(Scatter ):是指把Channel通道的数据读入到多个缓冲区中去
聚集写入(Gathering )是指将多个 Buffer 中的数据“聚集”到 Channel。
```java //分散和聚集 @Test public void test() throws IOException{ RandomAccessFile raf1 = new RandomAccessFile("1.txt", "rw"); //1. 获取通道 FileChannel channel1 = raf1.getChannel();
//2. 分配指定大小的缓冲区
ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
//3. 分散读取
ByteBuffer[] bufs = {buf1, buf2};
channel1.read(bufs);
for (ByteBuffer byteBuffer : bufs) {
byteBuffer.flip();
}
System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
System.out.println("-----------------");
System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
//4. 聚集写入
RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = raf2.getChannel();
channel2.write(bufs);
} ```
零拷贝-SendFile实现
案例5-osChannel-transferFrom()
从目标通道中去复制原通道数据
java @Test public void test02() throws Exception { // 1、字节输入管道 FileInputStream is = new FileInputStream("data01.txt"); FileChannel isChannel = is.getChannel(); // 2、字节输出流管道 FileOutputStream fos = new FileOutputStream("data03.txt"); FileChannel osChannel = fos.getChannel(); // 3、复制 osChannel.transferFrom(isChannel,isChannel.position(),isChannel.size()); isChannel.close(); osChannel.close(); }
案例6-isChannel-transferTo()
把原通道数据复制到目标通道
java @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(); }
Buffer 和 Channel的注意事项和细节
ByteBuffer 支持类型化的put 和 get, put 放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有 BufferUnderflowException 异常。
类型化的put和get
```java public class NIOByteBufferPutGet { public static void main(String[] args) { //创建一个Buffer ByteBuffer buffer = ByteBuffer.allocate(64); //类型化方式放入数据 buffer.putInt(100); buffer.putLong(9); buffer.putChar('尚'); buffer.putShort((short) 4);
//切换读模式 buffer.flip(); System.out.println(); //顺序取出 System.out.println(buffer.getInt()); System.out.println(buffer.getLong()); System.out.println(buffer.getChar()); System.out.println(buffer.getShort()); } } ```
普通Buffer 转成只读Buffer
java package com.atguigu.nio; import java.nio.ByteBuffer; public class ReadOnlyBuffer { public static void main(String[] args) { //创建一个buffer ByteBuffer buffer = ByteBuffer.allocate(64); for(int i = 0; i < 64; i++) { buffer.put((byte)i); } //读取 buffer.flip(); //得到一个只读的Buffer ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer(); System.out.println(readOnlyBuffer.getClass()); //读取 while (readOnlyBuffer.hasRemaining()) { System.out.println(readOnlyBuffer.get()); } readOnlyBuffer.put((byte)100); //ReadOnlyBufferException } }
MappedByteBuffer
NIO 还提供了 MappedByteBuffer, 可以让文件直接在内存(堆外的内存)中进行修改, 而如何同步到文件则由NIO 来完成.
java package com.atguigu.nio; import java.io.RandomAccessFile; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; /* 说明 1. MappedByteBuffer 可让文件在直接内存(堆外内存)修改, 操作系统不需要拷贝一次 */ public class MappedByteBufferTest { public static void main(String[] args) throws Exception { RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw"); //获取对应的通道 FileChannel channel = randomAccessFile.getChannel(); /** * 参数1: FileChannel.MapMode.READ_WRITE 使用的读写模式 * 参数2: 0 : 可以直接修改的起始位置 * 参数3: 5: 是映射到内存的大小(不是索引位置) ,即将 1.txt 的多少个字节映射到内存 * 可以直接修改的范围就是 0-5 * 实际类型 DirectByteBuffer */ MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); //修改1.txt中的第一个字节为H mappedByteBuffer.put(0, (byte) 'H'); //修改1.txt中的第四个字节是9 mappedByteBuffer.put(3, (byte) '9'); mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException randomAccessFile.close(); System.out.println("修改成功~~"); } }
前面我们讲的读写操作,都是通过一个Buffer 完成的,NIO 还支持 通过多个Buffer (即 Buffer 数组) 完成读写操作。
即 Scattering 和 Gathering 【举例说明】
Scattering 和 Gathering
java package com.atguigu.nio; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Arrays; /** * Scattering:将数据写入到buffer时,可以采用buffer数组,依次写入 [分散] * Gathering: 从buffer读取数据时,可以采用buffer数组,依次读 */ public class ScatteringAndGatheringTest { public static void main(String[] args) throws Exception { //使用 ServerSocketChannel 和 SocketChannel 网络 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); InetSocketAddress inetSocketAddress = new InetSocketAddress(7000); //绑定端口到socket ,并启动服务端 serverSocketChannel.socket().bind(inetSocketAddress); //创建buffer数组 ByteBuffer[] byteBuffers = new ByteBuffer[2]; byteBuffers[0] = ByteBuffer.allocate(5); byteBuffers[1] = ByteBuffer.allocate(3); //服务端等待客户端连接(telnet) SocketChannel socketChannel = serverSocketChannel.accept(); int messageLength = 8; //假定从客户端接收8个字节 //循环的读取 while (true) { int byteRead = 0; while (byteRead < messageLength ) { long l = socketChannel.read(byteBuffers); byteRead += l; //累计读取的字节数 System.out.println("byteRead=" + byteRead); //使用流打印, 看看当前的这个buffer的position 和 limit Arrays.asList(byteBuffers).stream().map(buffer -> "postion=" + buffer.position() + ", limit=" + buffer.limit()).forEach(System.out::println); } //将所有的buffer进行flip Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip()); //将数据读出显示到客户端 long byteWirte = 0; while (byteWirte < messageLength) { long l = socketChannel.write(byteBuffers); // byteWirte += l; } //将所有的buffer 进行clear Arrays.asList(byteBuffers).forEach(buffer-> { buffer.clear(); }); System.out.println("byteRead:=" + byteRead + " byteWrite=" + byteWirte + ", messagelength" + messageLength); } } }