NIO学习笔记通道Channel

通道基础

通道(Channel)是 java.nio 的第二个主要创新。它们既不是一个扩展也不是一项增强,而是全新、极好的 Java I/O 示例,提供与 I/O 服务的直接连接。Channel 用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。
首先, Channel 接口的完整源码:

package java.nio.channels;
public interface Channel
{
    public boolean isOpen( );
    public void close( ) throws IOException;
}

 与缓冲区不同,通道 API 主要由接口指定。不同的操作系统上通道实现(Channel Implementation)会有根本性的差异,所以通道 API 仅仅描述了可以做什么。因此很自然地,通道实现经常使用操作系统的本地代码。通道接口允许您以一种受控且可移植的方式来访问底层的 I/O服务。对所有通道来说只有两种共同的操作:检查一个通道是否打开(IsOpen())和关闭一个打开的通道(close())。所有的东西都是那些实现Channel 接口以及它的子接口的类。InterruptibleChannel是一个标记接口,当被通道使用时可以标示该通道是可以中断的(Interruptible)。如果连接可中断通道的线程被中断,那么该通道会以特别的方式工作,大多数但非全部的通道都是可以中断的。
 从 Channel 接口引申出的其他接口都是面向字节的子接口,包括WritableByteChannel和ReadableByteChannel。这也正好支持了我们之前所学的:通道只能在字节缓冲区上操作。

通道是访问 I/O 服务的导管。I/O可以分为广义的两大类别:FileI/O和StreamI/O。那么相应地有两种类型的通道,它们是文件(file)通道和套接字(socket)通道。对应的FileChannel类和三个socket通道类:SocketChannel、ServerSocketChannel 和 DatagramChannel

 通道可以以多种方式创建。Socket通道有可以直接创建新socket通道的工厂方法。但是一个FileChannel对象却只能通过在一个打开的 RandomAccessFile、FileInputStream 或FileOutputStream对象上调用 getChannel()方法来获取。不能直接创建一个 FileChannel 对象。

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( );

 前面关于缓冲区的学习中已经知道了,通道将数据传输给ByteBuffer 对象或者从 ByteBuffer 对象获取数据进行传输。

public interface ReadableByteChannel extends Channel {
       public int read (ByteBuffer dst) throws IOException;
    }

public interface WritableByteChannel extends Channel {
        public int write (ByteBuffer src) throws IOException;
    }

public interface ByteChannel extends ReadableByteChannel, WritableByteChannel {
    }

ByteChannel继承图如下图1

image

图1

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

 图1显示了一个 ByteChannel 接口,该接口引申出了 ReadableByteChannel 和WritableByteChannel 两个接口。ByteChannel 接口本身并不定义新的 API 方法,它是一种用来聚集它自己以一个新名称继承的多个接口的便捷接口。根据定义,实现 ByteChannel 接口的通道会同时实现 ReadableByteChannel 和 WritableByteChannel 两个接口,所以此类通道是双向的。这是简化类定义的语法糖(syntacticsugar),它使得用操作器(operator)实例来测试通道对象变得更加简单。

 ByteChannel 的 read( ) 和 write( )方法使用 ByteBuffer 对象作为参数。两种方法均返回已传输的字节数,可能比缓冲区的字节数少甚至可能为零。缓冲区的位置也会发生与已传输字节相同数量的前移。如果只进行了部分传输,缓冲区可以被重新提交给通道并从上次中断的地方继续传输。该过程重复进行直到缓冲区的 hasRemaining( )方法返回 false 值。示例如下:

package NIO;

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

/**
 * @author: <a href="mailto:[email protected]">凌霄</a>
 * @time: Created in 23:24 2018/1/8
 * @desc
 */
public class ChannelCopy {


    public static void main (String [] argv)
            throws IOException
    {
        ReadableByteChannel source = Channels.newChannel (System.in);
        WritableByteChannel dest = Channels.newChannel (System.out);
        channelCopy1 (source, dest);
        source.close( );
        dest.close( );
    }

