Java NIO(下)

只读缓冲区

只读缓冲区只能读取,不能写入。可以通过调用缓冲区的asReadOnlyBuffer()方法,将任何普通缓冲区转成只读缓冲区。这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据),只不过它是只读的。

注:不能将只读缓冲区转换成可写缓冲区。

直接和间接缓冲区

另一种有用的ByteBuffer是直接缓冲区。直接缓冲区是为了加快IO速度,而用一种特殊的方式分配其内存的缓冲区。Sun 的文档是这样描述直接缓冲区的:

给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。

使用下面的语句就可以创建一个直接缓冲区:

ByteBuffer buffer = ByteBuffer.allocateDirect( 1024 );
内存映射文件IO

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。

内存映射文件 I/O 是通过使文件中的数据神奇般地出现为内存数组的内容来完成的。这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会送入(或者 映射 )到内存中。

内存映射并不真的神奇或者多么不寻常。现代操作系统一般根据需要将文件的部分映射为内存的部分,从而实现文件系统。Java 内存映射机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。

尽管创建内存映射文件相当简单,但是向它写入可能是危险的。仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。

将文件映射到内存

了解内存映射的最好方法是使用例子。在下面的例子中,我们要将一个 FileChannel (它的全部或者部分)映射到内存中。为此我们将使用 FileChannel.map() 方法。下面代码行将文件的前 1024 个字节映射到内存中:

MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 0, 1024 );

map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,你可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行行映射。

分散和聚集(Scatter和Gather)

分散/聚集 I/O 是使用多个而不是单个缓冲区来保存数据的读写方法。

一个分散的读取就像一个常规通道读取,只不过它是将数据读到一个缓冲区数组中而不是读到单个缓冲区中。同样地,一个聚集写入是向缓冲区数组而不是向单个缓冲区写入数据。

分散/聚集 I/O 对于将数据流划分为单独的部分很有用,这有助于实现复杂的数据格式。

分散读取

通道可以有选择地实现两个新的接口: ScatteringByteChannel 和 GatheringByteChannel。一个 ScatteringByteChannel 是一个具有两个附加读方法的通道:

long read( ByteBuffer[] dsts );
long read( ByteBuffer[] dsts, int offset, int length );
这些 long read() 方法很像标准的 read 方法,只不过它们不是取单个缓冲区而是取一个缓冲区数组。

在 分散读取 中,通道依次填充每个缓冲区。填满一个缓冲区后,它就开始填充下一个。在某种意义上,缓冲区数组就像一个大缓冲区。因为分散读取在移动到下一个buffer前,必须先填满当前buffer,所以它不适用于动态消息(消息大小不固定的消息)。也就是说,如果存在消息头和消息体,消息头必须完成填充,分散读取才能正常工作

分散/聚集的应用

分散/聚集 I/O 对于将数据划分为几个部分很有用。例如,您可能在编写一个使用消息对象的网络应用程序,每一个消息被划分为固定长度的头部和固定长度的正文。您可以创建一个刚好可以容纳头部的缓冲区和另一个刚好可以容纳正文的缓冲区。当你将它们放入一个数组中并使用分散读取来向它们读入消息时,头部和正文将整齐地划分到这两个缓冲区中。
我们从缓冲区所得到的方便性对于缓冲区数组同样有效。因为每一个缓冲区都跟踪自己还可以接受多少数据,所以分散读取会自动找到有空间接受数据的第一个缓冲区。在这个缓冲区填满后,它就会移动到下一个缓冲区。

聚集写入

聚集写入 类似于分散读取,只不过是用来写入。它也有接受缓冲区数组的方法:

long write( ByteBuffer[] srcs );
long write( ByteBuffer[] srcs, int offset, int length );
聚集写对于把一组单独的缓冲区中组成单个数据流很有用。为了与上面的消息示例保持一致,你可以使用聚集写入来自动将网络消息的各个部分组装为单个数据流,以便跨越网络传输消息。

在下面的例子 中可以看到分散读取和聚集写入的实际应用。

public class UseScatterGather {
    static private final int firstHeaderLength = 2;
    static private final int secondHeaderLength = 4;
    static private final int bodyLength = 6;

    static public void main(String args[]) throws Exception {

        int port = 8000;

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress address = new InetSocketAddress(port);
        serverSocketChannel.socket().bind(address);

        int messageLength = firstHeaderLength + secondHeaderLength + bodyLength;

        ByteBuffer buffers[] = new ByteBuffer[3];
        buffers[0] = ByteBuffer.allocate(firstHeaderLength);
        buffers[1] = ByteBuffer.allocate(secondHeaderLength);
        buffers[2] = ByteBuffer.allocate(bodyLength);

        SocketChannel sc = serverSocketChannel.accept();

        while (true) {
            // Scatter-read into buffers
            int bytesRead = 0;
            while (bytesRead < messageLength) {
                long read = sc.read(buffers);
                bytesRead += read;

                System.out.println("read " + read);
                for (int i = 0; i < buffers.length; ++i) {
                    ByteBuffer bb = buffers[i];
                    System.out.println("buffer " + i + " " + bb.position() + " " + bb.limit());
                }
            }

            // Process message here
            // Flip buffers
            for (int i = 0; i < buffers.length; ++i) {
                ByteBuffer bb = buffers[i];
                bb.flip();
            }

            // Scatter-write back out
            long bytesWritten = 0;
            while (bytesWritten < messageLength) {
                long r = sc.write(buffers);
                bytesWritten += r;
            }

            // Clear buffers
            for (int i = 0; i < buffers.length; ++i) {
                ByteBuffer bb = buffers[i];
                bb.clear();
            }

            System.out.println(bytesRead + " " + bytesWritten + " " + messageLength);
        }
    }
}
文件锁定

