3.JAVA NIO通道

第三章 通道

通道(Channel)是 java.nio 的第二個主要創新。它們既不是一個擴展也不是一項增強,而 是全新、極好的 Java I/O 示例,提供與 I/O 服務的直接連接。Channel 用於在字節緩衝區和位於通 道另一側的實體(通常是一個文件或套接字)之間有效地傳輸數據。

通道可以形象地比喻爲銀行出納窗口使用的氣動導管。您的薪水支票就是您要傳送的信息,載 體(Carrier)就好比一個緩衝區。您先填充緩衝區(將您的支票放到載體上),接着將緩衝“寫”到 通道中(將載體丟進導管中),然後信息負載就被傳遞到通道另一側的 I/O 服務(銀行出納員)。

出納員填充緩衝區(將您的收據放到載體上),接着開始一個反方向的通道 傳輸(將載體丟回到導管中)。載體就到了通道的您這一側(一個填滿了的緩衝區正等待您的查 驗),然後您就會 flip 緩衝區(打開蓋子)並將它清空(移除您的收據)。現在您可以開車走了, 下一個對象(銀行客戶)將使用同樣的載體(Buffer)和導管(Channel)對象來重複上述過程。

多數情況下,通道與操作系統的文件描述符(File Descriptor)和文件句柄(File Handle)有着 一對一的關係。雖然通道比文件描述符更廣義,但您將經常使用到的多數通道都是連接到開放的文 件描述符的。Channel 類提供維持平臺獨立性所需的抽象過程,不過仍然會模擬現代操作系統本身 的 I/O 性能。

通道是一種途徑,藉助該途徑,可以用最小的總開銷來訪問操作系統本身的 I/O 服務。緩衝區 則是通道內部用來發送和接收數據的端點。 (見圖 3-1)

觀察圖 3-2 所示的 UML 類圖會發現,channel 類的繼承關係要比 buffer 類複雜一些。Channel 類相互之間的關係更復雜,並且部分 channel 類依賴於在 java.nio.channels.spi 子包中定義的類。本章我們將對該困惑進行澄清。通道 SPI 歸納參見附錄 B。

1.通道基礎

/*
 * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.nio.channels;

import java.io.IOException;
import java.io.Closeable;


/**
 * A nexus for I/O operations.
 *
 * <p> A channel represents an open connection to an entity such as a hardware
 * device, a file, a network socket, or a program component that is capable of
 * performing one or more distinct I/O operations, for example reading or
 * writing.
 *
 * <p> A channel is either open or closed.  A channel is open upon creation,
 * and once closed it remains closed.  Once a channel is closed, any attempt to
 * invoke an I/O operation upon it will cause a {@link ClosedChannelException}
 * to be thrown.  Whether or not a channel is open may be tested by invoking
 * its {@link #isOpen isOpen} method.
 *
 * <p> Channels are, in general, intended to be safe for multithreaded access
 * as described in the specifications of the interfaces and classes that extend
 * and implement this interface.
 *
 *
 * @author Mark Reinhold
 * @author JSR-51 Expert Group
 * @since 1.4
 */

public interface Channel extends Closeable {

    /**
     * Tells whether or not this channel is open.
     *
     * @return <tt>true</tt> if, and only if, this channel is open
     */
    public boolean isOpen();

    /**
     * Closes this channel.
     *
     * <p> After a channel is closed, any further attempt to invoke I/O
     * operations upon it will cause a {@link ClosedChannelException} to be
     * thrown.
     *
     * <p> If this channel is already closed then invoking this method has no
     * effect.
     *
     * <p> This method may be invoked at any time.  If some other thread has
     * already invoked it, however, then another invocation will block until
     * the first invocation is complete, after which it will return without
     * effect. </p>
     *
     * @throws  IOException  If an I/O error occurs
     */
    public void close() throws IOException;

}

與緩衝區不同,通道 API 主要由接口指定。不同的操作系統上通道實現(Channel Implementation)會有根本性的差異,所以通道 API 僅僅描述了可以做什麼。因此很自然地,通道 實現經常使用操作系統的本地代碼。通道接口允許您以一種受控且可移植的方式來訪問底層的 I/O 服務。

您可以從頂層的 Channel 接口看到,對所有通道來說只有兩種共同的操作:檢查一個通道是否 打開(IsOpen())和關閉一個打開的通道(close())。圖 3-2 顯示,所有有趣的東西都是那些實現 Channel 接口以及它的子接口的類。

InterruptibleChannel 是一個標記接口,當被通道使用時可以標示該通道是可以中斷的 (Interruptible)。如果連接可中斷通道的線程被中斷,那麼該通道會以特別的方式工作,關於這一 點我們會在 3.1.3 節中進行討論。大多數但非全部的通道都是可以中斷的。

從 Channel 接口引申出的其他接口都是面向字節的子接口,包括 Writable ByteChannel 和 ReadableByteChannel。這也正好支持了我們之前所學的:通道只能在字節緩衝區上操作。層次結構 表明其他數據類型的通道也可以從 Channel 接口引申而來。這是一種很好的類設計,不過非字節實 現是不可能的,因爲操作系統都是以字節的形式實現底層 I/O 接口的。

觀察圖 3-2,您還會發現類層次結構中有兩個類位於一個不同的包: java.nio.channels.spi。這兩個類是 AbstractInterruptibleChannel 和 AbstractSelectableChannel,它們分別爲可中斷的(interruptible)和可選擇的(selectable)的通道實 現提供所需的常用方法。儘管描述通道行爲的接口都是在 java.nio.channels 包中定義的,不 過具體的通道實現卻都是從 java.nio.channels.spi 中的類引申來的。這使得他們可以訪問受 保護的方法,而這些方法普通的通道用戶永遠都不會調用。

作爲通道的一個使用者,您可以放心地忽視 SPI 包中包含的中間類。這種有點費解的繼承層次 只會讓那些使用新通道的用戶感興趣。SPI 包允許新通道實現以一種受控且模塊化的方式被植入到Java 虛擬機上。這意味着可以使用專爲某種操作系統、文件系統或應用程序而優化的通道來使性能 最大化。

1)打開通道

通道是訪問 I/O 服務的導管。正如我們在第一章中所討論的,I/O 可以分爲廣義的兩大類別: File I/O 和 Stream I/O。那麼相應地有兩種類型的通道也就不足爲怪了,它們是文件(file)通道和 套接字(socket)通道。如果您參考一下圖 3-2,您就會發現有一個 FileChannel 類和三個 socket 通 道類:SocketChannel、ServerSocketChannel 和 DatagramChannel。

通道可以以多種方式創建。Socket 通道有可以直接創建新 socket 通道的工廠方法。但是一個 FileChannel 對象卻只能通過在一個打開的 RandomAccessFile、FileInputStream 或 FileOutputStream 對象上調用 getChannel( )方法來獲取。您不能直接創建一個 FileChannel 對象。File 和 socket 通道會 在後面的章節中予以詳細討論。

SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("somehost", someport));
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(somelocalport));
DatagramChannel dc = DatagramChannel.open();
RandomAccessFile raf = new RandomAccessFile("somefile", "r");
FileChannel fc = raf.getChannel();

在 3.5 節中您會發現,java.net 的 socket 類也有新的 getChannel( )方法。這些方法雖然 能返回一個相應的 socket 通道對象,但它們卻並非新通道的來源, RandomAccessFile.getChannel( )方法纔是。只有在已經有通道存在的時候,它們才返回與 一個 socket 關聯的通道;它們永遠不會創建新通道。

 

2)使用通道

通道將數據傳輸給 ByteBuffer 對象或者從 ByteBuffer 對象 獲取數據進行傳輸。

將圖 3-2 中大部分零亂內容移除可以得到圖 3-3 所示的 UML 類圖。子接口 API 代碼如下:

/*
 * Copyright (c) 2000, 2001, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.nio.channels;

import java.io.IOException;
import java.nio.ByteBuffer;


/**
 * A channel that can read bytes.
 *
 * <p> Only one read operation upon a readable channel may be in progress at
 * any given time.  If one thread initiates a read operation upon a channel
 * then any other thread that attempts to initiate another read operation will
 * block until the first operation is complete.  Whether or not other kinds of
 * I/O operations may proceed concurrently with a read operation depends upon
 * the type of the channel. </p>
 *
 *
 * @author Mark Reinhold
 * @author JSR-51 Expert Group
 * @since 1.4
 */

public interface ReadableByteChannel extends Channel {

    /**
     * Reads a sequence of bytes from this channel into the given buffer.
     *
     * <p> An attempt is made to read up to <i>r</i> bytes from the channel,
     * where <i>r</i> is the number of bytes remaining in the buffer, that is,
     * <tt>dst.remaining()</tt>, at the moment this method is invoked.
     *
     * <p> Suppose that a byte sequence of length <i>n</i> is read, where
     * <tt>0</tt>&nbsp;<tt>&lt;=</tt>&nbsp;<i>n</i>&nbsp;<tt>&lt;=</tt>&nbsp;<i>r</i>.
     * This byte sequence will be transferred into the buffer so that the first
     * byte in the sequence is at index <i>p</i> and the last byte is at index
     * <i>p</i>&nbsp;<tt>+</tt>&nbsp;<i>n</i>&nbsp;<tt>-</tt>&nbsp;<tt>1</tt>,
     * where <i>p</i> is the buffer's position at the moment this method is
     * invoked.  Upon return the buffer's position will be equal to
     * <i>p</i>&nbsp;<tt>+</tt>&nbsp;<i>n</i>; its limit will not have changed.
     *
     * <p> A read operation might not fill the buffer, and in fact it might not
     * read any bytes at all.  Whether or not it does so depends upon the
     * nature and state of the channel.  A socket channel in non-blocking mode,
     * for example, cannot read any more bytes than are immediately available
     * from the socket's input buffer; similarly, a file channel cannot read
     * any more bytes than remain in the file.  It is guaranteed, however, that
     * if a channel is in blocking mode and there is at least one byte
     * remaining in the buffer then this method will block until at least one
     * byte is read.
     *
     * <p> This method may be invoked at any time.  If another thread has
     * already initiated a read operation upon this channel, however, then an
     * invocation of this method will block until the first operation is
     * complete. </p>
     *
     * @param  dst
     *         The buffer into which bytes are to be transferred
     *
     * @return  The number of bytes read, possibly zero, or <tt>-1</tt> if the
     *          channel has reached end-of-stream
     *
     * @throws  NonReadableChannelException
     *          If this channel was not opened for reading
     *
     * @throws  ClosedChannelException
     *          If this channel is closed
     *
     * @throws  AsynchronousCloseException
     *          If another thread closes this channel
     *          while the read operation is in progress
     *
     * @throws  ClosedByInterruptException
     *          If another thread interrupts the current thread
     *          while the read operation is in progress, thereby
     *          closing the channel and setting the current thread's
     *          interrupt status
     *
     * @throws  IOException
     *          If some other I/O error occurs
     */
    public int read(ByteBuffer dst) throws IOException;

}
/*
 * Copyright (c) 2000, 2005, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.nio.channels;

import java.io.IOException;
import java.nio.ByteBuffer;


/**
 * A channel that can write bytes.
 *
 * <p> Only one write operation upon a writable channel may be in progress at
 * any given time.  If one thread initiates a write operation upon a channel
 * then any other thread that attempts to initiate another write operation will
 * block until the first operation is complete.  Whether or not other kinds of
 * I/O operations may proceed concurrently with a write operation depends upon
 * the type of the channel. </p>
 *
 *
 * @author Mark Reinhold
 * @author JSR-51 Expert Group
 * @since 1.4
 */

public interface WritableByteChannel
    extends Channel
{

    /**
     * Writes a sequence of bytes to this channel from the given buffer.
     *
     * <p> An attempt is made to write up to <i>r</i> bytes to the channel,
     * where <i>r</i> is the number of bytes remaining in the buffer, that is,
     * <tt>src.remaining()</tt>, at the moment this method is invoked.
     *
     * <p> Suppose that a byte sequence of length <i>n</i> is written, where
     * <tt>0</tt>&nbsp;<tt>&lt;=</tt>&nbsp;<i>n</i>&nbsp;<tt>&lt;=</tt>&nbsp;<i>r</i>.
     * This byte sequence will be transferred from the buffer starting at index
     * <i>p</i>, where <i>p</i> is the buffer's position at the moment this
     * method is invoked; the index of the last byte written will be
     * <i>p</i>&nbsp;<tt>+</tt>&nbsp;<i>n</i>&nbsp;<tt>-</tt>&nbsp;<tt>1</tt>.
     * Upon return the buffer's position will be equal to
     * <i>p</i>&nbsp;<tt>+</tt>&nbsp;<i>n</i>; its limit will not have changed.
     *
     * <p> Unless otherwise specified, a write operation will return only after
     * writing all of the <i>r</i> requested bytes.  Some types of channels,
     * depending upon their state, may write only some of the bytes or possibly
     * none at all.  A socket channel in non-blocking mode, for example, cannot
     * write any more bytes than are free in the socket's output buffer.
     *
     * <p> This method may be invoked at any time.  If another thread has
     * already initiated a write operation upon this channel, however, then an
     * invocation of this method will block until the first operation is
     * complete. </p>
     *
     * @param  src
     *         The buffer from which bytes are to be retrieved
     *
     * @return The number of bytes written, possibly zero
     *
     * @throws  NonWritableChannelException
     *          If this channel was not opened for writing
     *
     * @throws  ClosedChannelException
     *          If this channel is closed
     *
     * @throws  AsynchronousCloseException
     *          If another thread closes this channel
     *          while the write operation is in progress
     *
     * @throws  ClosedByInterruptException
     *          If another thread interrupts the current thread
     *          while the write operation is in progress, thereby
     *          closing the channel and setting the current thread's
     *          interrupt status
     *
     * @throws  IOException
     *          If some other I/O error occurs
     */
    public int write(ByteBuffer src) throws IOException;

}
/*
 * Copyright (c) 2000, 2001, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.nio.channels;

import java.io.IOException;


/**
 * A channel that can read and write bytes.  This interface simply unifies
 * {@link ReadableByteChannel} and {@link WritableByteChannel}; it does not
 * specify any new operations.
 *
 * @author Mark Reinhold
 * @author JSR-51 Expert Group
 * @since 1.4
 */

public interface ByteChannel
    extends ReadableByteChannel, WritableByteChannel
{

}

通道可以是單向(unidirectional)或者雙向的(bidirectional)。一個 channel 類可能實現定義 read( )方法的 ReadableByteChannel 接口,而另一個 channel 類也許實現 WritableByteChannel 接口以 提供 write( )方法。實現這兩種接口其中之一的類都是單向的,只能在一個方向上傳輸數據。如果 一個類同時實現這兩個接口,那麼它是雙向的,可以雙向傳輸數據。

圖 3-3 顯示了一個 ByteChannel 接口,該接口引申出了 ReadableByteChannel 和 WritableByteChannel 兩個接口。ByteChannel 接口本身並不定義新的 API 方法,它是一種用來聚集 它自己以一個新名稱繼承的多個接口的便捷接口。根據定義,實現 ByteChannel 接口的通道會同時 實現 ReadableByteChannel 和 WritableByteChannel 兩個接口,所以此類通道是雙向的。這是簡化類 定義的語法糖(syntactic sugar),它使得用操作器(operator)實例來測試通道對象變得更加簡 單。

這是一種好的類設計技巧,如果您在寫您自己的 Channel 實現的話,您可以適當地實現這些接 口。不過對於使用 java.nio.channels 包中標準通道類的程序員來說,這些接口並沒有太大的 吸引力。假如您快速回顧一下圖 3-2 或者向前跳躍到關於 file 和 socket 通道的章節,您將發現每一 個 file 或 socket 通道都實現全部三個接口。從類定義的角度而言,這意味着全部 file 和 socket 通道 對象都是雙向的。這對於 sockets 不是問題,因爲它們一直都是雙向的,不過對於 files 卻是個問題了。

一個文件可以在不同的時候以不同的權限打開。從 FileInputStream 對象的 getChannel( )方法獲取的 FileChannel 對象是隻讀的,不過從接口聲明的角度來看卻是雙向的,因爲 FileChannel 實現 ByteChannel 接口。在這樣一個通道上調用 write( )方法將拋出未經檢查的 NonWritableChannelException 異常,因爲 FileInputStream 對象總是以 read-only 的權限打開文件。

通道會連接一個特定 I/O 服務且通道實例(channel instance)的性能受它所連接的 I/O 服務的 特徵限制,記住這很重要。一個連接到只讀文件的 Channel 實例不能進行寫操作,即使該實例所屬 的類可能有 write( )方法。基於此,程序員需要知道通道是如何打開的,避免試圖嘗試一個底層 I/O 服務不允許的操作。

// A ByteBuffer named buffer contains data to be written
FileInputStream input = new FileInputStream(fileName);
FileChannel channel = input.getChannel();
// This will compile but will throw an IOException
// because the underlying file is read-only 
channel.write(buffer);

ByteChannel 的 read( ) 和 write( )方法使用 ByteBuffer 對象作爲參數。兩種方法均返回已傳輸的 字節數,可能比緩衝區的字節數少甚至可能爲零。緩衝區的位置也會發生與已傳輸字節相同數量的 前移。如果只進行了部分傳輸,緩衝區可以被重新提交給通道並從上次中斷的地方繼續傳輸。該過 程重複進行直到緩衝區的 hasRemaining( )方法返回 false 值。例 3-1 表示瞭如何從一個通道複製 數據到另一個通道。

