1.1 NIO基础

来源:互联网 发布:传奇霸业宝石升级数据 编辑:程序博客网 时间:2024/05/16 15:47

 

一、缓冲区

 

传统的流和通道的对比

通道

处理简单

处理复杂

单字节的传输

一块数据的传输

-

Java.io.*已经重新写过

-

是对流的模拟

单向的

双向的

可直接访问

必须通过Buffer和通道打交道

 

1.1只读缓冲区

只读缓冲区非常简单您可以读取它们,但是不能向它们写入。可以通过调用缓冲区的 asReadOnlyBuffer() 方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据),只不过它是只读的。

只读缓冲区对于保护数据很有用。在将缓冲区传递给某个对象的方法时,您无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以 保证 该缓冲区不会被修改。

不能将只读的缓冲区转换为可写的缓冲区。

 

1.2直接和间接缓冲区

 

另一种有用的 ByteBuffer 是直接缓冲区。 直接缓冲区 是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区。

实际上,直接缓冲区的准确定义是与实现相关的。Sun 的文档是这样描述直接缓冲区的:

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

您可以在例子程序 FastCopyFile.java 中看到直接缓冲区的实际应用,这个程序是 CopyFile.java 的另一个版本,它使用了直接缓冲区以提高速度。

还可以用内存映射文件创建直接缓冲区。

 

import java.io.*;

import java.nio.*;

import java.nio.channels.*;

 

public class FastCopyFile {

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

        if (args.length < 2) {

            System.err.println("Usage: java FastCopyFile infile outfile");

            System.exit(1);

        }

 

        String infile = args[0];

        String outfile = args[1];

 

        FileInputStream fin = new FileInputStream(infile);

        FileOutputStream fout = new FileOutputStream(outfile);

 

        FileChannel fcin = fin.getChannel();

        FileChannel fcout = fout.getChannel();

 

        ByteBuffer buffer=ByteBuffer.allocateDirect(1024);//直接缓冲区

 

        while (true) {

            buffer.clear();

 

            int r = fcin.read(buffer);

 

            if (r == -1) {

                break;

            }

 

            buffer.flip();

 

            fcout.write(buffer);

        }

    }

}

 

1.3内存映射文件 I/O 

 

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

 

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

 

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

 

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

 

1.4将文件映射到内存

 

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

 

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

 

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

 

1.5分散/聚集 I/O

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

 

long read( ByteBuffer[] dsts );

long read( ByteBuffer[] dsts, int offset, int length );

 

这些 long read() 方法很像标准的 read 方法,只不过它们不是取单个缓冲区而是取一个缓冲区数组。

 

在 分散读取中,通道依次填充每个缓冲区。填满一个缓冲区后,它就开始填充下一个。在某种意义上,缓冲区数组就像一个大缓冲区。

 

1.6分散/聚集的应用

 

分散/聚集 I/O 对于将数据划分为几个部分很有用。例如,您可能在编写一个使用消息对象的网络应用程序,每一个消息被划分为固定长度的头部和固定长度的正文。您可以创建一个刚好可以容纳头部的缓冲区和另一个刚好可以容难正文的缓冲区。当您将它们放入一个数组中并使用分散读取来向它们读入消息时,头部和正文将整齐地划分到这两个缓冲区中。

 

我们从缓冲区所得到的方便性对于缓冲区数组同样有效。因为每一个缓冲区都跟踪自己还可以接受多少数据,所以分散读取会自动找到有空间接受数据的第一个缓冲区。在这个缓冲区填满后,它就会移动到下一个缓冲区。

 

import java.io.*;

import java.net.*;

import java.nio.*;

