基础 | NIO - [Channel]

@

§1 概述

  • 是可以对数据源操作的双向通道
  • 操作的数据源可以是文件,也可以是网络 IO
  • 连接 字节缓冲区另一端的实体
  • 主要实现包括
    • FileChannel
    • AsynchronousFileChannel
    • DatagramChannel
    • SocketChannel
    • ServerSocketChannel

与 stream 区别

  • 双向
  • 异步
  • 不直接读写数据(读写数据的工作交给缓冲区)

§2 FileChannel

§2.1 FileChannel

作用
读写文件的通道

常用方法
打开 FileChannel

  • 通过 RandomAccessFile 获取
    // 创建 RandomAccessFile 文件
    RandomAccessFile raf = new RandomAccessFile(file,mode;
    // 从 RandomAccessFile 文件获取 FileChannel
    FileChannel channel = raf.getChannel();
    
  • 通过 FileChannel.open() 开启
    FileChannel.open(path, options);
    

FileChannel

int length = channel.read(buf);
  • 必须借助缓冲区
  • 返回读取到的长度

FileChannel

channel.write(buf);
channel.write(buf,position);
channel.write(buf[]);
  • 必须借助缓冲区
  • annel.write(buf) 默认向 FileChannel 关联文件的结尾写
    效果等同于 annel.write(buf,buf.size())
  • 可以同时使用多个缓冲区以达到分散读取聚集写入的效果

关闭 FileChannel

channel.close();
  • 注意处理异常
  • 推荐使用 try-with-resource 的写法,可以省略 close()
try (
     RandomAccessFile raf = new RandomAccessFile(path + file,"rw");
     FileChannel channel = raf.getChannel()
){

} catch (Exception e){ /* 异常处理 */ }

操作 FileChannel 位置

// 获取 FileChannel 的位置
long pos = channel.position();
// 设置 FileChannel 的位置
channel.position(pos + 100);
  • 通过 position() 可以实现读写 FileChannel 的指定位置
  • 设置 position 时,若超出了文件结束符,并向其中写入数据,会形成文件空洞

获取 FileChannel 关联文件的大小

channel.size();
  • RandomAccessFile.length() 的返回相同

截取 FileChannel 关联文件

channel.truncate(length);
  • 用于截取 FileChannel 关联的文件的前 length 个字节

强制写 FileChannel 关联文件

channel.force();
  • 用于将 FileChannel 中未完成写操作的内容强制写入磁盘
    NIO 是异步的,默认不保证写入 FileChannel 的数据可以实时写入磁盘
    当需要保证数据即时写入磁盘时,需要调用 force()
  • 可以传入 boolean 参数,以指定文件元数据是否也强制写入磁盘

FileChannel 间传递数据

channel.transferFrom(fromChannel,0,fromChannel.size());
channel.transferTo(0,channel.size(), toChannel);
  • transferFrom()transferTo() 中任一都可以实现在 FileChannel 间传递数据
  • 通过 transferFrom()transferTo() 传输数据不需要缓冲区
    • 方法内部会自动使用一个 8M/2G 的缓冲区
    • 若传输数据长度小于 8M/2G ,缓冲区大小就使用此长度

Selector 注册
参考 基础 | NIO - [Selector]#注册

标准写法

读文件

public void read(String path, String file, int buffer) {
    try (
            RandomAccessFile raf = new RandomAccessFile(path + file,"rw");
            // 获取 FileChannel
            FileChannel channel = raf.getChannel()
    ){
        // 创建读取时的 缓冲区
        ByteBuffer buf = ByteBuffer.allocate(buffer);
        // 读取 channel.read(buf)
        for(int length = channel.read(buf); length != -1; length = channel.read(buf)){
            buf.flip();

            //读取后的动作
            for(;buf.hasRemaining();)
                System.out.print((char) buf.get());

            buf.clear();
        }
    } catch (Exception e){ /* 异常处理 */ }
}

写文件

public void write(String path, String file, int buffer, String content) {
    try (
            RandomAccessFile raf = new RandomAccessFile(path + file,"rw");
            // 获取 FileChannel
            FileChannel channel = raf.getChannel();
    ){
        // 创建读取时的 缓冲区
        ByteBuffer buf = ByteBuffer.allocate(buffer);

        buf.clear();
        buf.put(content.getBytes(StandardCharsets.UTF_8));
        buf.flip();

        for(;buf.hasRemaining();){
            channel.write(buf);
        }
    } catch (Exception e){ /* 异常处理 */ }
}

复制文件

public void copy(String srcFilePath, String descFilePath){
    try (
            RandomAccessFile srcFile = new RandomAccessFile(srcFilePath,"r");
            RandomAccessFile descFile = new RandomAccessFile(descFilePath,"rw");
            FileChannel rc = srcFile.getChannel();
            FileChannel wc = descFile.getChannel();
    ) {
       ByteBuffer buf = ByteBuffer.allocate(1024);
        for(int length=rc.read(buf) ; ; buf.clear(), length= rc.read(buf)){
            if(-1==length)
                break;
            buf.flip();
            while (buf.hasRemaining()) {
                wc.write(buf);
            }
        }
    } catch (Exception e){ /* 异常处理 */ }
}


复制文件,基于 transfer

public void copyByTransfer(String srcFilePath, String descFilePath){
    try (
            RandomAccessFile srcFile = new RandomAccessFile(srcFilePath,"r");
            RandomAccessFile descFile = new RandomAccessFile(descFilePath,"rw");
            FileChannel rc = srcFile.getChannel();
            FileChannel wc = descFile.getChannel();
    ) {
        rc.transferTo(0,rc.size(),wc);
    } catch (Exception e){ /* 异常处理 */ }
}

复制文件,基于 Scatter / Gather

public void copyScatterGather(String srcFilePath, String descFilePath){
    try (
            RandomAccessFile srcFile = new RandomAccessFile(srcFilePath,"r");
            RandomAccessFile descFile = new RandomAccessFile(descFilePath,"rw");
            FileChannel rc = srcFile.getChannel();
            FileChannel wc = descFile.getChannel();
    ) {
        ByteBuffer[] bufs = new ByteBuffer[5];
        Arrays.setAll(bufs, e->ByteBuffer.allocate(100));

        for(long length=rc.read(bufs) ; length!=-1 ; length=rc.read(bufs)){
            Arrays.stream(bufs).forEach(ByteBuffer::flip);
            wc.write(bufs);
            Arrays.stream(bufs).forEach(ByteBuffer::clear);
        }

    } catch (Exception e){ /* 异常处理 */ }
}

§2.2 AsynchronousFileChannel

作用
异步读写文件的通道

常用方法
创建
open()

AsynchronousFileChannel channel = AsynchronousFileChannel.open(path,StandardOpenOption.READ);

异步读
future()

Future<Integer> count = channel.read(buf, pos); 

标准写法
异步读文件

public void read(String path, String file, int buffer) {
    try (
            AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get(path+file), StandardOpenOption.READ)
    ){
        ByteBuffer buf = ByteBuffer.allocate(buffer);
        long pos = 0;// 文件读取位置
        List<byte[]> datas = new ArrayList<>();
        for(Future<Integer> count = channel.read(buf, pos); pos<channel.size();pos+= count.get(), count = channel.read(buf,pos)){
            while(!count.isDone()){
                TimeUnit.MILLISECONDS.sleep(200);
            }
            buf.flip();

            //读取后的动作
            datas.add(ArrayUtil.sub(buf.array(),0,Math.min(buf.array().length,count.get())));

            buf.clear();
        }
        System.out.println(new String(ArrayUtil.addAll(datas.toArray(new byte[][]{})), StandardCharsets.UTF_8));
    } catch (Exception e){ /* 异常处理 */ }
}