例 3-1 在通道之間複製數據


package javatest.niotest;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;

/**
 * Test copying between channels.
 *
 * @author Ron Hitchens ([email protected])
 */
public class ChannelCopy {

    /**
     * This code copies data from stdin to stdout. Like the 'cat'
     * command, but without any useful options.
     */
    public static void main(String[] argv) throws IOException {
        ReadableByteChannel source = Channels.newChannel(System.in);
        WritableByteChannel dest = Channels.newChannel(System.out);
        channelCopy1(source, dest);
        // alternatively, call channelCopy2 (source, dest);
        source.close();
        dest.close();
    }

    /**
     * Channel copy method 1. This method copies data from the src
     * channel and writes it to the dest channel until EOF on src.
     * <p>
     * This implementation makes use of compact( ) on the temp buffer
     * to pack down the data if the buffer wasn't fully drained. This
     * may result in data copying, but minimizes system calls. It also
     * requires a cleanup loop to make sure all the data gets sent.
     */
    private static void channelCopy1(ReadableByteChannel src, WritableByteChannel dest) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);
        while (src.read(buffer) != -1) {
            // Prepare the buffer to be drained
            buffer.flip();
            // Write to the channel; may block
            dest.write(buffer);
            // If partial transfer, shift remainder down
            // If buffer is empty, same as doing clear( )
            buffer.compact();
        }
        // EOF will leave buffer in fill state
        buffer.flip();
        // Make sure that the buffer is fully drained
        while (buffer.hasRemaining()) {
            dest.write(buffer);
        }
    }

    /**
     * Channel copy method 2. This method performs the same copy, but
     * assures the temp buffer is empty before reading more data. This
     * never requires data copying but may result in more systems calls.
     * No post-loop cleanup is needed because the buffer will be empty
     * when the loop is exited.
     */
    private static void channelCopy2(ReadableByteChannel src, WritableByteChannel dest) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);
        while (src.read(buffer) != -1) {
            // Prepare the buffer to be drained
            buffer.flip();
            // Make sure that the buffer was fully drained
            while (buffer.hasRemaining()) {
                dest.write(buffer);
            }
            // Make the buffer empty, ready for filling
            buffer.clear();
        }
    }
}

通道可以以阻塞(blocking)或非阻塞(nonblocking)模式運行。非阻塞模式的通道永遠不會 讓調用的線程休眠。請求的操作要麼立即完成,要麼返回一個結果表明未進行任何操作。只有面向 流的(stream-oriented)的通道,如 sockets 和 pipes 才能使用非阻塞模式。

從圖 3-2 可以看出,socket 通道類從 SelectableChannel 引申而來。從 SelectableChannel 引申而 來的類可以和支持有條件的選擇(readiness selectio)的選擇器(Selectors)一起使用。將非阻塞 I/O 和選擇器組合起來可以使您的程序利用多路複用 I/O(multiplexed I/O)。選擇和多路複用將在 第四章中予以討論。關於怎樣將 sockets 置於非阻塞模式的細節會在 3.5 節中涉及。

 

3)關閉通道

與緩衝區不同,通道不能被重複使用。一個打開的通道即代表與一個特定 I/O 服務的特定連接 並封裝該連接的狀態。當通道關閉時,那個連接會丟失,然後通道將不再連接任何東西。

/*
 * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.nio.channels;

import java.io.IOException;
import java.io.Closeable;


/**
 * A nexus for I/O operations.
 *
 * <p> A channel represents an open connection to an entity such as a hardware
 * device, a file, a network socket, or a program component that is capable of
 * performing one or more distinct I/O operations, for example reading or
 * writing.
 *
 * <p> A channel is either open or closed.  A channel is open upon creation,
 * and once closed it remains closed.  Once a channel is closed, any attempt to
 * invoke an I/O operation upon it will cause a {@link ClosedChannelException}
 * to be thrown.  Whether or not a channel is open may be tested by invoking
 * its {@link #isOpen isOpen} method.
 *
 * <p> Channels are, in general, intended to be safe for multithreaded access
 * as described in the specifications of the interfaces and classes that extend
 * and implement this interface.
 *
 *
 * @author Mark Reinhold
 * @author JSR-51 Expert Group
 * @since 1.4
 */

public interface Channel extends Closeable {

    /**
     * Tells whether or not this channel is open.
     *
     * @return <tt>true</tt> if, and only if, this channel is open
     */
    public boolean isOpen();

    /**
     * Closes this channel.
     *
     * <p> After a channel is closed, any further attempt to invoke I/O
     * operations upon it will cause a {@link ClosedChannelException} to be
     * thrown.
     *
     * <p> If this channel is already closed then invoking this method has no
     * effect.
     *
     * <p> This method may be invoked at any time.  If some other thread has
     * already invoked it, however, then another invocation will block until
     * the first invocation is complete, after which it will return without
     * effect. </p>
     *
     * @throws  IOException  If an I/O error occurs
     */
    public void close() throws IOException;

}

調用通道的close( )方法時,可能會導致在通道關閉底層I/O服務的過程中線程暫時阻塞 7 ,哪怕 該通道處於非阻塞模式。通道關閉時的阻塞行爲(如果有的話)是高度取決於操作系統或者文件系 統的。在一個通道上多次調用close( )方法是沒有壞處的,但是如果第一個線程在close( )方法中阻 塞,那麼在它完成關閉通道之前,任何其他調用close( )方法都會阻塞。後續在該已關閉的通道上調 用close( )不會產生任何操作,只會立即返回。

可以通過 isOpen( )方法來測試通道的開放狀態。如果返回 true 值,那麼該通道可以使用。如 果返回 false 值,那麼該通道已關閉,不能再被使用。嘗試進行任何需要通道處於開放狀態作爲 前提的操作,如讀、寫等都會導致 ClosedChannelException 異常。

通道引入了一些與關閉和中斷有關的新行爲。如果一個通道實現 InterruptibleChannel 接口(參 見圖 3-2),它的行爲以下述語義爲準:如果一個線程在一個通道上被阻塞並且同時被中斷(由調 用該被阻塞線程的 interrupt( )方法的另一個線程中斷),那麼該通道將被關閉,該被阻塞線程也會 產生一個 ClosedByInterruptException 異常。

此外,假如一個線程的 interrupt status 被設置並且該線程試圖訪問一個通道,那麼這個通道將 立即被關閉,同時將拋出相同的 ClosedByInterruptException 異常。線程的 interrupt status 在線程的 interrupt( )方法被調用時會被設置。我們可以使用 isInterrupted( )來測試某個線程當前的 interrupt status。當前線程的 interrupt status 可以通過調用靜態的 Thread.interrupted( )方法清除。

僅僅因爲休眠在其上的線程被中斷就關閉通道,這看起來似乎過於苛刻了。不過這卻是 NIO 架構師們所做出的明確的設計決定。經驗表明,想要在所有的操作系統上一致而可靠地處理被中斷 的 I/O 操作是不可能的。“在全部平臺上提供確定的通道行爲”這一需求導致了“當 I/O 操作被中斷時 總是關閉通道”這一設計選擇。這個選擇被認爲是可接受的,因爲大部分時候一個線程被中斷就是 希望以此來關閉通道。java.nio 包中強制使用此行爲來避免因操作系統獨特性而導致的困境,因 爲該困境對 I/O 區域而言是極其危險的。這也是爲增強健壯性(robustness)而採用的一種經典的權 衡。

可中斷的通道也是可以異步關閉的。實現 InterruptibleChannel 接口的通道可以在任何時候被關 閉,即使有另一個被阻塞的線程在等待該通道上的一個 I/O 操作完成。當一個通道被關閉時,休眠 在該通道上的所有線程都將被喚醒並接收到一個 AsynchronousCloseException 異常。接着通道就被 關閉並將不再可用。

不實現 InterruptibleChannel 接口的通道一般都是不進行底層本地代碼實現的有特殊用途的通 道。這些也許是永遠不會阻塞的特殊用途通道,如舊系統數據流的封裝包或不能實現可中斷語義的 writer 類等。(參見 3.7 節)

2.Scatter/Gather

通道提供了一種被稱爲 Scatter/Gather 的重要新功能(有時也被稱爲矢量 I/O)。Scatter/Gather 是一個簡單卻強大的概念(參見 1.4.1.1 節),它是指在多個緩衝區上實現一個簡單的 I/O 操作。對 於一個 write 操作而言,數據是從幾個緩衝區按順序抽取(稱爲 gather)並沿着通道發送的。緩衝 區本身並不需要具備這種 gather 的能力(通常它們也沒有此能力)。該 gather 過程的效果就好比全 部緩衝區的內容被連結起來,並在發送數據前存放到一個大的緩衝區中。對於 read 操作而言,從 通道讀取的數據會按順序被散佈(稱爲 scatter)到多個緩衝區,將每個緩衝區填滿直至通道中的數據或者緩衝區的最大空間被消耗完。

大多數現代操作系統都支持本地矢量 I/O(native vectored I/O)。當您在一個通道上請求一個 Scatter/Gather 操作時,該請求會被翻譯爲適當的本地調用來直接填充或抽取緩衝區。這是一個很大 的進步,因爲減少或避免了緩衝區拷貝和系統調用。Scatter/Gather 應該使用直接的 ByteBuffers 以從 本地 I/O 獲取最大性能優勢。

將 scatter/gather 接口添加到圖 3-3 的 UML 類圖中可以得到圖 3-4。下面的代碼描述了 scatter 是 如何擴展讀操作的,以及 gather 是如何基於寫操作構建的:

/*
 * Copyright (c) 2000, 2006, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.nio.channels;

import java.io.IOException;
import java.nio.ByteBuffer;


/**
 * A channel that can read bytes into a sequence of buffers.
 *
 * <p> A <i>scattering</i> read operation reads, in a single invocation, a
 * sequence of bytes into one or more of a given sequence of buffers.
 * Scattering reads are often useful when implementing network protocols or
 * file formats that, for example, group data into segments consisting of one
 * or more fixed-length headers followed by a variable-length body.  Similar
 * <i>gathering</i> write operations are defined in the {@link
 * GatheringByteChannel} interface.  </p>
 *
 *
 * @author Mark Reinhold
 * @author JSR-51 Expert Group
 * @since 1.4
 */

public interface ScatteringByteChannel
    extends ReadableByteChannel
{

    /**
     * Reads a sequence of bytes from this channel into a subsequence of the
     * given buffers.
     *
     * <p> An invocation of this method attempts to read up to <i>r</i> bytes
     * from this channel, where <i>r</i> is the total number of bytes remaining
     * the specified subsequence of the given buffer array, that is,
     *
     * <blockquote><pre>
     * dsts[offset].remaining()
     *     + dsts[offset+1].remaining()
     *     + ... + dsts[offset+length-1].remaining()</pre></blockquote>
     *
     * at the moment that this method is invoked.
     *
     * <p> Suppose that a byte sequence of length <i>n</i> is read, where
     * <tt>0</tt>&nbsp;<tt>&lt;=</tt>&nbsp;<i>n</i>&nbsp;<tt>&lt;=</tt>&nbsp;<i>r</i>.
     * Up to the first <tt>dsts[offset].remaining()</tt> bytes of this sequence
     * are transferred into buffer <tt>dsts[offset]</tt>, up to the next
     * <tt>dsts[offset+1].remaining()</tt> bytes are transferred into buffer
     * <tt>dsts[offset+1]</tt>, and so forth, until the entire byte sequence
     * is transferred into the given buffers.  As many bytes as possible are
     * transferred into each buffer, hence the final position of each updated
     * buffer, except the last updated buffer, is guaranteed to be equal to
     * that buffer's limit.
     *
     * <p> This method may be invoked at any time.  If another thread has
     * already initiated a read operation upon this channel, however, then an
     * invocation of this method will block until the first operation is
     * complete. </p>
     *
     * @param  dsts
     *         The buffers into which bytes are to be transferred
     *
     * @param  offset
     *         The offset within the buffer array of the first buffer into
     *         which bytes are to be transferred; must be non-negative and no
     *         larger than <tt>dsts.length</tt>
     *
     * @param  length
     *         The maximum number of buffers to be accessed; must be
     *         non-negative and no larger than
     *         <tt>dsts.length</tt>&nbsp;-&nbsp;<tt>offset</tt>
     *
     * @return The number of bytes read, possibly zero,
     *         or <tt>-1</tt> if the channel has reached end-of-stream
     *
     * @throws  IndexOutOfBoundsException
     *          If the preconditions on the <tt>offset</tt> and <tt>length</tt>
     *          parameters do not hold
     *
     * @throws  NonReadableChannelException
     *          If this channel was not opened for reading
     *
     * @throws  ClosedChannelException
     *          If this channel is closed
     *
     * @throws  AsynchronousCloseException
     *          If another thread closes this channel
     *          while the read operation is in progress
     *
     * @throws  ClosedByInterruptException
     *          If another thread interrupts the current thread
     *          while the read operation is in progress, thereby
     *          closing the channel and setting the current thread's
     *          interrupt status
     *
     * @throws  IOException
     *          If some other I/O error occurs
     */
    public long read(ByteBuffer[] dsts, int offset, int length)
        throws IOException;

    /**
     * Reads a sequence of bytes from this channel into the given buffers.
     *
     * <p> An invocation of this method of the form <tt>c.read(dsts)</tt>
     * behaves in exactly the same manner as the invocation
     *
     * <blockquote><pre>
     * c.read(dsts, 0, dsts.length);</pre></blockquote>
     *
     * @param  dsts
     *         The buffers into which bytes are to be transferred
     *
     * @return The number of bytes read, possibly zero,
     *         or <tt>-1</tt> if the channel has reached end-of-stream
     *
     * @throws  NonReadableChannelException
     *          If this channel was not opened for reading
     *
     * @throws  ClosedChannelException
     *          If this channel is closed
     *
     * @throws  AsynchronousCloseException
     *          If another thread closes this channel
     *          while the read operation is in progress
     *
     * @throws  ClosedByInterruptException
     *          If another thread interrupts the current thread
     *          while the read operation is in progress, thereby
     *          closing the channel and setting the current thread's
     *          interrupt status
     *
     * @throws  IOException
     *          If some other I/O error occurs
     */
    public long read(ByteBuffer[] dsts) throws IOException;

}
/*
 * Copyright (c) 2000, 2001, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.nio.channels;

import java.io.IOException;
import java.nio.ByteBuffer;


/**
 * A channel that can write bytes from a sequence of buffers.
 *
 * <p> A <i>gathering</i> write operation writes, in a single invocation, a
 * sequence of bytes from one or more of a given sequence of buffers.
 * Gathering writes are often useful when implementing network protocols or
 * file formats that, for example, group data into segments consisting of one
 * or more fixed-length headers followed by a variable-length body.  Similar
 * <i>scattering</i> read operations are defined in the {@link
 * ScatteringByteChannel} interface.  </p>
 *
 *
 * @author Mark Reinhold
 * @author JSR-51 Expert Group
 * @since 1.4
 */

public interface GatheringByteChannel
    extends WritableByteChannel
{

    /**
     * Writes a sequence of bytes to this channel from a subsequence of the
     * given buffers.
     *
     * <p> An attempt is made to write up to <i>r</i> bytes to this channel,
     * where <i>r</i> is the total number of bytes remaining in the specified
     * subsequence of the given buffer array, that is,
     *
     * <blockquote><pre>
     * srcs[offset].remaining()
     *     + srcs[offset+1].remaining()
     *     + ... + srcs[offset+length-1].remaining()</pre></blockquote>
     *
     * at the moment that this method is invoked.
     *
     * <p> Suppose that a byte sequence of length <i>n</i> is written, where
     * <tt>0</tt>&nbsp;<tt>&lt;=</tt>&nbsp;<i>n</i>&nbsp;<tt>&lt;=</tt>&nbsp;<i>r</i>.
     * Up to the first <tt>srcs[offset].remaining()</tt> bytes of this sequence
     * are written from buffer <tt>srcs[offset]</tt>, up to the next
     * <tt>srcs[offset+1].remaining()</tt> bytes are written from buffer
     * <tt>srcs[offset+1]</tt>, and so forth, until the entire byte sequence is
     * written.  As many bytes as possible are written from each buffer, hence
     * the final position of each updated buffer, except the last updated
     * buffer, is guaranteed to be equal to that buffer's limit.
     *
     * <p> Unless otherwise specified, a write operation will return only after
     * writing all of the <i>r</i> requested bytes.  Some types of channels,
     * depending upon their state, may write only some of the bytes or possibly
     * none at all.  A socket channel in non-blocking mode, for example, cannot
     * write any more bytes than are free in the socket's output buffer.
     *
     * <p> This method may be invoked at any time.  If another thread has
     * already initiated a write operation upon this channel, however, then an
     * invocation of this method will block until the first operation is
     * complete. </p>
     *
     * @param  srcs
     *         The buffers from which bytes are to be retrieved
     *
     * @param  offset
     *         The offset within the buffer array of the first buffer from
     *         which bytes are to be retrieved; must be non-negative and no
     *         larger than <tt>srcs.length</tt>
     *
     * @param  length
     *         The maximum number of buffers to be accessed; must be
     *         non-negative and no larger than
     *         <tt>srcs.length</tt>&nbsp;-&nbsp;<tt>offset</tt>
     *
     * @return  The number of bytes written, possibly zero
     *
     * @throws  IndexOutOfBoundsException
     *          If the preconditions on the <tt>offset</tt> and <tt>length</tt>
     *          parameters do not hold
     *
     * @throws  NonWritableChannelException
     *          If this channel was not opened for writing
     *
     * @throws  ClosedChannelException
     *          If this channel is closed
     *
     * @throws  AsynchronousCloseException
     *          If another thread closes this channel
     *          while the write operation is in progress
     *
     * @throws  ClosedByInterruptException
     *          If another thread interrupts the current thread
     *          while the write operation is in progress, thereby
     *          closing the channel and setting the current thread's
     *          interrupt status
     *
     * @throws  IOException
     *          If some other I/O error occurs
     */
    public long write(ByteBuffer[] srcs, int offset, int length)
        throws IOException;


    /**
     * Writes a sequence of bytes to this channel from the given buffers.
     *
     * <p> An invocation of this method of the form <tt>c.write(srcs)</tt>
     * behaves in exactly the same manner as the invocation
     *
     * <blockquote><pre>
     * c.write(srcs, 0, srcs.length);</pre></blockquote>
     *
     * @param  srcs
     *         The buffers from which bytes are to be retrieved
     *
     * @return  The number of bytes written, possibly zero
     *
     * @throws  NonWritableChannelException
     *          If this channel was not opened for writing
     *
     * @throws  ClosedChannelException
     *          If this channel is closed
     *
     * @throws  AsynchronousCloseException
     *          If another thread closes this channel
     *          while the write operation is in progress
     *
     * @throws  ClosedByInterruptException
     *          If another thread interrupts the current thread
     *          while the write operation is in progress, thereby
     *          closing the channel and setting the current thread's
     *          interrupt status
     *
     * @throws  IOException
     *          If some other I/O error occurs
     */
    public long write(ByteBuffer[] srcs) throws IOException;

}