import java.nio.channels.*;

 

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 {

       if (args.length != 1) {

           System.err.println("Usage: java UseScatterGather port");

           // System.exit( 1 );

       }

 

       // int port = Integer.parseInt( args[0] );

       int port = 58585;

 

       ServerSocketChannel ssc = ServerSocketChannel.open();

       InetSocketAddress address = new InetSocketAddress(port);

       ssc.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 = ssc.accept();

 

       while (true) {

 

           // Scatter-read into buffers

           int bytesRead = 0;

           while (bytesRead < messageLength) {

              long r = sc.read(buffers);

              bytesRead += r;

 

              System.out.println("r " + r);

              for (int i = 0; i < buffers.length; ++i) {

                  ByteBuffer bb = buffers[i];

                  System.out.println("b " + 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);

       }

    }

}

 

 

二、文件锁

2.1概述——文件锁

 

文件锁定初看起来可能让人迷惑。它 似乎 指的是防止程序或者用户访问特定文件。事实上,文件锁就像常规的 Java 对象锁 — 它们是 劝告式的(advisory) 锁。它们不阻止任何形式的数据访问,相反,它们通过锁的共享和获取赖允许系统的不同部分相互协调。

 

您可以锁定整个文件或者文件的一部分。如果您获取一个排它锁,那么其他人就不能获得同一个文件或者文件的一部分上的锁。如果您获得一个共享锁,那么其他人可以获得同一个文件或者文件一部分上的共享锁,但是不能获得排它锁。文件锁定并不总是出于保护数据的目的。例如,您可能临时锁定一个文件以保证特定的写操作成为原子的,而不会有其他程序的干扰。

 

大多数操作系统提供了文件系统锁,但是它们并不都是采用同样的方式。有些实现提供了共享锁,而另一些仅提供了排它锁。事实上,有些实现使得文件的锁定部分不可访问,尽管大多数实现不是这样的。

 

在本节中,您将学习如何在 NIO 中执行简单的文件锁过程,我们还将探讨一些保证被锁定的文件尽可能可移植的方法。

 

2.2文件锁定和可移植性

 

文件锁定可能是一个复杂的操作,特别是考虑到不同的操作系统是以不同的方式实现锁这一事实。下面的指导原则将帮助您尽可能保持代码的可移植性:

只使用排它锁。

将所有的锁视为劝告式的(advisory)。

 

 

三、联网和异步I/O

 

3.1概述

连网是学习异步 I/O 的很好基础,而异步 I/O 对于在 Java 语言中执行任何输入/输出过程的人来说,无疑都是必须具备的知识。NIO 中的连网与 NIO 中的其他任何操作没有什么不同 — 它依赖通道和缓冲区,而您通常使用 InputStream OutputStream 来获得通道。

 

本节首先介绍异步 I/O 的基础 — 它是什么以及它不是什么,然后转向更实用的、程序性的例子。

 

3.2异步 I/O

异步 I/O 是一种 没有阻塞地 读写数据的方法。通常,在代码进行 read() 调用时,代码会阻塞直至有可供读取的数据。同样, write() 调用将会阻塞直至数据能够写入。

 

另一方面,异步 I/O 调用不会阻塞。相反,您将注册对特定 I/O 事件的兴趣 — 可读的数据的到达、新的套接字连接,等等,而在发生这样的事件时,系统将会告诉您。

 

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

 

我们将通过研究一个名为 MultiPortEcho.java 的例子程序来查看异步 I/O 的实际应用。这个程序就像传统的 echo server,它接受网络连接并向它们回响它们可能发送的数据。不过它有一个附加的特性,就是它能同时监听多个端口,并处理来自所有这些端口的连接。并且它只在单个线程中完成所有这些工作。

 

3.3 Selectors

 

本节的阐述对应于 MultiPortEcho 的源代码中的 go() 方法的实现,因此应该看一下源代码,以便对所发生的事情有个更全面的了解。

异步 I/O 中的核心对象名为 SelectorSelector 就是您注册对各种 I/O 事件的兴趣的地方,而且当那些事件发生时,就是这个对象告诉您所发生的事件。

所以,我们需要做的第一件事就是创建一个 Selector

 

Selector selector = Selector.open();

 

然后,我们将对不同的通道对象调用 register() 方法,以便注册我们对这些对象中发生的 I/O 事件的兴趣。register() 的第一个参数总是这个 Selector

 

 

3.4打开一个 ServerSocketChannel

 

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

 

ServerSocketChannel ssc = ServerSocketChannel.open();

ssc.configureBlocking( false );

 

ServerSocket ss = ssc.socket();

InetSocketAddress address = new InetSocketAddress( ports[i] );

ss.bind( address );

 

第一行创建一个新的 ServerSocketChannel ,最后三行将它绑定到给定的端口。第二行将 ServerSocketChannel 设置为 非阻塞的 。我们必须对每一个要使用的套接字通道调用这个方法,否则异步 I/O 就不能工作。

 

3.5选择键

 

下一步是将新打开的 ServerSocketChannels 注册到 Selector上。为此我们使用 ServerSocketChannel.register() 方法,如下所示:

 

SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );

 

