NIO通道

通道接口允许您以一种受控且可移植的方式来访问底层的 I/O服务。

Channel 用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。

1 通道基础

您可以从顶层的 Channel 接口看到,对所有通道来说只有两种共同的操作:检查一个通道是否打开(IsOpen())和关闭一个打开的通道(close())。

public interface Channel
{
public boolean isOpen( );
public void close( ) throws IOException;
}

通道只能在字节缓冲区上操作。不过非字节实现是不可能的,因为操作系统都是以字节的形式实现底层 I/O 接口的。

1.1 打开通道

通道是访问 I/O 服务的导管。

I/O 可以分为广义的两大类别:File I/O 和 Stream I/O。那么相应地有两种类型的通道也就不足为怪了,它们是文件(file)通道和套接字(socket)通道。

Socket 通道有可以直接创建新 socket 通道的工厂方法。

FileChannel通道只能通过在一个打开的 RandomAccessFile、FileInputStream 或 FileOutputStream对象上调用 getChannel( )方法来获取。

socket 类也有新的 getChannel( )方法。这些方法虽然能返回一个相应的 socket 通道对象,但它们却并非新通道的来源,RandomAccessFile.getChannel( )方法才是。只有在已经有通道存在的时候,它们才返回与一个 socket 关联的通道;它们永远不会创建新通道。

1.2使用通道

ByteChanel.api

public interface ByteChannel
extends ReadableByteChannel, WritableByteChannel
{}

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

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

FileInputStream 对象总是以 read-only 的权限打开文件

通道可以是单向(unidirectional)或者双向的(bidirectional)。单向指只能读或写,双向指可以读也可以写。只实现了ReadableByteChannel或只实现了 WritableByteChannel,都是单向的,如果两个接口都实现了就是双向的。

通道会连接一个特定 I/O 服务且通道实例(channel instance)的性能受它所连接的 I/O 服务的特征限制。例如文件是只读的,但是我们却想往里面写数据,这样是不允许的。

例:

FileInputStream input = new FileInputStream (fileName);
FileChannel channel = input.getChannel( );
channel.write (buffer);

因为FileInputStream 对象总是以 read-only 的权限打开文件,所以只能读,不能写,所以上诉代码会报错!

int read (ByteBuffer dst)    

int write (ByteBuffer src)

上面的两个方法返回的都是int,代表的是已传输的字节数,可能比缓冲区少甚至可能为零。缓冲区的位置即position也会发生相同数量的前移。参数都是Bytebuffer,即在这个缓冲区里实现上述操作。

例:通道间复制值-例子1

FileInputStream fileInputStream = new FileInputStream("F:\\message\\测试io文件\\read-channel.txt");
FileOutputStream fileOutputStream = new FileOutputStream("F:\\message\\测试io文件\\write-cahnnel.txt");
/*读通道*/
ReadableByteChannel source = Channels.newChannel (fileInputStream);
/*写通道*/
WritableByteChannel dest = Channels.newChannel (fileOutputStream);
/*创建直接字节缓冲区*/
ByteBuffer buffer = ByteBuffer.allocateDirect (16 * 1024);
/*每次从通道里读出数据,存到缓冲区里*/
while (source.read (buffer) != -1) {
    /*翻转*/
    buffer.flip( );
    /*将缓冲区的数据写入写通道里*/
    dest.write (buffer);
    /*压缩*/
    buffer.compact( );
}
/*翻转*/
buffer.flip( );
/*这里是为了确定缓冲区的数据是否全部已经写入通道*/
while (buffer.hasRemaining( )) {
    dest.write (buffer);
}

例:通道间复制值-例子2

FileInputStream fileInputStream = new FileInputStream("F:\\message\\测试io文件\\read-channel.txt");
FileOutputStream fileOutputStream = new FileOutputStream("F:\\message\\测试io文件\\write-cahnnel.txt");
/*读通道*/
ReadableByteChannel source = Channels.newChannel (fileInputStream);
/*写通道*/
WritableByteChannel dest = Channels.newChannel (fileOutputStream);
/*创建直接字节缓冲区*/
ByteBuffer buffer = ByteBuffer.allocateDirect (16);
while (source.read (buffer) != -1) {
    /*翻转,准备被填充*/
    buffer.flip( );
    /*确定缓冲区是否被排干*/
    while (buffer.hasRemaining( )) {
	dest.write (buffer);
    }
    /*清空缓冲区,让其准备被填充*/
    buffer.clear( );
}

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