從上圖您可以看到,這兩個接口都添加了兩種以緩衝區陣列作爲參數的新方法。另外,每種方 法都提供了一種帶 offset 和 length 參數的形式。讓我們先來理解一下怎樣使用方法的簡單形式。在 下面的代碼中,我們假定 channel 連接到一個有 48 字節數據等待讀取的 socket 上:

        ByteBuffer header = ByteBuffer.allocateDirect(10);
        ByteBuffer body = ByteBuffer.allocateDirect(80);
        ByteBuffer[] buffers = {header, body};
        int bytesRead = channel.read(buffers);

一旦 read( )方法返回,bytesRead 就被賦予值 48,header 緩衝區將包含前 10 個從通道讀 取的字節而 body 緩衝區則包含接下來的 38 個字節。通道會自動地將數據 scatter 到這兩個緩衝區 中。緩衝區已經被填充了(儘管此例中 body 緩衝區還有空間填充更多數據),那麼將需要被 flip 以便其中數據可以被抽取。在類似這樣的例子中,我們可能並不會費勁去 flip 這個 header 緩衝區 而是以絕對 get 的方式隨機訪問它以檢查各種 header 字段;不過 body 緩衝區會被 flip 並傳遞到另 一個通道的 write( )方法上,然後在通道上發送出去。例如:

        switch (header.getShort(0)) {
            case TYPE_PING:
                break;
            case TYPE_FILE:
                body.flip();
                fileChannel.write(body);
                break;
            default:
                logUnknownPacket(header.getShort(0), header.getLong(2), body);
                break;
        }

同樣,很簡單地,我們可以用一個 gather 操作將多個緩衝區的數據組合併發送出去。使用相同 的緩衝區,我們可以像下面這樣彙總數據並在一個 socket 通道上發送包:

        body.clear();
        body.put("FOO".getBytes()).flip(); // "FOO" as bytes
        header.clear();
        header.putShort(TYPE_FILE).putLong(body.limit()).flip();
        long bytesWritten = channel.write(buffers);

以上代碼從傳遞給 write( )方法的 buffers 陣列所引用的緩衝區中 gather 數據,然後沿着通道發送了總共 13 個字節。

圖 3-5 描述了一個 gather 寫操作。數據從緩衝區陣列引用的每個緩衝區中 gather 並被組合成沿 着通道發送的字節流。

圖 3-6 描述了一個 scatter 讀操作。從通道傳輸來的數據被 scatter 到所列緩衝區,依次填充每個 緩衝區(從緩衝區的 position 處開始到 limit 處結束)。這裏顯示的 position 和 limit 值是讀操作開 始之前的。

帶 offset 和 length 參數版本的 read( ) 和 write( )方法使得我們可以使用緩衝區陣列的子集 緩衝區。這裏的 offset 值指哪個緩衝區將開始被使用,而不是指數據的 offset。這裏的 length 參 數指示要使用的緩衝區數量。舉個例子,假設我們有一個五元素的 fiveBuffers 陣列,它已經被 初始化並引用了五個緩衝區,下面的代碼將會寫第二個、第三個和第四個緩衝區的內容:

int bytesRead = channel.write (fiveBuffers, 1, 3);

使用得當的話,Scatter/Gather 會是一個極其強大的工具。它允許您委託操作系統來完成辛苦 活:將讀取到的數據分開存放到多個存儲桶(bucket)或者將不同的數據區塊合併成一個整體。這 是一個巨大的成就,因爲操作系統已經被高度優化來完成此類工作了。它節省了您來回移動數據的工作,也就避免了緩衝區拷貝和減少了您需要編寫、調試的代碼數量。既然您基本上通過提供數據 容器引用來組合數據,那麼按照不同的組合構建多個緩衝區陣列引用,各種數據區塊就可以以不同 的方式來組合了。例 3-2 很好地詮釋了這一點:

例 3-2 以 gather 寫操作來集合多個緩衝區的數據

package org.example;

import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.GatheringByteChannel;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

/**
 * Demonstrate gathering write using many buffers.
 * <p>
 *
 * @author Ron Hitchens ([email protected])
 */
public class Marketing {

    private static final String DEMOGRAPHIC = "blahblah.txt";

    // "Leverage frictionless methodologies"
    public static void main(String[] argv) throws Exception {
        int reps = 10;
        if (argv.length > 0) {
            reps = Integer.parseInt(argv[0]);
        }
        FileOutputStream fos = new FileOutputStream(DEMOGRAPHIC);
        GatheringByteChannel gatherChannel = fos.getChannel();
        // Generate some brilliant marcom, er, repurposed content
        ByteBuffer[] bs = utterBS(reps);
        // Deliver the message to the waiting market
        while (gatherChannel.write(bs) > 0) {
            // Empty body
            // Loop until write( ) returns zero
        }
        System.out.println("Mindshare paradigms synergized to " + DEMOGRAPHIC);
        fos.close();
    }

    // -----------------------------------------------
    // These are just representative; add your own
    private static String[] col1 = {
            "Aggregate", "Enable", "Leverage",
            "Facilitate", "Synergize", "Repurpose",
            "Strategize", "Reinvent", "Harness"
    };
    private static String[] col2 = {
            "cross-platform", "best-of-breed", "frictionless",
            "ubiquitous", "extensible", "compelling",
            "mission-critical", "collaborative", "integrated"
    };
    private static String[] col3 = {
            "methodologies", "infomediaries", "platforms",
            "schemas", "mindshare", "paradigms",
            "functionalities", "web services", "infrastructures"};
    private static String newline = System.getProperty("line.separator");

    // The Marcom-atic 9000
    private static ByteBuffer[] utterBS(int howMany)
            throws Exception {
        List list = new LinkedList();
        for (int i = 0; i < howMany; i++) {
            list.add(pickRandom(col1, " "));
            list.add(pickRandom(col2, " "));
            list.add(pickRandom(col3, newline));
        }
        ByteBuffer[] bufs = new ByteBuffer[list.size()];
        list.toArray(bufs);
        return (bufs);
    } // The communications director

    private static Random rand = new Random();

    // Pick one, make a buffer to hold it and the suffix, load it with
    // the byte equivalent of the strings (will not work properly for
    // non-Latin characters), then flip the loaded buffer so it's ready
    // to be drained
    private static ByteBuffer pickRandom(String[] strings, String suffix) throws Exception {
        String string = strings[rand.nextInt(strings.length)];
        int total = string.length() + suffix.length();
        ByteBuffer buf = ByteBuffer.allocate(total);
        buf.put(string.getBytes("US-ASCII"));
        buf.put(suffix.getBytes("US-ASCII"));
        buf.flip();
        return (buf);
    }
}

下面是實現 Marketing 類的輸出。雖然這種輸出沒什麼意義,但是 gather 寫操作卻能讓我們 非常高效地把它生成出來。

     Aggregate compelling methodologies
     Harness collaborative platforms
     Aggregate integrated schemas
     Aggregate frictionless platforms
     Enable integrated platforms
     Leverage cross-platform functionalities
     Harness extensible paradigms
     Synergize compelling infomediaries
     Repurpose cross-platform mindshare 
     Facilitate cross-platform infomediaries

3.文件通道

直到現在,我們都還只是在泛泛地討論通道,比如討論那些對所有通道都適用的內容。是時候 具體點了,本節我們來討論文件通道(socket 通道將在下一節討論)。從圖 3-7 可以發現, FileChannel 類可以實現常用的 read,write 以及 scatter/gather 操作,同時它也提供了很多專用於文 件的新方法。這些方法中的許多都是我們所熟悉的文件操作,不過其他的您可能之前並未接觸過。 現在我們將在此對它們全部予以討論。

文件通道總是阻塞式的,因此不能被置於非阻塞模式。現代操作系統都有複雜的緩存和預取機 制,使得本地磁盤 I/O 操作延遲很少。網絡文件系統一般而言延遲會多些,不過卻也因該優化而受 益。面向流的 I/O 的非阻塞範例對於面向文件的操作並無多大意義,這是由文件 I/O 本質上的不同 性質造成的。對於文件 I/O,最強大之處在於異步 I/O(asynchronous I/O),它允許一個進程可以 從操作系統請求一個或多個 I/O 操作而不必等待這些操作的完成。發起請求的進程之後會收到它請 求的 I/O 操作已完成的通知。異步 I/O 是一種高級性能,當前的很多操作系統都還不具備。以後的 NIO 增強也會把異步 I/O 納入考慮範圍。

我們在 3.1.1 節中提到,FileChannel對象不能直接創建。一個FileChannel實例只能通過在一個 打開的file對象(RandomAccessFile、FileInputStream或 FileOutputStream)上調用getChannel( )方法 獲取 8 。調用getChannel( )方法會返回一個連接到相同文件的FileChannel對象且該FileChannel對象 具有與file對象相同的訪問權限,然後您就可以使用該通道對象來利用強大的FileChannel API了:

package java.nio.channels;

import java.nio.MappedByteBuffer;
import java.nio.channels.*;

public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel {

    // This is a partial API listing
    // All methods listed here can throw java.io.IOException
    public abstract int read(ByteBuffer dst, long position)

    public abstract int write(ByteBuffer src, long position)

    public abstract long size()

    public abstract long position()

    public abstract void position(long newPosition)

    public abstract void truncate(long size)

    public abstract void force(boolean metaData)

    public final FileLock lock()

    public abstract FileLock lock(long position, long size, boolean shared)

    public final FileLock tryLock()

    public abstract FileLock tryLock(long position, long size, boolean shared)

    public abstract MappedByteBuffer map(MapMode mode, long position, long size)

    public static class MapMode {

        public static final MapMode READ_ONLY
        public static final MapMode READ_WRITE
        public static final MapMode PRIVATE
    }

    public abstract long transferTo(long position, long count, WritableByteChannel target)

    public abstract long transferFrom(ReadableByteChannel src, long position, long count)
}

上面的代碼中給出了 FileChannel 類引入的新 API 方法。所有這些方法都可以拋出 java.io.IOException 異常,不過拋出語句並未在此列出。

同大多數通道一樣,只要有可能,FileChannel 都會嘗試使用本地 I/O 服務。FileChannel 類本身是抽象的,您從 getChannel( )方法獲取的實際對象是一個具體子類(subclass)的一個實例 (instance),該子類可能使用本地代碼來實現以上 API 方法中的一些或全部。

FileChannel 對象是線程安全(thread-safe)的。多個進程可以在同一個實例上併發調用方法而 不會引起任何問題,不過並非所有的操作都是多線程的(multithreaded)。影響通道位置或者影響 文件大小的操作都是單線程的(single-threaded)。如果有一個線程已經在執行會影響通道位置或文 件大小的操作,那麼其他嘗試進行此類操作之一的線程必須等待。併發行爲也會受到底層的操作系 統或文件系統影響。

同大多數 I/O 相關的類一樣,FileChannel 是一個反映 Java 虛擬機外部一個具體對象的抽象。 FileChannel 類保證同一個 Java 虛擬機上的所有實例看到的某個文件的視圖均是一致的,但是 Java 虛擬機卻不能對超出它控制範圍的因素提供擔保。通過一個 FileChannel 實例看到的某個文件的視 圖同通過一個外部的非 Java 進程看到的該文件的視圖可能一致,也可能不一致。多個進程發起的 併發文件訪問的語義高度取決於底層的操作系統和(或)文件系統。一般而言,由運行在不同 Java 虛擬機上的 FileChannel 對象發起的對某個文件的併發訪問和由非 Java 進程發起的對該文件的併發 訪問是一致的。

1)訪問文件

每個 FileChannel 對象都同一個文件描述符(file descriptor)有一對一的關係,所以上面列出的 API 方法與在您最喜歡的 POSIX(可移植操作系統接口)兼容的操作系統上的常用文件 I/O 系統調 用緊密對應也就不足爲怪了。名稱也許不盡相同,不過常見的 suspect(“可疑分子”)都被集中 起來了。您可能也注意到了上面列出的 API 方法同 java.io 包中 RandomAccessFile 類的方法的相 似之處了。本質上講,RandomAccessFile 類提供的是同樣的抽象內容。在通道出現之前,底層的文 件操作都是通過 RandomAccessFile 類的方法來實現的。FileChannel 模擬同樣的 I/O 服務,因此它 的 API 自然也是很相似的。

爲了便於比較,表 3-1 列出了 FileChannel、RandomAccessFile 和 POSIX I/O system calls 三者在 方法上的對應關係。

讓我們來進一步看下基本的文件訪問方法(請記住這些方法都可以拋出 java.io.IOException 異 常):

package java.nio.channels;

import java.nio.channels.ByteChannel;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.ScatteringByteChannel;

public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel {

    // This is a partial API listing
    public abstract long position()

    public abstract void position(long newPosition)

    public abstract int read(ByteBuffer dst)

    public abstract int read(ByteBuffer dst, long position)

    public abstract int write(ByteBuffer src)

    public abstract int write(ByteBuffer src, long position)

    public abstract long size()

    public abstract void truncate(long size)

    public abstract void force(boolean metaData)
}

同底層的文件描述符一樣,每個 FileChannel 都有一個叫“file position”的概念。這個 position 值 決定文件中哪一處的數據接下來將被讀或者寫。從這個方面看,FileChannel 類同緩衝區很類似, 並且 MappedByteBuffer 類使得我們可以通過 ByteBuffer API 來訪問文件數據(我們會在後面的章節 中瞭解到這一點)。

您可以從前面的API清單中看到,有兩種形式的position( )方法。第一種,不帶參數的,返回當 前文件的position值。返回值是一個長整型(long),表示文件中的當前字節位置。

第二種形式的 position( )方法帶一個 long(長整型)參數並將通道的 position 設置爲指定值。 如果嘗試將通道 position 設置爲一個負值會導致 java.lang.IllegalArgumentException 異常,不過可以 把 position 設置到超出文件尾,這樣做會把 position 設置爲指定值而不改變文件大小。假如在將 position 設置爲超出當前文件大小時實現了一個 read( )方法,那麼會返回一個文件尾(end-of-file) 條件;倘若此時實現的是一個 write( )方法則會引起文件增長以容納寫入的字節,具體行爲類似於 實現一個絕對 write( )並可能導致出現一個文件空洞(file hole,參見“文件空洞究竟是什麼?”)。

文件空洞究竟是什麼?

當磁盤上一個文件的分配空間小於它的文件大小時會出現“文件空洞”。對於內容稀疏的文 件,大多數現代文件系統只爲實際寫入的數據分配磁盤空間(更準確地說,只爲那些寫入數 據的文件系統頁分配空間)。假如數據被寫入到文件中非連續的位置上,這將導致文件出現 在邏輯上不包含數據的區域(即“空洞”)。例如,下面的代碼可能產生一個如圖 3-8 所示的文 件:

package org.example;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * Create a file with holes in it
 *
 * @author Ron Hitchens ([email protected])
 */
public class FileHole {

