7.JAVA NIO核心之通道(Channel)

通道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

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

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的常用方法

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 中.

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 中的数据读入到程序,并显示在控制台屏幕

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(通道) ,完成文件的拷贝。

@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。

//分散和聚集
@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()

从目标通道中去复制原通道数据

@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()

把原通道数据复制到目标通道

@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

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

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 来完成.

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

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);
        }
    }
}
全部评论

相关推荐

醒工硬件:如果你想投硬件,可以考虑这么改: 1.个人荣誉没太有保留价值,除非一页凑不满 2.主修课程太多了,可以考虑删减一部分,或者分成硬件和嵌入式2个简历,侧重点不一样 3.个人技能放到学习经历下面,项目经历上面。学习一下AD画板,你有基础一两周也差不多学会了,面试官问你就说你会(总不能拉你实操吧),公司里一般用AD和Cadence比较多,AD好上手一些。增加常用仪器工具说明,例如示波器、信号发生器、电子负载、烙铁、风枪等 4.项目,项目可以多换换行,挤在一起不好阅读。可以说下红外那边用什么接口,蓝牙那边用什么接口,用了哪些关键技术点,多用术语。如果你投硬件,就增加项目1描述比重,降低项目2描述比重
点赞 评论 收藏
分享
01-26 18:45
门头沟学院 Java
一天代码十万三:哥们实习再包一下吧,产出太笼统了,尽量体现业务
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务