Java NIO Channel 通道


Channel 类(通道)

  • Channel(通道):Channel 是一个对象,可以通过它读取和写入数据。可以把它看做是 IO 中的流,不同的是:
    • 为所有的原始类型提供(Buffer)缓存支持;
    • 字符集编码解决方案(Charset);
    • Channel : 一个新的原始 I/O 抽象;
    • 支持锁和内存映射文件的文件访问接口;
    • 提供多路(non-bloking)非阻塞式的高伸缩性网路 I/O;
  • 正如上面提到的,所有数据都通过 Buffer 对象处理,所以,你永远不会将字节直接写入到 Channel 中,相反,您是将数据写入到 Buffer 中;同样,您也不会从 Channel 中读取字节,而是将数据从 Channel 读入 Buffer,再从 Buffer 获取这个字节;
  • 因为 Channel 是双向的,所以 Channel 可以比流更好地反映出底层操作系统的真实情况。特别是在 Unix 模型中,底层操作系统通常都是双向的;
  • 在 Java NIO 中的 Channel 主要有如下几种类型:
    • FileChannel:从文件读取数据;
    • DatagramChannel:读写 UDP 网络协议数据;
    • SocketChannel:读写 TCP 网络协议数据;
    • ServerSocketChannel:可以监听 TCP 连接;

1. FileChannel 类的基本使用

  • java.nio.channels.FileChannel:是用于读、写文件的通道;
  • FileChannel 是抽象类,可以通过 FileInputStream 和 FileOutputStream 的 getChannel() 方法方便的获取一个它的子类对象;
    FileInputStream fi = new FileInputStream(new File(src));
    FileOutputStream fo = new FileOutputStream(new File(dst));
    //获得传输通道channel
    FileChannel inChannel = fi.getChannel();
    FileChannel outChannel = fo.getChannel();
  • 通过 CopyFile 的例子可以更好体会 NIO 的操作过程。CopyFile 执行三个基本的操作:创建一个 Buffer,然后从源文件读取数据到缓冲区,然后再将缓冲区写入目标文件:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class Test {
    public static void main(String[] args) throws Exception {
        //声明源文件和目标文件
        FileInputStream fi = new FileInputStream("C:\\Users\\80626\\Desktop\\1.png");
        FileOutputStream fo = new FileOutputStream("C:\\Users\\80626\\Desktop\\1_copy.png");
        //获得传输通道channel
        FileChannel inChannel = fi.getChannel();
        FileChannel outChannel = fo.getChannel();
        //获得容器buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int eof = 0;
        while ((eof = inChannel.read(buffer)) != -1) {//读取的字节将会填充buffer的position到limit位置
            //重设一下buffer:limit=position , position=0
            buffer.flip();
            //开始写
            outChannel.write(buffer);//只输出position到limit之间的数据
            //写完要重置buffer,重设position=0,limit=capacity,用于下次读取
            buffer.clear();
        }
        inChannel.close();
        outChannel.close();
        fi.close();
        fo.close();
    }
}

2. FileChannel 结合 MappedByteBuffer 实现高效读写

  • 上例直接使用 FileChannel 结合 ByteBuffer 实现的管道读写,但并不能提高文件的读写效率,ByteBuffer 有个子类:MappedByteBuffer,它可以创建一个“直接缓冲区”,并可以将文件直接映射至内存,可以提高大文件的读写效率;
  • ByteBuffer 是抽象类,MappedByteBuffer 也是抽象类;
  • 可以调用 FileChannel 的 map() 方法获取一个 MappedByteBuffer,map() 方法的原型:MappedByteBuffer map(MapMode mode, long position, long size);:将节点中从 position 开始的 size 个字节映射到返回的 MappedByteBuffer 中;

