本文通过实现一个简单的时间服务器和客户端,分别对JDK的BIO、NIO和JDK1.7中的NIO 2.0的使用进行介绍和对比,了解基于java的网络编程的发展。本文内容主要参考《Netty权威指南》。
BIO
BIO即同步阻塞IO,采用BIO通信方式的服务器,对于每一个连接请求都由服务器创建一个新的线层来接受和处理请求,处理完成后销毁线程。这就是典型的一请求一应答的模型。
同步阻塞IO服务端通信模型图
这种方式的坏处是缺乏弹性伸缩的能力,当客户端并发访问量很大时,服务器需要创建大量线程来处理请求,而在短时间内创建大量线程就会导致服务器资源不足,进而引起僵死甚至宕机,无法提供服务。
同步阻塞IO的TimeServer
package bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author elricboa on 2018/1/10.
*/
public class TimeServer {
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
//use the default value;
}
}
try (ServerSocket server = new ServerSocket(port)) {
System.out.println("The time server started on port : " + port);
Socket socket = null;
while (true) {
socket = server.accept();
new Thread(new TimeServerHandler(socket)).start();
}
} finally {
System.out.println("The time server closed.");
}
}
}
服务器端程序启动后,在一个无限循环中接收来自客户端的连接,当没有客户端连接时,主线程则会阻塞在accept操作上。当有新的客户端接入的时候,主线程以该socket为参数创建一个TimeServerHandler对象,在新的线程中处理这条socket。
package bio;
import java.io.*;
import java.net.Socket;
/**
* @author elricboa on 2018/1/10.
*/
public class TimeServerHandler implements Runnable {
private Socket socket;
public TimeServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream()))) {
String currentTime = null;
String body = null;
while (true) {
body = in.readLine();
if (body == null) {
break;
}
System.out.println("The time server received order : " + body);
if ("query time order".equalsIgnoreCase(body.trim())) {
currentTime = new java.util.Date().toString();
} else {
currentTime = "bad order";
}
out.println(currentTime);
out.flush();
}
} catch (IOException e) {
e.printStackTrace();
if (socket != null) {
try {
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
socket = null;
}
}
}
}
TimeServerHanler包含处理请求的逻辑:读取输入并判断是否为合法查询,如果是合法查询就返回服务器当前时间,否则返回"bad order"。
package bio;
import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* @author elricboa on 2018/1/10.
*/
public class TimeClient {
public static void main(String[] args){
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
//use the default value;
}
}
try(Socket socket = new Socket("127.0.0.1", port);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()))){
out.println("QUERY TIME ORDER");
out.flush();
System.out.println("send order to server");
String resp = in.readLine();
System.out.println("Now is : " + resp);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
imeClient创建了连接到服务器的socket之后,向服务器发送请求,将收到的返回结果打印之后退出。
BIO的主要问题在于每一个客户端连接都需要一个线程来处理,一个处理线程也只能处理一个客户端连接。而在高性能服务器应用领域,往往要面对成千上万个并发连接,这种模型显然无法满足高性能、高并发的接入场景。
采用线程池做缓冲的同步阻塞IO
为了解决BIO方式中每一个请求都需要一个线程来处理的问题,有人对它的线程模型进行了优化,在服务器端采取线程池的形式来处理客户端的多个请求,这样就可以有M个服务器线程来处理N个客户端请求。这里的M可以远远大于N,这样一来可以根据服务器情况灵活配置M和N的比例,防止创建海量的并发访问耗尽服务器资源。
当一个用户请求来到服务器时,服务器会将客户端Socket封装成一个Task(继承了java.lang.Runnable接口),然后将Task交由服务器端的线程池处理。服务器维护一个消息队列和若干个worker线程,worker线程从消息队列中取出Task执行。由于消息队列和worker线程的数量都是灵活可控的,它们占用的资源也是可控的,所以不用担心会耗尽服务器资源。
伪异步IO服务端通信模型
这种解决方案来自于在JDK NIO没有流行之前,为了解决Tomcat通信线程同步I/O导致业务线程被挂住的情况,实现者在通信线程和业务线程之间加了一个缓冲区,用于隔离I/O线程和业务线程间的直接访问,这样业务线程就不会被I/O线程阻塞。
采用线程池做缓冲的TimeServer
package fnio;
import bio.TimeServerHandler;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author elricboa on 20