    private static void channelCopy1 (ReadableByteChannel src,
                                      WritableByteChannel dest)
            throws IOException
    {
        ByteBuffer buffer = ByteBuffer.allocateDirect (16 * 1024);
        while (src.read (buffer) != -1) {
            buffer.flip( );
            dest.write (buffer);
            buffer.compact( );
        }
        buffer.flip( );
        while (buffer.hasRemaining( )) {
            dest.write (buffer);
        }
    }

    private static void channelCopy2(ReadableByteChannel src,
                                      WritableByteChannel dest)
            throws IOException
    {
        ByteBuffer buffer = ByteBuffer.allocateDirect (16 * 1024);
        while (src.read (buffer) != -1) {
            buffer.flip( );
            while (buffer.hasRemaining( )) {
                dest.write (buffer);
            }
            buffer.clear( );
        }
    }
}

 与缓冲区不同,通道不能被重复使用。一个打开的通道即代表与一个特定 I/O 服务的特定连接并封装该连接的状态。当通道关闭时,那个连接会丢失,然后通道将不再连接任何东西。

package java.nio.channels;
public interface Channel
{
    public boolean isOpen( );
    public void close( ) throws IOException;
}

 调用通道的close( )方法时,可能会导致在通道关闭底层I/O服务的过程中线程暂时阻塞,哪怕该通道处于非阻塞模式。通道关闭时的阻塞行为(如果有的话)是高度取决于操作系统或者文件系统的。在一个通道上多次调用close( )方法是没有坏处的,但是如果第一个线程在close( )方法中阻塞,那么在它完成关闭通道之前,任何其他调用close( )方法都会阻塞。后续在该已关闭的通道上调用close( )不会产生任何操作,只会立即返回。

 可以通过 isOpen( )方法来测试通道的开放状态。如果返回 true 值,那么该通道可以使用。如果返回 false 值,那么该通道已关闭,不能再被使用。尝试进行任何需要通道处于开放状态作为前提的操作,如读、写等都会导致 ClosedChannelException 异常。

 通道引入了一些与关闭和中断有关的新行为。如果一个通道实现 InterruptibleChannel 接口,它的行为以下述语义为准:如果一个线程在一个通道上被阻塞并且同时被中断(由调用该被阻塞线程的 interrupt( )方法的另一个线程中断),那么该通道将被关闭,该被阻塞线程也会产生一个 ClosedByInterruptException 异常。

 可中断的通道也是可以异步关闭的。实现InterruptibleChannel 接口的通道可以在任何时候被关闭,即使有另一个被阻塞的线程在等待该通道上的一个 I/O 操作完成。当一个通道被关闭时,休眠在该通道上的所有线程都将被唤醒并接收到一个AsynchronousCloseException 异常。接着通道就被关闭并将不再可用。

