NIO Channel(通道)类

Channel(通道)类

NIO中一个连接就是用一个Channel(通道)来表示。大家知道,从更广泛的层面来说,一个通道可以表示一个底层的文件描述符,例如硬件设备、文件、网络连接等。然而,远远不止如此,除了可以对应到底层文件描述符,Java NIO的通道还可以更加细化。例如,对应不同的网络传输协议类型,在Java中都有不同的NIO Channel(通道)实现。

1 Channel(通道)的主要类型

这里不对纷繁复杂的Java NIO通道类型进行过多的描述,仅仅聚焦于介绍其中最为重要的四种Channel(通道)实现:FileChannelSocketChannelServerSocketChannelDatagramChannel。对于以上四种通道,说明如下:

(1)FileChannel文件通道,用于文件的数据读写。
(2)SocketChannel套接字通道,用于Socket套接字TCP连接的数据读写。(3)ServerSocketChannel服务器嵌套字通道(或服务器监听通道),允许我们监听TCP连接请求,为每个监听到的请求,创建一个SocketChannel套接字通道。(4)DatagramChannel数据报通道,用于UDP协议的数据读写。这个四种通道,涵盖了文件IO、TCP网络、UDP IO基础IO。下面从Channel(通道)的获取、读取、写入、关闭四个重要的操作,来对四种通道进行简单的介绍

2 FileChannel文件通道

可以通过文件的输入流、输出流获取FileChannel文件通道,示例如下:

        //创建一条文件输入流
        FileInputStreamfis = new FileInputStream(srcFile);
        //获取文件流的通道
        FileChannelinChannel = fis.getChannel();

        //创建一条文件输出流
        FileOutputStreamfos = new FileOutputStream(destFile);
        //获取文件流的通道
        FileChanneloutchannel = fos.getChannel();

也可以通过RandomAccessFile文件随机访问类,获取FileChannel文件通道:

        // 创建RandomAccessFile随机访问对象
        RandomAccessFileaFile = new RandomAccessFile("filename.txt", "rw");
        //获取文件流的通道
        FileChannelinChannel = aFile.getChannel();

2.读取FileChannel通道

在大部分应用场景,从通道读取数据都会调用通道的int read(ByteBufferbuf)方法,它从通道读取到数据写入到ByteBuffer缓冲区,并且返回读取到的数据量。

        RandomAccessFileaFile = new RandomAccessFile(fileName, "rw");
        //获取通道
        FileChannelinChannel=aFile.getChannel();
        //获取一个字节缓冲区
        ByteBufferbuf = ByteBuffer.allocate(CAPACITY);
        int length = -1;
        //调用通道的read方法,读取数据并买入字节类型的缓冲区
        while ((length = inChannel.read(buf)) ! = -1) {
        //省略……处理读取到的buf中的数据
        }

虽然对于通道来说是读取数据,但是对于ByteBuffer缓冲区来说是写入数据,这时,ByteBuffer缓冲区处于写入模式

3.写入FileChannel通道

写入数据到通道,在大部分应用场景,都会调用通道的int write(ByteBufferbuf)方法。此方法的参数——ByteBuffer缓冲区,是数据的来源。write方法的作用,是从ByteBuffer缓冲区中读取数据,然后写入到通道自身,而返回值是写入成功的字节数。

        //如果buf刚写完数据,需要flip翻转buf,使其变成读取模式
        buf.flip();
        int outlength = 0;
        //调用write方法,将buf的数据写入通道
        while ((outlength = outchannel.write(buf)) ! = 0) {
                System.out.println("写入的字节数:" + outlength);
        }

此时的ByteBuffer缓冲区要求是可读的,处于读模式下。

4.关闭通道

当通道使用完成后,必须将其关闭。关闭非常简单,调用close方法即可。

        //关闭通道
        channel.close();

5.强制刷新到磁盘

在将缓冲区写入通道时,出于性能原因,操作系统不可能每次都实时将数据写入磁盘。如果需要保证写入通道的缓冲数据,最终都真正地写入磁盘,可以调用FileChannel的force()方法。

        //强制刷新到磁盘
        channel.force(true);

