1.BIO
同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
2.NIO
同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
3.AIO
(NIO2.0) 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,
几种I/O模型的对比如图(图表来源《Netty权威指南》):
BIO模型的程序例子
网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务器监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。
在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。
BIO通信模型图
通过上图所示的通信模型图来熟悉下BIO的服务端通信模型:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的——请求——应答通信模型。
该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出,创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。
同步阻塞式I/O创建的TimeServer源码分析
1. 同步阻塞I/O的TimeServer
TimeServer根据传入的参数设置监听端口,如果没有参数,使用默认值8080,接下来通过构造函数创建ServerSocket,如果端口合法且没有被占用,服务端监听成功。然后通过一个无限循环来监听客户端的连接,如果没有客户端接入,则主线程阻塞在ServerSocket的accept操作上。
当有新的客户端接入的时候,执行代码 new Thread(new TimeServerHandler(socket)).start(),以Socket为参数构造TimeServerHandler对象,TimeServerHandler是一个Runnable,使用它为构造函数的参数创建一个新的客户端线程处理这条Socket链路。下面我们继续分析TimeServerHandler的代码:
24行通过BufferedReader读取一行,如果已经读到了输入流的尾部,则返回值为null,退出循环。如果读到了非空值,则对内容进行判断,如果请求消息为查询时间的指令“QUERY TIME ORDER”则获取当前最新的系统时间,通过PrintWriter的println函数发送给客户端,最后退出循环。代码32~49行释放输入流,输出流,和Socket套接字句柄资源,最后线程自动销毁并被虚拟机回收。
同步阻塞式I/O创建的TimeClient源码分析
客户端通过Socket创建,发送查询时间服务器的“QUERY TIME ORDER”指令,然后读取服务端的响应并将结果打印出来,随后关闭连接,释放资源,程序退出执行。
同步阻塞I/O的TimeClient:
第26行客户端通过PrintWriter向服务端发送“QUERY TIME ORDER”指令,然后通过BufferedReader的readLine读取响应并打印。
BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接。在高性能服务器应用领域,往往需要面向成千上万个客户端的并发连接,这种模型显然无法满足高性能,高并发接入的场景。
程序代码:
TimeServer.java:
package BIO;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class TimeServer {
public static void main(String[] args) throws IOException{
int port = 8380;
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;
}
}
}
}
============================================================================
TimeServerHandler.java
package BIO;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class TimeServerHandler implements Runnable {
private Socket socket;
public TimeServerHandler(Socket socket){
this.socket = socket;
}
@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;
}
e.printStackTrace();
}
}
}
==============================================================
TimeClient.java
package BIO;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class TimeClient {
public static void main(String[] args) {
int port = 8380;
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); //创建socket
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(),true);
out.println("QUERY TIME ORDER"); //向服务端写出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 e2) {
e2.printStackTrace();
}
in = null;
}
if(socket != null){
try {
socket.close();
} catch (IOException e3) {
e3.printStackTrace();
}
socket = null;
}
}
}
}
================================================================
JDK7的环境下,注意原程序已经被修改了端口号
服务端输出:
The time server is start in port : 8380
The time server receive order: QUERY TIME ORDER
客户端输出:
Send order 2 server succeed.
Now is : Mon Oct 31 17:22:16 CST 2016
=======================================================================
2.1.1.伪异步IO模型图
采用线程池和任务队列可以实现一种叫做伪异步的IO通信框架,它的模型图如下:
asyn-io
伪异步IO服务端通信模型(M:N)
当有新的客户端接入的时候,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK的线程池维护一个消息队列和N个活跃线程对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。 下面的小节,我们依然采用时间服务器程序,将其改造成伪异步IO时间服务器,然后通过对代码进行分析,找出其弊端。
2.1.1.伪异步式IO创建的TimeServer源码分析
我们对服务端代码进行一些改造,源码如下:
伪异步IO的主函数代码发生了变化,我们首先创建一个时间服务器处理类的线程池,当接收到新的客户端连接的时候,将请求Socket封装成一个Task,然后调用线程池的execute方法执行,从而避免了每个请求接入都创建一个新的线程。 伪异步IO的TimeServerHandlerExecutePool:
由于线程池和消息队列都是有界的,因此,无论客户端并发连接数多大,它都不会导致线程个数过于膨胀或者内存溢出,相比于传统的一连接一线程模型,是一种改良。 由于客户端代码并没有改变,因此,我们直接运行服务端和客户端,看执行结果: 服务端运行结果:
1
伪异步IO时间服务器服务端运行结果
客户端运行结果:
2
伪异步IO时间服务器客户端运行结果
伪异步IO通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。下个小节我们对伪异步IO进行深入分析,找到它的弊端,然后看看NIO是如何从根本上解决这个问题的。
2.1.1.伪异步IO弊端分析
要对伪异步IO的弊端进行深入分析,首先我们看两个JAVA同步IO的API说明,随后我们结合代码进行详细分析。
/**
* Reads some number of bytes from the input stream and stores them into
* the buffer array <code>b</code>. The number of bytes actually read is
* returned as an integer. This method blocks until input data is
* available, end of file is detected, or an exception is thrown.
*
*
If the length of <code>b</code> is zero, then no bytes are read and
* <code>0</code> is returned; otherwise, there is an attempt to read at
* least one byte. If no byte is available because the stream is at the
* end of the file, the value <code>-1</code> is returned; otherwise, at
* least one byte is read and stored into <code>b</code>.
*
*
The first byte read is stored into element <code>b[0]</code>, the
* next one into <code>b[1]</code>, and so on. The number of bytes read is,
* at most, equal to the length of <code>b</code>. Let <i>k</i> be the
* number of bytes actually read; these bytes will be stored in elements
* <code>b[0]</code> through <code>b[</code><i>k</i><code>-1]</code>,
* leaving elements <code>b[</code><i>k</i><code>]</code> through
* <code>b[b.length-1]</code> unaffected.
*
* @param b the buffer into which the data is read.
* @return the total number of bytes read into the buffer, or
* <code>-1</code> if there is no more data because the end of
* the stream has been reached.
* @exception IOException If the first byte cannot be read for any reason
* other than the end of the file, if the input stream has been closed, or
* if some other I/O error occurs.
* @exception NullPointerException if <code>b</code> is <code>null</code>.
*/
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
请注意加粗斜体字部分的API说明,当对Socket的输入流进行读取操作的时候,它会一直阻塞下去,直到发生如下三种事件: 1) 有数据可读 2) 可用数据已经读取完毕 3) 发生空指针或者IO异常 这意味着当对方发送请求或者应答消息比较缓慢、或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞,如果对方60S才能够将数据发送完成,读取一方的IO线程也将会被同步阻塞60S,在此期间,其它接入消息只能在消息队列中排队。 下面我们接着对输出流进行分析,还是看JDK IO类库输出流的API文档,然后结合文档说明进行故障分析。 Java 输入流OutputStream:
public void write(byte b[]) throws IOException
*Writes an array of bytes. This method will block until the bytes are *actually written.
Parameters:
b - the data to be written
Throws: IOException
If an I/O error has occurred.
当调用OutputStream的write方法写输出流的时候,它将会被阻塞直到所有要发送的字节全部写入完毕,或者发生异常。学习过TCP/IP相关知识的都知道,当消息的接收方处理缓慢的时候,将不能及时的从TCP缓冲区读取数据,这将会导致发送方的TCP window size不断减小,直到为0,双方处于Keep-Alive状态,消息发送方将不能再向TCP缓冲区写入消息,这时如果采用的是同步阻塞IO,write操作将会被无限期阻塞,直到TCP window size大于0或者发生IO异常。
通过对输入和输出流的API文档进行分析,我们了解到读和写操作都是同步阻塞的,阻塞的时间取决于对方IO线程的处理速度和网络IO的传输速度。本质上来讲,我们无法保证生产环境的网络状况和对端的应用程序能够足够快,如果我们的应用程序依赖对方的处理速度,它的可靠性就非常差。也许在实验室进行的性能测试结果令大家满意,但是一旦上线运行,面对恶劣的网络环境和良莠不齐的第三方系统,问题就会如火山一样喷发。
伪异步IO实际上仅仅只是对之前IO线程模型的一个简单优化,它无法从根本上解决同步IO导致的通信线程阻塞问题。下面我们就简单分析下如果通信对方返回应答时间过长引起的级联故障:
服务端处理缓慢,返回应答消息耗费60S,平时只需要10MS;
采用伪异步IO的线程正在读取故障服务节点的响应,由于读取输入流是阻塞的,因此,它将会被同步阻塞60S;
假如所有的可用线程都被故障服务器阻塞,那后续所有的IO消息都将在队列中排队;
由于线程池采用阻塞队列实现,当队列积满之后,后续入队列的操作将被阻塞;
由于前端只有一个Accptor线程接收客户端接入,它被阻塞在线程池的同步阻塞队列之后,新的客户端请求消息将被拒绝,客户端会发生大量的连接超时;
由于几乎所有的连接都超时,调用者会认为系统已经崩溃,无法接收新的请求消息。
=================================================================================
伪异步程序代码:
package WYB;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class TimeServer {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
int port = 8380;
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;
}
}
}
}
==================================================================
package WYB;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class TimeServerHandler implements Runnable {
private Socket socket;
public TimeServerHandler(Socket socket){
this.socket = socket;
}
@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;
}
e.printStackTrace();
}
}
}
=============================================================
package WYB;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TimeServerHandlerExecutePool {
private ExecutorService executor;
public TimeServerHandlerExecutePool(int maxPoolSize, int queueSize) {
executor = new ThreadPoolExecutor(Runtime.getRuntime()
.availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS,
new ArrayBlockingQueue(queueSize));
}
public void execute(java.lang.Runnable task) {
executor.execute(task);
}
}
===============================================================
客户端代码没有改变,仍旧用BIO的客户端代码。
运行结果如下:
服务端:
The time server is start in port : 8380
The time server receive order: QUERY TIME ORDER
客户端:
Send order 2 server succeed.
Now is : Tue Nov 01 10:59:22 CST 2016