Scatter/Gather

 通道提供了一种被称为 Scatter/Gather 的重要新功能(有时也被称为矢量 I/O。它是指在多个缓冲区上实现一个简单的 I/O 操作。对于一个 write 操作而言,数据是从几个缓冲区按顺序抽取(称为 gather)并沿着通道发送的。缓冲区本身并不需要具备这种 gather 的能力(通常它们也没有此能力)。该 gather 过程的效果就好比全部缓冲区的内容被连结起来,并在发送数据前存放到一个大的缓冲区中。对于 read 操作而言,从通道读取的数据会按顺序被散布(称为scatter)到多个缓冲区,将每个缓冲区填满直至通道中的数据或者缓冲区的最大空间被消耗完。

 大多数现代操作系统都支持本地矢量 I/O(native vectored I/O)。当您在一个通道上请求一个Scatter/Gather 操作时,该请求会被翻译为适当的本地调用来直接填充或抽取缓冲区。这是一个很大的进步,因为减少或避免了缓冲区拷贝和系统调用。Scatter/Gather 应该使用直接的 ByteBuffers 以从本地 I/O 获取最大性能优势。

public interface ScatteringByteChannel extends ReadableByteChannel
    {
        public long read (ByteBuffer [] dsts)
        throws IOException;

        public long read (ByteBuffer [] dsts, int offset, int length)
        throws IOException;
    }
public interface GatheringByteChannel extends WritableByteChannel
    {
        public long write(ByteBuffer[] srcs)
        throws IOException;

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

 带 offset 和 length 参数版本的 read( ) 和 write( )方法使得可以使用缓冲区阵列的子集缓冲区。这里的 offset 值指哪个缓冲区将开始被使用,而不是指数据的offset。这里的length参数指示要使用的缓冲区数量。举个例子,假设我们有一个五元素的 fiveBuffers 阵列,它已经被初始化并引用了五个缓冲区,下面的代码将会写第二个、第三个和第四个缓冲区的内容:

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

 Scatter/Gather 会是一个极其强大的工具。它允许委托操作系统来完成辛苦活:将读取到的数据分开存放到多个存储桶(bucket)或者将不同的数据区块合并成一个整体。因为操作系统已经被高度优化来完成此类工作了。它节省了您来回移动数据的工作,也就避免了缓冲区拷贝和减少了您需要编写、调试的代码数量。既然您基本上通过提供数据容器引用来组合数据,那么按照不同的组合构建多个缓冲区阵列引用,各种数据区块就可以以不同的方式来组合了。

package NIO;

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;

/**
 * @author: <a href="mailto:[email protected]">凌霄</a>
 * @time: Created in 23:37 2018/1/8
 * @desc
 */
public class Marketing {

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

    public static void main(String[] args) throws Exception {
        int reps = 10;
        if (args.length > 0) {
            reps = Integer.parseInt(args[0]);
        }
        FileOutputStream fos = new FileOutputStream(DEMOGRAPHIC);
        GatheringByteChannel gatherChannel = fos.getChannel();
        ByteBuffer[] bs = utterBS(reps);
        while (gatherChannel.write(bs) > 0) {
            System.out.println("writing data to out.txt ...");
        }
        System.out.println("Mindshare paradigms synergized to " + DEMOGRAPHIC);
        fos.close();
    }

    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");

    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);
    }

    private static Random rand = new Random();

    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);
    }
}

Socket通道

 全部socket通道类(DatagramChannel、SocketChannel 和ServerSocketChannel)都是由位于 java.nio.channels.spi 包中的 AbstractSelectableChannel引申而来。这意味着可以用一个Selector对象来执行socket通道的有条件的选择(readinessselection)。基础图如下图2:

image

图2

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

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 通道可以在非阻塞模式下运行。这个陈述虽然简单却有着深远的含义。传统 Java socket的阻塞性质曾经是 Java 程序可伸缩性的最重要制约之一。非阻塞 I/O 是许多复杂的、高性能的程序
构建的基础。要把一个 socket 通道置于非阻塞模式,我们要依靠所有 socket 通道类的公有超级类:SelectableChannel。下面的方法就是关于通道的阻塞模式的:

public abstract class SelectableChannel
   extends AbstractChannel
  implements Channel {
    // This is a partial API listing
    public abstract void configureBlocking (boolean block)
    throws IOException;
    public abstract boolean isBlocking( );
    public abstract Object blockingLock( );
}

 有条件的选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写。非阻塞 I/O 和可选择性是紧密相连的,那也正是管理阻塞模式的 API 代码要在 SelectableChannel 超级类中定义的原因。

 设置或重新设置一个通道的阻塞模式是很简单的,只要调用 configureBlocking( )方法即可,传递参数值为 true 则设为阻塞模式,参数值为 false 值设为非阻塞模式。真的,就这么简单!可以通过调用 isBlocking( )方法来判断某个 socket 通道当前处于哪种模式:

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

 服务器端的使用经常会考虑到非阻塞 socket 通道,因为它们使同时管理很多 socket 通道变得更容易。但是,在客户端使用一个或几个非阻塞模式的 socket 通道也是有益处的.

 偶尔地,我们也会需要防止 socket 通道的阻塞模式被更改。API 中有一个 blockingLock( )方法,该方法会返回一个非透明的对象引用。返回的对象是通道实现修改阻塞模式时内部使用的。只有拥有此对象的锁的线程才能更改通道的阻塞模式。对于确保在执行代码的关键部分时 socket 通道的阻塞模式不会改变以及在不影响其他线程的前提下暂时改变阻塞模式来说,这个方法都是非常方便的。