1.3 关闭通道

与缓冲区不同,通道不能被重复使用。 

public interface Channel
{
public boolean isOpen( );
public void close( ) throws IOException;
}   

调用通道的close( )方法时,可能会导致在通道关闭底层I/O服务的过程中线程暂时阻塞,哪怕该通道处于非阻塞模式。
    
 在一个通道上多次调用close( )方法是没有坏处的,但是如果第一个线程在close( )方法中阻塞,那么在它完成关闭通道之前,任何其他调用close( )方法都会阻塞。后续在该已关闭的通道上调用close( )不会产生任何操作,只会立即返回。
    
可以通过 isOpen( )方法来测试通道的开放状态。
            
如果一个通道实现 InterruptibleChannel 接口(参见图 3-2),它的行为以下述语义为准:如果一个线程在一个通道上被阻塞并且同时被中断(由调用该被阻塞线程的 interrupt( )方法的另一个线程中断),那么该通道将被关闭,该被阻塞线程也会产生一个 ClosedByInterruptException 异常。

我们可以使用 isInterrupted( )来测试某个线程当前的 interrupt status(中断状态)。当前线程的 interrupt status 可以通过调用静态的 Thread.interrupted( )方法清除。
    
休眠在其上的线程被中断就关闭通道
            
可中断的通道也是可以异步关闭的。
            
请不要将在 Channels 上休眠的中断线程同在 Selectors 上休眠的中断线程混淆。前者会关闭通道,而后者则不会。
            
经验表明,想要在所有的操作系统上一致而可靠地处理被中断的 I/O 操作是不可能的。
            
可中断的通道也是可以异步关闭的。实现 InterruptibleChannel 接口的通道可以在任何时候被关闭,即使有另一个被阻塞的线程在等待该通道上的一个 I/O 操作完成。

不实现 InterruptibleChannel 接口的通道一般都是不进行底层本地代码实现的有特殊用途的通道。所以是不会阻塞的特殊通道,所以不需要中断。

2 Scatter和Gather

数据是从几个缓冲区按顺序抽取(称为 gather)并沿着通道发送的。
从通道读取的数据会按顺序被散布(称为 scatter)到多个缓冲区

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

例子:

FileInputStream fileInputStream = new FileInputStream("F:\\message\\测试io文件\\read-channel.txt");
ScatteringByteChannel read = (ScatteringByteChannel) Channels.newChannel(fileInputStream);
ByteBuffer header = ByteBuffer.allocateDirect(10);
ByteBuffer body = ByteBuffer.allocateDirect(60);
ByteBuffer[] buffers = {header,body};
int bytesRead = (int) read.read(buffers);

一旦 read( )方法返回,bytesRead 就被赋予值 48,header 缓冲区将包含前 10 个从通道读取的字节而 body 缓冲区则包含接下来的 38 个字节。通道会自动地将数据 scatter 到这两个缓冲区中。

以此类推,gather形式也一样,

FileOutputStream fileOutputStream = new FileOutputStream("F:\\message\\测试io文件\\write-cahnnel.txt");
GatheringByteChannel gather = fileOutputStream.getChannel();
ByteBuffer header = ByteBuffer.allocateDirect(10);
header.put((byte) 1).put((byte) 1).put((byte) 1);
ByteBuffer body = ByteBuffer.allocateDirect(60);
body.put((byte) 2).put((byte) 2).put((byte) 2);
ByteBuffer[] buffers = {header,body};
long byteWrite =  gather.write(buffers);

带 offset 和 length 参数版本的 read( ) 和 write( )方法使得我们可以使用缓冲区阵列的子集缓冲区。这里的 offset 值指哪个缓冲区将开始被使用,而不是指数据的 offset。这里的 length 参数指示要使用的缓冲区数量。

3 文件通道

FileChannel 类可以实现常用的 read,write 以及 scatter/gather 操作,同时它也提供了很多专用于文件的新方法。

文件通道总是阻塞式的,因此不能被置于非阻塞模式。

对于文件 I/O,最强大之处在于异步 I/O(asynchronous I/O),它允许一个进程可以从操作系统请求一个或多个 I/O 操作而不必等待这些操作的完成。
        