异步读文件(CompletionHandler)
案例比想象中的复杂,因此另开了一个帖子
详情参考 坑 | NIO - [AsynchronousFileChannel + CompletionHandler]

§3 SocketChannel

下面三个 channel 都属于 SocketChannel

  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel
DatagramChannel SocketChannel ServerSocketChannel
可被多路复用
可读写 ×

SocketSocketChannel

  • Socket 不需要继续实现对应 SocketChannel 的API
  • 可以从SocketChannel 实例中通过 getSocket() 获取对应的 socket() 对象
  • 也可以在 Socket 实例通过 getChannel() 获取关联的 SocketChannel
  • SocketChannel 可以通过 AbstractSelectableChannel 实现多路复用,达到异步效果

§3.1 ServerSocketChannel

特点

  • 是基于通道的 socket 监听器,功能类似 ServerSocket,相当于非阻塞版
  • ServerSocketChannel 通过获取其 socket 实例以绑定端口进行监听
  • ServerSocketChannelaccept() 方法返回可以在非阻塞模式下运行的 SocketChannel

常用方法
打开 ServerSockerChannel

ServerSocketChannel channel = ServerSocketChannel.open();
  • 通过 ServerSocketChannel.open() 打开

关闭 ServerSockerChannel

channel.close();
  • 通过 ServerSocketChannel.close() 关闭
  • 推荐使用 try-with-resource 语法

