前面学习到了伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。
伪异步I/O弊端分析
Socket和ServerSocket都是字节流传输数据,需要使用到输入输出流,而输入输出流的read和write方法都是阻塞式的。
从JDK中可以看到,在输入数据可用、检测到流末尾或者抛出异常前,read方法一直阻塞。
当调用OutputStream的write方法写输出流的时候,它也会被阻塞,直到所有要发送的字节全部写入完毕,或者发送异常。
还有一个关键的问题是,当消息的接收方处理缓慢的时候,将不能及时地从TCP缓冲区读取数据,这将会导致发送方的TCP window size 不断减小,直到为0,双方处于Keep-Alive状态,消息发送方将不能再向TCP缓冲区写入消息,这时如果采用的是同步阻塞I/O,write操作将会被无限期阻塞,直到TCP window size 大于0或者发生I/O异常。
总的来说,输入输出流的读和写操作都是同步阻塞的,阻塞的时间取决于对方I/O线程的处理速度和网络I/O的传输速度。
伪异步I/O实际上仅仅只是对之前I/O线程模型的一个简单优化(采用线程池),他无法从根本上解决同步I/O导致的通信线程阻塞问题:
(1) 假如所有的可用线程都被阻塞,那后续所有的I/O消息都将在队列中排队;
(2) 当队列满之后,线程池也将拒绝服务;
(3) 又由于只有一个Accptor线程接收客户端接入,它被阻塞在线程池的同步阻塞队列之后,新的客户端请求消息将被拒绝,客户端会发生大量的连接超时。
NIO简介
NIO库是在JDK 1.4中引入的,NIO弥补了原来同步阻塞I/O的不足,它在标准Java代码中提供了高速的、面向块的I/O。
NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
NIO核心组件
在Socket网络通信中主要用到的NIO核心组件是:缓冲区Buffer、通道Channel和多路复用器Selector。
缓冲区Buffer
Buffer是一个缓冲区对象,实质上是一个数组。它包含一些要写入或者要读出的数据。最常用的缓冲区是ByteBuffer,每一种Java基本类型都对应有一种缓冲区。
Buffer对象是NIO库与IO库的一个重要区别。在原面向流的I/O中,可以将数据直接写入或者将数据直接读到Stream对象中;在NIO库中,所有的数据都是用缓冲区处理的,在读取数据的时,它是直接读到缓冲区中的,在写数据时也是写入到缓冲区中。
通道Channel
Channel是一个通道,可以通过它读取和写入数据,它就像自来水管道一样,网络数据通过Channel读取和写入。
通道与流的不同之处在于通道是双向的,而流是单向的(一个流必须是InputStream或者OutputStream的子类)。
实际上Channel可以分为两大类:分别是用于网络读写的SelectableChannel和用于文件操作的FileChannel。Socket网络通信中要用到的ServerSocketChannel和SocketChannel都是SelectableChannel的子类。
多路复用器 Selector
多路复用器Selector提供选择已经就绪的任务的能力。Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的IO操作。
一个Selector可以轮询多个Channel。
示例代码
服务端:
package com.niotest;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;
public class MutiplexerServerTest implements Runnable{
private Selector selector;
private ServerSocketChannel servChannel;
private volatile boolean stop;
/**
* 初始化多路复用器、绑定监听端口
*
* @param port
*/
public MutiplexerServerTest(int port) {
// TODO Auto-generated constructor stub
try {
//1、打开ServerSocketChannel和Selector
selector = Selector.open();
servChannel = ServerSocketChannel.open();
//2、设置为非阻塞方式
servChannel.configureBlocking(false);
//3、绑定监听端口
servChannel.bind(new InetSocketAddress(port), 1024);
//4、将ServerSocketChannel注册到多路复用器Selector上
servChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("The time server is start in port : " + port);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void stop() {
this.stop = stop;
}
@Override
public void run() {
//5、轮询就绪准备的key
while (!stop) {
try {
//每隔一秒轮询一次
selector.select(1000);
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
//7、处理新接入的请求
handleInput(key);
}catch (Exception e) {
// TODO: handle exception
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//多路复用器关闭后,所有注册在上面的Channel和 Pipe等资源都会被自动去注册并关闭,
//所以不需要重复释放资源
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
//处理新接入的请求消息
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//监听客户端连接
SocketChannel sc = ssc.accept();
//设置客户端链路为非阻塞模式
sc.configureBlocking(false);
//将新接入的客户端连接注册到多路复用器Selector,监听读操作
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
//读取数据
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
//读取数据到缓冲区
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("The time server receive order : " + body);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)
? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
//8、将消息异步发送到客户端
doWrite(sc, currentTime);
}else if (readBytes < 0) {
//对端链路关闭
key.cancel();
sc.close();
}
}
}
}
private void doWrite(SocketChannel channel, String response) throws IOException {
if (response != null && response.trim().length() > 0) {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer);
}
}
}
服务端测试主代码:
package com.niotest;
public class NIOServerTest {
public static void main(String[] args) {
int port = 3333;
MutiplexerServerTest mutiplexerServer = new MutiplexerServerTest(port);
new Thread(mutiplexerServer,"NIOServerTest-001").start();
}
}
客户端:
package com.niotest;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class ClientHandle implements Runnable{
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop;
/**
* 构造方法
* @param host
* @param port
*/
public ClientHandle(String host, int port) {
// TODO Auto-generated constructor stub
this.host = host;
this.port = port;
try {
//1、打开ServerSocketChannel和Selector
selector = Selector.open();
socketChannel = SocketChannel.open();
//2、设置为非阻塞方式
socketChannel.configureBlocking(false);
} catch (IOException e) {
// TODO: handle exception
e.printStackTrace();
}
}
@Override
public void run() {
// TODO Auto-generated method stub
try {
//3、异步连接服务端
doConnect();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
while(!stop) {
//轮询准备就绪的Key
try {
selector.select(1000);
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
// TODO: handle exception
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//多路复用器关闭后,所有注册在上面的Channel和 Pipe等资源都会被自动去注册并关闭,
//所以不需要重复释放资源
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
//判断是否连接成功
SocketChannel sc = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (sc.finishConnect()) {
//注册读事件到多路复用器
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
}
}
if (key.isReadable()) {
//异步读客户端请求消息到缓冲区
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("Now is : " + body);
this.stop = true;
}else if (readBytes < 0) {
//对端链路关闭
key.cancel();
sc.close();
}
}
}
}
private void doConnect() throws IOException {
//判断是否连接成功,如果连接成功,则直接注册状态位到多路复用器中
if (socketChannel.connect(new InetSocketAddress(host,port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
}else {
//向Reactor线程的多路复用器注册OP_CONNECT状态位,监听服务端的TCP ACK应答
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
}
private void doWrite(SocketChannel sc) throws IOException {
byte[] req = "QUERY TIME ORDER".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
sc.write(writeBuffer);
if (!writeBuffer.hasRemaining()) {
System.out.println("Send order 2 server succeed.");
}
}
}
客户端测试主代码:
package com.niotest;
public class NIOClientTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
int port = 3333;
new Thread(new ClientHandle("127.0.0.1", port), "Client-001").start();
}
}
运行结果:
左边位服务端,右边位客户端。