一个FileChannel实例只能通过在一个打开的file对象(RandomAccessFile、FileInputStream或 FileOutputStream)上调用getChannel( )方法获取
    
FileChannel 对象是线程安全(thread-safe)的。影响通道位置或者影响文件大小的操作都是单线程的(single-threaded)。

public abstract class FileChannel extends AbstractChannel
implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
{
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 类保证同一个 Java 虚拟机上的所有实例看到的某个文件的视图均是一致的。

通过一个 FileChannel 实例看到的某个文件的视图同通过一个外部的非 Java 进程看到的该文件的视图可能一致,也可能不一致。

3.1 访问文件

文件通道的基本操作:

public abstract long position( )

//返回当前位置
public abstract void position (long newPosition)

//设置位置,设置负值会报错,也不能超出文件尾,若超出文件尾,在执行write时,会引起文件增长以容纳写入的字节,会导致文件空洞。

文件空洞:当磁盘上一个文件的分配空间小于它的文件大小时会出现“文件空洞”。假如数据被写入到文件中非连续的位置上,这将导致文件出现在逻辑上不包含数据的区域(即“空洞”)。


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 值决定文件中哪一处的数据接下来将被读或者写。
position信息是从文件描述中获得的,即该信息的所有通道共享的,这意味着一个对象对该position的更新hi被另外一个对象看到。

类似于缓冲区的 get( ) 和 put( )方法,当字节被 read( )或 write( )方法传输时,文件 position 会自动更新。            

不同于缓冲区的是,如果实现 write( )方法时 position前进到超过文件大小的值,该文件会扩展以容纳新写入的字节。

同样类似于缓冲区,也有带 position 参数的绝对形式的 read( )和 write( )方法。这种绝对形式的方法在返回值时不会改变当前的文件 position。

size( )方法会返回一个 end-of-file。

当需要减少一个文件的 size 时,truncate( )方法会砍掉您所指定的新 size 值之外的所有数据。如果当前 size 大于新 size,超出新 size 的所有字节都会被悄悄地丢弃。如果提供的新 size 值大于或等于当前的文件 size 值,该文件不会被修改。

多个线程可以并发访问同一个文件而不会相互产生干扰。这是因为每次调用都是原子性的(atomic),并不依靠调用之间系统所记住的状态。

force( )。该方法告诉通道强制将全部待定的修改都应用到磁盘的文件上。所有的现代文件系统都会缓存数据和延迟磁盘文件更新以提高性能。调用 force( )方法要求文件的所有待定修改立即同步到磁盘。

force( )方法的布尔型参数表示在方法返回值前文件的元数据(metadata)是否也要被同步更新到磁盘。元数据指文件所有者、访问权限、最后一次修改时间等信息。大多数情形下,该信息对数据恢复而言是不重要的。

3.2 锁定

只有FileChanel才有锁

锁(lock)可以是共享的(shared)或独占的(exclusive),并非所有的操作系统和文件系统都支持共享文件锁。对于那些不支持的,对一个共享锁的请求会被自动提升为对独占锁的请求。

共享锁:如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。

排它锁:如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。

另外,并非所有平台都以同一个方式来实现基本的文件锁定。在不同的操作系统上,甚至在同一个操作系统的不同文件系统上,文件锁定的语义都会有所差异。

如果一个线程在某个文件上获得了一个独占锁,然后第二个线程利用一个单独打开的通道来请求该文件的独占锁,那么第二个线程的请求会抛出OverlappingFileLockException异常。但如果这两个线程运行在不同的 Java 虚拟机上,那么第二个线程会阻塞。

锁的对象是文件而不是通道或线程,这意味着文件锁不适用于判优同一台 Java 虚拟机上的多个线程发起的访问。

锁都是与一个文件关联的,而不是与单个的文件句柄或通道关联。

我们使用锁来判优外部进程,而不是判优同一个 Java 虚拟机上的线程。

看看文件锁有关的FileChannel API方法

public abstract class FileChannel extends AbstractChannel
implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
{
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)
}

lock (long position, long size,boolean shared)方法:锁是在文件内部区域上获得的。调用带参数的 Lock( )方法会指定文件内部锁定区域的开始 position 以及锁定区域的 size。第三个参数 shared 表示您想获取的锁是共享的(参数值为 true)还是独占的(参数值为 false)。

