Java NIO之通道(channel)

  • 什么是通道?   

       通道(Channel)是 java.nio 的第二个主要创新。它们提供与 I/O 服务的直接连接。Channel 用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。通道是一种途径,借助该途径,可以用最小的总开销来访问操作系统本身的 I/O 服务。缓冲区则是通道内部用来发送和接收数据的端点。

  • 创建通道

        通道主要分为两类:通道主要分为两类:文件通道(FileChannel)和Socket通道(ServerSocketChannel,SocketChannel,DataGramChannel),FileChannel通道只能通过在一个打开的RandomAccessFile、FileInputStream或FileOutputStream对象上调用getChannel( )方法来获取,如下所示:

        //创建FileChannel实例
    RandomAccessFile file = new RandomAccessFile("hello","r");
    FileChannel fc = file.getChannel();
        //创建SocketChannel实例
    SocketChannel sc = SocketChannel.open();
    sc.connect(new InetSocketAddress("127.0.0.1",1234));
        //创建ServerSocketChannel实例
    ServerSocketChannel ssc = ServerSocketChannel.open();
    ssc.bind(new InetSocketAddress(1234));
        //创建DatagramChannel实例
    DatagramChannel dc = DatagramChannel.open();
  • 使用通道

       通道可以是单向(unidirectional)或者双向的(bidirectional);一个 channel 类可能实现定义read( )方法的 ReadableByteChannel 接口,而另一个 channel 类也许实现 WritableByteChannel 接口以提供 write( )方法。实现这两种接口其中之一的类都是单向的,只能在一个方向上传输数据。如果一个类同时实现这两个接口,那么它是双向的,可以双向传输数据。

       通道可以以阻塞(blocking)或非阻塞(nonblocking)模式运行。非阻塞模式的通道永远不会让调用的线程休眠。请求的操作要么立即完成,要么返回一个结果表明未进行任何操作。只有面向流的(stream-oriented)的通道,如 sockets 和 pipes 才能使用非阻塞模式。

      实例展示两个通道间的数据拷贝:

public  static void main(String[] args) throws IOException {
        ReadableByteChannel rbc = Channels.newChannel(System.in);
        WritableByteChannel wbc = Channels.newChannel(System.out);
        channelCopy1(rbc,wbc);
        //channelCopy2(rbc,wbc);
        rbc.close();
        wbc.close();
    }
    public static void channelCopy1(ReadableByteChannel src,WritableByteChannel dst) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while(src.read(buffer) != -1){
            //切换为读状态
            buffer.flip();
            //不能保证全部写入
            dst.write(buffer);
            //释放已读数据空间,等待写入新数据
            buffer.compact();
        }
        //退出循环时,compact()方法调用后,缓冲区中可能还有数据,需要再次读数据
        buffer.flip();
        while(buffer.hasRemaining()){
            dst.write(buffer);
        }
    }
    public static void channelCopy2(ReadableByteChannel src,WritableByteChannel dst) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while(src.read(buffer) != -1){
            //切换为读状态
            buffer.flip();
            //保证全部写入
            while(buffer.hasRemaining()){
                dst.write(buffer);
            }
            //清空缓冲区
            buffer.clear();
        }
    }
  •  关闭通道

        通道不能被重复使用。一个打开的通道即代表与一个特定 I/O 服务的特定连接并封装该连接的状态。当通道关闭时,那个连接会丢失,然后通道将不再连接任何东西。调用close()方法关闭通道。可以通过 isOpen( )方法来测试通道的开放状态。如果一个通道实现 InterruptibleChannel 接口,则若一个线程在一个通道上被阻塞并且同时被中断(由调用该被阻塞线程的 interrupt( )方法的另一个线程中断),那么该通道将被关闭,该被阻塞线程也会产生一个 ClosedByInterruptException 异常。

  • 文件通道

        文件通道总是阻塞式的,文件 I/O,最强大之处在于异步 I/O(asynchronous I/O),它允许一个进程可以从操作系统请求一个或多个 I/O 操作而不必等待这些操作的完成。发起请求的进程之后会收到它请求的 I/O 操作已完成的通知。

     FileChannel 对象是线程安全(thread-safe)的

1、访问文件

       每个 FileChannel 都有一个叫“file position”的概念。这个 position 值决定文件中哪一处的数据接下来将被读或者写。有两种形式的position( )方法。第一种,不带参数的,返回当前文件的position值。返回值是一个长整型(long),表示文件中的当前字节位置。第二种形式的 position( )方法带一个 long(长整型)参数并将通道的 position 设置为指定值。FileChannel 位置(position)是从底层的文件描述符获得的,该 position 同时被作为通道引用获取来源的文件对象共享。

      当字节被 read( )或 write( )方法传输时,文件 position 会自动更新。如果 position 值达到了文件大小的值,read( )方法会返回一个文件尾条件值(-1)。不同于缓冲区的是,如果实现 write( )方法时 position前进到超过文件大小的值,该文件会扩展以容纳新写入的字节。同样类似于缓冲区,也有带 position 参数的绝对形式的 read( )和 write( )方法。这种绝对形式的方法在返回值时不会改变当前的文件 position。多个线程可以并发访问同一个文件而不会相互产生干扰。这是因为每次调用都是原子性的,并不依靠调用之间系统所记住的状态。尝试在文件末尾之外的 position 进行一个绝对读操作,size( )方法会返回一个 end-of-file。在超出文件大小的 position 上做一个绝对 write( )会导致文件增加以容纳正在被写入的新字节。

      当需要减少一个文件的 size 时,truncate( )方法会砍掉您所指定的新 size 值之外的所有数据。如果当前 size 大于新 size,超出新 size 的所有字节都会被悄悄地丢弃。如果提供的新 size 值大于或等于当前的文件 size 值,该文件不会被修改。这两种情况下,truncate( )都会产生副作用:文件的position 会被设置为所提供的新 size 值。

      调用 force( )方法要求文件的所有待定修改立即同步到磁盘,给 force( )方法传递 false 值表示在方法返回前只需要同步文件数据的更改。