a. 复制 2 GB 以下的文件

  • 此例不能复制大于 2 G 的文件,因为 map 的第三个参数被限制在 Integer.MAX_VALUE(字节) = 2G
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class Test {
    public static void main(String[] args) throws Exception {
        try {
            //java.io.RandomAccessFile类,可以设置读、写模式的IO流类。
            //"r"表示:只读--输入流,只读就可以。
            RandomAccessFile source = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1.png", "r");
            //"rw"表示:读、写--输出流,需要读、写。
            RandomAccessFile target = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1_copy.png", "rw");
            //分别获取FileChannel通道
            FileChannel in = source.getChannel();
            FileChannel out = target.getChannel();
            //获取文件大小
            long size = in.size();
            //调用Channel的map方法获取MappedByteBuffer
            MappedByteBuffer mbbi = in.map(FileChannel.MapMode.READ_ONLY, 0, size);
            MappedByteBuffer mbbo = out.map(FileChannel.MapMode.READ_WRITE, 0, size);
            long start = System.currentTimeMillis();
            System.out.println("开始...");
            for (int i = 0; i < size; i++) {
                byte b = mbbi.get(i);//读取一个字节
                mbbo.put(i, b);//将字节添加到mbbo中
            }
            long end = System.currentTimeMillis();
            System.out.println("用时: " + (end - start) + " 毫秒");
            source.close();
            target.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/*
输出
开始...
用时: 2 毫秒
 */
  • map() 方法的第一个参数 mode:映射的三种模式,在这三种模式下得到的将是三种不同的 MappedByteBuffer:三种模式都是 Channel 的内部类 MapMode 中定义的静态常量,这里以 FileChannel 举例:
    (1)FileChannel.MapMode.READ_ONLY:得到的镜像只能读不能写(只能使用 get 之类的读取 Buffer 中的内容);
    (2)FileChannel.MapMode.READ_WRITE:得到的镜像可读可写(既然可写了必然可读),对其写会直接更改到存储节点;
    (3)FileChannel.MapMode.PRIVATE:得到一个私有的镜像,其实就是一个 (position, size) 区域的副本罢了,也是可读可写,只不过写不会影响到存储节点,就是一个普通的 ByteBuffer 了;
  • 为什么使用 RandomAccessFile?
    (1) 使用 InputStream 获得的 Channel 可以映射,使用 map 时只能指定为 READ_ONLY 模式,不能指定为 READ_WRITE 和 PRIVATE,否则会抛出运行时异常;
    (2) 使用 OutputStream 得到的 Channel 不可以映射!并且 OutputStream 的 Channel 也只能 write不能 read;
    (3) 只有 RandomAccessFile 获取的 Channel 才能开启任意的这三种模式;

b. 复制 2 GB 以上的文件

  • 下例使用循环,将文件分块,可以高效的复制大于 2 G 的文件;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class Test {
    public static void main(String[] args) throws Exception {
        try {
            RandomAccessFile source = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1.png", "r");
            RandomAccessFile target = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1_copy.png", "rw");
            FileChannel in = source.getChannel();
            FileChannel out = target.getChannel();
            long size = in.size();//获取文件大小
            long count = 1;//存储分的块数,默认初始化为:1
            long copySize = size;//每次复制的字节数,默认初始化为:文件大小
            long everySize = 1024 * 1024 * 512;//每块的大小,初始化为:512M
            if (size > everySize) {//判断文件是否大于每块的大小
                //判断"文件大小"和"每块大小"是否整除,来计算"块数"
                count = (int) (size % everySize != 0 ? size / everySize + 1 : size / everySize);
                //第一次复制的大小等于每块大小。
                copySize = everySize;
            }

            MappedByteBuffer mbbi = null;//输入的MappedByteBuffer
            MappedByteBuffer mbbo = null;//输出的MappedByteBuffer
            long startIndex = 0;//记录复制每块时的起始位置
            long start = System.currentTimeMillis();
            System.out.println("开始...");
            for (int i = 0; i < count; i++) {
                mbbi = in.map(FileChannel.MapMode.READ_ONLY, startIndex, copySize);
                mbbo = out.map(FileChannel.MapMode.READ_WRITE, startIndex, copySize);

                for (int j = 0; j < copySize; j++) {
                    byte b = mbbi.get(i);
                    mbbo.put(i, b);
                }
                startIndex += copySize;//计算下一块的起始位置
                //计算下一块要复制的字节数量。
                copySize = in.size() - startIndex > everySize ? everySize : in.size() - startIndex;
            }


            long end = System.currentTimeMillis();
            source.close();
            target.close();
            System.out.println("用时: " + (end - start) + " 毫秒");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/*
输出
开始...
用时: 4 毫秒
 */

3. ServerSocketChannel 和 SocketChannel 创建连接

a. 服务器端 ServerSocketChannel

  • 该类用于连接的服务器端,它相当于:ServerSocket;(1) 调用 ServerSocketChannel 的静态方法 open():ServerSocketChannel serverChannel = ServerSocketChannel.open();,打开一个通道,新频道的套接字最初未绑定;必须通过其套接字的 bind 方法将其绑定到特定地址,才能接受连接;
    (2) 调用 ServerSocketChannel 的实例方法 bind(SocketAddress add):serverChannel.bind(new InetSocketAddress(8888));,绑定本机监听端口,准备接受连接。注意,java.net.SocketAddress 是一个抽象类,代表一个 Socket 地址,可以使用它的子类:java.net.InetSocketAddress 类,其构造方法 InetSocketAddress(int port) 可以指定本机监听端口;
    (3) 调用 ServerSocketChannel 的实例方法 accept():SocketChannel accept = serverChannel.accept(); System.out.println("后续代码...");,等待连接;

i. 示例:服务器端等待连接(默认-阻塞模式)

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class Test {
    public static void main(String[] args) {
        try (ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
            serverChannel.bind(new InetSocketAddress(8888));
            System.out.println("【服务器】等待客户端连接...");
            SocketChannel accept = serverChannel.accept();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
/*
输出
【服务器】等待客户端连接...
 */

ii. 通过 ServerSocketChannel 的 configureBlocking(boolean b) 方法设置 accept() 是否阻塞

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class Test {
    public static void main(String[] args) throws Exception {
        try (ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
            serverChannel.bind(new InetSocketAddress(8888));
            System.out.println("【服务器】等待客户端连接...");
            //   serverChannel.configureBlocking(true);//默认--阻塞
            serverChannel.configureBlocking(false);//非阻塞
            SocketChannel accept = serverChannel.accept();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
/*
输出
【服务器】等待客户端连接...
 */
  • 可以看到,accept() 方法并没有阻塞,而是直接执行后续代码,返回值为 null;
  • 这种非阻塞的方式,通常用于"客户端"先启动,"服务器端"后启动,来查看是否有客户端连接,有,则接受连接;没有,则继续工作;

b. 客户端 SocketChannel

  • 该类用于连接的客户端,它相当于:Socket;
    (1) 先调用 SocketChannel 的 open() 方法打开通道:ServerSocketChannel serverChannel = ServerSocketChannel.open();
    (2) 调用 SocketChannel 的实例方法 connect(SocketAddress add) 连接服务器:socket.connect(new InetSocketAddress("localhost", 8888));

i. 示例:客户端连接服务器

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;

public class Test {
    public static void main(String[] args) {
        try (SocketChannel socket = SocketChannel.open()) {
            socket.connect(new InetSocketAddress("localhost", 8888));
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("客户端完毕!");
    }
}
/*
输出
客户端完毕!
 */

4. ServerSocketChannel 和 SocketChannel 收发信息

  • 模拟客户端和服务器端实现信息交互的过程:

a. 创建服务器端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class Server {
    public static void main(String[] args) {
        try (ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
            serverChannel.bind(new InetSocketAddress("localhost", 8888));
            System.out.println("【服务器】等待客户端连接...");
            SocketChannel accept = serverChannel.accept();
            System.out.println("【服务器】有连接到达...");
            //1.先发一条
            ByteBuffer outBuffer = ByteBuffer.allocate(100);
            outBuffer.put("你好客户端,我是服务器".getBytes());
            outBuffer.flip();//limit设置为position,position设置为0
            accept.write(outBuffer);//输出从position到limit之间的数据

            //2.再收一条,不确定字数是多少,但最多是100字节。先准备100字节空间
            ByteBuffer inBuffer = ByteBuffer.allocate(100);
            accept.read(inBuffer);
            inBuffer.flip();//limit设置为position,position设置为0
            String msg = new String(inBuffer.array(), 0, inBuffer.limit());
            System.out.println("【服务器】收到信息:" + msg);
            accept.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

b. 创建客户端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Client {
    public static void main(String[] args) {
        try (SocketChannel socket = SocketChannel.open()) {
            socket.connect(new InetSocketAddress("localhost", 8888));

            //1.先发一条
            ByteBuffer buf = ByteBuffer.allocate(100);
            buf.put("你好服务器,我是客户端".getBytes());
            buf.flip();//limit设置为position,position设置为0
            socket.write(buf);//输出从position到limit之间的数据

            //2.再收一条,不确定字数是多少,但最多是100字节。先准备100字节空间
            ByteBuffer inBuffer = ByteBuffer.allocate(100);
            socket.read(inBuffer);
            inBuffer.flip();//limit设置为position,position设置为0
            String msg = new String(inBuffer.array(), 0, inBuffer.limit());
            System.out.println("【客户端】收到信息:" + msg);
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("客户端完毕!");
    }
}

c. 效果展示

  • 服务器端打印结果:
    在这里插入图片描述
  • 客户端打印结果:
    在这里插入图片描述
发布了310 篇原创文章 · 获赞 315 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Regino/article/details/105120561