要获得一个共享锁,您必须先以只读权限打开文件,而请求独占锁时则需要写权限

锁定区域的范围不一定要限制在文件的 size 值以内,锁可以扩展从而超出文件尾。因此,我们可以提前把待写入数据的区域锁定,我们也可以锁定一个不包含任何文件内容的区域,比如文件最后一个字节以外的区域。如果之后文件增长到达那块区域,那么您的文件锁就可以保护该区域的文件内容了。相反地,如果您锁定了文件的某一块区域,然后文件增长超出了那块区域,那么新增加的文件内容将不会受到您的文件锁的保护。

不带参数的简单形式的 lock( )方法是一种在整个文件上请求独占锁的便捷方法,锁定区域等于它能达到的最大范围。该方法等价于:
fileChannel.lock (0L, Long.MAX_VALUE, false);

tryLock( )和 lock( )方法起相同的作用,不过tryLock如果请求的锁不能立即获取到则会返回一个 null,不会阻塞。

接下来看看FileLock的API

public final FileChannel channel( ):查询该lock 对象是由哪个通道创建的。
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;//关闭锁

FileLock 类封装一个锁定的文件区域。FileLock 对象由 FileChannel 创建并且总是关联到那个特定的通道实例。

一个 FileLock 对象创建之后即有效,直到它的 release( )方法被调用或它所关联的通道被关闭或Java 虚拟机关闭时才会失效。

 isShared( )方法来测试一个锁以判断它是共享的还是独占的。如果底层的操作系统或文件系统不支持共享锁,那么该方法将总是返回 false 值,即使您申请锁时传递的参数值是 true。

overlaps( )方法来查询一个 FileLock 对象是否与一个指定的文件区域重叠。注意,即时返回false,也只能表名改锁对象没有锁该区域。其他先可能会锁住了该区域,所以最好用偶tryLock()方法判断一下。

尽管一个 FileLock 对象是与某个特定的 FileChannel 实例关联的,它所代表的锁却是与一个底层文件关联的,而不是与通道关联。因此,如果您在使用完一个锁后而不释放它的话,可能会导致冲突或者死锁。所以在程序的最后一定要释放锁资源。

FileLock lock = fileChannel.lock( )
try {
    throw new Exception(“报错了,需要释放锁资源”):
} catch (Exception e) [

} finally {
lock.release( )
}

如果所有的锁都是强制性的(mandatory)

文件锁旨在在进程级别上判优文件访问

锁是与进程而不是 Java 线程关联

4 内存映射文件

新的 FileChannel 类提供了一个名为 map( )的方法,该方法可以在一个打开的文件和一个特殊类型的 ByteBuffer 之间建立一个虚拟内存映射

通过内存映射机制来访问一个文件会比使用常规方法读写高效得多,甚至比使用通道的效率都高。因为不需要做明确的系统调用,那会很消耗时间。更重要的是,操作系统的虚拟内存可以自动缓存内存页(memory page)。这些页是用系统内存来缓存的,所以不会消耗 Java 虚拟机内存堆(memory heap)。

创建映射文件

buffer = fileChannel.map (FileChannel.MapMode.READ_ONLY, 100, 200);
        映射文件是只读的,且只映射100到200包含200的数据

如果要映射整个文件则使用:
buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());

与文件锁的范围机制不一样,映射文件的范围不应超过文件的实际大小。因为当你请求超过匹配映射的大小时,文件大小值会膨胀,即时你只是一个读映射,系统会认为你修改了文件,抛出Io异常

如果通道是以只读的权限打开的而您却请求 MapMode.READ_WRITE 模式,那么map( )方法会抛出一个 NonWritableChannelException 异常

通过内存映射机制来访问一个文件会比使用常规方法读写高效得多,甚至比使用通道的效率都高。

三种模式

MapMode.READ_ONLY:只读      

MapMode.READ_WRITE :可读可写,

MapMode.PRIVATE:可读可写,但是修改的内容不会写入文件,只是buffer自身的改变。

        表示您想要一个写时拷贝(copy-on-write)的映射,该过程不会对底层文件做任何修改。而且一旦缓冲区被施以垃圾收集动作  (garbage collected),那些修改都会丢失。尽管写时拷贝的映射可以防止底层文件被修改,您也必须以 read/write 权限来打开文件以建立 MapMode.PRIVATE 映射。只有这样,返回的MappedByteBuffer 对象才能允许使用 put( )方法。