建立 ServerSockerChannel 与端口的绑定

channel.bind(new InetSocketAddress(ip,port));
  • 可以直接调用 ServerSocketChannel.bind()
  • 可以直接通过获取其对应 socket 并在此实例上 bind()

监听网络连接

SocketChannel accepted = channel.accept();
  • 工作在阻塞模式下的 ServerSockerChannel 会阻塞在 accept() 方法上
  • 工作在非阻塞模式下的 ServerSockerChannel 不会阻塞,但因此可能得到 null ,需要判断
  • accept() 会返回一个工作在非阻塞模式下的 SockerChannel

标准写法

public void listen(String ip, int port,String content){
  try (
            ServerSocketChannel channel = ServerSocketChannel.open();
    ){
        channel.bind(new InetSocketAddress(ip,port));
        channel.configureBlocking(false);

        SocketChannel accepted = null;
        while(true){
            accepted = channel.accept();
            if(null==accepted) {
                TimeUnit.SECONDS.sleep(1);
                continue;
            }
            System.out.println(accepted.socket().getRemoteSocketAddress());
            accepted.close();
        }

    } catch (Exception e) { /* 异常处理 */ }
}

§3.2 SocketChannel

特点

  • 是对 socket 的异步包装,用于处理网络 IO 的通道,基于 TCP 连接
  • 通过 open() 打开,通过 connect() 连接到指定地址,对未连接的 SocketChannel 进行 IO 操作会抛异常
  • 支持阻塞和非阻塞模式,可以被多路复用
  • 支持异步关闭
    • 读阻塞时,其他线程通过 shutdownInput 中断阻塞,没读到数据时阻塞线程返回 -1
    • 写阻塞时,其他线程通过 shutdownWrite 中断阻塞,阻塞线程抛出 AsynchronousCloseException

常用参数

  • SO_SNDBUF 发送缓冲区大小
  • SO_RCVBUF 接受缓冲区大小
  • SO_KEEPALIVE 连接保持存活
  • O_REUSEADDR 复用地址
  • SO_LINGER 非阻塞模式下,数据传输过程中延时关闭 SocketChannel
  • TCP_NODELY 禁用 Nagle 算法,在不确认数据时,不启用缓存去缓存数据

常用方法
打开 SockerChannel并连接

SocketChannel channel = SocketChannel.open(new InetSocketAddress(ip,port));
// 或
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress(ip,port));
  • 通过 SocketChannel.open() 打开
  • 可以在 open() 的同时连接,也可以 open() 后通过 connect() 连接

关闭 SockerChannel

channel.close();
  • 通过 SocketChannel.close() 关闭
  • 推荐使用 try-with-resource 语法

测试 SockerChannel连接

boolean opened = channel.isOpen();
boolean connecting = channel.isConnectionPending();
boolean connected = channel.isConnected();
boolean finished = channel.finishConnect();
  • 是否已经打开
  • 是否建立连接中
  • 是否已经建立连接
  • 是否已经结束连接

