第2章 NIO入门
2.1 传统的BIO编程
以服务器为例,在传统BIO模型下的服务器,每当一个新的请求到来的时候回分配一个线程去处理该请求,并且该线程在执行IO操作的时候会一直阻塞,知道IO操作完成或抛出异常才会返回。当网络情况不佳时,网络IO可能会耗费大量时间,那么就会同时有大量线程在服务器上阻塞着,很容易造成内存溢出。
这种模型被称为同步阻塞模型,同步指的是只有等待线程IO操作完成该线程才会返回,阻塞指的是IO没有完成的时候一直等待。
2.1.1 服务端代码
Server类,监听8080端口,在while循环里Server端阻塞在server.accept上,即等待请求传到8080端口上,从accept方法返回。后续new Thread是新建一个线程去处理请求。
public class TimeServer {
public static void main(String[] args) throws IOException {
int port = 8080;
ServerSocket server = null;
try {
server = new ServerSocket(port);
System.out.println("The server start in port "+port);
Socket socket = null;
while (true){
socket = server.accept();
Thread thread = new Thread(new TimeServerHandler(socket));
thread.start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (server!=null){
System.out.println("Time server close");
server.close();
server=null;
}
}
}
}
执行代码用jstack打印线程状态,看到server线程在17行“停止”,即阻塞在17行等待请求传过来。这种阻塞就是BIO里的B,block,一个IO没有完成就一直卡在那里。
有新的客户端接入新建线程执行处理方法,通过检查传过来的字符串是否是要求的“QUERY TIME ORDER”,如果是就返回当前服务器的时间,否则返回错误信息。
public class TimeServerHandler implements Runnable { private Socket socket; public TimeServerHandler(Socket socket) { this.socket = socket; } public void run() { BufferedReader in = null; PrintWriter out = null; try { in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(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 Date(System.currentTimeMillis()).toString():"BAD ORDER"; out.println(currentTime); } } catch (IOException e) { e.printStackTrace(); if (in!=null){ try { in.close(); } catch (IOException ex) { ex.printStackTrace(); } } if (out!=null){ out.close(); out = null; } if (socket!=null){ try { socket.close(); } catch (IOException ex) { ex.printStackTrace(); }finally { socket=null; } } } } }
同时开启服务端和客户端后可以在控制台看到输出。
2.1.2 缺点
- 每个请求都需要一个新建线程去处理,扛不住太大的并发,因为没有给线程数目设置瓶颈。
- BIO会在网络不佳情况导致大量线程。
2.2 伪异步IO编程
2.2.1 代码
用线程池代替不停的新建线程,好处是线程池是有界的,避免在极端情况下不停新建线程。
public class TimeServer_ThreadPool { public static void main(String[] args) { int port = 8080; ServerSocket server = null; try { server = new ServerSocket(port); System.out.println("server start at port "+ port); Socket socket = null; ExecutorService pool = Executors.newFixedThreadPool(10); while (true){ socket = server.accept(); pool.execute(new TimeServerHandler(socket)); } } catch (IOException e) { e.printStackTrace(); } } }
2.2.2 弊端
- 使用BIO读取数据时,线程会一直阻塞直到 1、有数据读 2、数据读取完毕 3、抛出异常。当客户端请求发送较慢或者网络时延较时,读取数据的线程会一直阻塞。
- 使用BIO输出数据时,线程会一直阻塞直到 1、有数据写 2、数据写完 3、抛出异常。当服务端写数据时,如果网络情况不佳,客户端不能及时读取数据,大量数据留在TCP缓冲区,当发送端即服务端的Window size为0的时候写线程就会无法继续写从而阻塞。
- 无论读还是写,都是阻塞的,阻塞与否以及阻塞是否严重依赖于网络传输的质量。
- 当线程池的队列使用阻塞队列时,前台线程负责把请求封装成对象加入线程池的阻塞队列,如果网络状况十分的差,阻塞队列也满了,那么复制把请求对象加入阻塞队列的前台线程也会阻塞,整个系统失去异步性,所有的请求都会超时。
2.3 NIO 编程
NIO与BIO的区别在两点
- 面向的对象不同。BIO面向Stream,该Stream是单向通信,只能是读Stream或者写Stream。NIO面向Buffer,NIO面向buffer和channel,channel是铁路,buffer是铁路上运输的数据。
- 阻塞性。BIO是阻塞的,NIO通过Selector实现多路复用。
2.3.1 Buffer与Channel