选择使用 MapMode.PRIVATE 模式并不会导致您的缓冲区看不到通过其他方式对文件所做的修改。对文件某个区域的修改在使用 MapMode.PRIVATE 模式的缓冲区中都能反映出来,除非该缓冲区已经修改了文件上的同一个区域。

同锁不一样的是,映射缓冲区没有绑定到创建它们的通道上。

一个映射一旦建立之后将保持有效,直到MappedByteBuffer 对象被施以垃圾收集动作为止。

MemoryMappedBuffer 直接反映它所关联的磁盘文件。如果映射有效时文件大小变化了,那么缓冲区的部分或全部内容都可能无法访问,并将返回未定义的数据或者抛出未检查的异常。

所有的 MappedByteBuffer 对象都是直接的,这意味着它们占用的内存空间位于 Java 虚拟机内存堆之外。

MappedByteBuffer
            MappedByteBuffer load( ):
                加载整个文件以使它常驻内存。
                然而,load( )方法返回并不能保证文件就会完全常驻内存,这是由于请求页面调入(demand paging)是动态的
                对于那些要求近乎实时访问(near-realtime access)的程序,解决方案就是预加载。
                
            boolean isLoaded( )
                判断一个被映射的文件是否完全常驻内存了
                因为是异步的,所以不能完全确定在该方法判断完后,映射文件是否加载完。
            MappedByteBuffer force( )
                 强制将映射缓冲区上的更改应用到永久磁盘存储器上
                 如果映射是以 MapMode.READ_ONLY 或 MAP_MODE.PRIVATE 模式建立的,那么调用 force( )方法将不起任何作用,因为这两个模式永远不会有写操作需要应用到磁盘上

我们下面的测试会用到RandomAccessFile,这里大概介绍一下RandomAccessFile!

        1、是JAVA I/O流体系中功能最丰富的文件内容访问类,它提供了众多方法来访问文件内容。

        2、由于可以自由访问文件的任意位置,所以如果需要访问文件的部分内容,RandomAccessFile将是更好的选择。

        3、可以用来访问保存数据记录的文件,文件的记录的大小不必相同,但是其大小和位置必须是可知的。

       新建RandomAccessFile:    
            RandomAccessFile raf = newRandomAccessFile(File file, String mode);
            mode有种选择
                "r":只允许只读方式打开,不允许write
                "rw":允许读和写,如果文件不存在,就会尝试创建该文件
                "rws":允许写和读,要求对文件的内容或元数据的每个更新都同步写入到底层存储设备
                "rwd":允许写和读,要求对文件的内容的每个更新都同步写入到底层存储设备

File file = new File("F:\\message\\测试io文件\\read-channel.txt");
        RandomAccessFile randomFile = new RandomAccessFile (file, "rw");
        FileChannel fileChannel = randomFile.getChannel();
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.PRIVATE,0,fileChannel.size());
        mappedByteBuffer.put((byte)2).put((byte)2);

修改结果并没有作用到源文件中。

一般数据量比较大的时候,用MappedByteBuffer比较快,,数据量小的话,反而因为direct buffer的初始化时间较长,而比较慢。

4.1 Chanel-to-chanel传输

FileChannel
                abstract long transferTo (long position, long count,WritableByteChannel target)                
                abstract long transferFrom (ReadableByteChannel src,long position, long count)
                transferTo( )和 transferFrom( )方法允许将一个通道交叉连接到另一个通道,而不需要通过一个中间缓冲区来传递数据。只有 FileChannel 类有这两个方法。因此 channel-to-channel 传输中通道之一必须是 FileChannel。
                
                不过 socket 通道实现WritableByteChannel 和 ReadableByteChannel 接口,因此文件的内容可以用 transferTo( )方法传输给一个 socket 通道,或者也可以用 transferFrom( )方法将数据从一个 socket 通道直接读取到一个文件中。
                
            直接的通道传输不会更新与某个 FileChannel 关联的 position 值。请求的数据传输将从position 参数指定的位置开始,传输的字节数不超过 count 参数的值。
            
            对于传输数据来源是一个文件的 transferTo( )方法,如果 position + count 的值大于文件的 size 值,传输会在文件尾的位置终止。

  
            传输的目的地是一个非阻塞模式的 socket 通道,那么当发送队列(send queue)满了之后传输就可能终止,并且如果输出队列(output queue)已满的话可能不会发送任何数据。
            
            Channel-to-channel 传输是可以极其快速的