    public static void main(String[] argv) throws IOException {
        // Create a temp file, open for writing, and get
        // a FileChannel
        File temp = File.createTempFile("holy", null);
        RandomAccessFile file = new RandomAccessFile(temp, "rw");
        FileChannel channel = file.getChannel();
        // Create a working buffer
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100);
        putData(0, byteBuffer, channel);
        putData(5000000, byteBuffer, channel);
        putData(50000, byteBuffer, channel);
        // Size will report the largest position written, but
        // there are two holes in this file. This file will
        // not consume 5 MB on disk (unless the filesystem is
        // extremely brain-damaged)
        System.out.println("Wrote temp file '" + temp.getPath() + "', size=" + channel.size());
        channel.close();
        file.close();
    }

    private static void putData(int position, ByteBuffer buffer, FileChannel channel) throws IOException {
        String string = "*<-- location " + position;
        buffer.clear();
        buffer.put(string.getBytes("US-ASCII"));
        buffer.flip();
        channel.position(position);
        channel.write(buffer);
    }
}

如果該文件被順序讀取的話,所有空洞都會被“0”填充但不佔用磁盤空間。讀取該文件的 進程會看到 5,000,021 個字節,大部分字節都以“0”表示。試試在該文件上運行 strings 命令, 看看您會得到什麼。再試試將文件大小的值提高到 50 或 100MB,看看您的全部磁盤空間消耗 以及順序掃描該文件所需時間會發生何種變化(前者不會改變,但是後者將有非常大的增 加)。

FileChannel 位置(position)是從底層的文件描述符獲得的,該 position 同時被作爲通道引用 獲取來源的文件對象共享。這也就意味着一個對象對該 position 的更新可以被另一個對象看到:

        RandomAccessFile randomAccessFile = new RandomAccessFile("filename", "r");
        // Set the file position
        randomAccessFile.seek(1000);
        // Create a channel from the file
        FileChannel fileChannel = randomAccessFile.getChannel();
        // This will print "1000"
        System.out.println("file pos: " + fileChannel.position());
        // Change the position using the RandomAccessFile object
        randomAccessFile.seek(500);
        // This will print "500"
        System.out.println("file pos: " + fileChannel.position());
        // Change the position using the FileChannel object
        fileChannel.position(200);
        // This will print "200"
        System.out.println("file pos: " + randomAccessFile.getFilePointer());

類似於緩衝區的 get( ) 和 put( )方法,當字節被 read( )或 write( )方法傳輸時,文件 position 會 自動更新。如果 position 值達到了文件大小的值(文件大小的值可以通過 size( )方法返回),read( ) 方法會返回一個文件尾條件值(-1)。可是,不同於緩衝區的是,如果實現 write( )方法時 position 前進到超過文件大小的值,該文件會擴展以容納新寫入的字節。

同樣類似於緩衝區,也有帶 position 參數的絕對形式的 read( )和 write( )方法。這種絕對形式 的方法在返回值時不會改變當前的文件 position。由於通道的狀態無需更新,因此絕對的讀和寫可 能會更加有效率,操作請求可以直接傳到本地代碼。更妙的是,多個線程可以併發訪問同一個文件 而不會相互產生干擾。這是因爲每次調用都是原子性的(atomic),並不依靠調用之間系統所記住 的狀態。

類似於緩衝區的 get( ) 和 put( )方法,當字節被 read( )或 write( )方法傳輸時,文件 position 會 自動更新。如果 position 值達到了文件大小的值(文件大小的值可以通過 size( )方法返回),read( ) 方法會返回一個文件尾條件值(-1)。可是,不同於緩衝區的是,如果實現 write( )方法時 position 前進到超過文件大小的值,該文件會擴展以容納新寫入的字節。

同樣類似於緩衝區,也有帶 position 參數的絕對形式的 read( )和 write( )方法。這種絕對形式 的方法在返回值時不會改變當前的文件 position。由於通道的狀態無需更新,因此絕對的讀和寫可 能會更加有效率,操作請求可以直接傳到本地代碼。更妙的是,多個線程可以併發訪問同一個文件 而不會相互產生干擾。這是因爲每次調用都是原子性的(atomic),並不依靠調用之間系統所記住 的狀態。

package java.nio.channels;

import java.nio.channels.ByteChannel;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.ScatteringByteChannel;

public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel {
    // This is a partial API listing

    public abstract void truncate(long size)

    public abstract void force(boolean metaData)
}

上面列出的最後一個 API 是 force( )。該方法告訴通道強制將全部待定的修改都應用到磁盤的 文件上。所有的現代文件系統都會緩存數據和延遲磁盤文件更新以提高性能。調用 force( )方法要 求文件的所有待定修改立即同步到磁盤。

如果文件位於一個本地文件系統,那麼一旦 force( )方法返回,即可保證從通道被創建(或上 次調用 force( ))時起的對文件所做的全部修改已經被寫入到磁盤。對於關鍵操作如事務 (transaction)處理來說,這一點是非常重要的,可以保證數據完整性和可靠的恢復。然而,如果 文件位於一個遠程的文件系統,如 NFS 上,那麼不能保證待定修改一定能同步到永久存儲器 (permanent storage)上,因 Java 虛擬機不能做操作系統或文件系統不能實現的承諾。如果您的程 序在面臨系統崩潰時必須維持數據完整性,先去驗證一下您在使用的操作系統和(或)文件系統在 同步修改方面是可以依賴的。

force( )方法的布爾型參數表示在方法返回值前文件的元數據(metadata)是否也要被同步更新到磁盤。元數據指文件所有者、訪問權限、最後一次修改時間等信息。大多數情形下,該信息對數 據恢復而言是不重要的。給 force( )方法傳遞 false 值表示在方法返回前只需要同步文件數據的更 改。大多數情形下,同步元數據要求操作系統進行至少一次額外的底層 I/O 操作。一些大數量事務 處理程序可能通過在每次調用 force( )方法時不要求元數據更新來獲取較高的性能提升,同時也不 會犧牲數據完整性。

 

2)文件鎖定

在 JDK 1.4 版本之前,Java I/O 模型都未能提供文件鎖定(file locking),缺少這一特性讓人們 很頭疼。絕大多數現代操作系統早就有了文件鎖定功能,而直到 JDK 1.4 版本發佈時 Java 編程人員 纔可以使用文件鎖(file lock)。在集成許多其他非 Java 程序時,文件鎖定顯得尤其重要。此外, 它在判優(判斷多個訪問請求的優先級別)一個大系統的多個 Java 組件發起的訪問時也很有價 值。

我們在第一章中討論到,鎖(lock)可以是共享的(shared)或獨佔的(exclusive)。本節中描 述的文件鎖定特性在很大程度上依賴本地的操作系統實現。並非所有的操作系統和文件系統都支持 共享文件鎖。對於那些不支持的,對一個共享鎖的請求會被自動提升爲對獨佔鎖的請求。這可以保 證準確性卻可能嚴重影響性能。舉個例子,僅使用獨佔鎖將會串行化圖 1-7 中所列的全部 reader 進 程。如果您計劃部署程序,請確保您瞭解所用操作系統和文件系統的文件鎖定行爲,因爲這將嚴重 影響您的設計選擇。

另外,並非所有平臺都以同一個方式來實現基本的文件鎖定。在不同的操作系統上,甚至在同 一個操作系統的不同文件系統上,文件鎖定的語義都會有所差異。一些操作系統僅提供勸告鎖定 (advisory locking),一些僅提供獨佔鎖(exclusive locks),而有些操作系統可能兩種鎖都提供。 您應該總是按照勸告鎖的假定來管理文件鎖,因爲這是最安全的。但是如能瞭解底層操作系統如何 執行鎖定也是非常好的。例如,如果所有的鎖都是強制性的(mandatory)而您不及時釋放您獲得 的鎖的話,運行在同一操作系統上的其他程序可能會受到影響。

有關 FileChannel 實現的文件鎖定模型的一個重要注意項是:鎖的對象是文件而不是通道或線 程,這意味着文件鎖不適用於判優同一臺 Java 虛擬機上的多個線程發起的訪問。

如果一個線程在某個文件上獲得了一個獨佔鎖,然後第二個線程利用一個單獨打開的通道來請 求該文件的獨佔鎖,那麼第二個線程的請求會被批准。但如果這兩個線程運行在不同的 Java 虛擬 機上,那麼第二個線程會阻塞,因爲鎖最終是由操作系統或文件系統來判優的並且幾乎總是在進程 級而非線程級上判優。鎖都是與一個文件關聯的,而不是與單個的文件句柄或通道關聯。

文件鎖旨在在進程級別上判優文件訪問,比如在主要的程序組件之間或者在集成其他供應商的 組件時。如果您需要控制多個 Java 線程的併發訪問,您可能需要實施您自己的、輕量級的鎖定方 案。那種情形下,內存映射文件(本章後面會進行詳述)可能是一個合適的選擇。

現在讓我們來看下與文件鎖定有關的 FileChannel API 方法:

package java.nio.channels;

import java.io.IOException;
import java.nio.channels.ByteChannel;
import java.nio.channels.FileLock;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.ScatteringByteChannel;

public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel {

    public abstract FileLock lock(long position, long size, boolean shared)
            throws IOException;

    public final FileLock lock() throws IOException

    public abstract FileLock tryLock(long position, long size, boolean shared)
            throws IOException;

    public final FileLock tryLock() throws IOException
}

這次我們先看帶參數形式的 lock( )方法。鎖是在文件內部區域上獲得的。調用帶參數的 Lock( ) 方法會指定文件內部鎖定區域的開始 position 以及鎖定區域的 size。第三個參數 shared 表 示您想獲取的鎖是共享的(參數值爲 true)還是獨佔的(參數值爲 false)。要獲得一個共享 鎖,您必須先以只讀權限打開文件,而請求獨佔鎖時則需要寫權限。另外,您提供的 position 和 size 參數的值不能是負數。

鎖定區域的範圍不一定要限制在文件的 size 值以內,鎖可以擴展從而超出文件尾。因此,我們 可以提前把待寫入數據的區域鎖定,我們也可以鎖定一個不包含任何文件內容的區域,比如文件最 後一個字節以外的區域。如果之後文件增長到達那塊區域,那麼您的文件鎖就可以保護該區域的文 件內容了。相反地,如果您鎖定了文件的某一塊區域,然後文件增長超出了那塊區域,那麼新增加 的文件內容將不會受到您的文件鎖的保護。

不帶參數的簡單形式的 lock( )方法是一種在整個文件上請求獨佔鎖的便捷方法,鎖定區域等於 它能達到的最大範圍。該方法等價於:

fileChannel.lock (0L, Long.MAX_VALUE, false);

如果您正請求的鎖定範圍是有效的,那麼 lock( )方法會阻塞,它必須等待前面的鎖被釋放。假 如您的線程在此情形下被暫停,該線程的行爲受中斷語義(類似我們在 3.1.3 節中所討論的)控 制。如果通道被另外一個線程關閉,該暫停線程將恢復併產生一個 AsynchronousCloseException 異 常。假如該暫停線程被直接中斷(通過調用它的 interrupt( )方法),它將醒來併產生一個 FileLockInterruptionException 異常。如果在調用 lock( )方法時線程的 interrupt status 已經被設置,也 會產生 FileLockInterruptionException 異常。

在上面的 API 列表中有兩個名爲 tryLock( )的方法,它們是 lock( )方法的非阻塞變體。這兩個 tryLock( )和 lock( )方法起相同的作用,不過如果請求的鎖不能立即獲取到則會返回一個 null。

您可以看到,lock( )和 tryLock( )方法均返回一個 FileLock 對象。以下是完整的 FileLock API:

package java.nio.channels;

import java.io.IOException;

public abstract class FileLock implements AutoCloseable {

    public final FileChannel channel()

    public final long position()

    public final long size()

    public final boolean isShared()

    public final boolean overlaps(long position, long size)

    public abstract boolean isValid();

    public abstract void release() throws IOException;

    public final void close() throws IOException
}

FileLock 類封裝一個鎖定的文件區域。FileLock 對象由 FileChannel 創建並且總是關聯到那個特定的通道實例。您可以通過調用 channel( )方法來查詢一個 lock 對象以判斷它是由哪個通道創建 的。

一個 FileLock 對象創建之後即有效,直到它的 release( )方法被調用或它所關聯的通道被關閉或 Java 虛擬機關閉時纔會失效。我們可以通過調用 isValid( )布爾方法來測試一個鎖的有效性。一個鎖 的有效性可能會隨着時間而改變,不過它的其他屬性——位置(position)、範圍大小(size)和獨 佔性(exclusivity)——在創建時即被確定,不會隨着時間而改變。

您可以通過調用 isShared( )方法來測試一個鎖以判斷它是共享的還是獨佔的。如果底層的操作 系統或文件系統不支持共享鎖,那麼該方法將總是返回 false 值,即使您申請鎖時傳遞的參數值 是 true。假如您的程序依賴共享鎖定行爲,請測試返回的鎖以確保您得到了您申請的鎖類型。 FileLock 對象是線程安全的,多個線程可以併發訪問一個鎖對象。

最後,您可以通過調用 overlaps( )方法來查詢一個 FileLock 對象是否與一個指定的文件區域重 疊。這將使您可以迅速判斷您擁有的鎖是否與一個感興趣的區域(region of interest)有交叉。不過 即使返回值是 false 也不能保證您就一定能在期望的區域上獲得一個鎖,因爲 Java 虛擬機上的其 他地方或者外部進程可能已經在該期望區域上有一個或多個鎖了。您最好使用 tryLock( )方法確認 一下。

儘管一個 FileLock 對象是與某個特定的 FileChannel 實例關聯的,它所代表的鎖卻是與一個底 層文件關聯的,而不是與通道關聯。因此,如果您在使用完一個鎖後而不釋放它的話,可能會導致 衝突或者死鎖。請小心管理文件鎖以避免出現此問題。一旦您成功地獲取了一個文件鎖,如果隨後 在通道上出現錯誤的話,請務必釋放這個鎖。推薦使用類似下面的代碼形式:

FileLock lock = fileChannel.lock() 
try {
		<perform read/write/whatever on channel> 
} catch (IOException) {
		<handle unexpected exception>
} finally { 
		lock.release()
}

例 3-3 中的代碼使用共享鎖實現了 reader 進程,使用獨佔鎖實現了 writer 進程,圖 1-7 和圖 1-8 對此有詮釋。由於鎖是與進程而不是 Java 線程關聯的,您將需要運行該程序的多個拷貝。先從一 個 writer 和兩個或更多的 readers 開始,我們來看下不同類型的鎖是如何交互的。

例 3-3 共享鎖同獨佔鎖交互

package org.example;

import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Random;

/**
 * Test locking with FileChannel.
 * <p>
 * Run one copy of this code with arguments "-w /tmp/locktest.dat"
 * and one or more copies with "-r /tmp/locktest.dat" to see the
 * interactions of exclusive and shared locks. Note how too many
 * readers can starve out the writer.
 * <p>
 * Note: The filename you provide will be overwritten. Substitute
 * an appropriate temp filename for your favorite OS.
 * <p>
 * Created April, 2002
 *
 * @author Ron Hitchens ([email protected])
 */
public class LockTest {

    private static final int SIZEOF_INT = 4;
    private static final int INDEX_START = 0;
    private static final int INDEX_COUNT = 10;
    private static final int INDEX_SIZE = INDEX_COUNT * SIZEOF_INT;
    private ByteBuffer buffer = ByteBuffer.allocate(INDEX_SIZE);
    private IntBuffer indexBuffer = buffer.asIntBuffer();
    private Random rand = new Random();

    public static void main(String[] argv)
            throws Exception {
        boolean writer = false;
        String filename;
        if (argv.length != 2) {
            System.out.println("Usage: [ -r | -w ] filename");
            return;
        }
        writer = argv[0].equals("-w");
        filename = argv[1];
        RandomAccessFile raf = new RandomAccessFile(filename, (writer) ? "rw" : "r");
        FileChannel fc = raf.getChannel();
        LockTest lockTest = new LockTest();
        if (writer) {
            lockTest.doUpdates(fc);
        } else {
            lockTest.doQueries(fc);
        }
    }

    // ---------------------------------------------------------------
    // Simulate a series of read-only queries while
    // holding a shared lock on the index area
    void doQueries(FileChannel fc)
            throws Exception {
        while (true) {
            println("trying for shared lock...");
            FileLock lock = fc.lock(INDEX_START, INDEX_SIZE, true);
            int reps = rand.nextInt(60) + 20;
            for (int i = 0; i < reps; i++) {
                int n = rand.nextInt(INDEX_COUNT);
                int position = INDEX_START + (n * SIZEOF_INT);
                buffer.clear();
                fc.read(buffer, position);
                int value = indexBuffer.get(n);
                println("Index entry " + n + "=" + value);
                // Pretend to be doing some work Thread.sleep (100);
            }
            lock.release();
            println("<sleeping>");
            Thread.sleep(rand.nextInt(3000) + 500);
        }
    }

    // Simulate a series of updates to the index area
    // while holding an exclusive lock
    void doUpdates(FileChannel fc) throws Exception {
        while (true) {
            println("trying for exclusive lock...");
            FileLock lock = fc.lock(INDEX_START, INDEX_SIZE, false);
            updateIndex(fc);
            lock.release();
            println("<sleeping>");
            Thread.sleep(rand.nextInt(2000) + 500);
        }
    }

    // Write new values to the index slots
    private int idxval = 1;

    private void updateIndex(FileChannel fc) throws Exception {
        // "indexBuffer" is an int view of "buffer"
        indexBuffer.clear();
        for (int i = 0; i < INDEX_COUNT; i++) {
            idxval++;
            println("Updating index " + i + "=" + idxval);
            indexBuffer.put(idxval);
            // Pretend that this is really hard work
            Thread.sleep(500);
        }
        // leaves position and limit correct for whole buffer
        buffer.clear();
        fc.write(buffer, INDEX_START);
    }