文件锁定指的并不是阻止程序或用户访问特定文件。而是像常规java对象锁一样,用于允许系统的不同部分相互协调。

锁定文件

要获取文件的一部分上的锁,需要调用一个打开的FileChannel上的lock()方法。如果要获取一个排他锁,必须以写的方式打开文件:

RandomAccessFile raf = new RandomAccessFile( "testfilelocks.txt", "rw" );
FileChannel fc = raf.getChannel();
FileLock lock = fc.lock( start, end, false );
获取锁后,执行你想执行的操作,然后释放锁:

lock.release();
释放锁后,尝试获取锁的任何程序都有机会获得它。

连网和异步IO

异步IO是一种没有阻塞的读写数据的方法。通常,在代码进行read()调用时,代码会阻止直到有可供读取的数据。同样,write()调用会阻塞直到数据能够写入。而异步IO调用不会阻塞。相反的,你将注册对特定IO事件的兴趣-可读的数据的到达、新的套接字连接等。

异步IO的一个优势是,它允许你同时根据大量的输入和输出执行IO。同步程序需要求助于轮询,或者创建许多的线程以处理大量连接。使用异步IO,你可以监听任意数量的通道上的事件,不需要轮询,也不用额外的线程。

selector

异步IO的核心对象名为selector。selector就是你注册对各种IO事件感兴趣的地方,当那些事件发生时,该对象会告诉你发生的事件。首先,需要创建一个selector:

Selector selector = Selector.open();
然后,我们需要对不同的通道对象调用register方法,以便注册我们对这些对象中发生的IO事件中的哪些感兴趣。
1)打开一个ServerSocketChannel

为了接收连接,我们得打开一个ServerSocketChannel。我们要监听的每个端口都需要有一个ServerSocketChannel。对于每一个端口,我们打开一个ServerSocketChannnel,如下:

ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking( false );

ServerSocket ss = ssc.socket();
InetSocketAddress address = new InetSocketAddress( ports[i] );
ss.bind( address );
第二行将通道设置为非阻塞的。必须要对每个要使用的通道调用该方法,否则异步IO就不能正常工作。

2)选择键

将打开的通道注册到selector上。使用register()方法。如下:

SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );
register()方法的第一个参数总是selector。第二个参数是SelectionKey.OP_ACCEPT,就是我们要监听accpet事件。也就是在新连接建立时发生的事件。这是适用于ServerSocketChannel的唯一事件类型。

3)内部循环

int num = selector.select();

Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();

while (it.hasNext()) {
     SelectionKey key = (SelectionKey)it.next();
     // ... deal with I/O event ...
}
首先我们调用selector的select()方法。该方法会阻塞,直到至少有一个已注册的事件发生。当一个或更多事件发生时,select()返回所发生事件的数量。

selector的selectedKeys()方法返回了事件SelectionKey对象的一个集合。通过迭代SelectionKeys并以此处理每个SelectionKey来处理事件。

使用readyOps()方法,并检查发生了什么类型的事件。

if ((key.readyOps() & SelectionKey.OP_ACCEPT)
     == SelectionKey.OP_ACCEPT) {

     // Accept the new connection
     // ...
}

readyOps()方法高速我们该事件是新连接。

接受新的连接

因为我们知道这个服务器套接字上有一个传入连接在等待,所以可以安全地接受它;也就是说,不用担心 accept() 操作会阻塞:

ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();
下一步是将新连接的 SocketChannel 配置为非阻塞的。而且由于接受这个连接的目的是为了读取来自套接字的数据,所以我们还必须将 SocketChannel 注册到 Selector上,如下所示:
sc.configureBlocking( false );
SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ );
注意我们使用 register() 的 OP_READ 参数,将 SocketChannel 注册用于 读取 而不是 接受 新连接。

删除处理过的 SelectionKey

在处理 SelectionKey 之后,我们几乎可以返回主循环了。但是我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活的键出现,这会导致我们尝试再次处理它。我们调用迭代器的 remove() 方法来删除处理过的 SelectionKey:

it.remove();
现在我们可以返回主循环并接受从一个套接字中传入的数据(或者一个传入的 I/O 事件)了。

传入的 I/O

当来自一个套接字的数据到达时,它会触发一个 I/O 事件。这会导致在主循环中调用 Selector.select(),并返回一个或者多个 I/O 事件。这一次, SelectionKey 将被标记为 OP_READ 事件,如下所示:

} else if ((key.readyOps() & SelectionKey.OP_READ)
     == SelectionKey.OP_READ) {
     // Read the data
     SocketChannel sc = (SocketChannel)key.channel();
     // ...
}
与以前一样,我们取得发生 I/O 事件的通道并处理它。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值