3 使用FileChannel完成文件复制的实践案例
下面是一个简单的实战案例:使用文件通道复制文件。其功能是:使用FileChannel文件通道,将原文件复制一份,也就是把原文中的数据都复制到目标文件中。完整代码如下:

        package com.crazymakercircle.iodemo.fileDemos;
        //...省略import的类,具体请参见源代码工程
        public class FileNIOCopyDemo {
            public static void main(String[] args) {
              //演示复制资源文件
              nioCopyResouceFile();
            }
            /**
            * 复制两个资源目录下的文件
            */
            public static void nioCopyResouceFile() {
              String sourcePath = NioDemoConfig.FILE_RESOURCE_SRC_PATH;
              String srcPath = IOUtil.getResourcePath(sourcePath);
              Logger.info("srcPath=" + srcPath);

              String destPath = NioDemoConfig.FILE_RESOURCE_DEST_PATH;
              String destDecodePath = IOUtil.builderResourcePath(destPath);
              Logger.info("destDecodePath=" + destDecodePath);

              nioCopyFile(srcDecodePath, destDecodePath);
            }
            /**
            * nio方式复制文件
            * @param srcPath
            * @param destPath
            */
            public static void nioCopyFile(String srcPath, String destPath) {
              File srcFile = new File(srcPath);
              File destFile = new File(destPath);
              try {
                  //如果目标文件不存在,则新建
                  if (! destFile.exists()) {
                      destFile.createNewFile();
                  }
              long startTime = System.currentTimeMillis();
              FileInputStreamfis = null;
              FileOutputStreamfos = null;
              FileChannelinChannel = null;
              FileChanneloutchannel = null;
              try {
                  fis = new FileInputStream(srcFile);
                  fos = new FileOutputStream(destFile);
                  inChannel = fis.getChannel();
                  outchannel = fos.getChannel();
                  int length = -1;
                  ByteBufferbuf = ByteBuffer.allocate(1024);
                  //从输入通道读取到buf
                  while ((length = inChannel.read(buf)) ! = -1) {
                  //第一次切换:翻转buf,变成读取模式
                      buf.flip();

                      int outlength = 0;
                      //将buf写入到输出的通道
                      while ((outlength = outchannel.write(buf)) ! = 0) {
                          System.out.println("写入的字节数:" + outlength);
                      }
                      //第二次切换:清除buf,变成写入模式
                      buf.clear();
                  }
                  //强制刷新到磁盘
                  outchannel.force(true);
                } finally {
                  //关闭所有的可关闭对象
                  IOUtil.closeQuietly(outchannel);
                  IOUtil.closeQuietly(fos);
                  IOUtil.closeQuietly(inChannel);
                  IOUtil.closeQuietly(fis);
                }
                long endTime = System.currentTimeMillis();
                Logger.info("base复制毫秒数:" + (endTime - startTime));
            } catch (IOException e) {
                  e.printStackTrace();
                }
        }

特别强调一下,除了FileChannel的通道操作外,还需要注意ByteBuffer的模式切换。新建的ByteBuffer,默认是写入模式,可以作为inChannel.read(ByteBuffer)的参数。inChannel.read方法将从通道inChannel读到的数据写入到ByteBuffer。

此后,需要调用缓冲区的flip方法,将ByteBuffer切换成读取模式,才能作为outchannel.write(ByteBuffer)方法的参数,从ByteBuffer读取数据,再写入到outchannel输出通道。

如此,便是完成一次复制。在进入下一次复制前,还要进行一次缓冲区的模式切换。ByteBuffer数据读完之后,需要将通过clear方法切换成写入模式,才能进入下一次的复制。

在示例代码中,外层的每一轮while循环,都需要两次模式ByteBuffer切换:第一次切换时,翻转buf,变成读取模式;第二次切换时,清除buf,变成写入模式。

上面的示例代码,主要的目的在于:演示文件通道以及字节缓冲区的使用。作为文件复制的程序来说,实战代码的效率不是最高的。

更高效的文件复制,可以调用文件通道的transferFrom方法。具体的代码,可以参见源代码工程中的FileNIOFastCopyDemo类,完整源文件的路径为:com.crazymakercircle.iodemo.fileDemos.FileNIOFastCopyDemo

4 SocketChannel套接字通道

在NIO中,涉及网络连接的通道有两个,一个是SocketChannel负责连接传输,另一个是ServerSocketChannel负责连接的监听。NIO中的SocketChannel传输通道,与OIO中的Socket类对应。NIO中的ServerSocketChannel监听通道,对应于OIO中的ServerSocket类。ServerSocketChannel应用于服务器端,而SocketChannel同时处于服务器端和客户端。换句话说,对应于一个连接,两端都有一个负责传输的SocketChannel传输通道。无论是ServerSocketChannel,还是SocketChannel,都支持阻塞和非阻塞两种模式。如何进行模式的设置呢?调用configureBlocking方法,具体如下:
(1)socketChannel.configureBlocking(false)设置为非阻塞模式。(2)socketChannel.configureBlocking(true)设置为阻塞模式。

在阻塞模式下,SocketChannel通道的connect连接、read读、write写操作,都是同步的和阻塞式的,在效率上与Java旧的OIO的面向流的阻塞式读写操作相同。因此,在这里不介绍阻塞模式下的通道的具体操作。在非阻塞模式下,通道的操作是异步、高效率的,这也是相对于传统的OIO的优势所在。下面详细介绍在非阻塞模式下通道的打开、读写和关闭操作等操作。

1.获取SocketChannel传输通道

在客户端,先通过SocketChannel静态方法open()获得一个套接字传输通道;然后,将socket套接字设置为非阻塞模式;最后,通过connect()实例方法,对服务器的IP和端口发起连接。

              //获得一个套接字传输通道
        SocketChannelsocketChannel = SocketChannel.open();
             //设置为非阻塞模式
        socketChannel.configureBlocking(false);
              //对服务器的IP和端口发起连接
        socketChannel.connect(new InetSocketAddress("127.0.0.1",80));

非阻塞情况下,与服务器的连接可能还没有真正建立,socketChannel.connect方法就返回了,因此需要不断地自旋,检查当前是否是连接到了主机:

        while(! socketChannel.finishConnect() ){
            //不断地自旋、等待,或者做一些其他的事情……
        }

在服务器端,如何获取传输套接字呢?

当新连接事件到来时,在服务器端的ServerSocketChannel能成功地查询出一个新连接事件,并且通过调用服务器端ServerSocketChannel监听套接字的accept()方法,来获取新连接的套接字通道:

        //新连接事件到来,首先通过事件,获取服务器监听通道
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        //获取新连接的套接字通道
        SocketChannelsocketChannel = server.accept();
        //设置为非阻塞模式
        socketChannel.configureBlocking(false);

强调一下,NIO套接字通道,主要用于非阻塞应用场景。所以,需要调用configureBlocking(false),从阻塞模式设置为非阻塞模式。

2.读取SocketChannel传输通道

当SocketChannel通道可读时,可以从SocketChannel读取数据,具体方法与前面的文件通道读取方法是相同的。调用read方法,将数据读入缓冲区ByteBuffer。

        ByteBufferbuf = ByteBuffer.allocate(1024);
        int bytesRead = socketChannel.read(buf);

在读取时,因为是异步的,因此我们必须检查read的返回值,以便判断当前是否读取到了数据。read()方法的返回值,是读取的字节数。如果返回-1,那么表示读取到对方的输出结束标志,对方已经输出结束,准备关闭连接。实际上,通过read方法读数据,本身是很简单的,比较困难的是,在非阻塞模式下,如何知道通道何时是可读的呢?这就需要用到NIO的新组件——Selector通道选择器,稍后介绍。

3.写入到SocketChannel传输通道

和前面的把数据写入到FileChannel文件通道一样,大部分应用场景都会调用通道的int write(ByteBufferbuf)方法。

        //写入前需要读取缓冲区,要求ByteBuffer是读取模式
        buffer.flip();
        socketChannel.write(buffer);

4.关闭SocketChannel传输通道

在关闭SocketChannel传输通道前,如果传输通道用来写入数据,则建议调用一次shutdownOutput()终止输出方法,向对方发送一个输出的结束标志(-1)。然后调用socketChannel.close()方法,关闭套接字连接。

        //终止输出方法,向对方发送一个输出的结束标志
        socketChannel.shutdownOutput();
        //关闭套接字连接
        IOUtil.closeQuietly(socketChannel);

5 使用SocketChannel发送文件的实践案例

下面的实践案例是使用FileChannel文件通道读取本地文件内容,然后在客户端使用SocketChannel套接字通道,把文件信息和文件内容发送到服务器。客户端的完整代码如下:

        package com.crazymakercircle.iodemo.socketDemos;
        //...
        public class NioSendClient {
            private Charset charset = Charset.forName("UTF-8");
            /**
            * 向服务器端传输文件
            */
            public void sendFile() throws Exception {
                try {
                    String sourcePath = NioDemoConfig.SOCKET_SEND_FILE;
                    String srcPath = IOUtil.getResourcePath(sourcePath);
                    Logger.info("srcPath=" + srcPath);
                    String destFile = NioDemoConfig.SOCKET_RECEIVE_FILE;
                    Logger.info("destFile=" + destFile);
                    File file = new File(srcPath);
                    if (! file.exists()) {
                        Logger.info("文件不存在");
                        return;
                    }
                    FileChannelfileChannel = new FileInputStream(file).getChannel();
                    SocketChannelsocketChannel = SocketChannel.open();
                    socketChannel.socket().connect(
                        InetSocketAddress(NioDemoConfig.SOCKET_SERVER_IP,
                                          NioDemoConfig.SOCKET_SERVER_PORT));
                        socketChannel.configureBlocking(false);
                        while(! socketChannel.finishConnect() ){
                            //不断地自旋、等待,或者做一些其他的事情
                        }
                        Logger.info("Client成功连接服务器端");
                        //发送文件名称
                        ByteBufferfileNameByteBuffer = charset.encode(destFile);
                        socketChannel.write(fileNameByteBuffer);
                        //发送文件长度
                        ByteBuffer buffer = ByteBuffer.allocate
                                            (NioDemoConfig.SEND_BUFFER_SIZE);
                        buffer.putLong(file.length());
                        buffer.flip();
                        socketChannel.write(buffer);
                        buffer.clear();
                        //发送文件内容
                        Logger.info("开始传输文件");
                        int length = 0;
                        long progress = 0;
                        while ((length = fileChannel.read(buffer)) > 0) {
                            buffer.flip();
                            socketChannel.write(buffer);
                            buffer.clear();
                            progress += length;
                            Logger.info("| "+(100 * progress / file.length()) + "% |");
                        }
                        if (length == -1) {
                            IOUtil.closeQuietly(fileChannel);
                            //在SocketChannel传输通道关闭前,尽量发送一个输出结束标志到对端
                            socketChannel.shutdownOutput();
                            IOUtil.closeQuietly(socketChannel);
                        }
                        Logger.info("======== 文件传输成功 ========");
                    } catch (Exception e) {
                        e.printStackTrace();
                      }
              }
              public static void main(String[] args) {
              NioSendClient client = new NioSendClient(); // 启动客户端连接
              client.sendFile(); // 传输文件
            }
        }

以上代码中的文件发送过程:首先发送目标文件名称(不带路径),然后发送文件长度,最后是发送文件内容。代码中的配置项,如服务器的IP、服务器端口、待发送的源文件名称(带路径)、远程的目标文件名称等配置信息,都是从system.properties配置文件中读取的,通过自定义的NioDemoConfig配置类来完成配置。

在运行以上客户端的程序之前,需要先运行服务器端的程序。服务器端的类与客户端的源代码在同一个包下,类名为NioReceiveServer,具体参见源代码工程,我们稍后再详细介绍这个类。

6 DatagramChannel数据报通道

和Socket套接字的TCP传输协议不同,UDP协议不是面向连接的协议。使用UDP协议时,只要知道服务器的IP和端口,就可以直接向对方发送数据。在Java中使用UDP协议传输数据,比TCP协议更加简单。在Java NIO中,使用DatagramChannel数据报通道来处理UDP协议的数据传输。

1.获取DatagramChannel数据报通道

获取数据报通道的方式很简单,调用DatagramChannel类的open静态方法即可。然后调用configureBlocking(false)方法,设置成非阻塞模式。

        //获取DatagramChannel数据报通道
        DatagramChannel channel = DatagramChannel.open();
        //设置为非阻塞模式
        datagramChannel.configureBlocking(false);

如果需要接收数据,还需要调用bind方法绑定一个数据报的监听端口,具体如下:

        //调用bind方法绑定一个数据报的监听端口
        channel.socket().bind(new InetSocketAddress(18080));

2.读取DatagramChannel数据报通道数据

当DatagramChannel通道可读时,可以从DatagramChannel读取数据。和前面的SocketChannel的读取方式不同,不是调用read方法,而是调用receive(ByteBufferbuf)方法将数据从DatagramChannel读入,再写入到ByteBuffer缓冲区中。

        //创建缓冲区
        ByteBufferbuf = ByteBuffer.allocate(1024);
        //从DatagramChannel读入,再写入到ByteBuffer缓冲区
        SocketAddressclientAddr= datagramChannel.receive(buffer);

通道读取receive(ByteBufferbuf)方法的返回值,是SocketAddress类型,表示返回发送端的连接地址(包括IP和端口)。通过receive方法读数据非常简单,但是,在非阻塞模式下,如何知道DatagramChannel通道何时是可读的呢?和SocketChannel一样,同样需要用到NIO的新组件——Selector通道选择器,稍后介绍。

3.写入DatagramChannel数据报通道

向DatagramChannel发送数据,和向SocketChannel通道发送数据的方法也是不同的。这里不是调用write方法,而是调用send方法。示例代码如下:

        //把缓冲区翻转到读取模式
        buffer.flip();
        //调用send方法,把数据发送到目标IP+端口
        dChannel.send(buffer,   new  InetSocketAddress(NioDemoConfig.SOCKET_SERVER_IP,
                        NioDemoConfig.SOCKET_SERVER_PORT));
        //清空缓冲区,切换到写入模式
        buffer.clear();

由于UDP是面向非连接的协议,因此,在调用send方法发送数据的时候,需要指定接收方的地址(IP和端口)。

4.关闭DatagramChannel数据报通道

这个比较简单,直接调用close()方法,即可关闭数据报通道。

        //简单关闭即可
        dChannel.close();

使用DatagramChannel数据包通道发送数据的实践案例
下面是一个使用DatagramChannel数据包通到发送数据的客户端示例程序代码。其功能是:获取用户的输入数据,通过DatagramChannel数据报通道,将数据发送到远程的服务器。客户端的完整程序代码如下:

        package com.crazymakercircle.iodemo.udpDemos;
        //...
        public class UDPClient {
            public void send() throws IOException {
              //获取DatagramChannel数据报通道
              DatagramChanneldChannel = DatagramChannel.open();
              //设置为非阻塞
              dChannel.configureBlocking(false);
              ByteBuffer buffer = ByteBuffer.allocate(NioDemoConfig.SEND_BUFFER_
                                    SIZE);
              Scanner scanner = new Scanner(System.in);
              Print.tcfo("UDP客户端启动成功!");
              Print.tcfo("请输入发送内容:");
              while (scanner.hasNext()) {
                  String next = scanner.next();
                  buffer.put((Dateutil.getNow() + " >>" + next).getBytes());
                  buffer.flip();
                  //通过DatagramChannel数据报通道发送数据
                  dChannel.send(buffer, new InetSocketAddress(NioDemoConfig.
                              SOCKET_SERVER_IP, NioDemoConfig.SOCKET_SERVER_PORT));
                  buffer.clear();
              }
              //操作四:关闭DatagramChannel数据报通道
              dChannel.close();
            }
            public static void main(String[] args) throws IOException {
              new UDPClient().send();
            }
        }

通过示例程序代码可以看出,在客户端使DatagramChannel数据报通道发送数据,比起在客户端使用套接字SocketChannel发送数据,简单很多。

接下来看看在服务器端应该如何使用DatagramChannel数据包通道接收数据呢?

下面贴出服务器端通过DatagramChannel数据包通道接收数据的程序代码,可能大家目前不一定可以看懂,因为代码中用到了Selector选择器,但是不要紧,下一个小节就介绍它。

服务器端的接收功能是:通过DatagramChannel数据报通道,绑定一个服务器地址(IP+端口),接收客户端发送过来的UDP数据报。服务器端的完整代码如下:

        package com.crazymakercircle.iodemo.udpDemos;
        //...
        public class UDPServer {
            public void receive() throws IOException {
              //获取DatagramChannel数据报通道
              DatagramChanneldatagramChannel = DatagramChannel.open();
              //设置为非阻塞模式
              datagramChannel.configureBlocking(false);
              //绑定监听地址
              datagramChannel.bind(new InetSocketAddress(NioDemoConfig.SOCKET
                                    _SERVER_IP, NioDemoConfig.SOCKET_SERVER_PORT));
              Print.tcfo("UDP服务器启动成功!");
              //开启一个通道选择器
              Selector selector = Selector.open();
              //将通道注册到选择器
              datagramChannel.register(selector, SelectionKey.OP_READ);
              //通过选择器,查询IO事件
              while (selector.select() > 0) {
                  Iterator<SelectionKey> iterator = selector.selectedKeys()
                                                    .iterator();
                  ByteBuffer buffer = ByteBuffer.allocate(NioDemoConfig.SEND
                                      _BUFFER_SIZE);
                  //迭代IO事件
                  while (iterator.hasNext()) {
                      SelectionKeyselectionKey = iterator.next();
                      //可读事件,有数据到来
                      if (selectionKey.isReadable()) {
                          //读取DatagramChannel数据报通道的数据
                          SocketAddress client = datagramChannel.receive(buffer);
                          buffer.flip();
                          Print.tcfo(new String(buffer.array(), 0, buffer.limit()));
                          buffer.clear();
                      }
                  }
                  iterator.remove();
              }
              //关闭选择器和通道
              selector.close();
              datagramChannel.close();
            }
            public static void main(String[] args) throws IOException {
              new UDPServer().receive();
            }
        }

在服务器端,首先调用了bind方法绑定datagramChannel的监听端口。当数据到来后,调用了receive方法,从datagramChannel数据包通道接收数据,再写入到ByteBuffer缓冲区中。除此之外,在服务器端代码中,为了监控数据的到来,使用了Selector选择器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yitian_hm

您的支持是我最大鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值