    // ---------------------------------------------------------------
    private int lastLineLen = 0;

    // Specialized println that repaints the current line
    private void println(String msg) {
        System.out.print("\r ");
        System.out.print(msg);
        for (
                int i = msg.length();
                i < lastLineLen; i++) {
            System.out.print(" ");
        }
        System.out.print("\r");
        System.out.flush();
        lastLineLen = msg.length();
    }
}

以上代碼直接忽略了我之前說給的用 try/catch/finally 來釋放鎖的建議,在您自己所寫 的實際代碼中請不要這麼懶。

 

4.內存映射文件

新的 FileChannel 類提供了一個名爲 map( )的方法,該方法可以在一個打開的文件和一個特殊 類型的 ByteBuffer 之間建立一個虛擬內存映射(第一章中已經歸納了什麼是內存映射文件以及它們 如何同虛擬內存交互)。在 FileChannel 上調用 map( )方法會創建一個由磁盤文件支持的虛擬內存 映射(virtual memory mapping)並在那塊虛擬內存空間外部封裝一個 MappedByteBuffer 對象(參見 圖 1-6)。

由 map( )方法返回的 MappedByteBuffer 對象的行爲在多數方面類似一個基於內存的緩衝區,只 不過該對象的數據元素存儲在磁盤上的一個文件中。調用 get( )方法會從磁盤文件中獲取數據,此 數據反映該文件的當前內容,即使在映射建立之後文件已經被一個外部進程做了修改。通過文件映 射看到的數據同您用常規方法讀取文件看到的內容是完全一樣的。相似地,對映射的緩衝區實現一 個 put( )會更新磁盤上的那個文件(假設對該文件您有寫的權限),並且您做的修改對於該文件的 其他閱讀者也是可見的。

通過內存映射機制來訪問一個文件會比使用常規方法讀寫高效得多,甚至比使用通道的效率都高。因爲不需要做明確的系統調用,那會很消耗時間。更重要的是,操作系統的虛擬內存可以自動 緩存內存頁(memory page)。這些頁是用系統內存來緩存的,所以不會消耗 Java 虛擬機內存堆 (memory heap)。

一旦一個內存頁已經生效(從磁盤上緩存進來),它就能以完全的硬件速度再次被訪問而不需 要再次調用系統命令來獲取數據。那些包含索引以及其他需頻繁引用或更新的內容的巨大而結構化 文件能因內存映射機制受益非常多。如果同時結合文件鎖定來保護關鍵區域和控制事務原子性,那 您將能瞭解到內存映射緩衝區如何可以被很好地利用。

下面讓我們來看一下如何使用內存映射:

package java.nio.channels;

import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.ScatteringByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.spi.AbstractInterruptibleChannel;

public abstract class FileChannel
        extends AbstractInterruptibleChannel
        implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel {

    public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;

    public static class MapMode {

        public static final MapMode READ_ONLY
        public static final MapMode READ_WRITE
        public static final MapMode PRIVATE
    }
}

可以看到,只有一種 map( )方法來創建一個文件映射。它的參數有 mode,position 和 size。參 數 position 和 size 同 lock( )方法的這兩個參數是一樣的(在前面的章節中已有討論)。我們可 以創建一個 MappedByteBuffer 來代表一個文件中字節的某個子範圍。例如,要映射 100 到 299(包 含 299)位置的字節,可以使用下面的代碼:

buffer = fileChannel.map (FileChannel.MapMode.READ_ONLY, 100, 200);

如果要映射整個文件則使用:

buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());

與文件鎖的範圍機制不一樣,映射文件的範圍不應超過文件的實際大小。如果您請求一個超出 文件大小的映射,文件會被增大以匹配映射的大小。假如您給 size 參數傳遞的值是Integer.MAX_VALUE,文件大小的值會膨脹到超過 2.1GB。即使您請求的是一個只讀映射, map( )方法也會嘗試這樣做並且大多數情況下都會拋出一個 IOException 異常,因爲底層的文件不 能被修改。該行爲同之前討論的文件“空洞”的行爲是一致的。詳情請參考 3.3.1 節。

FileChannel 類定義了代表映射模式的常量,且是使用一個類型安全的枚舉而非數字值來定義 這些常量。這些常量是 FileChannel 內部定義的一個內部類(inner class)的靜態字段,它們可以在 編譯時被檢查類型,不過您可以像使用一個數值型常量那樣使用它們。

同常規的文件句柄類似,文件映射可以是可寫的或只讀的。前兩種映射模式 MapMode.READ_ONLY 和 MapMode.READ_WRITE 意義是很明顯的,它們表示您希望獲取的映射 只讀還是允許修改映射的文件。請求的映射模式將受被調用 map( )方法的 FileChannel 對象的訪問 權限所限制。如果通道是以只讀的權限打開的而您卻請求 MapMode.READ_WRITE 模式,那麼 map( )方法會拋出一個 NonWritableChannelException 異常;如果您在一個沒有讀權限的通道上請求 MapMode.READ_ONLY 映射模式,那麼將產生 NonReadableChannelException 異常。不過在以 read/write 權限打開的通道上請求一個 MapMode.READ_ONLY 映射卻是允許的。MappedByteBuffer 對象的可變性可以通過對它調用 isReadOnly( )方法來檢查。

第三種模式 MapMode.PRIVATE 表示您想要一個寫時拷貝(copy-on-write)的映射。這意味着 您通過 put( )方法所做的任何修改都會導致產生一個私有的數據拷貝並且該拷貝中的數據只有 MappedByteBuffer 實例可以看到。該過程不會對底層文件做任何修改,而且一旦緩衝區被施以垃圾 收集動作(garbage collected),那些修改都會丟失。儘管寫時拷貝的映射可以防止底層文件被修 改,您也必須以 read/write 權限來打開文件以建立 MapMode.PRIVATE 映射。只有這樣,返回的 MappedByteBuffer 對象才能允許使用 put( )方法。

寫時拷貝這一技術經常被操作系統使用,以在一個進程生成另一個進程時管理虛擬地址空間 (virtual address spaces)。使用寫時拷貝可以允許父進程和子進程共享內存頁直到它們中的一方實 際發生修改行爲。在處理同一文件的多個映射時也有相同的優勢(當然,這需要底層操作系統的支 持)。假設一個文件被多個 MappedByteBuffer 對象映射並且每個映射都是 MapMode.PRIVATE 模 式,那麼這份文件的大部分內容都可以被所有映射共享。

選擇使用 MapMode.PRIVATE 模式並不會導致您的緩衝區看不到通過其他方式對文件所做的 修改。對文件某個區域的修改在使用 MapMode.PRIVATE 模式的緩衝區中都能反映出來,除非該 緩衝區已經修改了文件上的同一個區域。正如第一章中所描述的,內存和文件系統都被劃分成了 頁。當在一個寫時拷貝的緩衝區上調用 put( )方法時,受影響的頁會被拷貝,然後更改就會應用到 該拷貝中。具體的頁面大小取決於具體實現,不過通常都是和底層文件系統的頁面大小時一樣的。 如果緩衝區還沒對某個頁做出修改,那麼這個頁就會反映被映射文件的相應位置上的內容。一旦某 個頁因爲寫操作而被拷貝,之後就將使用該拷貝頁,並且不能被其他緩衝區或文件更新所修改。例 3-5 的代碼詮釋了這一行爲。

您應該注意到了沒有 unmap( )方法。也就是說,一個映射一旦建立之後將保持有效,直到 MappedByteBuffer 對象被施以垃圾收集動作爲止。同鎖不一樣的是,映射緩衝區沒有綁定到創建它 們的通道上。關閉相關聯的 FileChannel 不會破壞映射,只有丟棄緩衝區對象本身才會破壞該映 射。NIO 設計師們之所以做這樣的決定是因爲當關閉通道時破壞映射會引起安全問題,而解決該安 全問題又會導致性能問題。如果您確實需要知道一個映射是什麼時候被破壞的,他們建議使用虛引 用(phantom references,參見 java.lang.ref.PhantomReference)和一個 cleanup 線程。不過有此需要 的概率是微乎其微的。

MemoryMappedBuffer 直接反映它所關聯的磁盤文件。如果映射有效時文件被在結構上修改, 就會產生奇怪的行爲(當然具體的行爲是取決於操作系統和文件系統的)。MemoryMappedBuffer 有固定的大小,不過它所映射的文件卻是彈性的。具體來說,如果映射有效時文件大小變化了,那 麼緩衝區的部分或全部內容都可能無法訪問,並將返回未定義的數據或者拋出未檢查的異常。關於 被內存映射的文件如何受其他線程或外部進程控制這一點,請務必小心對待。

所有的 MappedByteBuffer 對象都是直接的,這意味着它們佔用的內存空間位於 Java 虛擬機內 存堆之外(並且可能不會算作 Java 虛擬機的內存佔用,不過這取決於操作系統的虛擬內存模 型)。

因爲 MappedByteBuffers 也是 ByteBuffers,所以能夠被傳遞 SocketChannel 之類通道的 read( )或 write( )以有效傳輸數據給被映射的文件或從被映射的文件讀取數據。如能再結合 scatter/gather,那 麼從內存緩衝區和被映射文件內容中組織數據就變得很容易了。例 3-4 就是以此方式寫 HTTP 迴應 的。3.4.1 節中將描述一個傳輸數據給通道或從其他通道讀取數據的更加有效的方式。

到現在爲止,我們已經討論完了映射緩衝區同其他緩衝區相同的特性,這些也是您會用得最多 的。不過 MappedByteBuffer 還定義了幾個它獨有的方法:

package java.nio;

public abstract class MappedByteBuffer
        extends ByteBuffer {

    public final boolean isLoaded()

    public final MappedByteBuffer load()

    public final MappedByteBuffer force()
}

當我們爲一個文件建立虛擬內存映射之後,文件數據通常不會因此被從磁盤讀取到內存(這取 決於操作系統)。該過程類似打開一個文件:文件先被定位,然後一個文件句柄會被創建,當您準 備好之後就可以通過這個句柄來訪問文件數據。對於映射緩衝區,虛擬內存系統將根據您的需要來把文件中相應區塊的數據讀進來。這個頁驗證或防錯過程需要一定的時間,因爲將文件數據讀取到 內存需要一次或多次的磁盤訪問。某些場景下,您可能想先把所有的頁都讀進內存以實現最小的緩 衝區訪問延遲。如果文件的所有頁都是常駐內存的,那麼它的訪問速度就和訪問一個基於內存的緩 衝區一樣了。

load( )方法會加載整個文件以使它常駐內存。正如我們在第一章所討論的,一個內存映射緩衝 區會建立與某個文件的虛擬內存映射。此映射使得操作系統的底層虛擬內存子系統可以根據需要將 文件中相應區塊的數據讀進內存。已經在內存中或通過驗證的頁會佔用實際內存空間,並且在它們 被讀進 RAM 時會擠出最近較少使用的其他內存頁。

在一個映射緩衝區上調用 load( )方法會是一個代價高的操作,因爲它會導致大量的頁調入 (page-in),具體數量取決於文件中被映射區域的實際大小。然而,load( )方法返回並不能保證文 件就會完全常駐內存,這是由於請求頁面調入(demand paging)是動態的。具體結果會因某些因素 而有所差異,這些因素包括:操作系統、文件系統,可用 Java 虛擬機內存,最大 Java 虛擬機內 存,垃圾收集器實現過程等等。請小心使用 load( )方法,它可能會導致您不希望出現的結果。該方 法的主要作用是爲提前加載文件埋單,以便後續的訪問速度可以儘可能的快。

對於那些要求近乎實時訪問(near-realtime access)的程序,解決方案就是預加載。但是請記 住,不能保證全部頁都會常駐內存,不管怎樣,之後可能還會有頁調入發生。內存頁什麼時候以及 怎樣消失受多個因素影響,這些因素中的許多都是不受 Java 虛擬機控制的。JDK 1.4 的 NIO 並沒有 提供一個可以把頁面固定到物理內存上的 API,儘管一些操作系統是支持這樣做的。

對於大多數程序,特別是交互性的或其他事件驅動(event-driven)的程序而言,爲提前加載文 件消耗資源是不划算的。在實際訪問時分攤頁調入開銷纔是更好的選擇。讓操作系統根據需要來調 入頁意味着不訪問的頁永遠不需要被加載。同預加載整個被映射的文件相比,這很容易減少 I/O 活 動總次數。操作系統已經有一個複雜的內存管理系統了,就讓它來替您完成此工作吧!

我們可以通過調用 isLoaded( )方法來判斷一個被映射的文件是否完全常駐內存了。如果該方法 返回 true 值,那麼很大概率是映射緩衝區的訪問延遲很少或者根本沒有延遲。不過,這也是不能 保證的。同樣地,返回 false 值並不一定意味着訪問緩衝區將很慢或者該文件並未完全常駐內 存。isLoaded( )方法的返回值只是一個暗示,由於垃圾收集的異步性質、底層操作系統以及運行系 統的動態性等因素,想要在任意時刻準確判斷全部映射頁的狀態是不可能的。

上面代碼中列出的最後一個方法 force( )同 FileChannel 類中的同名方法相似(參見 3.3.1 節) 該方法會強制將映射緩衝區上的更改應用到永久磁盤存儲器上。當用 MappedByteBuffer 對象來更新 一個文件,您應該總是使用 MappedByteBuffer.force( )而非 FileChannel.force( ),因爲通道對象可能 不清楚通過映射緩衝區做出的文件的全部更改。MappedByteBuffer 沒有不更新文件元數據的選項—元數據總是會同時被更新的。請注意,非本地文件系統也同樣影響 MappedByteBuffer.force( )方 法,正如它會對 FileChannel.force( )方法有影響,在這裏(參見 3.3.1 節)。

如果映射是以 MapMode.READ_ONLY 或 MAP_MODE.PRIVATE 模式建立的,那麼調用 force( ) 方法將不起任何作用,因爲永遠不會有更改需要應用到磁盤上(但是這樣做也是沒有害處的)。

例 3-4 詮釋了內存映射緩衝區如何同 scatter/gather 結合使用。

例 3-4 使用映射文件和 gathering 寫操作來編寫 HTTP 回覆

package org.example;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

/**
 * Dummy HTTP server using MappedByteBuffers.
 * <p>
 * Given a filename on the command line, pretend to be
 * a web server and generate an HTTP response containing
 * the file content preceded by appropriate headers. The
 * data is sent with a gathering write.
 * <p>
 * * @author Ron Hitchens ([email protected])
 */
public class MappedHttp {

    private static final String OUTPUT_FILE = "MappedHttp.out";
    private static final String LINE_SEP = "\r\n";
    private static final String SERVER_ID = "Server: Ronsoft Dummy Server";
    private static final String HTTP_HDR =
            "HTTP/1.0 200 OK" + LINE_SEP + SERVER_ID + LINE_SEP;
    private static final String HTTP_404_HDR =
            "HTTP/1.0 404 Not Found" + LINE_SEP + SERVER_ID + LINE_SEP;
    private static final String MSG_404 = "Could not open file: ";

    public static void main(String[] argv)
            throws Exception {
        if (argv.length < 1) {
            System.err.println("Usage: filename");
            return;
        }
        String file = argv[0];
        ByteBuffer header = ByteBuffer.wrap(bytes(HTTP_HDR));
        ByteBuffer dynhdrs = ByteBuffer.allocate(128);
        ByteBuffer[] gather = {header, dynhdrs, null};
        String contentType = "unknown/unknown";
        long contentLength = -1;
        try {
            FileInputStream fis = new FileInputStream(file);
            FileChannel fc = fis.getChannel();
            MappedByteBuffer filedata = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
            gather[2] = filedata;
            contentLength = fc.size();
            contentType = URLConnection.guessContentTypeFromName(file);
        } catch (IOException e) {
            // file could not be opened; report problem
            ByteBuffer buf = ByteBuffer.allocate(128);
            String msg = MSG_404 + e + LINE_SEP;
            buf.put(bytes(msg));
            buf.flip();
            // Use the HTTP error response
            gather[0] = ByteBuffer.wrap(bytes(HTTP_404_HDR));
            gather[2] = buf;
            contentLength = msg.length();
            contentType = "text/plain";
        }
        StringBuffer sb = new StringBuffer();
        sb.append("Content-Length: " + contentLength);
        sb.append(LINE_SEP);
        sb.append("Content-Type: ").append(contentType);
        sb.append(LINE_SEP).append(LINE_SEP);
        dynhdrs.put(bytes(sb.toString()));
        dynhdrs.flip();
        FileOutputStream fos = new FileOutputStream(OUTPUT_FILE);
        FileChannel out = fos.getChannel();
        // All the buffers have been prepared; write 'em out
        while (out.write(gather) > 0) {
            // Empty body; loop until all buffers are empty
        }
        out.close();
        System.out.println("output written to " + OUTPUT_FILE);
    }

    // Convert a string to its constituent bytes
    // from the ASCII character set 
    private static byte[] bytes(String string)
            throws Exception {
        return (string.getBytes("US-ASCII"));
    }
}

