服务器在合理的时间内处理大量客户端请求的能力取决于服务器使用I / O流的效率。 同时满足数百个客户端的服务器必须能够同时使用I / O服务。 在JDK 1.4(又名Merlin)之前,Java平台不支持非阻塞I / O调用。 由于线程与客户端的比例几乎为一比一,因此用Java语言编写的服务器容易受到巨大的线程开销,从而导致性能问题和可伸缩性不足。
为了解决此问题,最新版本的Java平台引入了一组新的类。 Merlin的java.nio包塞满了解决线程开销的技巧,其中最重要的是新的SelectableChannel
和Selector
类。 通道表示客户端和服务器之间的通信方式。 选择器类似于Windows消息循环,其中选择器捕获来自不同客户端的各种事件,并将它们分派给各自的事件处理程序。 在本文中,我们将向您展示这两个类如何共同发挥作用,从而为Java平台创建非阻塞I / O机制。
Merlin之前的I / O编程
我们将从一个基本的,在Merlin之前的服务器套接字程序开始。 在ServerSocket
类的生存ServerSocket
,重要功能如下:
- 接受传入的连接
- 阅读来自客户的请求
- 服务那些请求
让我们使用代码片段来说明每个步骤。 首先,我们创建一个新的ServerSocket
:
ServerSocket s = new ServerSocket();
接下来,我们要接受来电。 调用accept()
应该可以解决问题,但是您需要注意一些陷阱:
Socket conn = s.accept( );
在服务器套接字接受客户端连接请求之前,对accept()
的调用将一直阻塞。 建立连接后,服务器将使用LineNumberReader
读取客户端请求。 由于LineNumberReader
会分块读取数据,直到缓冲区已满,因此调用将在读取时阻塞。 下面的代码片段显示了LineNumberReader
的实际作用(块和全部)。
InputStream in = conn.getInputStream();
InputStreamReader rdr = new InputStreamReader(in);
LineNumberReader lnr = new LineNumberReader(rdr);
Request req = new Request();
while (!req.isComplete() )
{
String s = lnr.readLine();
req.addLine(s);
}
InputStream.read()
是读取数据的另一种方法。 不幸的是, read
也块,直到可用数据的方法,一样的write
方法。
图1描述了服务器的典型工作方式。 粗线表示阻止操作。
图1.运行中的典型服务器
![阻塞的1 / O图](https://i-blog.csdnimg.cn/blog_migrate/f0e0c511b5ef35c7b2dc403b88170a0c.gif)
在JDK 1.4之前,自由使用线程是解决阻塞的最典型方法。 但是此解决方案产生了自己的问题-即线程开销,这会影响性能和可伸缩性。 但是,随着Merlin和java.nio软件包的到来,一切都发生了变化。
在以下各节中,我们将研究java.nio的基础,然后将我们学到的一些知识用于修改上述的服务器套接字示例。
React堆模式
NIO设计背后的主要力量是React堆设计模式。 分布式系统中的服务器应用程序必须处理向其发送服务请求的多个客户端。 但是,在调用特定服务之前,服务器应用程序必须多路分解并将每个传入请求分派到其相应的服务提供者。 Reactor模式恰好可以实现此功能。 它允许事件驱动的应用程序对服务请求进行多路分解和分派,然后将服务请求从一个或多个客户端同时传递到应用程序。
在这方面,Reactor模式与Observer模式密切相关:当单个主题发生更改时,将通知所有从属。 观察者模式与单个事件源关联,但是,React堆模式与多个事件源关联。
请参阅相关主题 ,以了解更多关于React堆模式。
频道和选择器
NIO的非阻塞I / O机制围绕选择器和通道构建。 Channel
类表示服务器与客户端之间的通信机制。 与Reactor模式保持一致, Selector
类是Channel
的多路复用Selector
。 它对传入的客户端请求进行多路分解,并将其分派到各自的请求处理程序。
我们将仔细研究Channel
类和Selector
类的各自功能,以及两者如何共同创建一个非阻塞I / O实现。
渠道做什么
通道表示与诸如硬件设备,文件,网络套接字或程序组件之类的实体的开放连接,该实体能够执行一个或多个不同的I / O操作(例如,读取或写入)。 NIO通道可以异步关闭和中断。 因此,如果某个线程在某个通道的I / O操作中被阻止,则另一个线程可以关闭该通道。 同样,如果某个线程在通道的I / O操作中被阻塞,则另一个线程可以中断该阻塞的线程。
图2. java.nio.channels的类层次结构
![java.nio包的类层次结构图](https://i-blog.csdnimg.cn/blog_migrate/167488e6e3e6e00ad5e860bba27aea7f.gif)
如图2所示,java.nio.channels包中有很多通道接口。 我们主要关注java.nio.channels.SocketChannel
和java.nio.channels.ServerSocketChannel
接口。 这些通道可以分别视为java.net.Socket
和java.net.ServerSocket
替代。 通道可以在阻塞或非阻塞模式下使用,尽管我们当然会着重于在非阻塞模式下使用通道。
创建一个非阻塞通道
为了实现基本的非阻塞套接字读取和写入操作,我们要处理两个新类。 这些是java.net包中的InetSocketAddress
类,用于指定要连接的位置;而java.nio.channels包中的SocketChannel
类,用于执行实际的读写操作。
本节中的代码片段展示了一种修改后的,无阻塞的创建基本服务器套接字程序的方法。 请注意这些代码示例与第一个示例中使用的代码示例之间的更改,首先要添加两个新类:
String host = ......;
InetSocketAddress socketAddress = new InetSocketAddress(host, 80);
SocketChannel channel = SocketChannel.open();
channel.connect(socketAddress);
为了使通道成为非阻塞通道,我们在通道上调用configureBlockingMethod(false)
,如下所示:
channel.configureBlockingMethod(false);
在阻塞模式下,线程将在读取或写入时阻塞,直到操作完全完成。 如果在读取期间数据还没有完全到达套接字,则线程将阻塞读取操作,直到所有数据可用为止。
在非阻塞模式下,线程将读取可用数据量,然后返回执行其他任务。 如果将configureBlockingMethod()
传递为true,则该通道的行为将与阻塞Socket
上的读写完全相同。 上面提到的一个主要区别是,这些阻塞的读写可能会被其他线程中断。
仅Channel
不足以创建非阻塞I / O实现。 Channel
类必须与Selector
类一起使用才能实现非阻塞I / O。
选择器的作用
Selector
类在Reactor模式方案中扮演Reactor
的角色。 Selector
器在多个SelectableChannels
上复用事件。 每个Channel
向Selector
注册事件。 当事件从客户端到达时, Selector
对它们进行解复用,并将事件分派到相应的Channel
。
创建Selector
的最简单方法是使用open()
方法,如下所示:
Selector selector = Selector.open();
频道遇到选择器
必须服务客户请求的每个Channel
必须首先创建一个连接。 下面的代码创建一个名为Server
的ServerSocketChannel
并将其绑定到本地端口:
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
InetAddress ia = InetAddress.getLocalHost();
InetSocketAddress isa = new InetSocketAddress(ia, port );
serverChannel.socket().bind(isa);
每个必须处理客户请求的Channel
都必须接下来向Selector
注册自己。 Channel
应根据将处理的事件进行注册。 例如,应注册一个接受传入连接的Channel
,如下所示:
SelectionKey acceptKey =
channel.register( selector,SelectionKey.OP_ACCEPT);
Channel
在Selector
的注册由SelectionKey
对象表示。 Key
在满足以下三个条件之一之前一直有效:
-
Channel
已关闭。 -
Selector
已关闭。 - 通过调用自己的
cancel()
方法可以取消Key
本身。
Selector
阻止select()
调用。 然后,它等待直到建立了新的连接,另一个线程将其唤醒,或者另一个线程中断了原始被阻塞的线程。
注册服务器
Server
是ServerSocketChannel
,它向Selector
注册以接受所有传入的连接,如下所示:
SelectionKey acceptKey = serverChannel.register(sel, SelectionKey.OP_ACCEPT);
while (acceptKey.selector().select() > 0 ){
......
Server
注册后,我们将遍历一组密钥并根据其类型处理每个密钥。 处理密钥后,将其从就绪密钥列表中删除,如下所示:
Set readyKeys = sel.selectedKeys();
Iterator it = readyKeys.iterator();
while (it.hasNext())
{
SelectionKey key = (SelectionKey)it.next();
it.remove();
....
....
....
}
如果密钥是可接受的,则接受连接,并为进一步的事件(例如读取或写入操作)注册通道。 如果密钥是可读或可写的,则服务器指示已准备好在其末端读取或写入数据:
SocketChannel socket;
if (key.isAcceptable()) {
System.out.println("Acceptable Key");
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
socket = (SocketChannel) ssc.accept();
socket.configureBlocking(false);
SelectionKey another =
socket.register(sel,SelectionKey.OP_READ|SelectionKey.OP_WRITE);
}
if (key.isReadable()) {
System.out.println("Readable Key");
String ret = readMessage(key);
if (ret.length() > 0) {
writeMessage(socket,ret);
}
}
if (key.isWritable()) {
System.out.println("Writable Key");
String ret = readMessage(key);
socket = (SocketChannel)key.channel();
if (result.length() > 0 ) {
writeMessage(socket,ret);
}
}
Abracadabra-出现非阻塞服务器套接字!
JDK 1.4中的非阻塞I / O简介的最后部分留给您:运行示例。
在这个简单的无阻塞服务器套接字示例中,服务器读取从客户端发送的文件名,显示文件内容,然后将内容写回到客户端。
这是运行示例所需的操作:
- 安装JDK 1.4(请参阅参考资料 )。
- 将两个源文件都复制到您的目录中。
- 以
java NonBlockingServer
身份编译并运行服务器。 - 以
java Client
身份编译并运行客户java Client
。 - 在存在类文件的目录中输入文本或Java文件的名称。
- 服务器将读取该文件并将其内容发送给客户端。
- 客户端将打印出从服务器接收的数据。 (由于这是使用的
ByteBuffer
的限制,因此将仅读取1024个字节。) - 通过输入要退出或关闭的命令来关闭客户端。
结论
Merlin的新I / O软件包涵盖了广泛的范围。 Merlin的新非阻塞I / O实现的主要优点是双重的:线程不再在读取或写入时阻塞,并且Selector
能够处理多个连接,从而大大减少了服务器应用程序的开销。
我们已经强调了新的java.nio包的这两个主要优点。 我们希望您将在这里学到的知识应用到实时应用程序开发工作中。
翻译自: https://www.ibm.com/developerworks/java/library/j-javaio/index.html