设置 SockerChannel 是否阻塞

channel.configureBlocking(false);

SockerChannel 中读

channel.read(buf);

SockerChannel 中获取参数

channel.getOptition(StandardSocketOptions.SO_KEEPALIVE);
  • 参数见上文 常见参数
  • 参数已被 StandardSocketOptions 封装

SockerChannel 设置参数

channel.setOption(StandardSocketOptions.SO_KEEPALIVE,Boolean.TRUE);
  • 参数见上文 常见参数
  • 参数已被 StandardSocketOptions 封装

监听网络连接

SocketChannel accepted = channel.accept();
  • 工作在阻塞模式下的 ServerSockerChannel 会阻塞在 accept() 方法上
  • 工作在非阻塞模式下的 ServerSockerChannel 不会阻塞,但因此可能得到 null ,需要判断
  • accept() 会返回一个工作在非阻塞模式下的 SockerChannel

标准写法

§3.3 DatagramChannel

特点

  • 是对 datafram 的异步包装,用于处理网络 IO 的通道,基于 UDP 连接
  • 通过 open() 打开,DatagramChannel 是无连接的
    • 可以发送数据报给不同地址
    • 可以接受不同地址发来的数据报
    • 每个数据报都包含原地址

常用方法
打开 DatagramChannel

DatagramChannel channel = DatagramChannel.open();
  • 通过 DatagramChannel.open() 打开
  • 可以在 open() 的同时连接,也可以 open() 后通过 connect() 连接

关闭 DatagramChannel

channel.close();
  • 通过 DatagramChannel.close() 关闭
  • 推荐使用 try-with-resource 语法

DatagramChannel 绑定

channel.bind(new InetSocketAddress(ip,port));
  • 建立绑定后可以使用 DatagramChannel 进行 receive()send() 操作

DatagramChannel 绑定

channel.bind(new InetSocketAddress(ip,port));
  • 建立连接后可以使用 DatagramChannel 进行 read()write() 操作
  • 因为 DatagramChannel 是基于 UDP 的,因此不存在真正意义上的连接
    这里的连接只是为了可以进行读写操作
  • 未连接使用读写方法会抛出 NotYetConnectedException
  • read() 没有接收到数据报时,抛出 PortUnreachableException

通过 DatagramChannel 接收数据

ByteBuffer buf = ByteBuffer.allocate(1024);
SocketAddress received = channel.receive(buf);

通过 DatagramChannel 发送数据

ByteBuffer buf = ByteBuffer.wrap(content.getBytes());
channel.send(buf,new InetSocketAddress(ip,port));

DatagramChannel 中读

// 先
channel.connect(new InetSocketAddress(ip,port));
// 然后才可以
int leangth = channel.read(buf);

DatagramChannel 中写

// 先
channel.connect(new InetSocketAddress(ip,port));
// 然后才可以
channel.write(buf);

标准写法
本地给本地发包

public void receive(int port){
    try(
            DatagramChannel channel = DatagramChannel.open();
    ) {
        channel.bind(new InetSocketAddress(port));

        for(ByteBuffer buf = ByteBuffer.allocate(1024);;buf.clear()){
            SocketAddress received = channel.receive(buf);
            if(null == received){
                TimeUnit.SECONDS.sleep(1);
            }

            buf.flip();
            System.out.println(received.toString());
            System.out.println(Charset.forName("UTF-8").decode(buf));
        }
    } catch (Exception e) { e.printStackTrace();}
}
public void send(String ip,int port){
    try(
            DatagramChannel channel = DatagramChannel.open();
    ) {
        ByteBuffer buf = null;
        String content = null;

        for(;;buf.clear()){
            content = String.valueOf(System.currentTimeMillis());
            buf = ByteBuffer.wrap(content.getBytes());
            channel.send(buf,new InetSocketAddress(ip,port));
            System.out.println(content);
            TimeUnit.SECONDS.sleep(1);
        }
    } catch (Exception e) { e.printStackTrace(); }
}
posted @ 2025-05-20 14:51  问仙长何方蓬莱  阅读(10)  评论(0)    收藏  举报