2、文件锁定

       锁的对象是文件而不是通道或线程,这意味着文件锁不适用于判优同一台 Java 虚拟机上的多个线程发起的访问。如果一个线程在某个文件上获得了一个独占锁,然后第二个线程利用一个单独打开的通道来请求该文件的独占锁,那么第二个线程的请求会被批准。但如果这两个线程运行在不同的 Java 虚拟机上,那么第二个线程会阻塞,因为锁最终是由操作系统或文件系统来判优的并且几乎总是在进程级而非线程级上判优。锁都是与一个文件关联的,而不是与单个的文件句柄或通道关联。

      锁是在文件内部区域上获得的。调用带参数的 Lock( )方法会指定文件内部锁定区域的开始 position 以及锁定区域的 size。第三个参数 shared 表示您想获取的锁是共享的(参数值为 true)还是独占的(参数值为 false)。要获得一个共享锁,您必须先以只读权限打开文件,而请求独占锁时则需要写权限。

      锁定区域的范围不一定要限制在文件的 size 值以内,锁可以扩展从而超出文件尾。不带参数的简单形式的 lock( )方法是一种在整个文件上请求独占锁的便捷方法,锁定区域等于它能达到的最大范围。如果您正请求的锁定范围是有效的,那么 lock( )方法会阻塞,它必须等待前面的锁被释放。通过调用 channel( )方法来查询一个 lock 对象以判断它是由哪个通道创建。一个 FileLock 对象创建之后即有效,直到它的 release( )方法被调用或它所关联的通道被关闭或Java 虚拟机关闭时才会失效。我们可以通过调用 isValid( )布尔方法来测试一个锁的有效性。一个锁的有效性可能会随着时间而改变,不过它的其他属性——位置(position)、范围大小(size)和独占性(exclusivity)—在创建时即被确定,不会随着时间而改。调用 isShared( )方法来测试一个锁以判断它是共享的还是独占的。调用 overlaps( )方法来查询一个 FileLock 对象是否与一个指定的文件区域重叠。

  • channel-to-channnel(通道间传输数据)

       transferTo()方法和transferFrom()方法允许将一个通道交叉连接到另一个通道,而不需要通过一个中间缓冲区来传递数据。

  • Socket通道

       DatagramChannel 和 SocketChannel 实现定义读和写功能的接口而 ServerSocketChannel不实现。ServerSocketChannel 负责监听传入的连接和创建新的 SocketChannel 对象,它本身从不传输数据。

1、ServerSocketChannel

    ServerSocketChannel 是一个基于通道的 socket 监听器。用静态的 open( )工厂方法创建一个新的 ServerSocketChannel 对象,将会返回同一个未绑定的java.net.ServerSocket 关联的通道。该对等 ServerSocket 可以通过在返回的 ServerSocketChannel 上调用 socket( )方法来获取。ServerSocketChannel 也有 accept( )方法。一旦创建了一个 ServerSocketChannel 并用对等 socket 绑定了它,然后就可以在其中一个上调用 accept( ),选择在 ServerSocketChannel 上调用 accept( )方法则会返回 SocketChannel 类型的对象,返回的对象能够在非阻塞模式下运行。

2、SocketChannel

       open()方法创建通道,调用 connect( )方法或在通道关联的 Socket 对象上调用 connect( )来将该 socket 通道连接。调用布尔型的 isConnected( )方法来测试某个SocketChannel 当前是否已连接。调用 finishConnect( )方法来完成连接过程,该方法任何时候都可以安全地进行调用。Socket 通道是线程安全的。

      connect( )和 finishConnect( )方法是互相同步的,并且只要其中一个操作正在进行,任何读或写的方法调用都会阻塞,即使是在非阻塞模式下。

3、DatagramChannel

       DatagramChannel 对象既可以充当服务器(监听者)也可以充当客户端(发送者)。DatagramChannel 是无连接的,DatagramChannel 可以发送单独的数据报给不同的目的地址。一个未绑定的 DatagramChannel 仍能接收数据包,数据的实际发送或接收是通过 send( )和 receive( )方法来实现的。数据报协议的不可靠性是固有的,它们不对数据传输做保证。

下面列出了一些选择数据报 socket 而非流 socket 的理由:

 程序可以承受数据丢失或无序的数据。

 希望“发射后不管”(fire and forget)而不需要知道您发送的包是否已接收。

 数据吞吐量比可靠性更重要。

 需要同时发送数据给多个接受者(多播或者广播)。

 包隐喻比流隐喻更适合手边的任务。

   关于通道中的scatter/gather问题,请参考https://www.cnblogs.com/chenpi/p/6483431.html

你可能感兴趣的:(Java网络编程)