register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT,这里它指定我们想要监听 accept 事件,也就是在新的连接建立时所发生的事件。这是适用于 ServerSocketChannel 的唯一事件类型。

 

请注意对 register() 的调用的返回值。 SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。

 

 

 

3.6内部循环

 

现在已经注册了我们对一些 I/O 事件的兴趣,下面将进入主循环。使用 Selectors 的几乎每个程序都像下面这样使用内部循环:

 

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 来处理事件。对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象

 

3.7监听新连接

 

程序执行到这里,我们仅注册了 ServerSocketChannel,并且仅注册它们“接收”事件。为确认这一点,我们对 SelectionKey 调用 readyOps() 方法,并检查发生了什么类型的事件:

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

       == SelectionKey.OP_ACCEPT) {

         // Accept the new connection

        // ...

}

可以肯定地说, readOps() 方法告诉我们该事件是新的连接。

 

3.8接受新的连接

因为我们知道这个服务器套接字上有一个传入连接在等待,所以可以安全地接受它;也就是说,不用担心 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 注册用于 读取 而不是接受 新连接。

 

3.9传入的 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 事件的通道并处理它。在本例中,由于这是一个 echo server,我们只希望从套接字中读取数据并马上将它发送回去。关于这个过程的细节,请参见 参考资料 中的源代码 (MultiPortEcho.java)

 

3.10回到主循环

 

每次返回主循环,我们都要调用 select Selector()方法,并取得一组 SelectionKey。每个键代表一个 I/O 事件。我们处理事件,从选定的键集中删除 SelectionKey,然后返回主循环的顶部。

 

这个程序有点过于简单,因为它的目的只是展示异步 I/O 所涉及的技术。在现实的应用程序中,您需要通过将通道从 Selector 中删除来处理关闭的通道。而且您可能要使用多个线程。这个程序可以仅使用一个线程,因为它只是一个演示,但是在现实场景中,创建一个线程池来负责 I/O 事件处理中的耗时部分会更有意义。

 

3.11

import java.io.*;

import java.net.*;

import java.nio.*;

import java.nio.channels.*;

import java.util.*;

 

public class MultiPortEcho {

    private int ports[];

    private ByteBuffer echoBuffer = ByteBuffer.allocate(1024);

 

    public MultiPortEcho(int ports[]) throws IOException {

        this.ports = ports;

 

        go();

    }

 

    private void go() throws IOException {

        // Create a new selector

        Selector selector = Selector.open();

 

        // Open a listener on each port, and register each one

        // with the selector

        for (int i = 0; i < ports.length; ++i) {

            ServerSocketChannel ssc = ServerSocketChannel.open();

            ssc.configureBlocking(false);

            ServerSocket ss = ssc.socket();

            InetSocketAddress address = new InetSocketAddress(ports[i]);

            ss.bind(address);

 

            SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT);

 

            System.out.println("Going to listen on " + ports[i]);

        }

 