Socket socket = null;
Object lockObj = serverChannel.blockingLock( );
synchronize (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);
}

ServerSocketChannel

 从最简单的 ServerSocketChannel 来开始对 socket 通道类的讨论。以下是ServerSocketChannel 的完整 API:

public abstract class ServerSocketChannel
 extends AbstractSelectableChannel{
    public static ServerSocketChannel open( ) throws IOException
    public abstract ServerSocket socket( );
    public abstract ServerSocket 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 选项。

 同它的对等体 java.net.ServerSocket 一样,ServerSocketChannel 也有 accept( )方法。一旦您创建了一个 ServerSocketChannel 并用对等 socket 绑定了它,然后就可以在其中一个上调用 accept( )。如果您选择在 ServerSocket 上调用 accept( )方法,那么它会同任何其他的 ServerSocket 表现一样的行为:总是阻塞并返回一个 java.net.Socket 对象。如果选择在 ServerSocketChannel 上调用 accept( ).方法则会返SocketChannel 类型的对象,返回的对象能够在非阻塞模式下运行。假设系统已经有一个安全管理器(security manager),两种形式的方法调用都执行相同的安全检查。

 如果以非阻塞模式被调用,当没有传入连接在等待时ServerSocketChannel.accept( )会立即返回 null。正是这种检查连接而不阻塞的能力实现了可伸缩性并降低了复杂性。可选择性也因此得到实现。我们可以使用一个选择器实例来注册一ServerSocketChannel 对象以实现新连接到达时自动通知的功能。下面演示了如何使用一个非阻塞的 accept( )方法:

package NIO;

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

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) {
                Thread.sleep(2000);
            } else {
                System.out.println("Incoming connection from: "
                        + sc.socket().getRemoteSocketAddress());
                buffer.rewind();
                sc.write(buffer);
                sc.close();
            }
        }
    }
}

SocketChannel

 SocketChannel,它是使用最多的 socket 通道类:

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

    public static SocketChannel open( ) throws IOException

    public static SocketChannel open (InetSocketAddress 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 扮演客户端发起同一个监听服务器的连接。直到连接成功,它才能收到数据并且只会从连接到的地址接收。每个 SocketChannel 对象创建时都是同一个对等的 java.net.Socket 对象串联的。静态的 open( )方法可以创建一个新的 SocketChannel 对象,而在新创建的 SocketChannel 上调用 socket( )方法能返回它对等的 Socket 对象;在该 Socket 上调用 getChannel( )方法则能返回最初的那个 SocketChannel。

虽然每个 SocketChannel 对象都会创建一个对等的 Socket 对象,反过来却不成立。直接创建的 Socket 对象不会关联 SocketChannel 对象,它们的getChannel( )方法只返回 null。

 新创建的 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值。

以下是一段用来管理异步连接的可用代码。

package NIO;

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

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");
        sc.close();
    }

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

 如果尝试异步连接失败,那么下次调用 finishConnect( )方法会产生一个适当的经检查的异常以指出问题的性质。通道然后就会被关闭并将不能被连接或再次使用。

 与连接相关的方法使得我们可以对一个通道进行轮询并在连接进行过程中判断通道所处的状态。

 Socket 通道是线程安全的。并发访问时无需特别措施来保护发起访问的多个线程,不过任何时候都只有一个读操作和一个写操作在进行中。请记住,sockets 是面向流的而非包导向的。它们可以保证发送的字节会按照顺序到达但无法承诺维持字节分组。某个发送器可能给一个 socket 写入了20 个字节而接收器调用 read( )方法时却只收到了其中的 3 个字节。剩下的 17 个字节还是传输中。由于这个原因,让多个不配合的线程共享某个流 socket 的同一侧绝非一个好的设计选择。

 connect( )和 finishConnect( )方法是互相同步的,并且只要其中一个操作正在进行,任何读或写的方法调用都会阻塞,即使是在非阻塞模式下。如果此情形下您有疑问或不能承受一个读或写操作在某个通道上阻塞,请用 isConnected( )方法测试一下连接状态。