代码:

File file = new File("F:\\message\\测试io文件\\read-channel.txt");
        RandomAccessFile randomFile = new RandomAccessFile (file, "rw");
        File targetFile = new File("F:\\message\\测试io文件\\write-cahnnel.txt");
        RandomAccessFile randomTargetFile = new RandomAccessFile (targetFile, "rw");
        FileChannel channel = randomFile.getChannel( );
        FileChannel targetChannel = randomTargetFile.getChannel( );
        channel.transferTo (0, channel.size( ),targetChannel);
        channel.close( );
        targetChannel.close();
        randomFile.close( );
        randomTargetFile.close();

5 Scoket通道(套接字通道)

新的 socket 通道类可以运行非阻塞模式并且是可选择的。没有为每个 socket 连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换总开销。

借助新的 NIO 类,一个或几个线程就可以管理成百上千的活动 socket 连接了并且只有很少甚至可能没有性能损失。

全部 socket 通道类(DatagramChannel、SocketChannel 和ServerSocketChannel)

        DatagramChannel 和 SocketChannel 实现定义读和写功能的接口//创建方式:SocketChannel sc = SocketChannel.open( );和DatagramChannel da = DatagramChannel.open();
        ServerSocketChannel 负责监听传入的连接和创建新的 SocketChannel 对象,它本身从不传输数据。

全部 socket 通道类(DatagramChannel、SocketChannel 和 ServerSocketChannel)在被实例化时都会创建一个对等 socket 对象。这些是我们所熟悉的来自 java.net 的类(Socket、ServerSocket和 DatagramSocket),对等 socket 可以通过调用 socket( )方法从一个通道上获取。
        虽然每个 socket 通道(在 java.nio.channels 包中)都有一个关联的 java.net socket 对象,却并非所有的 socket 都有一个关联的通道。如果您用传统方式(直接实例化)创建了一个Socket 对象,它就不会有关联的 SocketChannel 并且它的 getChannel( )方法将总是返回 null。

Socket 通道委派协议操作给对等 socket 对象。

5.1 非阻塞模式

Socket 通道可以在非阻塞模式下运行。
            
            要把一个 socket 通道置于非阻塞模式,我们要依靠所有 socket 通道类的公有超级类:SelectableChannel。
            
            有条件选择是一种可以用来查询通道的机制,该查询判断该通道是否可以进行下一个目标操作。如读或写。
        
            SelectableChannel
                abstract void configureBlocking (boolean block):
                    设置或重新设置一个通道的阻塞模式是很简单的,只要调用 configureBlocking( )方法即可
                abstract boolean isBlocking( );
                    判断某个 socket 通道当前处于哪种模式:true阻塞,false是非阻塞
                abstract Object blockingLock( );    
                    该方法会返回一个非透明的对象引用。返回的对象是通道实现修改阻塞模式时内部使用的。只有拥有此对象的锁的线程才能更改通道的阻塞模式

例子:

SocketChannel sc = SocketChannel.open( );
Socket socket = null;
Object lockObj = sc.blockingLock( );
synchronized (lockObj) {
    boolean prevState = sc.isBlocking( );
    sc.configureBlocking (false);
    sc.configureBlocking (prevState);
}

服务器端的使用经常会考虑到非阻塞 socket 通道,因为它们使同时管理很多 socket 通道变得更容易。

5.2 ServerSocketChannel

SocketChannel 对应 Socket,ServerSocketChannel 是一个基于通道的 socket 监听器。
             
            public static ServerSocketChannel open( ) throws IOException
                创建一个新的ServerSocketDChannel对象,将会返回一个未绑定的SeverSocket关联的通道。该对等 ServerSocket 可以通过在返回的 ServerSocketChannel 上调用 socket( )方法来获取。由于 ServerSocketChannel 没有 bind( )方法,因此有必要取出对等的 socket 并使用它来绑定到一个端口以开始监听连接。

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

ServerSocketChannel 是一个基于通道的 socket 监听器,它增加了通道语义,因此能够在非阻塞模式下运行。具体实现会在选择器的时候讲述该类是如何监听通道!
        

5.3 SocketChannel

SocketChannel对等Socket