例 3-5 詮釋了各種模式的內存映射如何交互。具體來說,例中代碼詮釋了寫時拷貝是如何頁導 向(page-oriented)的。當在使用 MAP_MODE.PRIVATE 模式創建的 MappedByteBuffer 對象上 調用 put( )方法而引發更改時,就會生成一個受影響頁的拷貝。這份私有的拷貝不僅反映本地更 改,而且使緩衝區免受來自外部對原來頁更改的影響。然而,對於被映射文件其他區域的更改還是 可以看到的。

例 3-5 三種類型的內存映射緩衝區

package org.example;

import java.io.File;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

/**
 * Test behavior of Memory mapped buffer types. Create a file, write
 * some data to it, then create three different types of mappings
 * to it. Observe the effects of changes through the buffer APIs
 * and updating the file directly. The data spans page boundaries
 * to illustrate the page-oriented nature of Copy-On-Write mappings.
 *
 * @author Ron Hitchens ([email protected])
 */
public class MapFile {

    public static void main(String[] argv) throws Exception {
        // Create a temp file and get a channel connected to it
        File tempFile = File.createTempFile("mmaptest", null);
        RandomAccessFile file = new RandomAccessFile(tempFile, "rw");
        FileChannel channel = file.getChannel();
        ByteBuffer temp = ByteBuffer.allocate(100);
        // Put something in the file, starting at location 0
        temp.put("This is the file content".getBytes());
        temp.flip();
        channel.write(temp, 0);
        // Put something else in the file, starting at location 8192.
        // 8192 is 8 KB, almost certainly a different memory/FS page.
        // This may cause a file hole, depending on the
        // filesystem page size.
        temp.clear();
        temp.put("This is more file content".getBytes());
        temp.flip();
        channel.write(temp, 8192);
        // Create three types of mappings to the same file
        MappedByteBuffer ro = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
        MappedByteBuffer rw = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());
        MappedByteBuffer cow = channel.map(FileChannel.MapMode.PRIVATE, 0, channel.size());
        // the buffer states before any modifications
        System.out.println("Begin");
        showBuffers(ro, rw, cow);
        // Modify the copy-on-write buffer
        cow.position(8);
        cow.put("COW".getBytes());
        System.out.println("Change to COW buffer");
        showBuffers(ro, rw, cow);
        // Modify the read/write buffer
        rw.position(9);
        rw.put(" R/W ".getBytes());
        rw.position(8194);
        rw.put(" R/W ".getBytes());
        rw.force();
        System.out.println("Change to R/W buffer");
        showBuffers(ro, rw, cow);
        // Write to the file through the channel; hit both pages
        temp.clear();
        temp.put("Channel write ".getBytes());
        temp.flip();
        channel.write(temp, 0);
        temp.rewind();
        channel.write(temp, 8202);
        System.out.println("Write on channel");
        showBuffers(ro, rw, cow);
        // Modify the copy-on-write buffer again
        cow.position(8207);
        cow.put(" COW2 ".getBytes());
        System.out.println("Second change to COW buffer");
        showBuffers(ro, rw, cow);
        // Modify the read/write buffer
        rw.position(0);
        rw.put(" R/W2 ".getBytes());
        rw.position(8210);
        rw.put(" R/W2 ".getBytes());
        rw.force();
        System.out.println("Second change to R/W buffer");
        showBuffers(ro, rw, cow);
        // cleanup
        channel.close();
        file.close();
        tempFile.delete();
    }

    // Show the current content of the three buffers
    public static void showBuffers(ByteBuffer ro, ByteBuffer rw, ByteBuffer cow)
            throws Exception {
        dumpBuffer("R/O", ro);
        dumpBuffer("R/W", rw);
        dumpBuffer("COW", cow);
        System.out.println("");
    }

    // Dump buffer content, counting and skipping nulls
    public static void dumpBuffer(String prefix, ByteBuffer buffer) throws Exception {
        System.out.print(prefix + ": '");
        int nulls = 0;
        int limit = buffer.limit();
        for (int i = 0; i < limit; i++) {
            char c = (char) buffer.get(i);
            if (c == '\u0000') {
                nulls++;
                continue;
            }
            if (nulls != 0) {
                System.out.print("|[" + nulls
                        + " nulls]|");
                nulls = 0;
            }
            System.out.print(c);
        }
        System.out.println("'");
    }
}

以下是運行上面程序的輸出:

Begin 
R/O: 'This is the file content|[8168 nulls]|This is more file content' 
R/W: 'This is the file content|[8168 nulls]|This is more file content' 
COW: 'This is the file content|[8168 nulls]|This is more file content'

Change to COW buffer 
R/O: 'This is the file content|[8168 nulls]|This is more file content' 
R/W: 'This is the file content|[8168 nulls]|This is more file content' 
COW: 'This is COW file content|[8168 nulls]|This is more file content'

Change to R/W buffer 
R/O: 'This is t R/W le content|[8168 nulls]|Th R/W more file content' 
R/W: 'This is t R/W le content|[8168 nulls]|Th R/W more file content' 
COW: 'This is COW file content|[8168 nulls]|Th R/W more file content'

Write on channel 
R/O: 'Channel write le content|[8168 nulls]|Th R/W moChannel write t' 
R/W: 'Channel write le content|[8168 nulls]|Th R/W moChannel write t' 
COW: 'This is COW file content|[8168 nulls]|Th R/W moChannel write t'

Second change to COW buffer 
R/O: 'Channel write le content|[8168 nulls]|Th R/W moChannel write t' 
R/W: 'Channel write le content|[8168 nulls]|Th R/W moChannel write t' 
COW: 'This is COW file content|[8168 nulls]|Th R/W moChann COW2 te t'

Second change to R/W buffer 
R/O: ' R/W2 l write le content|[8168 nulls]|Th R/W moChannel R/W2 t' 
R/W: ' R/W2 l write le content|[8168 nulls]|Th R/W moChannel R/W2 t' 
COW: 'This is COW file content|[8168 nulls]|Th R/W moChann COW2 te t'

1)Channel-to-Channel 傳輸

由於經常需要從一個位置將文件數據批量傳輸到另一個位置,FileChannel 類添加了一些優化 方法來提高該傳輸過程的效率:

package java.nio.channels;

import java.io.IOException;
import java.nio.channels.*;
import java.nio.channels.spi.AbstractInterruptibleChannel;

public abstract class FileChannel
        extends AbstractInterruptibleChannel
        implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel {

    public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;

    public abstract long transferFrom(ReadableByteChannel src, long position, long count) throws IOException;
}

transferTo( )和 transferFrom( )方法允許將一個通道交叉連接到另一個通道,而不需要通過一個 中間緩衝區來傳遞數據。只有 FileChannel 類有這兩個方法,因此 channel-to-channel 傳輸中通道之 一必須是 FileChannel。您不能在 socket 通道之間直接傳輸數據,不過 socket 通道實現 WritableByteChannel 和 ReadableByteChannel 接口,因此文件的內容可以用 transferTo( )方法傳輸給 一個 socket 通道,或者也可以用 transferFrom( )方法將數據從一個 socket 通道直接讀取到一個文件 中。

直接的通道傳輸不會更新與某個 FileChannel 關聯的 position 值。請求的數據傳輸將從 position 參數指定的位置開始,傳輸的字節數不超過 count 參數的值。實際傳輸的字節數會由 方法返回,可能少於您請求的字節數。

對於傳輸數據來源是一個文件的 transferTo( )方法,如果 position + count 的值大於文件 的 size 值,傳輸會在文件尾的位置終止。假如傳輸的目的地是一個非阻塞模式的 socket 通道,那麼 當發送隊列(send queue)滿了之後傳輸就可能終止,並且如果輸出隊列(output queue)已滿的話 可能不會發送任何數據。類似地,對於 transferFrom( )方法:如果來源 src 是另外一個 FileChannel 並且已經到達文件尾,那麼傳輸將提早終止;如果來源 src 是一個非阻塞 socket 通道,只有當前 處於隊列中的數據纔會被傳輸(可能沒有數據)。由於網絡數據傳輸的非確定性,阻塞模式的 socket 也可能會執行部分傳輸,這取決於操作系統。許多通道實現都是提供它們當前隊列中已有的 數據而不是等待您請求的全部數據都準備好。

此外,請記住:如果傳輸過程中出現問題,這些方法也可能拋出 java.io.IOException 異常。

Channel-to-channel 傳輸是可以極其快速的,特別是在底層操作系統提供本地支持的時候。某些 操作系統可以不必通過用戶空間傳遞數據而進行直接的數據傳輸。對於大量的數據傳輸,這會是一 個巨大的幫助(參見例 3-6)。

例 3-6 使用通道傳輸進行文件連結

package org.example;

import java.io.FileInputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

/**
 * Test channel transfer. This is a very simplistic concatenation
 * program. It takes a list of file names as arguments, opens each
 * in turn and transfers (copies) their content to the given
 * WritableByteChannel (in this case, stdout).
 * <p>
 * Created April 2002
 *
 * @author Ron Hitchens ([email protected])
 */
public class ChannelTransfer {

    public static void main(String[] argv)
            throws Exception {
        if (argv.length == 0) {
            System.err.println("Usage: filename ...");
            return;
        }
        catFiles(Channels.newChannel(System.out), argv);
    }
    // Concatenate the content of each of the named files to 
    // the given channel. A very dumb version of 'cat'.

    private static void catFiles(WritableByteChannel target, String[] files) throws Exception {
        for (int i = 0; i < files.length; i++) {
            FileInputStream fis = new FileInputStream(files[i]);
            FileChannel channel = fis.getChannel();
            channel.transferTo(0, channel.size(), target);
            channel.close();
            fis.close();
        }
    }
}

5.Socket 通道

現在讓我們來學習模擬網絡套接字的通道類。Socket 通道有與文件通道不同的特徵。

新的 socket 通道類可以運行非阻塞模式並且是可選擇的。這兩個性能可以激活大程序(如網絡 服務器和中間件組件)巨大的可伸縮性和靈活性。本節中我們會看到,再也沒有爲每個 socket 連接使用一個線程的必要了,也避免了管理大量線程所需的上下文交換總開銷。藉助新的 NIO 類,一 個或幾個線程就可以管理成百上千的活動 socket 連接了並且只有很少甚至可能沒有性能損失。

從圖 3-9 可知,全部 socket 通道類(DatagramChannel、SocketChannel 和 ServerSocketChannel)都是由位於 java.nio.channels.spi 包中的 AbstractSelectableChannel 引 申而來。這意味着我們可以用一個 Selector 對象來執行 socket 通道的有條件的選擇(readiness selection)。選擇和多路複用 I/O 會在第四章中討論。

請注意 DatagramChannel 和 SocketChannel 實現定義讀和寫功能的接口而 ServerSocketChannel 不實現。ServerSocketChannel 負責監聽傳入的連接和創建新的 SocketChannel 對象,它本身從不傳 輸數據。

在我們具體討論每一種 socket 通道前,您應該瞭解 socket 和 socket 通道之間的關係。之前的章 節中有寫道,通道是一個連接 I/O 服務導管並提供與該服務交互的方法。就某個 socket 而言,它不 會再次實現與之對應的 socket 通道類中的 socket 協議 API,而 java.net 中已經存在的 socket 通 道都可以被大多數協議操作重複使用。

全部 socket 通道類(DatagramChannel、SocketChannel 和 ServerSocketChannel)在被實例化時 都會創建一個對等 socket 對象。這些是我們所熟悉的來自 java.net 的類(Socket、ServerSocket 和 DatagramSocket),它們已經被更新以識別通道。對等 socket 可以通過調用 socket( )方法從一個 通道上獲取。此外,這三個 java.net 類現在都有 getChannel( )方法。

雖然每個 socket 通道(在 java.nio.channels 包中)都有一個關聯的 java.net socket 對 象,卻並非所有的 socket 都有一個關聯的通道。如果您用傳統方式(直接實例化)創建了一個 Socket 對象,它就不會有關聯的 SocketChannel 並且它的 getChannel( )方法將總是返回 null。

Socket 通道委派協議操作給對等 socket 對象。如果在通道類中存在似乎重複的 socket 方法,那 麼將有某個新的或者不同的行爲同通道類上的這個方法相關聯。

 

1)非阻塞模式

Socket 通道可以在非阻塞模式下運行。這個陳述雖然簡單卻有着深遠的含義。傳統 Java socket 的阻塞性質曾經是 Java 程序可伸縮性的最重要制約之一。非阻塞 I/O 是許多複雜的、高性能的程序 構建的基礎。

要把一個 socket 通道置於非阻塞模式,我們要依靠所有 socket 通道類的公有超級類: SelectableChannel。下面的方法就是關於通道的阻塞模式的:

package java.nio.channels;

import java.io.IOException;
import java.nio.channels.spi.AbstractInterruptibleChannel;

public abstract class SelectableChannel extends AbstractInterruptibleChannel implements Channel {

    public abstract SelectableChannel configureBlocking(boolean block) throws IOException;

    public abstract boolean isBlocking();

    public abstract Object blockingLock();
}

有條件的選擇(readiness selection)是一種可以用來查詢通道的機制,該查詢可以判斷通道是 否準備好執行一個目標操作,如讀或寫。非阻塞 I/O 和可選擇性是緊密相連的,那也正是管理阻塞 模式的 API 代碼要在 SelectableChannel 超級類中定義的原因。SelectableChannel 的剩餘 API 將在第 四章中討論。

設置或重新設置一個通道的阻塞模式是很簡單的,只要調用 configureBlocking( )方法即可,傳 遞參數值爲 true 則設爲阻塞模式,參數值爲 false 值設爲非阻塞模式。真的,就這麼簡單!您 可以通過調用 isBlocking( )方法來判斷某個 socket 通道當前處於哪種模式:

SocketChannel sc = SocketChannel.open( ); 
sc.configureBlocking (false); // nonblocking 
...
if (!sc.isBlocking()) { 
		doSomething (cs); 
}

服務器端的使用經常會考慮到非阻塞 socket 通道,因爲它們使同時管理很多 socket 通道變得更 容易。但是,在客戶端使用一個或幾個非阻塞模式的 socket 通道也是有益處的,例如,藉助非阻塞 socket 通道,GUI 程序可以專注於用戶請求並且同時維護與一個或多個服務器的會話。在很多程序 上,非阻塞模式都是有用的。

偶爾地,我們也會需要防止 socket 通道的阻塞模式被更改。API 中有一個 blockingLock( )方 法,該方法會返回一個非透明的對象引用。返回的對象是通道實現修改阻塞模式時內部使用的。只 有擁有此對象的鎖的線程才能更改通道的阻塞模式(對象的鎖是用同步的 Java 密碼獲取的,它不 同於我們在 3.3 節中介紹的 lock( )方法)。對於確保在執行代碼的關鍵部分時 socket 通道的阻塞模 式不會改變以及在不影響其他線程的前提下暫時改變阻塞模式來說,這個方法都是非常方便的。

        Socket socket = null;
        Object lockObj = serverChannel.blockingLock();
        // have a handle to the lock object, but haven't locked it yet
        // may block here until lock is acquired
        synchronized (lockObj) {
            // This thread now owns the lock; mode can't be changed
            boolean prevState = serverChannel.isBlocking();
            serverChannel.configureBlocking(false);
            socket = serverChannel.accept();
            serverChannel.configureBlocking(prevState);
        }
        // lock is now released, mode is allowed to change
        if (socket != null) {
            doSomethingWithTheSocket(socket);
        }

2)ServerSocketChannel

讓我們從最簡單的 ServerSocketChannel 來開始對 socket 通道類的討論。以下是 ServerSocketChannel 的完整 API:

package java.nio.channels;

import java.io.IOException;
import java.net.ServerSocket;
import java.nio.channels.NetworkChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.AbstractSelectableChannel;

public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel {

    public static ServerSocketChannel open() throws IOException

    public abstract ServerSocket socket();

    public abstract SocketChannel accept() throws IOException;

    public final int validOps()
}

ServerSocketChannel 是一個基於通道的 socket 監聽器。它同我們所熟悉的 java.net.ServerSocket 執行相同的基本任務,不過它增加了通道語義,因此能夠在非阻塞模式下運行。

用靜態的 open( )工廠方法創建一個新的 ServerSocketChannel 對象,將會返回同一個未綁定的 java.net.ServerSocket 關聯的通道。該對等 ServerSocket 可以通過在返回的 ServerSocketChannel 上調 用 socket( )方法來獲取。作爲 ServerSocketChannel 的對等體被創建的 ServerSocket 對象依賴通道實 現。這些 socket 關聯的 SocketImpl 能識別通道。通道不能被封裝在隨意的 socket 對象外面。

由於 ServerSocketChannel 沒有 bind( )方法,因此有必要取出對等的 socket 並使用它來綁定到一 個端口以開始監聽連接。我們也是使用對等 ServerSocket 的 API 來根據需要設置其他的 socket 選 項。

        ServerSocketChannel ssc = ServerSocketChannel.open();
        ServerSocket serverSocket = ssc.socket();
        // Listen on port 1234
        serverSocket.bind(new InetSocketAddress(1234));

