1、传统的BIO编程
网络编程的基本模型是 Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。
在基于传统同步阻塞模型开发中, ServerSocket负责绑定IP地址,启动监听端口Socket负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。下面,我们就以经典的时间服务器(TimeServer)为例,通过代码分析来回顾和熟悉BIO编程。
采用BIO通信模型的服务端,通常由一个独立的 Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的一请求一应答通信模型。
该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢岀、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。
同步阻塞式l/O创建的TimeServer
package com.phei.netty.bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author lilinfeng
* @version 1.0
* @date 2014年2月14日
*/
public class TimeServer {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
ServerSocket server = null;
try {
server = new ServerSocket(port);
System.out.println("The time server is start in port : " + port);
Socket socket = null;
while (true) {
socket = server.accept();
new Thread(new TimeServerHandler(socket)).start();
}
} finally {
if (server != null) {
System.out.println("The time server close");
server.close();
server = null;
}
}
}
}
package com.phei.netty.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/**
* @author Administrator
* @version 1.0
* @date 2014年2月14日
*/
public class TimeServerHandler implements Runnable {
private Socket socket;
public TimeServerHandler(Socket socket) {
this.socket = socket;
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(
this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
String currentTime = null;
String body = null;
while (true) {
body = in.readLine();
if (body == null)
break;
System.out.println("The time server receive order : " + body);
currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
System.currentTimeMillis()).toString() : "BAD ORDER";
out.println(currentTime);
}
} catch (Exception e) {
if (in != null) {
try {
in.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (out != null) {
out.close();
out = null;
}
if (this.socket != null) {
try {
this.socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
this.socket = null;
}
}
}
}
TimeServer根据传入的参数设置监听端口,如果没有入参,使用默认值8080。第29行通过构造函数创建 ServerSocket,如果端口合法且没有被占用,服务端监听成功。第3235行通过一个无限循环来监听客户端的连接,如果没有客户端接入,则主线程阻塞在ServerSocket的 accept操作上。启动 TimeServer,通过jvisualvm打印线程堆栈,我们可以发现主程序确实阻塞在 accept操作上,如图所示。
同步阻塞式l/O创建的TimeClient
客户端通过 Socket创建,发送查询时间服务器的"QUERY TIME ORDER"指令,然后读取服务端的响应并将结果打印岀来,随后关闭连接,释放资源,程序退出执行。
public class TimeClient {
/**
* @param args
*/
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket("127.0.0.1", port);
in = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
out.println("QUERY TIME ORDER");
System.out.println("Send order 2 server succeed.");
String resp = in.readLine();
System.out.println("Now is : " + resp);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
out = null;
}
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
in = null;
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
socket = null;
}
}
}
}
2、伪异步I/O编程
为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化——后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N。通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
当有新的客户端接入时,将客户端的 Socket封装成一个Task(该任务实现 java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK的线程池维护一个消息队列和N个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大
线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
2.1、伪l/O创建的TimeServer
public class TimeServer {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
ServerSocket server = null;
try {
server = new ServerSocket(port);
System.out.println("The time server is start in port : " + port);
Socket socket = null;
TimeServerHandlerExecutePool singleExecutor = new TimeServerHandlerExecutePool(
50, 10000);// 创建IO任务线程池
while (true) {
socket = server.accept();
singleExecutor.execute(new TimeServerHandler(socket));
}
} finally {
if (server != null) {
System.out.println("The time server close");
server.close();
server = null;
}
}
}
}
public class TimeServerHandlerExecutePool {
private ExecutorService executor;
public TimeServerHandlerExecutePool(int maxPoolSize, int queueSize) {
executor = new ThreadPoolExecutor(Runtime.getRuntime()
.availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS,
new ArrayBlockingQueue<java.lang.Runnable>(queueSize));
}
public void execute(java.lang.Runnable task) {
executor.execute(task);
}
}
2.2、伪异步I/O弊端分析
当对 Socket的输入流进行读取操作的时候,它会一直阻塞下去,直到发生如下三种事件。
- 有数据可读
- 可用数据已经读取完毕
- 发生空指针或者I/O异常
这意味着当对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞,如果对方要60s才能够将数据发送完成,读取一方的IO线程也将会被同步阻塞60s,在此期间,其他接入消息只能在消息队列中排队。
当调用 OutputStream的 write方法写输出流的时候,它将会被阻塞,直到所有要发送的字节全部写入完毕,或者发生异常。学习过TCP/IP相关知识的人都知道,当消息的接收方处理缓慢的时候,将不能及时地从TCP缓冲区读取数据,这将会导致发送方的TCP
window size不断减小,直到为0,双方处于Keep- Alive状态,消息发送方将不能再向TCP缓冲区写入消息,这时如果采用的是同步阻塞I/O, write操作将会被无限期阻塞,直到TCP window size大于0或者发生l/O异常通过对输入和输出流的API文档进行分析,我们了解到读和写操作都是同步阻塞的,阻塞的时间取决于对方IO线程的处理速度和网络I/O的传输速度。
3、NIO编程
与 Socket类和 ServerSocket类相对应,NlO也提供了 SocketChannel和 ServerSocketChannel两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式则正好相反。
3.1、NIO类库简介
3.1.1、缓冲区 Buffer
Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入 Buffer对象,体现了新库与原IO的一个重要区别。在面向流的I/O中,可以将数据直接写入或者将数据直接读到 Strean对象中在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
缓冲区实质上是一个数组。通常它是一个字节数组( ByteBuffer),也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit)等信息。
最常用的缓冲区是 ByteBuffer,一个 ByteBuffer提供了一组功能用于操作byte数组。除了 ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区,具体如下。
每一个Buffer类都是 Buffer接口的一个子实例。除了 ByteBuffer,每一个 Buffer类都有完全一样的操作,只是它们所处理的数据类型不一样。因为大多数标准IO操作都使用ByteBuffer,所以它在具有一般缓冲区的操作之外还提供了一些特有的操作,以方便网络读写。
3.1.2、通道 Channel
Channel是一个通道,它就像自来水管一样,网络数据通过 Channel读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者 OutputStream的子类),而通道可以用于读、写或者二者同时进行。
因为 Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作Channel的类图继承关系如图2-9所示。
自顶向下看,前三层主要是 Channel接口,用于定义它的功能,后面是一些具体的功能类(抽象类)。从类图可以看出,实际上 Channel可以分为两大类:用于网络读写的Selectablechannel和用于文件操作的 Filechannel。
3.1.3、多路复用器Selector
多路复用器提供选择已经就绪的任务的能力。简单来讲Selector会不断地轮询注册在其上的 Channel,如果某个 Channel上面发生读或者写事件,这个 Channel就处于就绪状态,会被 Selector轮询出来,然后通过 SelectionKey可以获取就绪 Channel的集合,进行后续的IO操作。一个多路复用器 Selector可以同时轮询多个 Channel,由于JDK使用了 epoll代替传统的 select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责 Selector的轮询,就可以接入成千上万的客户端,这确实是个非常巨大的进步。
3.2、NIO服务端时序图
注意:如果发送区TCP缓冲区满,会导致写半包,此时,需要注册监听写操作位,循环写,直到整包消息写入TCP缓冲区。
3.3、NIO创建的TimeServer
package com.phei.netty.nio;
import java.io.IOException;
/**
* @author lilinfeng
* @version 1.0
* @date 2014年2月14日
*/
public class TimeServer {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
new Thread(timeServer, "NIO-MultiplexerTimeServer-001").start();
}
}
下面对NIO创建的 TimeServer进行简单分析。第16~23行跟之前的一样,设置监听端口。第24~25行创建了一个被称为 MultiplexerTimeServer的多路复用类,它是个一个独立的线程,负责轮询多路复用器 Sector,可以处理多个客户端的并发接入。现在我们继续看 MultiplexerTimeServer的源码。
package com.phei.netty.nio;
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.Iterator;
import java.util.Set;
/**
* @author Administrator
* @version 1.0
* @date 2014年2月16日
*/
public class MultiplexerTimeServer implements Runnable {
private Selector selector;
private ServerSocketChannel servChannel;
private volatile boolean stop;
/**
* 初始化多路复用器、绑定监听端口
*
* @param port
*/
public MultiplexerTimeServer(int port) {
try {
selector = Selector.open();
servChannel = ServerSocketChannel.open();
servChannel.configureBlocking(false);
servChannel.socket().bind(new InetSocketAddress(port), 1024);
servChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("The time server is start in port : " + port);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
public void stop() {
this.stop = true;
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
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 {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null)
key.channel().close();
}
}
}
} catch (Throwable t) {
t.printStackTrace();
}
}
// 多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
if (selector != null)
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
// 处理新接入的请求消息
if (key.isAcceptable()) {
// Accept the new connection
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// Add the new connection to the selector
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
// Read the data
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 java.util.Date(
System.currentTimeMillis()).toString()
: "BAD ORDER";
doWrite(sc, currentTime);
} else if (readBytes < 0) {
// 对端链路关闭
key.cancel();
sc.close();
} else
; // 读到0字节,忽略
}
}
}
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);
}
}
}
(1)30~42行为构造方法,在构造方法中进行资源初始化。创建多路复用器 Selector、ServerSocketChannel,对 Channel和TCP参数进行配置。例如,将 ServerSocketchannel设置为异步非阻塞模式,它的 backlog设为1024。系统资源初始化成功后,将ServerSocketChannel注册到 Selector,监听 Selection Key.OP_ACCEPT操作位。如果资源初始化失败(例如端口被占用),则退出。
(2)55~77行是在线程的run方法的 while循环体中循环遍历 selector,它的休眠时间为1s。无论是否有读写等事件发生, selector每隔1s都被唤醒一次。 selector也提供了个无参的 select方法:当有处于就绪状态的 Channel时, selector将返回该 Channel的SelectionKey集合。通过对就绪状态的 Channel集合进行迭代,可以进行网络的异步读写操作。
(3)92~99行处理新接入的客户端请求消息,根据 SelectionKey的操作位进行判断即可获知网络事件的类型,通过ServerSocketChannel的 accept接收客户端的连接请求并创建 Socketchannel实例。完成上述操作后,相当于完成了TCP的三次握手,TCP物理链路正式建立。注意,我们需要将新创建的 Socketchanne设置为异步非阻塞,同时也可以对其TCP参数进行设置,例如TCP接收和发送缓冲区的大小等。但作为入门的例子,以上例程没有进行额外的参数设置
(4)100~125行用于读取客户端的请求消息。首先创建一个 ByteBuffer,由于我们事先无法得知客户端发送的码流大小,作为例程,我们开辟一个1MB的缓冲区。然后调用Socketchannel的read方法读取请求码流。注意,由于我们已经将 SocketChannel设置为异步非阻塞模式,因此它的read是非阻塞的。使用返回值进行判断,看读取到的字节数,返回值有以下三种可能的结果。
- 返回值大于0:读到了字节,对字节进行编解码
- 返回值等于0:没有读取到字节,属于正常场景,忽略
- 返回值为-1:链路已经关闭,需要关闭 Socketchannel,释放资源
当读取到码流以后,进行解码。首先对 readBuffer进行fip操作,它的作用是将缓冲区当前的 limit设置为 position, position设置为0,用于后续对缓冲区的读取操作。然后根据缓冲区可读的字节个数创建字节数组,调用 ByteBuffer的get操作将缓冲区可读的字节数组复制到新创建的字节数组中,最后调用字符串的构造函数创建请求消息体并打印。如果请求指令是" QUERY TIME ORDER",则把服务器的当前时间编码后返回给客户端。下面我们看看异步发送应答消息给客户端的情况。
(5)127~135行将应答消息异步发送给客户端。我们看下关键代码,首先将字符串编码成字节数组,根据字节数组的容量创建ByteBuffer,调用 ByteBuffer的put操作将字节数组复制到缓冲区中,然后对缓冲区进行fip操作,最后调用 Socketchannel的 write方法将缓冲区中的字节数组发送出去。需要指出的是,由于 Socketchannel是异步非阻塞的,它并不保证一次能够把需要发送的字节数组发送完,此时会出现“写半包”问题。我们需要注册写操作,不断轮询 Selector将没有发送完的 Byte Buffer发送完毕,然后可以通过ByteBuffer的 hasRemain()方法判断消息是否发送完成。此处仅仅是个简单的入门级例程,没有演示如何处理“写半包”场景,后续的章节会有详细说明。
3.4、NIO客户端时序图
3.5、NIO创建的TimeClient
public class TimeClient {
/**
* @param args
*/
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
new Thread(new TimeClientHandle("127.0.0.1", port), "TimeClient-001")
.start();
}
}
package com.phei.netty.nio;
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;
/**
* @author Administrator
* @version 1.0
* @date 2014年2月16日
*/
public class TimeClientHandle implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop;
public TimeClientHandle(String host, int port) {
this.host = host == null ? "127.0.0.1" : host;
this.port = port;
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
try {
doConnect();
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
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 {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null)
key.channel().close();
}
}
}
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
// 多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
if (selector != null)
try {
selector.close();
} catch (IOException e) {
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);
} else
System.exit(1);// 连接失败,进程退出
}
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();
} else
; // 读到0字节,忽略
}
}
}
private void doConnect() throws IOException {
// 如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答
if (socketChannel.connect(new InetSocketAddress(host, port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
} else
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.");
}
}
3.6、使用NIO编程的优点总结如下。
- 客户端发起的连接操作是异步的,可以通过在多路复用器注册 OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞
- Socketchannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待直接返回,这样IO通信线程就可以处理其他的链路,不需要同步等待这个链路可用。
- 线程模型的优化:由于JDK的 Selector在 Linux等主流操作系统上通过 epoll实现,它没有连接句柄数的限制(只受限于操作系统的最大句柄数或者对单个进程的句柄限制),这意味着一个 Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降。因此,它非常适合做高性能、高负载的网络服务器。
4、AIO编程
JDK1.7升级了NIO类库,升级后的NIO类库被称为NIO2.0。引人注目的是,Java正式提供了异步文件I/O操作,同时提供了与UNIX网络编程事件驱动IO对应的AIO。NIO2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。异步通道提供以下两种方式获取获取操作结果。
- 通过java.util.concurrent.Future类来表示异步操作的结果;
- 在执行异步操作的时候传入一个 java.nio.channels
CompletionHandler接口的实现类作为操作完成的回调。
NIO2.0的异步套接字通道是真正的异步非阻塞IO,对应于UNIX网络编程中的事件驱动IO(AIO)。它不需要通过多路复用器(Selector)对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO的编程模型。
4.1、AIO创建的TimeServer
package com.phei.netty.aio;
import java.io.IOException;
/**
* @author lilinfeng
* @version 1.0
* @date 2014年2月14日
*/
public class TimeServer {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
AsyncTimeServerHandler timeServer = new AsyncTimeServerHandler(port);
new Thread(timeServer, "AIO-AsyncTimeServerHandler-001").start();
}
}
我们直接从第25行开始看,首先创建异步的时间服务器处理类,然后启动线程将AsyncTimeServerHandler拉起,代码如下。
package com.phei.netty.aio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.util.concurrent.CountDownLatch;
/**
* @author Administrator
* @version 1.0
* @date 2014年2月16日
*/
public class AsyncTimeServerHandler implements Runnable {
private int port;
CountDownLatch latch;
AsynchronousServerSocketChannel asynchronousServerSocketChannel;
public AsyncTimeServerHandler(int port) {
this.port = port;
try {
asynchronousServerSocketChannel = AsynchronousServerSocketChannel
.open();
asynchronousServerSocketChannel.bind(new InetSocketAddress(port));
System.out.println("The time server is start in port : " + port);
} catch (IOException e) {
e.printStackTrace();
}
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
latch = new CountDownLatch(1);
doAccept();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void doAccept() {
asynchronousServerSocketChannel.accept(this,
new AcceptCompletionHandler());
}
}
我们重点对 AsyncTimeServerHandler进行分析。首先看第20~27行,在构造方法中首先创建一个异步的服务端通道 AsynchronousServerSocketChannel,然后调用它的bind方法绑定监听端口。如果端口合法且没被占用,则绑定成功,打印启动成功提示到控制台。
在线程的run方法中,第40行初始化 CountDownLatch对象,它的作用是在完成一组正在执行的操作之前,允许当前的线程一直阻塞。在本例程中,我们让线程在此阻塞,防止服务端执行完成退出。在实际项目应用中,不需要启动独立的线程来处理AsynchronousServerSocketChannel,这里仅仅是个demo演示。
第41行用于接收客户端的连接,由于是异步操作,我们可以传递一个 CompletionHandler<AsynchronousSocketChannel,? super A>类型的 handler实例接收 accept操作成功的通知消息。在本例程中我们通过 Accept Completion Handler实例作为 handler来接收通知消息,下面继续对 AcceptCompletionHandler进行分析。
package com.phei.netty.aio;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
/**
* @author lilinfeng
* @version 1.0
* @date 2014年2月16日
*/
public class AcceptCompletionHandler implements
CompletionHandler<AsynchronousSocketChannel, AsyncTimeServerHandler> {
@Override
public void completed(AsynchronousSocketChannel result,
AsyncTimeServerHandler attachment) {
attachment.asynchronousServerSocketChannel.accept(attachment, this);
ByteBuffer buffer = ByteBuffer.allocate(1024);
result.read(buffer, buffer, new ReadCompletionHandler(result));
}
@Override
public void failed(Throwable exc, AsyncTimeServerHandler attachment) {
exc.printStackTrace();
attachment.latch.countDown();
}
}
CompletionHandler有两个方法,分别如下。
public interface CompletionHandler<V,A> {
void completed(V result, A attachment);
void failed(Throwable exc, A attachment);
}
下面分别对这两个接口的实现进行分析。首先看 completed接口的实现,代码第18~20行,我们从 attachment获取成员变量 AsynchronousServerSocketChannel,然后继续调用它的accept方法。有的读者可能会心存疑惑:既然已经接收客户端成功了,为什么还要再次调用accept方法呢?原因是这样的:调用 AsynchronousServerSocketChannel的 accept方法后如果有新的客户端连接接入,系统将回调我们传入的 CompletionHandler实例的 completed方法,表示新的客户端已经接入成功。因为一个 AsynchronousServerSocketChanne可以接收成千上万个客户端,所以需要继续调用它的 accept方法,接收其他的客户端连接,最终形成一个循环。每当接收一个客户读连接成功之后,再异步接收新的客户端连接。链路建立成功之后,服务端需要接收客户端的请求消息,在代码第19行创建新的ByteBuffer,预分配1MB的缓冲区。第20行通过调用 AsynchronousSocketChannel的read方法进行异步读操作。下面我们看看异步read方法的参数。
◎ByteBuffer dst:接收缓冲区,用于从异步 Channel中读取数据包;
◎A attachment:异步 Channel携带的附件,通知回调的时候作为入参使用;
◎CompletionHandler<Integer,? super A>:接收通知回调的业务 Handler,在本例程中为ReadCompletionHandler
下面我们继续对 ReadCompletionHandler进行分析。
package com.phei.netty.aio;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
/**
* @author lilinfeng
* @version 1.0
* @date 2014年2月16日
*/
public class ReadCompletionHandler implements
CompletionHandler<Integer, ByteBuffer> {
private AsynchronousSocketChannel channel;
public ReadCompletionHandler(AsynchronousSocketChannel channel) {
if (this.channel == null)
this.channel = channel;
}
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
byte[] body = new byte[attachment.remaining()];
attachment.get(body);
try {
String req = new String(body, "UTF-8");
System.out.println("The time server receive order : " + req);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(req) ? new java.util.Date(
System.currentTimeMillis()).toString() : "BAD ORDER";
doWrite(currentTime);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
private void doWrite(String currentTime) {
if (currentTime != null && currentTime.trim().length() > 0) {
byte[] bytes = (currentTime).getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer, writeBuffer,
new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
// 如果没有发送完成,继续发送
if (buffer.hasRemaining())
channel.write(buffer, buffer, this);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
// ingnore on close
}
}
});
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
this.channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
首先看构造方法。我们将 AsynchronousSocketChannel通过参数传递到 ReadCompletionHandler中,当作成员变量来使用,主要用于读取半包消息和发送应答。本例程不对半包读写进行具体说明,对此感兴趣的读者可以关注后续章节对 Netty半包处理的专题介绍。
继续看代码,第25~38行是读取到消息后的处理。首先对 attachment进行fip操作,为后续从缓冲区读取数据做准备。根据缓冲区的可读字节数创建byte数组,然后通过new String方法创建请求消息,对请求消息进行判断,如果是" QUERY TIME ORDER"则获取当前系统服务器的时间,调用 doWrite方法发送给客户端。下面我们对 doWrite方法进行详细分析跳到代码第41行,首先对当前时间进行合法性校验,如果合法,调用字符串的解码方法将应答消息编码成字节数组,然后将它复制到发送缓冲区 writeBuffer中,最后调用AsynchronousSocketChannel的异步 write方法。正如前面介绍的异步read方法一样,它也有三个与read方法相同的参数,在本例程中我们直接实现 write方法的异步回调接口CompletionHandler。代码跳到第51行,对发送的 writeBuffer进行判断,如果还有剩余的字节可写,说明没有发送完成,需要继续发送,直到发送成功。
最后,我们关注下 failed方法,它的实现很简单,就是当发生异常的时候,对异常Throwable进行判断:如果是I/O异常,就关闭链路,释放资源:如果是其他异常,按照业务自己的逻辑进行处理。本例程作为简单的demo,没有对异常进行分类判断,只要发生了读写异常,就关闭链路,释放资源。
4.2、AIO创建的TimeClient
package com.phei.netty.aio;
/**
* @author lilinfeng
* @date 2014年2月14日
* @version 1.0
*/
public class TimeClient {
/**
* @param args
*/
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
new Thread(new AsyncTimeClientHandler("127.0.0.1", port),
"AIO-AsyncTimeClientHandler-001").start();
}
}
第22行通过一个独立的I/O线程创建异步时间服务器客户端 Handler。在实际项目中,我们不需要独立的线程创建异步连接对象,因为底层都是通过JDK的系统回调实现的,在后面运行时间服务器程序的时候,我们会抓取线程调用堆栈给大家展示。
继续看代码, AsyncTimeClientHandler的实现类源码如下。
package com.phei.netty.aio;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.CountDownLatch;
/**
* @author Administrator
* @version 1.0
* @date 2014年2月16日
*/
public class AsyncTimeClientHandler implements
CompletionHandler<Void, AsyncTimeClientHandler>, Runnable {
private AsynchronousSocketChannel client;
private String host;
private int port;
private CountDownLatch latch;
public AsyncTimeClientHandler(String host, int port) {
this.host = host;
this.port = port;
try {
client = AsynchronousSocketChannel.open();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
latch = new CountDownLatch(1);
client.connect(new InetSocketAddress(host, port), this, this);
try {
latch.await();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void completed(Void result, AsyncTimeClientHandler attachment) {
byte[] req = "QUERY TIME ORDER".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
client.write(writeBuffer, writeBuffer,
new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
if (buffer.hasRemaining()) {
client.write(buffer, buffer, this);
} else {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
client.read(
readBuffer,
readBuffer,
new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result,
ByteBuffer buffer) {
buffer.flip();
byte[] bytes = new byte[buffer
.remaining()];
buffer.get(bytes);
String body;
try {
body = new String(bytes,
"UTF-8");
System.out.println("Now is : "
+ body);
latch.countDown();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc,
ByteBuffer attachment) {
try {
client.close();
latch.countDown();
} catch (IOException e) {
// ingnore on close
}
}
});
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
client.close();
latch.countDown();
} catch (IOException e) {
// ingnore on close
}
}
});
}
@Override
public void failed(Throwable exc, AsyncTimeClientHandler attachment) {
exc.printStackTrace();
try {
client.close();
latch.countDown();
} catch (IOException e) {
e.printStackTrace();
}
}
}
第24~32行是构造方法,首先通过 AsynchronousSocketChannel的open方法创建一个新的 AsynchronousSocketChannel对象。然后跳到第36行,创建 CountDownLatch进行等待,防止异步操作没有执行完成线程就退出。第37行通过 connect方法发起异步操作,它有两个参数,分别如下
◎ A attachment: AsynchronousSocketChannel的附件,用于回调通知时作为入参被传递,调用者可以自定义;
◎ CompletionHandler<Void,? super A> handler:异步操作回调通知接口,由调用者实现。
在本例程中,这两个参数都使用 AsyncTimeClientHandler类本身,因为它实现了Completion Handler接口。
接下来我们看异步连接成功之后的方法回调— completed方法。代码第52行,我们创建请求消息体,对其进行编码,然后复制到发送缓冲区 writeBuffer中,调用 AsynchronousSocketchannel的 write方法进行异步写。与服务端类似,我们可以实现CompletionHandler<Integer, ByteBuffer>接口用于写操作完成后的回调。代码第60~62行,如果发送缓冲区中仍有尚未发送的字节,将继续异步发送,如果已经发送完成,则执行异步读取操作。
代码第64~97行是客户端异步读取时间服务器服务端应答消息的处理逻辑。代码第64行调用 AsynchronousSocketChannel的read方法异步读取服务端的响应消息。由于read操作是异步的,所以我们通过内部匿名类实现 CompletionHandler<Integer, ByteBuffer>接口,当读取完成被JDK回调时,构造应答消息。第71~78行从 CompletionHandler的ByteBuffer中读取应答消息,然后打印结果。
第102~111行,当读取发生异常时,关闭链路,同时调用 CountDownLatch的countDown方法让 AsyncTimeClientHandler线程执行完毕,客户端退出执行。
需要指出的是,正如之前的NIO例程,我们并没有完整的处理网络的半包读写,在对例程进行功能测试的时候没有问题。但是,如果对代码稍加改造,进行压力或者性能测试,就会发现输出结果存在问题。
下面继续看下JDK异步回调 CompletionHandler的线程执行堆栈。
从“Thread-2”线程堆栈中可以发现,JDK底层通过线程池 ThreadPoolExecutor来执行回调通知,异步回调通知类由sun.nio.ch.AsynchronousChannelGrouplmpl实现,它经过层层调用,最终回调com.phei.netty.aio.AsyncTimeClientHandlerS1.completed方法,完成回调通知。由此我们也可以得出结论:异步 SocketChannel是被动执行对象,我们不需要像NIO编程那样创建一个独立的O线程来处理读写操作。对于 AsynchronousServerSocketChannel和 AsynchronousSocketChannel,它们都由JDK底层的线程池负责回调并驱动读写操作。正因为如此,基于NIO2.0新的异步非阻塞Channel进行编程比NIO编程更为简单。
5、不同I/O模型对比
同步阻塞I/O(BIO) | 伪异步I/O | 非阻塞I/O(NIO) | 异步AIO(AIO) | |
客户端个数:I/O线程 | 1:1 | M:N(M可以大于N) | M:1(1个I/O线程处理多个客户端连接) | M:0(不需要启动额外的I/O线程,被动回调) |
I/O类型(阻塞) | 阻塞I/O | 阻塞I/O | 非阻塞I/O | 非阻塞I/O |
I/O类型(同步) | 同步I/O | 同步I/O | 同步I/O(I/O多路复用) | 异步I/O |
API使用难度 | 简单 | 简单 | 非常复杂 | 复杂 |
调试难度 | 简单 | 简单 | 复杂 | 复杂 |
可靠性 | 非常差 | 差 | 高 | 高 |
吞吐量 | 低 | 中 | 高 | 高 |
6、为什么选择 Netty
Netty是业界最流行的NlO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如 Hadoop的RPC框架 Avro就使用了Netty作为底层通信框架,其他还有业界主流的RPC框架,也使用Netty来构建高性能的异步通信能力。
通过对Netty的分析,我们将它的优点总结如下
- API使用简单,开发门槛低;
- 功能强大,预置了多种编解码功能,支持多种主流协议;
- 定制能力强,可以通过 Channelhandler对通信框架进行灵活地扩展;
- 性能高,通过与其他业界主流的NIO框架对比,Net!y的综合性能最优;
- 成熟、稳定,Net修复了已经发现的所有 JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;
- 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入;
- 经历了大规模的商业应用考验,质量得到验证。 Netty在互联网、大数据、网络游戏、企业应用、电信软件等众多行业已经得到了成功商用,证明它已经完全能够满足不同行业的商业应用了。
正是因为这些优点,Netty逐渐成为了 Java NIO编程的首选框架。