Socket 和 SocketChannel 类封装点对点、有序的网络连接,类似于我们所熟知并喜爱的 TCP/IP网络连接。SocketChannel 扮演客户端发起同一个监听服务器的连接。直到连接成功,它才能收到数据并且只会从连接到的地址接收。
            
            虽然每个 SocketChannel 对象都会创建一个对等的 Socket 对象,反过来却不成立。直接创建的 Socket 对象不会关联 SocketChannel 对象,它们的getChannel( )方法只返回 null。
            
            在一个未连接的 SocketChannel 对象上尝试一个 I/O 操作会导致 NotYetConnectedException 异常。
            
            我们可以通过在通道上直接调用 connect( )方法或在通道关联的 Socket 对象上调用 connect( )来将该 socket 通道连接。连接后,连接状态会一直持续到直到被关闭。

创建SocketChannel对象

第一种:静态的 open( )方法可以创建一个新的 SocketChannel 对象,而在新创建的 SocketChannel 上调用 socket( )方法能返回它对等的 Socket 对象;
第二种:带 InetSocketAddress 参数形式的 open( )是在返回之前进行连接的便捷方法。

SocketChannel socketChannel = SocketChannel.open (new InetSocketAddress ("somehost", somePort));
            
            SocketChannel不能指定连接超时的值,在非阻塞模式下,发送的请求会立即返回值,true就是连接成功。不能连接就返回false,且并发地继续连接建立过程。
            
            面向流的socket建立连接需要一定的时间,因为两个待连接系统之间必须进行包对话以建立维护流socket所需的状态信息。
            
            SocketChannel
                public static SocketChannel open( ) throws IOException:
                public static SocketChannel open (InetSocketAddress remote):
                public abstract Socket socket( );
                public abstract boolean connect (SocketAddress remote):
                public abstract boolean isConnectionPending( );
                public abstract boolean finishConnect( ) throws IOException;
                    如果此时通道还没有调用connect()开始取连接,调用finishConnect()会报异常。
                    该方法调用是确定完成连接过程,是返回true,还没有返回false。
                    
                public abstract boolean isConnected( );
                    一旦连接建立过程成功完成,isConnected( )将返回 true
                public final int validOps( ):  

调用 finishConnect( )方法来完成连接过程,该方法任何时候都可以安全地进行调用。假如在一
个非阻塞模式的 SocketChannel 对象上调用 finishConnect( )方法,将可能出现下列情形之一:
 connect( )方法尚未被调用。那么将产生 NoConnectionPendingException 异常。
 连接建立过程正在进行,尚未完成。那么什么都不会发生,finishConnect( )方法会立即返回
false 值。
 在非阻塞模式下调用 connect( )方法之后,SocketChannel 又被切换回了阻塞模式。那么如果
有必要的话,调用线程会阻塞直到连接建立完成,finishConnect( )方法接着就会返回 true
值。
 在初次调用 connect( )或最后一次调用 finishConnect( )之后,连接建立过程已经完成。那么
SocketChannel 对象的内部状态将被更新到已连接状态,finishConnect( )方法会返回 true
值,然后 SocketChannel 对象就可以被用来传输数据了。
 连接已经建立。那么什么都不会发生,finishConnect( )方法会返回 true 值。              
            
            Socket 通道是线程安全的。
            
            任何时候都只有一个读操作和一个写操作在进行中。
        
            它们可以保证发送的字节会按照顺序到达但无法承诺维持字节分组。例如:某个发送器可能给一个 socket 写入了20 个字节而接收器调用 read( )方法时却只收到了其中的 3 个字节。
            
            connect( )和 finishConnect( )方法是互相同步的,并且只要其中一个操作正在进行,任何读或写的方法调用都会阻塞,即使是在非阻塞模式下。

5.4 DatagramChannel:数据报通道

