`
cloud21
  • 浏览: 397536 次
  • 性别: Icon_minigender_2
  • 来自: 北京
社区版块
存档分类
最新评论

J2SE1.4的I/O新特性

阅读更多
这是一篇比较使用的I/O新特性的介绍文章。文中使用了大量的代码实例来演示和解说如何使用J2SE1.4的新I/O特性并提供你应用程序的性能,而且提供了两个完整的例子,其中包括一个循环WEB服务器的雏形,非常值得我们参考。

回溯到2000年的1月,当人们正在争论着公元2000年究竟是一个世纪的开始还是一个实际的结束的时候,一份新的JAVA规范——JSR(Java Specification Request)51也被审核通过了。这份JSR的名字是《New I/O APIs for the Java Platform》(JAVA平台的新I/O API)。许多人认为这份新的规范只会给大家带来非阻塞I/O操作的能力,但是在JSDK1.4Beta(JavaTM 2 Platform, Standard Edition)中引入的新的特性,却还包含其它的一些新而有趣的特征。新的API在提供了可升级的套接口(socket)和文件I/O操作的同时(这是理所当然的),你也可以找到一个正则表达的包来支持模式匹配,以及对字符集转换的编码器和解码器,和优化过的文件系统支持如文件锁定、内存映射等功能。我们在这篇文章中的讨论会全面覆盖上面所说的四个新特性。注意:JAVA本地接口(JNI)为新的I/O操作所做的修改我们将不会涉及,如果你需要了解有关的内容,请参考本文结尾“资源”部分的有关内容。

Buffers

按照从最简单到最复杂的习惯,我们将从java.nio包中的一系列Buffer类开始说起。Buffer提供了一种在内存容器中保存一系列原始数据的机制。基本上,你可以设想一下,把DataInputStream/DataOutputStream组合在一起封装成一个固定字节大小的数组而只允许读写一种数据类型,例如char,int,或者double。在这个包里,总共有7种这样的Buffer可用:

· ByteBuffer · CharBuffer · DoubleBuffer · FloatBuffer · IntBuffer · LongBuffer · ShortBuffer

实际上,ByteBuffer也能够对其它六种类型进行读写,但是这些特别的Buffer更有针对性,更专门化一些。为了示范如何使用一个Buffer,接下来这一小片代码将完成一个从String型变量到一个CharBuffer的转换,并从这个Buffer中逐一的读出单个字符。你可以用warp方法来完成转换,用get方法来取一个字符。

CharBuffer buff = CharBuffer.wrap(args[0]);
for (int i=0, n=buff.length(); i<n; i++)
{System.out.println(buff.get());}

在使用Buffer的时候,一定要注意它目前的大小(sizing)和位置(positioning)的值是有区别的,千万不要混淆了。方法length是不规范的,尤其是对于CharBuffer而言。当然这并非是出了什么错,而是它返回的是Buffer中的剩余长度的值,所以如果position并非在Buffer的开始处的话,返回值将不是Buffer的长度,而是在Buffer中剩余的字符的长度。换句话说,上面程序中的循环也可以修改成这样:

CharBuffer buff = CharBuffer.wrap(args[0]);
for (int i=0; buff.length() 0; i++)
{System.out.println(buff.get());}

我们回到正题,继续讨论大小(sizing)和位置(positioning)的关系,在这里,有四个概念必须明确,它们是mark(标记),position(位置),limit(限制),和capacity(容量)。· mark——用mark方法设置的可设位置,mark方法可以使用reset来重置position,<=position,=0;· position——在Buffer中目前读写的位置,<=limit;· limit——第一个不应该被读取的元素的位置的index(索引号),<=cpacity;· capcity——Buffer的大小,=size。Position(位置)属性值是我们在对一个Buffer读取或者写入的时候需要时刻牢记的信息。例如,如果你想读取你刚刚写入的字符,你不许把position移动到你想读取的位置,否则,你将越过limit的限制,而读到一个不知道是什么的字符。这时候你需要立刻使用flip方法,把limit移动到当前的位置,并把position移动到0位置。你也可以回绕一个buffer来保持当前的limit位置,而把position返回到0位置。举个例子,如果从下面这一小段代码中的flip调用去掉,将返回一个空白,因为在buffer中还什么都没有。

buff.put('a');
buff.flip();
buff.get();

上面的封装机制是一个非直接缓冲(non-direct buffer)的例子。非直接缓冲也可以通过allocate方法来创建和限定大小,本质上来说,只是把数据封装到一个数组里了。如果愿意消耗稍微多一点的创建资源,你也可以通过allocateDirect方法开辟一块连续的内存来保存数据,这也可以称作直接缓冲。直接缓冲是依赖于系统的本地接口的I/O操作来优化存取操作的。

文件映射

MappedByteBuffer是一个专门用于直接缓冲的ByteBuffer,这个类用字节缓冲来映射一个文件。想要映射一个文件到MappedByteBuffer,你必须先取得这个文件的通道(channel)。通道是某种已建立的连接,例如管道(pipe),套接口(socket),或者文件(file)等能够完成I/O操作的对象。如果是文件通道,你可以通过FileInputStream(文件输入流),FileOutputStream(文件输出流)或者RandomAccessFile(随机存取文件)的getChannel方法来获得一个通道。一旦你取得这个通道,你就可以通过它的map方法,指明映射模式来把你想映射的那一部分文件映射到缓冲中去。文件通道可以使用FileChannel.MapMode的任一个常数打开:只读(READ_ONLY),私有/写时拷贝(PRIVATE),或者读写(READ_WRITE)。下面是一个从文件中创建只读的MappedByteBuffer的基本例程:

String filename = ...;
FileInputStream input = new FileInputStream(filename);
FileChannel channel = input.getChannel();
int fileLength = (int)channel.size();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, fileLength);

你可以在java.nio.channels包里找到与通道有关的类。一旦MappedByteBuffer被建立了,你就可以象存取其它任何ByteBuffer一样来操作它。当然在这个例子里它是只读的,所以加入你试图写入一些东西的时候,它会抛出一个NonWritableChannelException的异常。假如你想把它当作字符来处理的话,你必须制定一个字符集把ByteBuffer转化成CharBuffer。这个字符集是在Charset类中定义的。然后你用CharsetDecoder类对文件的内容进行解码。它相反的操作是由CharsetEncoder类来完成的。

// ISO-8859-1 是ISO拉丁字符表#1Charset
charset = Charset.forName("ISO-8859-1");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(buffer);

这个类可以在java.nio.charset包中找到。

正则表达式

一旦你完成了从文件到CharBuffer的可输入映射,你就可以对文件内容进行模式匹配。就像我们分别使用grep命令和wc命令来进行正则表达的匹配和单词计数一样。其中使用到了java.util.regex中的Pattern和Matcher两个类。Pattern类为匹配正则表达提供了所有的构造类型。一般来说,你的模式表达是一个字符串,可以查阅类文档得到模式的完整细节,这里只提供一些简单常用的例子:· 行模式,任意个字符然后以回车换行借宿并且/或者行结束:.* ?或.*$ · 连续的数字:[0-9]* 或者 d*· 一个控制符:{cntrl}· 一个大写或者小写US-ASCII字符,接着一个空格,接着标点:[p{Lower}p{Upper}]sp{Punct}注:不幸的是,J2SE 1.4 beta3中打断了这一切,因为它对正则表达式所必须的字符缓冲的次序支持的非常不好。从SUN的Bug Parade可以看到这个问题的详细资料(希望你有JDC的帐号,呵呵,没有就快去申请啊,还愣着干什么?)。很遗憾,这意味着你不能用模式匹配同时去读取一个词或者一行。如果想获得更多的有关正则表达式库的信息,可以参考本文最后所列“资源”中的《Regular Expressions and the Java Programming Language》(正则表达和java编程语言)

套接口通道

下面我们要从文件通道转移到读写一个套接口连接的通道中来。这个通道可以用做阻塞模式,也可以用作非阻塞模式。如果是阻塞模式,取决于你的程序是服务器端还是客户端,只需把你的调用换成connect或者accept。而在非阻塞模式,它们的处理方式是不一样的。这些新类处理基本套接口的读写操作。在java.net包中的InetSocketAddress类指定连接地址,java.nio.channels包中的SocketChannel类来完成实际的读写操作。使用InetSocketAddress来进行连接非常类似于普通的Socket类的操作。你所要做的一切仅仅是提供主机和端口号:

String host = ...;
InetSocketAddress socketAddress = new InetSocketAddress(host, 80);

一旦你获得了InetSocketAddress,一切都改变了(怎么听着象童话^&^)。你可以打开一个SocketChannel来连接到InetSocketAddress,用它来取代我们以前从套接口的输入流来读取、向套接口的输出流写入的所有操作:

SocketChannel channel = SocketChannel.open();
channel.connect(socketAddress);

在连接完成之后,你立刻可以使用ByteBuffer对象对这个通道进行读写。例如,你可以把一个字符串使用CharsetEncoder封装到一个CharBuffer来发送一个http请求:

Charset charset = Charset.forName("ISO-8859-1");
CharsetEncoder encoder = charset.newEncoder();
String request = "GET / ";
channel.write(encoder.encode(CharBuffer.wrap(request)));

你可以从这个通道中读取请求的响应。由于这个http请求的响应是一个文本格式的,你需要使用CharsetDecoder来把响应转换到CharBuffer。你可以使用一个CharBuffer来完成这些操作,通过重用来避免在读取的时候产生不必要的多余连接:

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
CharBuffer charBuffer = CharBuffer.allocate(1024);
while ((channel.read(buffer)) != -1)
{buffer.flip();
decoder.decode(buffer, charBuffer, false);
charBuffer.flip();
System.out.println(charBuffer);
buffer.clear();charBuffer.clear();}

下面这个程序通过一个http请求来读取这个web站点的首页。你可以把这个输出保存成一个文件与用浏览器浏览该页的结果相对比。

import java.io.*;import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

public class ReadURL
{
public static void main(String args[]) {
String host = args[0];
SocketChannel channel = null;

try {

// SetupInetSocketAddress socketAddress = new InetSocketAddress(host, 80);
Charset charset = Charset.forName("ISO-8859-1");
CharsetDecoder decoder = charset.newDecoder();
CharsetEncoder encoder = charset.newEncoder();

// Allocate buffersByteBuffer buffer = ByteBuffer.allocateDirect(1024);
CharBuffer charBuffer = CharBuffer.allocate(1024);

// Connectchannel = SocketChannel.open();
channel.connect(socketAddress);

// Send requestString request = "GET / ";
channel.write(encoder.encode(CharBuffer.wrap(request)));

// Read response
while ((channel.read(buffer)) != -1) {
buffer.flip();
// Decode bufferdecoder.decode(buffer, charBuffer, false);
// DisplaycharBuffer.flip();
System.out.println(charBuffer);
buffer.clear();
charBuffer.clear();
}}
catch (UnknownHostException e)
{System.err.println(e);}
catch (IOException e) {System.err.println(e);}
finally {if (channel != null)
{try {channel.close();}
catch (IOException ignored) {}}}}}

非阻塞读取

现在,我们将要进入一个比较有趣的部分,也是人们在新的I/O包中最感兴趣的部分。怎么配置你的通道使它以非阻塞模式工作呢?基本的步骤是在打开通道后调用configureBlocking方法,输入false值。一旦你调用了connect方法,这个方法将马上返回。

String host = ...;
InetSocketAddress socketAddress = new InetSocketAddress(host, 80);
channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(socketAddress);

获得一个非阻塞的通道后,你需要考虑的是如何用这个通道做实际的工作。SocketChannel是一个SelectableChannel。这些可选择通道(selectable channels)通过一个Selector来工作。基本上,你把你的通道注册到一个Selector,然后告诉这个Selector你对那些事件有兴趣,它会在你有兴趣的事发生的时候提醒你的。要取得一个Selector的实例,只需调用这个类的静态方法open就可以了:

Selector selector = Selector.open();

通道的register方法可以完成到Selector类的注册。而你关心的事件名称则由SelectionKey类的各个字段来描述。对于SocketChannel类来说,有效的操作事件是OP_CONNECT,OP_READ 和 OP_WRITE。所以当你对连接和读取有兴趣的时候,可以如下进行注册:

channel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);

这时候,你就必须等待selector来通知你感兴趣的事件在你注册的通道上发生了。Selector的select方法将将阻塞到事件的发生。为了获取这个信息,你可以在它自己的线程里使用一个while (selector.select() 0)循环,当I/O操作执行的时候再跳出来做自己的事。当有事件发生的时候select方法会返回,返回值是事件发生的通道的序数,而并非事件本身。在你感兴趣的事发生之后,你不许计算出发生了什么事来对它进行响应。在我们这个例子中,我们已经向Select表示我们只对OP_CONNECT和OP_READ两项有兴趣,所以必然是二者之一。接着你所要做的是通过selectedKeys方法来取得一个已经就绪的对象集。在这个集合中的元素是一个SelectionKey,你可以检查它的isConnectable 和isReadable两个属性来得到它的状态。下面是这个循环的基本框架:

while (selector.select(500) 0) {
// Get set of ready objectsSet readyKeys = selector.selectedKeys();
Iterator readyItor = readyKeys.iterator();

// Walk through setwhile (readyItor else if (key}}

这里,remove方法的调用需要进行一下小小的解释。这个已经就绪的集合当你操作它的时候也会发生变化。所以,你应该在你操作这个元素的的时候,把它remove出来。这个移动操作并不会引发ConcurrentModificationException的异常的抛出。同时由于有一个超时设置,select方法的调用也不会在毫无事件发生的情况下僵死。同样也有一个调用来从集合中的键值获得相应的通道对象。你需要完成上述的所有操作。为了在例程中从HTTP连接中进行读取操作,你需要发送初始HTTP请求。基本上,一旦你知道这个连接完成了,你就发送一个GET请求来获得这个站点的根目录。当selector报告通道还是可连接的时候,就还没有完成连接。所以你需要经常的通过isConnectionPending方法检查这个连接是否未完成,并在它完成后调用finishConnect。而在连接完成之后,你就可对这个通道进行写入操作,但是必须使用ByteBuffer,而不是我们常见的I/O流了。下面是一段连接代码的示例:

// OUTSIDE WHILE LOOPCharsetcharset = Charset.forName("ISO-8859-1");
CharsetEncoder encoder = charset.newEncoder();

// INSIDE
if (channel.isConnectable())
// Finish connection
if (keyChannel

// Send requestString request = "GET / ";
keyChannel.write(encoder.encode(CharBuffer.wrap(request)));

从一个套接口通道中读取数据和从一个文件通道中读取是一样的。然后可能会引发一个异常,它更可能的情况是当从一个套接口读取数据的时候缓冲还没有被充满。在你准备好读取需要的数据的时候,这没什么大不了的。

// OUTSIDE WHILE LOOP
CharsetDecoder decoder = charset.newDecoder();
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
CharBuffer charBuffer = CharBuffer.allocate(1024);

// INSIDE
if (channel.isReadable())
// Read what's ready in responsekey
Channel.read(buffer);buffer.flip();

// Decode
bufferdecoder.decode(buffer, charBuffer, false);

// Display
charBuffer.flip();System.out.print(charBuffer);

// Clear for next
passbuffer.clear();charBuffer.clear();

加入必要的例外处理代码,你就可以使用你的套接口读取数据了。一定要在finally代码段中加入关闭通道的代码,这样即使在发生了例外的时候,也可以保证资源不被锁住。下面是完整的代码:

import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.*;

public class NonBlockingReadURL {
static Selector selector;

public static void main(String args[])
{String host = args[0];
SocketChannel channel = null;

try {

// SetupInet
SocketAddress socketAddress = new InetSocketAddress(host, 80);
Charset charset = Charset.forName("ISO-8859-1");
CharsetDecoder decoder = charset.newDecoder();
CharsetEncoder encoder = charset.newEncoder();

// Allocate buffers
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
CharBuffer charBuffer = CharBuffer.allocate(1024);

// Connect
channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(socketAddress);

// Open Selector
selector = Selector.open();

// Register interest in when connection
channel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);

// Wait for something of interest to happen
while (selector.select(500) 0) {
// Get set of ready objects
Set readyKeys = selector.selectedKeys();
Iterator readyItor = readyKeys.iterator();

// Walk through set
while (readyItor

// Send request
String request = "GET / ";keyChannel.write(encoder.encode(CharBuffer.wrap(request)));

} else if (key else {System.err.println("Ooops");}}}}
catch (UnknownHostException e) {System.err.println(e);}
catch (IOException e) {System.err.println(e);}
finally {if (channel != null)
{try {channel.close();}
catch (IOException ignored) {}}}
System.out.println();}}

非阻塞服务器

在最后这部分,我们使用NIO包实现一个Web服务器。通过新的I/O特性,你可以不必为每个连接都提供一个线程就可以实现WEB服务器的功能。当然你可以用线程来处理费时较长的任务,但是你所需要做的一切仅仅是调用select方法等待需要做的事的到来,而不在是让所有的线程都整装待命。这个服务器的基本设计思想是使用通道来包容已经通过调用bind方法绑定在InetSocketAddress上的ServerSocketChannel。

ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
InetSocketAddress isa = new InetSocketAddress(port);
channel.socket().bind(isa);

所有的步骤和客户端读取的准备差不多,除了这次你需要注册的是OP_ACCEPT键值,当selector提醒你事件到来的时候检查isAcceptable,并使用ServerSocketChannel代替SocketChannel外。非常简单!下面的例程正是向大家展示了它是如何的简单。这是一个基本的单线程的服务器,对每一个响应都发回一段固定的文字信息。使用telnet连接到9999端口可以看到它的响应:)

import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.util.*;

public class Server {private static int port = 9999;
public static void main(String args[]) throws Exception
{Selector selector = Selector.open();

ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
InetSocketAddress isa = new InetSocketAddress(port);
channel.socket().bind(isa);

// Register interest in when connection
channel.register(selector, SelectionKey.OP_ACCEPT);

// Wait for something of interest to happen
while (selector.select() 0) {
// Get set of ready
objectsSet readyKeys = selector.selectedKeys();
Iterator readyItor = readyKeys.iterator();

// Walk through set
while (readyItor
else {System.err.println("Ooops");}

}}// Never ends}}

在接受请求之后,你可以从套接口取得一个对应的通道,使它转到非阻塞模式,然后也注册到selector。这个结构框架提供了NIO类在制作一个WEB服务器上的基本用法。关于如何创建一个多线程的服务器的更多信息,可以参考在本文最后“资源”一节里JavaWorld的有关文章。

结论在J2SE 1.4Beta版本中引入的I/O新特性提供了令人振奋的新方法来增进你应用程序的性能。通过这些新特性的使用,你的应用程序不但能够变得更快,性能上也会得到攀升,因为你不必再为每一个连接新建一个线程。这对于服务器端来说更是尤其的重要,可以支持大幅度增长的同时连接的数目。注:如果你浏览过JSR51中的性能列表,你会注意到它提到了扫描和格式化输入输出的支持,和C语言中的printf非常相似。这个特性并没有被1.4Beta实现,将在下一个版本中引入。

分享到:
评论

相关推荐

    JAVA非阻塞NIO_IO2

    J2SE 1.4 版代号 Merlin,包含可观的 I/O 新特 性,对此我们将作详细论述。这些新的 I/O 特性主要包含在 java.nio 软件包及其子包中,并被命 名为 New I/O(NIO)。通过本书,您将学会如何使用这些令人兴奋的新特性...

    J2SE 1.3,1.4,1.5,1.6,1.7,1.8 API.zip

    2. J2SE 1.4 API: 这个版本引入了异常链、断言(assert)关键字、NIO(New I/O)框架、正则表达式支持、Swing的改进,以及对XML处理的加强,如SAX和DOM解析器。此外,它还增强了多线程处理和网络功能。 3. J2SE 1.5...

    J2SE各个版本帮助文档

    - **J2SE 1.4**:引入了异常链、断言、正则表达式和NIO(非阻塞I/O)等新特性。 - **J2SE 5.0**:重命名了版本号,从1.5开始,引入了泛型、枚举、可变参数、自动装箱/拆箱以及注解等语言特性。 - **Java SE 6**:...

    jdk 1.4 旧版 windows zip压缩包

    这个新API提供了基于通道(Channels)和缓冲区(Buffers)的I/O操作,提高了处理大量并发I/O操作的效率,特别是在网络编程和大数据处理场景下。 此外,异常处理得到了增强,增加了`finally`块中的`return`语句支持...

    Building Applications using J2EE 1.4.pdf

    这些知识点详细介绍了J2SE 1.4的新特性和改进之处,以及如何利用这些新特性来构建更高效、更可靠的应用程序。这对于开发者来说是非常宝贵的资源,可以帮助他们更好地理解和利用J2EE 1.4来构建高质量的企业级应用。

    旧版本JDK JDK1.4

    3. **NIO(非阻塞I/O)**:Java 1.4引入了`java.nio`包,提供了非阻塞I/O操作,提高了网络和文件系统I/O性能,尤其适合高并发场景。 4. **集合框架增强**:包括`java.util.concurrent`包的引入,提供了线程安全的...

    jdk1.4绿色免安装版

    2. **NIO(New I/O)**: 新的I/O API,也称为非阻塞I/O,位于`java.nio`包中,为高性能、低延迟的I/O操作提供了新的选择,特别适合网络编程和大文件处理。 3. **异常链(Exception Chaining)**: 异常链允许一个...

    发布急需的企业级应用的首选

    J2SE 1.4通过改进垃圾回收机制、优化JVM(Java虚拟机)和添加更高效的API,如NIO(非阻塞I/O),显著提高了运行效率。 其次,1.4版的规划性得到显著提升。开发人员现在可以利用新的设计模式和编程工具来更好地规划...

    java nio

    Java NIO,全称为Non-Blocking Input/Output,即非阻塞输入/输出,是Java在J2SE 1.4版本中引入的一种新的I/O模型,是对传统BIO(Blocking I/O)模型的一种有效补充。它提供了更高效的数据传输方式,尤其是在处理大量...

    Java NIO与IO的差别和比较

     J2SE1.4以上版本号中公布了全新的I/O类库。本文将通过一些实例来简介NIO库提供的一些新特性:非堵塞I/O,字符转换,缓冲以及通道。  一. 介绍NIO  NIO包(java.nio.*)引入了四个关键的抽象数据类型,它们...

    j2se教案

    NIO(New IO)是Java 1.4引入的扩展,提供了非阻塞I/O,选择器和通道等高级功能。 GUI编程在J2SE中通常通过JavaFX或Swing库实现。它们提供了丰富的组件,如按钮,文本框,滚动面板等,用于构建用户交互的图形界面。...

    J2SE进阶

    9. **NIO (New IO)**:NIO是Java 1.4引入的改进版I/O系统,提供了非阻塞I/O操作,适用于高并发场景。理解和使用Selector、Channel和Buffer等核心概念是进阶学习的一部分。 10. **并发工具类**:Java并发包(java....

    Java_NIO与IO的区别和比较.doc

    Java NIO(New Input/Output)是Java标准库在J2SE 1.4及后续版本中引入的一个重要特性,它的出现是为了改进传统的IO模型,提高I/O操作的性能和效率。NIO与传统的IO(Old IO)在设计模式、工作原理以及使用方式上存在...

    java异步socket调用[文].pdf

    Java 异步Socket调用是J2SE 1.4版本引入的重要特性,这一特性极大地改进了Java在网络编程中的性能和效率。在之前的版本中,Socket通信通常采用同步方式进行,这意味着输入输出操作必须等待完成才能进行下一步,这在...

    java NIO异步框架

    Java NIO(New IO)是Java 2平台标准版(J2SE 1.4)引入的一个新特性,它为Java应用程序提供了非阻塞I/O操作的能力,极大地提升了I/O性能,并降低了多线程编程的复杂性。NIO与传统的IO(-blocking I/O)相比,最大的...

Global site tag (gtag.js) - Google Analytics