        while (true) {

            int num = selector.select();

 

            Set selectedKeys = selector.selectedKeys();

            Iterator it = selectedKeys.iterator();

 

            while (it.hasNext()) {

                SelectionKey key = (SelectionKey) it.next();

 

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

                    // Accept the new connection

                    ServerSocketChannel ssc = (ServerSocketChannel) key

                            .channel();

                    SocketChannel sc = ssc.accept();

                    sc.configureBlocking(false);

 

                    // Add the new connection to the selector

                    SelectionKey newKey = sc.register(selector,

                            SelectionKey.OP_READ);

                    it.remove();

 

                    System.out.println("Got connection from " + sc);

                } else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {

                    // Read the data

                    SocketChannel sc = (SocketChannel) key.channel();

 

                    // Echo data

                    int bytesEchoed = 0;

                    while (true) {

                        echoBuffer.clear();

 

                        int r = sc.read(echoBuffer);

 

                        if (r <= 0) {

                            break;

                        }

 

                        echoBuffer.flip();

 

                        sc.write(echoBuffer);

                        bytesEchoed += r;

                    }

 

                    System.out.println("Echoed " + bytesEchoed + " from " + sc);

 

                    it.remove();

                }

 

            }

 

            // System.out.println( "going to clear" );

            // selectedKeys.clear();

            // System.out.println( "cleared" );

        }

    }

 

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

        if (args.length <= 0) {

            System.err

                    .println("Usage: java MultiPortEcho port [port port ...]");

            //System.exit(1);

        }

 

        //int ports[] = new int[args.length];

        int ports[] = {18081,18082,18083};

        for (int i = 0; i < args.length; ++i) {

            ports[i] = Integer.parseInt(args[i]);

        }

 

        new MultiPortEcho(ports);

    }

}

 

四、编码解码

 

 

41编码/解码

要读和写文本,我们要分别使用 CharsetDecoder CharsetEncoder。将它们称为 编码器 和 解码器 是有道理的。一个字符 不再表示一个特定的位模式,而是表示字符系统中的一个实体。因此,由某个实际的位模式表示的字符必须以某种特定的 编码 来表示。

 

CharsetDecoder 用于将逐位表示的一串字符转换为具体的 char 值。同样,一个 CharsetEncoder 用于将字符转换回位。

 

在下一个小节中,我们将考察一个使用这些对象来读写数据的程序。

 

4.2处理文本的正确方式

 

现在我们将分析这个例子程序 UseCharsets.java。这个程序非常简单 — 它从一个文件中读取一些文本,并将该文本写入另一个文件。但是它把该数据当作文本数据,并使用 CharBuffer 来将该数句读入一个 CharsetDecoder 中。同样,它使用 CharsetEncoder 来写回该数据。

 

我们将假设字符以 ISO-8859-1(Latin1) 字符集(这是 ASCII 的标准扩展)的形式储存在磁盘上。尽管我们必须为使用 Unicode 做好准备,但是也必须认识到不同的文件是以不同的格式储存的,而 ASCII 无疑是非常普遍的一种格式。事实上,每种 Java 实现都要求对以下字符编码提供完全的支持:

 

US-ASCII

ISO-8859-1

UTF-8

UTF-16BE

UTF-16LE

UTF-16

4.3

import java.io.*;

import java.nio.*;

import java.nio.channels.*;

import java.nio.charset.*;

 

public class UseCharsets {

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

        String inputFile = "e:/test/111.txt";

        String outputFile = "e:/test/222.txt";

 

        RandomAccessFile inf = new RandomAccessFile(inputFile, "r");

        RandomAccessFile outf = new RandomAccessFile(outputFile, "rw");

        long inputLength = new File(inputFile).length();

 

        FileChannel inc = inf.getChannel();

        FileChannel outc = outf.getChannel();

 

        MappedByteBuffer inputData = inc.map(FileChannel.MapMode.READ_ONLY, 0,

                inputLength);

 

        Charset latin1 = Charset.forName("ISO-8859-1");

        CharsetDecoder decoder = latin1.newDecoder();

        CharsetEncoder encoder = latin1.newEncoder();

 

        CharBuffer cb = decoder.decode(inputData);

 

        // Process char data here

 

        ByteBuffer outputData = encoder.encode(cb);

 

        outc.write(outputData);

 

        inf.close();

        outf.close();

    }

}

原创粉丝点击