DatagramChannel

DatagramChannel 则模拟包导向的无连接协议(如 UDP/IP)

public abstract class DatagramChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel
{
// This is a partial API listing
public static DatagramChannel open( ) throws IOException
public abstract DatagramSocket socket( );
public abstract DatagramChannel connect (SocketAddress remote)
throws IOException;
public abstract boolean isConnected( );
public abstract DatagramChannel disconnect( ) throws IOException;
public abstract SocketAddress receive (ByteBuffer dst)
throws IOException;
public abstract int send (ByteBuffer src, SocketAddress target)
public abstract int read (ByteBuffer dst) throws IOException;
public abstract long read (ByteBuffer [] dsts) throws IOException;
public abstract long read (ByteBuffer [] dsts, int offset,
int length)
throws IOException;
public abstract int write (ByteBuffer src) throws IOException;
public abstract long write(ByteBuffer[] srcs) throws IOException;
public abstract long write(ByteBuffer[] srcs, int offset,
int length)
throws IOException;
}

 DatagramChannel 是无连接的。每个数据报(datagram)都是一个自包含的实体,拥有它自己的目的地址及不依赖其他数据报的数据净荷。与面向流的的 socket 不同,DatagramChannel 可以发送单独的数据报给不同的目的地址。同样,DatagramChannel 对象也可以接收来自任意地址的数据包。每个到达的数据报都含有关于它来自何处的信息(源地址)。

 DatagramChannel 是无连接的。每个数据报(datagram)都是一个自包含的实体,拥有它自己的目的地址及不依赖其他数据报的数据净荷。与面向流的的 socket 不同,DatagramChannel 可以发送单独的数据报给不同的目的地址。同样,DatagramChannel 对象也可以接收来自任意地址的数据包。每个到达的数据报都含有关于它来自何处的信息(源地址)。

 不同于流 socket,数据报 socket 的无状态性质不需要同远程系统进行对话来建立连接状态。没有实际的连接,只有用来指定允许的远程地址的本地状态信息。由于此原因,DatagramChannel 上也就没有单独的 finishConnect( )方法。我们可以使用 isConnected( )方法来测试一个数据报通道的连接状态。

 不同于 SocketChannel(必须连接了才有用并且只能连接一次),DatagramChannel 对象可以任意次数地进行连接或断开连接。每次连接都可以到一个不同的远程地址。调用 disconnect( )方法可以配置通道,以便它能再次接收来自安全管理器(如果已安装)所允许的任意远程地址的数据或发送数据到这些地址上。

 当一个 DatagramChannel 处于已连接状态时,发送数据将不用提供目的地址而且接收时的源地址也是已知的。这意味着 DatagramChannel 已连接时可以使用常规的 read( )和 write( )方法,包括scatter/gather 形式的读写来组合或分拆包的数据:

public abstract class DatagramChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel
{
// This is a partial API listing
public abstract int read (ByteBuffer dst) throws IOException;
public abstract long read (ByteBuffer [] dsts) throws IOException;
public abstract long read (ByteBuffer [] dsts, int offset,
int length)
throws IOException;
public abstract int write (ByteBuffer src) throws IOException;
public abstract long write(ByteBuffer[] srcs) throws IOException;
public abstract long write(ByteBuffer[] srcs, int offset,
int length)
throws IOException;
}

 read( )方法返回读取字节的数量,如果通道处于非阻塞模式的话这个返回值可能是“0”。
 write( )方法的返回值同 send( )方法一致:要么返回您的缓冲区中的字节数量,要么返回“0”(如果由于通道处于非阻塞模式而导致数据报不能被发送)。当通道不是已连接状态时调用 read( )或write( )方法,都将产生NotYetConnectedException 异常。

 数据报通道不同于流 socket。由于它们的有序而可靠的数据传输特性,流 socket 非常得有用。大多数网络连接都是流 socket(TCP/IP 就是一个显著的例子)。但是,像 TCP/IP 这样面向流的的协议为了在包导向的互联网基础设施上维护流语义必然会产生巨大的开销,并且流隐喻不能适用所有的情形。数据报的吞吐量要比流协议高很多,并且数据报可以做很多流无法完成的事情。

管道

 java.nio.channels 包中含有一个名为 Pipe(管道)的类。广义上讲,管道就是一个用来在两个实体之间单向传输数据的导管。管道的概念对于 Unix(和类 Unix)操作系统的用户来说早就很熟悉了。Unix 系统中,管道被用来连接一个进程的输出和另一个进程的输入。Pipe 类实现一个管道范例,不过它所创建的管道是进程内(在 Java 虚拟机进程内部)而非进程间使用的。参见图3
image

图3

 Pipe 类创建一对提供环回机制的 Channel 对象。这两个通道的远端是连接起来的,以便任何写在 SinkChannel 对象上的数据都能出现在 SourceChannel 对象上。

package java.nio.channels;
public abstract class Pipe
{
    public static Pipe open( ) throws IOException

    public abstract SourceChannel source( );

    public abstract SinkChannel sink( );

    public static abstract class SourceChannel
    extends AbstractSelectableChannel
    implements ReadableByteChannel, ScatteringByteChannel

    public static abstract class SinkChannel
    extends AbstractSelectableChannel
    implements WritableByteChannel, GatheringByteChannel
}

 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 虚拟机内部传输数据。虽然有更加有效率的方式来在线程之间传输数据,但是使用管道的好处在于封装性。生产者线程和用户线程都能被写道通用的 ChannelAPI 中。根据给定的通道类型,相同的代码可以被用来写数据到一个文件、socket 或管道。选择器可以被用来检查管道上的数据可用性,如同在 socket 通道上使用那样地简单。这样就可以允许单个用户线程使用一个 Selector 来从多个通道有效地收集数据,并可任意结合网络连接或本地工作线程
使用。因此,这些对于可伸缩性、冗余度以及可复用性来说无疑都是意义重大的。

 管路所能承载的数据量是依赖实现的(implementation-dependent)。唯一可保证的是写到SinkChannel 中的字节都能按照同样的顺序在 SourceChannel 上重现。

通道工具类:Channels

 NIO 通道提供了一个全新的类似流的 I/O 隐喻,但是我们所熟悉的字节流以及字符读写器仍然存在并被广泛使用。通道可能最终会改进加入到 java.io 类中(这是一个实现细节),但是java.io 流所代表的 API 和读写器却不会很快消失(它们也不应该消失)。一个工具类(java.nio.channels.Channels 的一个稍微重复的名称)定义了几种静态的工厂方法以使通道可以更加容易地同流和读写器互联。表 3-2 对这些方法做了一个汇总。

 常规的流仅传输字节,readers 和 writers 则作用于字符数据。表 3-2 的前四行描述了用于连接流、通道的方法。因为流和通道都是运行在字节流基础上的,所以这四个方法直接将流封装在通道上,反之亦然。

总结

通道组成了基础设施或者说管道设施,该设施在操作系统(或通道连接到的任意东西)的ByteBuffers和I/O服务之间传输数据。

猜你喜欢

转载自blog.csdn.net/Pengjx2014/article/details/79091474