DatagramChannel 关联DatagramSocket
            
            正如 SocketChannel 模拟连接导向的流协议(如 TCP/IP),DatagramChannel 则模拟包导向的无连接协议(如 UDP/IP):
            
            DatagramChannel
                public static DatagramChannel open( ) throws IOException
                    创建实例
                public abstract DatagramSocket socket( );
                    获取对等的 DatagramSocket
                public abstract DatagramChannel connect (SocketAddress remote)
                    可以设置,只接受自己制定地址的数据包,其他路径的数据包都被无视丢弃。
                public abstract boolean isConnected( );
                    使用 isConnected( )方法来测试一个数据报通道的连接状态。
                public abstract DatagramChannel disconnect( ) throws IOException;
                public abstract SocketAddress receive (ByteBuffer dst)
                    receive( )方法将下次将传入的数据报的数据净荷复制到预备好的 ByteBuffer 中并返回一个SocketAddress 对象以指出数据来源。
                    如果通道处于阻塞模式,receive( )可能无限期地休眠直到有包到达。如果是非阻塞模式,当没有可接收的包时则会返回 null。
                    如果包内的数据超出缓冲区能承受的范围,多出的数据都会被悄悄地丢弃。
                public abstract int send (ByteBuffer src, SocketAddress target)
                    调用 send( )会发送给定 ByteBuffer 对象的内容到给定 SocketAddress 对象所描述的目的地址和端口,内容范围为从当前 position 开始到末尾处结束。
                    如果 DatagramChannel 对象处于阻塞模式,调用线程可能会休眠直到数据报被加入传输队列。
                    如果通道是非阻塞的,返回值要么是字节缓冲区的字节数,要么是“0”。
                    发送数据报是一个全有或全无(all-or-nothing)的行为。
                    如果传输队列没有足够空间来承载整个数据报,那么什么内容都不会被发送。
                    请注意,数据报协议的不可靠性是固有的,它们不对数据传输做保证。send( )方法返回的非零值并不表示数据报到达了目的地,仅代表数据报被成功加到本地网络层的传输队列。
                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)
                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)            
        
            DatagramChannel 对象既可以充当服务器(监听者)也可以充当客户端(发送者)。
            
            DatagramChannel 是无连接的。每个数据报(datagram)都是一个自包含的实体,拥有它自己的目的地址及不依赖其他数据报的数据净荷。
        
            与面向流的的 socket 不同,DatagramChannel 可以发送单独的数据报给不同的目的地址。
            
            一个未绑定的 DatagramChannel 仍能接收数据包。当一个底层 socket 被创建时,一个动态生成的端口号就会分配给它。
            
            数据的实际发送或接收是通过 send( )和 receive( )方法来实现的:
            
            DatagramChannel 对象可以任意次数地进行连接或断开连接。每次连接都可以到一个不同的远程地址。
            
            数据报的吞吐量要比流协议高很多
            
            在以下情况下,选择数据报socket而不选择流socket
                您的程序可以承受数据丢失或无序的数据。
                 您希望“发射后不管”(fire and forget)而不需要知道您发送的包是否已接收。
                 数据吞吐量比可靠性更重要。
                 您需要同时发送数据给多个接受者(多播或者广播)。
                 包隐喻比流隐喻更适合手边的任务。

6 管道

广义上讲,管道就是一个用来在两个实体之间单向传输数据的导管。
        
        Pipe 类实现一个管道范例,不过它所创建的管道是进程内(在 Java 虚拟机进程内部)而非进程间使用的。
        
        Pipe 类创建一对提供环回机制的 Channel 对象。这两个通道的远端是连接起来的,以便任何写在 SinkChannel 对象上的数据都能出现在 SourceChannel 对象上。
        
        Pipe
            public static Pipe open( ) throws IOException
                创建管道
            public abstract SourceChannel source( );
            public abstract SinkChannel sink( );        
        
        Pipe 类定义了两个嵌套的通道类来实现管路。这两个类是 Pipe.SourceChannel(管道负责读的一端)和 Pipe.SinkChannel(管道负责写的一端)。
        
        这两个通道实例是在 Pipe 对象创建的同时被创建的,可以通过在 Pipe 对象上分别调用 source( )和 sink( )方法来取回。
        
        pipe 通道可以同选择器一起使用
         
            管道可以被用来仅在同一个 Java 虚拟机内部传输数据。
            
            虽然有更加有效率的方式来在线程之间传输数据,但是使用管道的好处在于封装性。
        
        Pipes 的另一个有用之处是可以用来辅助测试。
            一个单元测试框架可以将某个待测试的类连接到管道的“写”端并检查管道的“读”端出来的数据。
            
        管路所能承载的数据量是依赖实现的(implementation-dependent)。
            唯一可保证的是写到SinkChannel 中的字节都能按照同样的顺序在 SourceChannel 上重现。

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值