同它的對等體 java.net.ServerSocket 一樣,ServerSocketChannel 也有 accept( )方法。一旦您創建 了一個 ServerSocketChannel 並用對等 socket 綁定了它,然後您就可以在其中一個上調用 accept( )。 如果您選擇在 ServerSocket 上調用 accept( )方法,那麼它會同任何其他的 ServerSocket 表現一樣的 行爲:總是阻塞並返回一個 java.net.Socket 對象。如果您選擇在 ServerSocketChannel 上調用 accept( ) 方法則會返回 SocketChannel 類型的對象,返回的對象能夠在非阻塞模式下運行。假設系統已經有 一個安全管理器(security manager),兩種形式的方法調用都執行相同的安全檢查。

如果以非阻塞模式被調用,當沒有傳入連接在等待時,ServerSocketChannel.accept( )會立即返 回 null。正是這種檢查連接而不阻塞的能力實現了可伸縮性並降低了複雜性。可選擇性也因此得 到實現。我們可以使用一個選擇器實例來註冊一個 ServerSocketChannel 對象以實現新連接到達時自 動通知的功能。例 3-7 演示瞭如何使用一個非阻塞的 accept( )方法:

例 3-7 使用 ServerSocketChannel 的非阻塞 accept( )方法

package org.example;

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

/**
 * Test nonblocking accept( ) using ServerSocketChannel.
 * Start this program, then "telnet localhost 1234" to
 * connect to it.
 * <p>
 *
 * @author Ron Hitchens ([email protected])
 */
public class ChannelAccept {

    public static final String GREETING = "Hello I must be going.\r\n";

    public static void main(String[] argv)
            throws Exception {
        int port = 1234; // default
        if (argv.length > 0) {
            port = Integer.parseInt(argv[0]);
        }
        ByteBuffer buffer = ByteBuffer.wrap(GREETING.getBytes());
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.socket().bind(new InetSocketAddress(port));
        ssc.configureBlocking(false);
        while (true) {
            System.out.println("Waiting for connections");
            SocketChannel sc = ssc.accept();
            if (sc == null) {
                // no connections, snooze a while
                Thread.sleep(2000);
            } else {
                System.out.println("Incoming connection from: " + sc.socket().getRemoteSocketAddress());
                buffer.rewind();
                sc.write(buffer);
                sc.close();
            }
        }
    }
}

前面列出的最後一個方法 validOps( )是同選擇器一起使用的。關於選擇器,我們將在第四章中 予以詳細討論並且會介紹到 validOps( )方法。

 

3)SocketChannel

下面開始學習 SocketChannel,它是使用最多的 socket 通道類:

package org.example;

import java.io.IOException;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.channels.ByteChannel;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.NetworkChannel;
import java.nio.channels.ScatteringByteChannel;
import java.nio.channels.spi.AbstractSelectableChannel;

public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel {

    public static SocketChannel open() throws IOException

    public static SocketChannel open(SocketAddress remote) throws IOException

    public abstract Socket socket();

    public abstract boolean connect(SocketAddress remote) throws IOException;

    public abstract boolean isConnectionPending();

    public abstract boolean finishConnect() throws IOException;

    public abstract boolean isConnected();

    public final int validOps()
}

Socket 和 SocketChannel 類封裝點對點、有序的網絡連接,類似於我們所熟知並喜愛的 TCP/IP 網絡連接。SocketChannel 扮演客戶端發起同一個監聽服務器的連接。直到連接成功,它才能收到 數據並且只會從連接到的地址接收。(對於 ServerSocketChannel,由於涉及到 validOps( )方法,我 們將在第四章檢查選擇器時進行討論。通用的 read/write 方法也未在此列出,詳情請參考 3.1.2 節。)

每個 SocketChannel 對象創建時都是同一個對等的 java.net.Socket 對象串聯的。靜態的 open( )方 法可以創建一個新的 SocketChannel 對象,而在新創建的 SocketChannel 上調用 socket( )方法能返回 它對等的 Socket 對象;在該 Socket 上調用 getChannel( )方法則能返回最初的那個 SocketChannel。

新創建的 SocketChannel 雖已打開卻是未連接的。在一個未連接的 SocketChannel 對象上嘗試一 個 I/O 操作會導致 NotYetConnectedException 異常。我們可以通過在通道上直接調用 connect( )方法 或在通道關聯的 Socket 對象上調用 connect( )來將該 socket 通道連接。一旦一個 socket 通道被連 接,它將保持連接狀態直到被關閉。您可以通過調用布爾型的 isConnected( )方法來測試某個 SocketChannel 當前是否已連接。

第二種帶 InetSocketAddress 參數形式的 open( )是在返回之前進行連接的便捷方法。這段代碼:

SocketChannel socketChannel = SocketChannel.open (new InetSocketAddress ("somehost", somePort));

等價於下面這段代碼:

SocketChannel socketChannel = SocketChannel.open(); 
socketChannel.connect (new InetSocketAddress ("somehost", somePort));

如果您選擇使用傳統方式進行連接——通過在對等 Socket 對象上調用 connect( )方法,那麼傳 統的連接語義將適用於此。線程在連接建立好或超時過期之前都將保持阻塞。如果您選擇通過在通道上直接調用 connect( )方法來建立連接並且通道處於阻塞模式(默認模式),那麼連接過程實際 上是一樣的。

在 SocketChannel 上並沒有一種 connect( )方法可以讓您指定超時(timeout)值,當 connect( )方 法在非阻塞模式下被調用時 SocketChannel 提供併發連接:它發起對請求地址的連接並且立即返回 值。如果返回值是 true,說明連接立即建立了(這可能是本地環回連接);如果連接不能立即建 立,connect( )方法會返回 false 且併發地繼續連接建立過程。

面向流的的 socket 建立連接狀態需要一定的時間,因爲兩個待連接系統之間必須進行包對話以 建立維護流 socket 所需的狀態信息。跨越開放互聯網連接到遠程系統會特別耗時。假如某個 SocketChannel 上當前正由一個併發連接,isConnectPending( )方法就會返回 true 值。

  • 調用 finishConnect( )方法來完成連接過程,該方法任何時候都可以安全地進行調用。假如在一 個非阻塞模式的 SocketChannel 對象上調用 finishConnect( )方法,將可能出現下列情形之一:

    • connect( )方法尚未被調用。那麼將產生 NoConnectionPendingException 異常。

    • 連接建立過程正在進行,尚未完成。那麼什麼都不會發生,finishConnect( )方法會立即返回false 值。

    • 在非阻塞模式下調用 connect( )方法之後,SocketChannel 又被切換回了阻塞模式。那麼如果有必要的話,調用線程會阻塞直到連接建立完成,finishConnect( )方法接着就會返回 true值。

    • 在初次調用 connect( )或最後一次調用 finishConnect( )之後,連接建立過程已經完成。那麼SocketChannel 對象的內部狀態將被更新到已連接狀態,finishConnect( )方法會返回 true值,然後 SocketChannel 對象就可以被用來傳輸數據了。

    • 連接已經建立。那麼什麼都不會發生,finishConnect( )方法會返回 true 值。

 

當通道處於中間的連接等待(connection-pending)狀態時,您只可以調用 finishConnect( )、 isConnectPending( )或 isConnected( )方法。一旦連接建立過程成功完成,isConnected( )將返回 true值。

        InetSocketAddress addr = new InetSocketAddress(host, port);
        SocketChannel sc = SocketChannel.open();
        sc.configureBlocking(false);
        sc.connect(addr);
        while (!sc.finishConnect()) {
            doSomethingElse();
        }
        doSomethingWithChannel(sc);
        sc.close();

例 3-8 是一段用來管理異步連接的可用代碼。

例 3-8 建立併發連接

package org.example;

import java.net.InetSocketAddress;

/**
 * Demonstrate asynchronous connection of a SocketChannel.
 *
 * @author Ron Hitchens ([email protected])
 */
public class ConnectAsync {

    public static void main(String[] argv) throws Exception {
        String host = "localhost";
        int port = 80;
        if (argv.length == 2) {
            host = argv[0];
            port = Integer.parseInt(argv[1]);
        }
        InetSocketAddress addr = new InetSocketAddress(host, port);
        SocketChannel sc = SocketChannel.open();
        sc.configureBlocking(false);
        System.out.println("initiating connection");
        sc.connect(addr);
        while (!sc.finishConnect()) {
            doSomethingUseful();
        }
        System.out.println("connection established");
        // Do something with the connected socket
        // The SocketChannel is still nonblocking 
        sc.close();
    }

    private static void doSomethingUseful() {
        System.out.println("doing something useless");
    }
}

如果嘗試異步連接失敗,那麼下次調用 finishConnect( )方法會產生一個適當的經檢查的異常以 指出問題的性質。通道然後就會被關閉並將不能被連接或再次使用。

與連接相關的方法使得我們可以對一個通道進行輪詢並在連接進行過程中判斷通道所處的狀 態。第四章中,我們將瞭解到如何使用選擇器來避免進行輪詢並在異步連接建立之後收到通知。

Socket 通道是線程安全的。併發訪問時無需特別措施來保護髮起訪問的多個線程,不過任何時 候都只有一個讀操作和一個寫操作在進行中。請記住,sockets 是面向流的而非包導向的。它們可 以保證發送的字節會按照順序到達但無法承諾維持字節分組。某個發送器可能給一個 socket 寫入了 20 個字節而接收器調用 read( )方法時卻只收到了其中的 3 個字節。剩下的 17 個字節還是傳輸中。 由於這個原因,讓多個不配合的線程共享某個流 socket 的同一側絕非一個好的設計選擇。

connect( )和 finishConnect( )方法是互相同步的,並且只要其中一個操作正在進行,任何讀或寫 的方法調用都會阻塞,即使是在非阻塞模式下。如果此情形下您有疑問或不能承受一個讀或寫操作 在某個通道上阻塞,請用 isConnected( )方法測試一下連接狀態。

 

4)DatagramChannel

最後一個 socket 通道是 DatagramChannel。正如 SocketChannel 對應 Socket, ServerSocketChannel 對應 ServerSocket,每一個 DatagramChannel 對象也有一個關聯的 DatagramSocket 對象。不過原命名模式在此並未適用:“DatagramSocketChannel”顯得有點笨拙, 因此採用了簡潔的“DatagramChannel”名稱。

正如 SocketChannel 模擬連接導向的流協議(如 TCP/IP),DatagramChannel 則模擬包導向的 無連接協議(如 UDP/IP):

package java.nio.channels;

import java.io.IOException;
import java.net.DatagramSocket;
import java.net.ProtocolFamily;
import java.net.SocketAddress;
import java.net.SocketOption;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.MulticastChannel;
import java.nio.channels.ScatteringByteChannel;
import java.nio.channels.spi.AbstractSelectableChannel;

public abstract class DatagramChannel
        extends AbstractSelectableChannel
        implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel {

    public static DatagramChannel open() throws IOException

    public static DatagramChannel open(ProtocolFamily family) throws IOException

    public final int validOps()

    public abstract DatagramChannel bind(SocketAddress local) throws IOException;

    public abstract <T> DatagramChannel setOption(SocketOption<T> name, T value) throws IOException;

    public abstract DatagramSocket socket();

    public abstract boolean isConnected();

    public abstract DatagramChannel connect(SocketAddress remote) throws IOException;

    public abstract DatagramChannel disconnect() throws IOException;

    public abstract SocketAddress getRemoteAddress() throws IOException;

    public abstract SocketAddress receive(ByteBuffer dst) throws IOException;

    public abstract int send(ByteBuffer src, SocketAddress target) throws IOException;

    public abstract int read(ByteBuffer dst) throws IOException;

    public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException;

    public final long read(ByteBuffer[] dsts) throws IOException

    public abstract int write(ByteBuffer src) throws IOException;

    public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException;

    public final long write(ByteBuffer[] srcs) throws IOException
}

創建 DatagramChannel 的模式和創建其他 socket 通道是一樣的:調用靜態的 open( )方法來創建 一個新實例。新 DatagramChannel 會有一個可以通過調用 socket( )方法獲取的對等 DatagramSocket 對象。DatagramChannel 對象既可以充當服務器(監聽者)也可以充當客戶端(發送者)。如果您 希望新創建的通道負責監聽,那麼通道必須首先被綁定到一個端口或地址/端口組合上。綁定 DatagramChannel 同綁定一個常規的 DatagramSocket 沒什麼區別,都是委託對等 socket 對象上的 API 實現的:

        DatagramChannel channel = DatagramChannel.open();
        DatagramSocket socket = channel.socket();
        socket.bind(new InetSocketAddress(portNumber));

DatagramChannel 是無連接的。每個數據報(datagram)都是一個自包含的實體,擁有它自己 的目的地址及不依賴其他數據報的數據淨荷。與面向流的的 socket 不同,DatagramChannel 可以發 送單獨的數據報給不同的目的地址。同樣,DatagramChannel 對象也可以接收來自任意地址的數據 包。每個到達的數據報都含有關於它來自何處的信息(源地址)。

一個未綁定的 DatagramChannel 仍能接收數據包。當一個底層 socket 被創建時,一個動態生成 的端口號就會分配給它。綁定行爲要求通道關聯的端口被設置爲一個特定的值(此過程可能涉及安 全檢查或其他驗證)。不論通道是否綁定,所有發送的包都含有 DatagramChannel 的源地址 (帶端口號)。未綁定的 DatagramChannel 可以接收發送給它的端口的包,通常是來回應該通道之 前發出的一個包。已綁定的通道接收發送給它們所綁定的熟知端口(wellknown port)的包。數據 的實際發送或接收是通過 send( )和 receive( )方法來實現的:

package java.nio.channels;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.MulticastChannel;
import java.nio.channels.ScatteringByteChannel;
import java.nio.channels.spi.AbstractSelectableChannel;

public abstract class DatagramChannel
        extends AbstractSelectableChannel
        implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel {

    // This is a partial API listing
    public abstract SocketAddress receive(ByteBuffer dst) throws IOException;

    public abstract int send(ByteBuffer src, SocketAddress target) throws IOException;
}

receive( )方法將下次將傳入的數據報的數據淨荷複製到預備好的 ByteBuffer 中並返回一個 SocketAddress 對象以指出數據來源。如果通道處於阻塞模式,receive( )可能無限期地休眠直到有包 到達。如果是非阻塞模式,當沒有可接收的包時則會返回 null。如果包內的數據超出緩衝區能承 受的範圍,多出的數據都會被悄悄地丟棄。

調用 send( )會發送給定 ByteBuffer 對象的內容到給定 SocketAddress 對象所描述的目的地址和端 口,內容範圍爲從當前 position 開始到末尾處結束。如果 DatagramChannel 對象處於阻塞模式,調 用線程可能會休眠直到數據報被加入傳輸隊列。如果通道是非阻塞的,返回值要麼是字節緩衝區的 字節數,要麼是“0”。發送數據報是一個全有或全無(all-or-nothing)的行爲。如果傳輸隊列沒有 足夠空間來承載整個數據報,那麼什麼內容都不會被髮送。

如果安裝了安全管理器,那麼每次調用 send( )或 receive( )時安全管理器的 checkConnect( )方法 都會被調用以驗證目的地址,除非通道處於已連接的狀態(本節後面會討論到)。

請注意,數據報協議的不可靠性是固有的,它們不對數據傳輸做保證。send( )方法返回的非零 值並不表示數據報到達了目的地,僅代表數據報被成功加到本地網絡層的傳輸隊列。此外,傳輸過 程中的協議可能將數據報分解成碎片。例如,以太網不能傳輸超過 1,500 個字節左右的包。如果您 的數據報比較大,那麼就會存在被分解成碎片的風險,成倍地增加了傳輸過程中包丟失的機率。被 分解的數據報在目的地會被重新組合起來,接收者將看不到碎片。但是,如果有一個碎片不能按時 到達,那麼整個數據報將被丟棄。

DatagramChannel 有一個 connect( )方法:

package java.nio.channels;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.ByteChannel;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.MulticastChannel;
import java.nio.channels.ScatteringByteChannel;
import java.nio.channels.spi.AbstractSelectableChannel;

public abstract class DatagramChannel
        extends AbstractSelectableChannel
        implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel {

    // This is a partial API listing
    public abstract DatagramChannel connect(SocketAddress remote) throws IOException;

    public abstract boolean isConnected();

    public abstract DatagramChannel disconnect() throws IOException;
}

DatagramChannel 對數據報 socket 的連接語義不同於對流 socket 的連接語義。有時候,將 數據報對話限制爲兩方是很可取的。將 DatagramChannel 置於已連接的狀態可以使除了它所“連 接”到的地址之外的任何其他源地址的數據報被忽略。這是很有幫助的,因爲不想要的包都已經被 網絡層丟棄了,從而避免了使用代碼來接收、檢查然後丟棄包的麻煩。

當 DatagramChannel 已連接時,使用同樣的令牌,您不可以發送包到除了指定給 connect( )方 法的目的地址以外的任何其他地址。試圖一定要這樣做的話會導致一個 SecurityException 異常。

我們可以通過調用帶 SocketAddress 對象的 connect( )方法來連接一個 DatagramChannel,該 SocketAddress 對象描述了 DatagramChannel 遠程對等體的地址。如果已經安裝了一個安全管理器, 那麼它會進行權限檢查。之後,每次 send/receive 時就不會再有安全檢查了,因爲來自或去到任何 其他地址的包都是不允許的。

已連接通道會發揮作用的使用場景之一是一個客戶端/服務器模式、使用 UDP 通訊協議的實時 遊戲。每個客戶端都只和同一臺服務器進行會話而希望忽視任何其他來源地數據包。將客戶端的 DatagramChannel 實例置於已連接狀態可以減少按包計算的總開銷(因爲不需要對每個包進行安全 檢查)和剔除來自欺騙玩家的假包。服務器可能也想要這樣做,不過需要每個客戶端都有一個 DatagramChannel 對象。

不同於流 socket,數據報 socket 的無狀態性質不需要同遠程系統進行對話來建立連接狀態。沒 有實際的連接,只有用來指定允許的遠程地址的本地狀態信息。由於此原因,DatagramChannel 上 也就沒有單獨的 finishConnect( )方法。我們可以使用 isConnected( )方法來測試一個數據報通道的連 接狀態。

不同於 SocketChannel(必須連接了纔有用並且只能連接一次),DatagramChannel 對象可以任 意次數地進行連接或斷開連接。每次連接都可以到一個不同的遠程地址。調用 disconnect( )方法可 以配置通道,以便它能再次接收來自安全管理器(如果已安裝)所允許的任意遠程地址的數據或發 送數據到這些地址上。

當一個 DatagramChannel 處於已連接狀態時,發送數據將不用提供目的地址而且接收時的源地 址也是已知的。這意味着 DatagramChannel 已連接時可以使用常規的 read( )和 write( )方法,包括scatter/gather 形式的讀寫來組合或分拆包的數據:

package java.nio.channels;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.MulticastChannel;
import java.nio.channels.ScatteringByteChannel;
import java.nio.channels.spi.AbstractSelectableChannel;

public abstract class DatagramChannel
        extends AbstractSelectableChannel
        implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel {

    // This is a partial API listing
    public abstract int read(ByteBuffer dst) throws IOException;

    public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException;

    public final long read(ByteBuffer[] dsts) throws IOException

    public abstract int write(ByteBuffer src) throws IOException;

    public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException;

    public final long write(ByteBuffer[] srcs) throws IOException
}

read( )方法返回讀取字節的數量,如果通道處於非阻塞模式的話這個返回值可能是“0”。 write( )方法的返回值同 send( )方法一致:要麼返回您的緩衝區中的字節數量,要麼返回“0”(如 果由於通道處於非阻塞模式而導致數據報不能被髮送)。當通道不是已連接狀態時調用 read( )或 write( )方法,都將產生 NotYetConnectedException 異常。

數據報通道不同於流 socket。由於它們的有序而可靠的數據傳輸特性,流 socket 非常得有用。 大多數網絡連接都是流 socket(TCP/IP 就是一個顯著的例子)。但是,像 TCP/IP 這樣面向流的的 協議爲了在包導向的互聯網基礎設施上維護流語義必然會產生巨大的開銷,並且流隱喻不能適用所 有的情形。數據報的吞吐量要比流協議高很多,並且數據報可以做很多流無法完成的事情。

  • 下面列出了一些選擇數據報 socket 而非流 socket 的理由:

    • 您的程序可以承受數據丟失或無序的數據。

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

    • 數據吞吐量比可靠性更重要。

    • 您需要同時發送數據給多個接受者(多播或者廣播)。

    • 包隱喻比流隱喻更適合手邊的任務。

如果以上特徵中的一個或多個適用於您的程序,那麼數據報設計對您來說就是合適的。

例 3-9 顯示瞭如何使用 DatagramChannel 發送請求到多個地址上的時間服務器。 DatagramChannel 接着會等待回覆(reply)的到達。對於每個返回的回覆,遠程時間會同本地時間 進行比較。由於數據報傳輸不保證一定成功,有些回覆可能永遠不會到達。大多數 Linux 和 Unix 系統都默認提供時間服務。互聯網上也有一個公共時間服務器,如 time.nist.gov。防火牆或者您的 ISP 可能會干擾數據報傳輸,這是因人而異的。

例 3-10 DatagramChannel 時間服務器

package org.example;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.DatagramChannel;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
 * Request time service, per RFC 868. RFC 868
 * (http://www.ietf.org/rfc/rfc0868.txt) is a very simple time protocol
 * whereby one system can request the current time from another system.
 * Most Linux, BSD and Solaris systems provide RFC 868 time service
 * on port 37. This simple program will inter-operate with those.
 * <p>
 * The National Institute of Standards and Technology (NIST) operates
 * a public time server at time.nist.gov.
 * <p>
 * The RFC 868 protocol specifies a 32 bit unsigned value be sent,
 * representing the number of seconds since Jan 1, 1900. The Java
 * epoch begins on Jan 1, 1970 (same as unix) so an adjustment is
 * made by adding or subtracting 2,208,988,800 as appropriate. To
 * avoid shifting and masking, a four-byte slice of an
 * eight-byte buffer is used to send/recieve. But getLong( )
 * is done on the full eight bytes to get a long value.
 * <p>
 * When run, this program will issue time requests to each hostname
 * given on the command line, then enter a loop to receive packets.
 * Note that some requests or replies may be lost, which means
 * this code could block forever.
 * <p>
 * * @author Ron Hitchens ([email protected])
 */
public class TimeClient {

    private static final int DEFAULT_TIME_PORT = 37;
    private static final long DIFF_1900 = 2208988800L;
    protected int port = DEFAULT_TIME_PORT;
    protected List remoteHosts;
    protected DatagramChannel channel;

    public TimeClient(String[] argv) throws Exception {
        if (argv.length == 0) {
            throw new Exception("Usage: [ -p port ] host ...");
        }
        parseArgs(argv);
        this.channel = DatagramChannel.open();
    }

    protected InetSocketAddress receivePacket(DatagramChannel channel, ByteBuffer buffer) throws Exception {
        buffer.clear();
        // Receive an unsigned 32-bit, big-endian value
        return ((InetSocketAddress) channel.receive(buffer));
    }

    // Send time requests to all the supplied hosts
    protected void sendRequests() throws Exception {
        ByteBuffer buffer = ByteBuffer.allocate(1);
        Iterator it = remoteHosts.iterator();
        while (it.hasNext()) {
            InetSocketAddress sa = (InetSocketAddress) it.next();
            System.out.println("Requesting time from " + sa.getHostName() + ":" + sa.getPort());
            // Make it empty (see RFC868)
            buffer.clear().flip();
            // Fire and forget
            channel.send(buffer, sa);
        }
    }

    // Receive any replies that arrive
    public void getReplies() throws Exception {
        // Allocate a buffer to hold a long value
        ByteBuffer longBuffer = ByteBuffer.allocate(8);
        // Assure big-endian (network) byte order
        longBuffer.order(ByteOrder.BIG_ENDIAN);
        // Zero the whole buffer to be sure
        longBuffer.putLong(0, 0);
        // Position to first byte of the low-order 32 bits
        longBuffer.position(4);
        // Slice the buffer; gives view of the low-order 32 bits
        ByteBuffer buffer = longBuffer.slice();
        int expect = remoteHosts.size();
        int replies = 0;
        System.out.println("");
        System.out.println("Waiting for replies...");
        while (true) {
            InetSocketAddress sa;
            sa =
                    receivePacket(channel, buffer);
            buffer.flip();
            replies++;
            printTime(longBuffer.getLong(0), sa);
            if (replies == expect) {
                System.out.println("All packets answered");
                break;
            }
            // Some replies haven't shown up yet
            System.out.println("Received " + replies + " of " + expect + " replies");
        }
    }

    // Print info about a received time reply
    protected void printTime(long remote1900, InetSocketAddress sa) {
        // local time as seconds since Jan 1, 1970
        long local = System.currentTimeMillis() / 1000;
        // remote time as seconds since Jan 1, 1970
        long remote = remote1900 - DIFF_1900;
        Date remoteDate = new Date(remote * 1000);
        Date localDate = new Date(local * 1000);
        long skew = remote - local;
        System.out.println("Reply from "
                + sa.getHostName() + ":" + sa.getPort());
        System.out.println(" there: " + remoteDate);
        System.out.println(" here: " + localDate);
        System.out.print(" skew: ");
        if (skew == 0) {
            System.out.println("none");
        } else if (skew > 0) {
            System.out.println(skew + " seconds ahead");
        } else {
            System.out.println((-skew) + " seconds behind");
        }
    }

    protected void parseArgs(String[] argv) {
        remoteHosts = new LinkedList();
        for (int i = 0; i < argv.length; i++) {
            String arg = argv[i];
            // Send client requests to the given port
            if (arg.equals("-p")) {
                i++;
                this.port = Integer.parseInt(argv[i]);
                continue;
            }
            // Create an address object for the hostname
            InetSocketAddress sa = new InetSocketAddress(arg, port);
            // Validate that it has an address
            if (sa.getAddress() == null) {
                System.out.println("Cannot resolve address: " + arg);
                continue;
            }
            remoteHosts.add(sa);
        }
    }

    // -------------------------------------------------------------
    public static void main(String[] argv)
            throws Exception {
        TimeClient client = new TimeClient(argv);
        client.sendRequests();
        client.getReplies();
    }
}

6.管道

java.nio.channels 包中含有一個名爲 Pipe(管道)的類。廣義上講,管道就是一個用來 在兩個實體之間單向傳輸數據的導管。管道的概念對於 Unix(和類 Unix)操作系統的用戶來說早 就很熟悉了。Unix 系統中,管道被用來連接一個進程的輸出和另一個進程的輸入。Pipe 類實現一 個管道範例,不過它所創建的管道是進程內(在 Java 虛擬機進程內部)而非進程間使用的。參見 圖 3-10。

Pipe 類創建一對提供環回機制的 Channel 對象。這兩個通道的遠端是連接起來的,以便任何寫 在 SinkChannel 對象上的數據都能出現在 SourceChannel 對象上。圖 3-11 顯示了 Pipe 的類層級。

package java.nio.channels;

import java.io.IOException;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.ScatteringByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.channels.spi.AbstractSelectableChannel;

public abstract class Pipe {

    public static abstract class SourceChannel extends AbstractSelectableChannel implements ReadableByteChannel, ScatteringByteChannel

    public static abstract class SinkChannel extends AbstractSelectableChannel implements WritableByteChannel, GatheringByteChannel

    public abstract SourceChannel source();

    public abstract SinkChannel sink();

    public static Pipe open() throws IOException
}

Pipe 實例是通過調用不帶參數的 Pipe.open( )工廠方法來創建的。Pipe 類定義了兩個嵌套的通 道類來實現管路。這兩個類是 Pipe.SourceChannel(管道負責讀的一端)和 Pipe.SinkChannel(管道 負責寫的一端)。這兩個通道實例是在 Pipe 對象創建的同時被創建的,可以通過在 Pipe 對象上分 別調用 source( )和 sink( )方法來取回。

此時,您可能在想管道到底有什麼作用。您不能使用 Pipe 在操作系統級的進程間建立一個類 Unix 管道(您可以使用 SocketChannel 來建立)。Pipe 的 source 通道和 sink 通道提供類似 java.io.PipedInputStream 和 java.io.PipedOutputStream 所提供的功能,不過它們可以執行全部的通道 語義。請注意,SinkChannel 和 SourceChannel 都由 AbstractSelectableChannel 引申而來(所以也是 從 SelectableChannel 引申而來),這意味着 pipe 通道可以同選擇器一起使用(參見第四章)。

管道可以被用來僅在同一個 Java 虛擬機內部傳輸數據。雖然有更加有效率的方式來在線程之 間傳輸數據,但是使用管道的好處在於封裝性。生產者線程和用戶線程都能被寫道通用的 Channel API 中。根據給定的通道類型,相同的代碼可以被用來寫數據到一個文件、socket 或管道。選擇器 可以被用來檢查管道上的數據可用性,如同在 socket 通道上使用那樣地簡單。這樣就可以允許單個 用戶線程使用一個 Selector 來從多個通道有效地收集數據,並可任意結合網絡連接或本地工作線程 使用。因此,這些對於可伸縮性、冗餘度以及可複用性來說無疑都是意義重大的。

Pipes 的另一個有用之處是可以用來輔助測試。一個單元測試框架可以將某個待測試的類連接 到管道的“寫”端並檢查管道的“讀”端出來的數據。它也可以將被測試的類置於通道的“讀”端 並將受控的測試數據寫進其中。兩種場景對於迴歸測試都是很有幫助的。

管路所能承載的數據量是依賴實現的(implementation-dependent)。唯一可保證的是寫到 SinkChannel 中的字節都能按照同樣的順序在 SourceChannel 上重現。例 3-11 詮釋瞭如何使用管 道。

例 3-11 工作線程對一個管道進行寫操作

package org.example;

import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.Pipe;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.Random;

/**
 * Test Pipe objects using a worker thread.
 * <p>
 * Created April, 2002
 *
 * @author Ron Hitchens ([email protected])
 */
public class PipeTest {

    public static void main(String[] argv)
            throws Exception {
        // Wrap a channel around stdout
        WritableByteChannel out = Channels.newChannel(System.out);
        // Start worker and get read end of channel
        ReadableByteChannel workerChannel = startWorker(10);
        ByteBuffer buffer = ByteBuffer.allocate(100);
        while (workerChannel.read(buffer) >= 0) {
            buffer.flip();
            out.write(buffer);
            buffer.clear();
        }
    }

    // This method could return a SocketChannel or
    // FileChannel instance just as easily
    private static ReadableByteChannel startWorker(int reps)
            throws Exception {
        Pipe pipe = Pipe.open();
        Worker worker = new Worker(pipe.sink(), reps);
        worker.start();
        return (pipe.source());
    }
    // ----------------------------------------------------------------

    /**
     * A worker thread object which writes data down a channel.
     * Note:this object knows  nothing about Pipe, uses only a
     * generic WritableByteChannel.
     */
    private static class Worker extends Thread {

        WritableByteChannel channel;
        private int reps;

        Worker(WritableByteChannel channel, int reps) {
            this.channel = channel;
            this.reps = reps;
        }

        // Thread execution begins here
        public void run() {
            ByteBuffer buffer = ByteBuffer.allocate(100);
            try {
                for (int i = 0; i < this.reps; i++) {
                    doSomeWork(buffer);
                    // channel may not take it all at once
                    while (channel.write(buffer) > 0) {
                        // empty
                    }
                }
                this.channel.close();
            } catch (Exception e) {
                // easy way out; this is demo code
                e.printStackTrace();
            }
        }

        private String[] products = {
                "No good deed goes unpunished",
                "To be, or what?",
                "No matter where you go, there you are",
                "Just say \"Yo\"",
                "My karma ran over my dogma"
        };
        private Random rand = new Random();

        private void doSomeWork(ByteBuffer buffer) {
            int product = rand.nextInt(products.length);
            buffer.clear();
            buffer.put(products[product].getBytes());
            buffer.put("\r\n".getBytes());
            buffer.flip();
        }
    }
}

 

7.通道工具類

 

回憶一下,常規的流僅傳輸字節,readers 和 writers 則作用於字符數據。表 3-2 的前四行描述 了用於連接流、通道的方法。因爲流和通道都是運行在字節流基礎上的,所以這四個方法直接將流 封裝在通道上,反之亦然。

Readers 和 Writers 運行在字符的基礎上,在 Java 的世界裏字符同字節是完全不同的。將一個 通道(僅瞭解字節)連接到一個 reader 或 writer 需要一箇中間對話來處理字節/字符 (byte/char)阻抗失配。爲此,表 3-2 的後半部分描述的工廠方法使用了字符集編碼器和解碼 器。字符集以及字符集轉碼將在第六章中詳細討論。

這些方法返回的包封 Channel 對象可能會也可能不會實現 InterruptibleChannel 接口,它們也可 能不是從 SelectableChannel 引申而來。因此,可能無法將這些包封通道同 java.nio.channels 包中定義的其他通道類型交換使用。細節是依賴實現的。如果您的程序依賴這些語義,那麼請使用 操作器實例測試一下返回的通道對象。

總結

本章中我們討論了通道的很多方面的內容。通道組成了基礎設施或者說管道設施,該設施在操 作系統(或通道連接到的任意東西)的 ByteBuffers 和 I/O 服務之間傳輸數據。本章中討論到的關鍵 概念有:

1.基本的通道操作

在 3.1 節中我們學習了通道的基本操作,具體包括:怎樣使用所有通道都通用的 API 方法調用 來打開一個通道以及完成操作時如何關閉通道。

2.Scatter/Gather 通道

在 3.2 節中我們介紹瞭如何使用通道來 scatter/gather I/O。矢量化的 I/O 使您可以在多個緩衝區 上自動執行一個 I/O 操作。

3.文件通道

在 3.3 節中我們討論了多層面的 FileChannel 類。這個強大的新通道提供了對高級文件操作的 訪問,以前這是不對 Java 編程開放的。新的功能特性包括:文件鎖定、內存映射文件以及 channel-to-channel 傳輸。

4.Socket 通道

在 3.5 節中我們覆蓋了幾種類型的 socket 通道。同時,我們也討論了 socket 通道所支持的一個 重要新特性——非阻塞模式。

5.管道

在 3.6 節中我們看了一下 Pipe 類,這是一個使用專門的通道實現的新循環機制,非常有用。

6.通道工具類

通道類中包含了工具方法,這些方法用於交叉連接通道和常規的字節流以及字符讀寫器對象。 參見 3.7 節。

 

 

 

摘自JAVA NIO(中文版)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章