演进之路
JDK1.4推出Java NIO之前,基于java的所有Socket通信都采用了同步阻塞模式BIO,对于这种一个请求,一个应答的通信模型简化了上层的应用开发,但是在性能和可靠性方面却存在着巨大的瓶颈。当并发访问量增大,响应时间延迟增大之后,采用Java BIO开发的服务端软件只有通过硬件的不断扩容来满足高并发和低延迟。
从JDK1.0到JDK1.3,Java的I/O类库都非常原始,很多UNIX网络编程中的概念或者接口在I/O类库中都没有体现,例如:Pipe,Channel,Buffer和Selector等。JDK1.4新增的功能:
进行异步I/O操作的缓冲区ByteBuffer,管道Pipe,Channel,包括ServerSocketChannel和SocketChannel;
多种字符集的编解码能力,实现非阻塞I/O操作的多路复用器selector,文件通道FileChannel。
新的NIO类库的提供,极大地促进了基于Java的异步非阻塞编程的发展和应用,但是它依然有不完善地方,特别对文件系统的处理仍显不足,问题如下:
没有统一的文件属性如文件读写属性;
API能力比较弱,如目录的级联创建和递归遍历,往往需要自己实现;
底层存储系统的一些高级API无法使用;
所有的文件操作都是同步阻塞调用,不支持异步文件读写操作;
JDK1.7正式发布,它的一个比较大的亮点就是将原来的NIO类库进行了升级,称为NIO2.0。做了如下改进:
提供能够批量获取文件属性的API;
提供AIO功能,支持基于文件的异步I/O操作和针对网络套件字的异步操作;
完成JSR-51定义的通道功能,包括对配置和多播数据报的支持等。
NIO介绍
网络编程的基于模型就是Client/Server模型,也就是两个进程之间相互通信。其中服务端提供位置信息(绑定IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,就是面向socket编程。
BIO:服务端通常由一个独立的Acceptor线程负责监听客户端的连接,接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。使用Socket类和ServerSocket类编程
NIO:由于之前老的I/O类库是阻塞I/O,New I/O类库的目标就是要让Java支持非阻塞I/O。提供了SocketChannel和ServerSocketChannel套接字通道实现。
NIO类库简介
缓冲区Buffer:NIO库中所有数据都是用缓冲区处理的。读写数据,都是进入缓冲区。其实质就是一个数组。提供了对数据的结构化访问以及维护读写位置等信息。最常用的缓冲区:ByteBuffer,CharBuffer,ShortBuffer等。
通道Channel:Channel如一个通道,可以通过它读取和写入数据。通道和流的不同之处在于通道是双向的,流只是在一个方向上移动(InputStream输入流,OutputStream输出流),而且通道可以用于读,写或者同时用于读写。可以分为两大类:分别是用于网络读写的SelectableChannel和用于文件操作的FileChannel。
多路复用器Selector:Selector是Java NIO编程的基础,熟练地掌握Selector对于掌握NIO编程至关重要。Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入,读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来。
NIO实例对比
同步阻塞IO(BIO)例子:
Server端:
package com.java.nio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by liuzhengqiu on 2017/10/14.
*/
public class NioTestServer
{
public static void main(String[] args) throws IOException {
int port = 8080;
ServerSocket serverSocket = null;
try
{
serverSocket = new ServerSocket(port);
Socket socket = null;
while (true)
{
socket = serverSocket.accept();
new Thread(new NioServerHandler(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
serverSocket.close();
serverSocket = null;
}
}
}
package com.java.nio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/**
* Created by liuzhengqiu on 2017/10/14.
*/
public class NioServerHandler implements Runnable
{
private Socket socket;
public NioServerHandler(Socket socket)
{
this.socket = socket;
}
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 body = null;
while (true)
{
body = in.readLine();
if (body == null) break;
System.out.println("receive text:"+body);
out.println("welcome to my world.");
}
}catch (Exception e)
{
try {
in.close();
} catch (IOException e1) {
e1.printStackTrace();
}
out.close();
}
}
}
Client端:
package com.java.nio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* Created by liuzhengqiu on 2017/10/14.
*/
public class NioTestClient
{
public static void main(String[] args) throws IOException {
int port = 8080;
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("hello my name is shishiqiuqiu");
String resp = in.readLine();
System.out.println("Now is :" + resp);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
in.close();
out.close();
socket.close();
}
}
}
---------------------------------------------------
伪异步I/O编程,对线程模型进行优化,后端通过一个线程池来处理多个客户端的请求接入,通过线程池灵活的调配线程资源。
客户端代码与上相同,服务端代码如下:
package com.java.nio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by liuzhengqiu on 2017/10/14.
*/
public class FicNioTestServer
{
public static void main(String[] args) throws IOException {
int port = 8080;
ServerSocket sever = null;
try
{
sever = new ServerSocket(port);
Socket socket = null;
NioServerHandlerExecutePool singleExecutor = new NioServerHandlerExecutePool(50,100);
while (true)
{
socket = sever.accept();
singleExecutor.execute(new NioServerHandler(socket));
}
}catch (Exception e)
{
}
finally {
sever.close();
sever = null;
}
}
}
package com.java.nio;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Created by liuzhengqiu on 2017/10/14.
*/
public class NioServerHandlerExecutePool
{
private ExecutorService executorService;
public NioServerHandlerExecutePool(int maxPoolSize,int queueSize)
{
executorService = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
maxPoolSize, 120L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(queueSize));
}
public void execute(Runnable task)
{
executorService.execute(task);
}
}
当对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞也就是accpet()事件,在此期间,其他接入消息只能在消息队列中排队。
----------------------------------------------------------
NIO实例如下:
服务端代码:
package com.java.nio;
/**
* Created by liuzhengqiu on 2017/9/14.
*/
public class TimeServer {
public static void main(String[] args)
{
int port = 8080;
MultiplexerTimeServer multiplexerTimeServer = new MultiplexerTimeServer(port);
new Thread(multiplexerTimeServer,"NIO-MultiplexerTimeServer-001").start();
}
}
package com.java.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.Date;
import java.util.Iterator;
import java.util.Set;
/**
* Created by liuzhengqiu on 2017/9/14.
*/
public class MultiplexerTimeServer implements Runnable
{
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private volatile boolean stop;
public MultiplexerTimeServer(int port)
{
try {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(port),1024);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("The time server is start in port:" + port);
} catch (IOException e) {
e.printStackTrace();
}
}
public void stop()
{
this.stop = true;
}
@Override
public void run() {
while (!stop)
{
try {
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
SelectionKey key = null;
while(it.hasNext())
{
key = it.next();
it.remove();
handleInput(key);
}
} catch (IOException e) {
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);
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";
doWrite(sc,currentTime);
}
else if (readBytes < 0)
{
key.cancel();
sc.close();
}
else
;
}
}
}
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.java.nio;
/**
* Created by liuzhengqiu on 2017/9/14.
*/
public class TimeClient
{
public static void main(String[] args)
{
int port = 8080;
new Thread(new TimeClientHandle("127.0.0.1",port),"TimeClient-001").start();
}
}
package com.java.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;
/**
* Created by liuzhengqiu on 2017/9/14.
*/
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 {
this.selector = Selector.open();
this.socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
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
;
}
}
}
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 succeed");
}
}
@Override
public void run()
{
try
{
doConnect();
}catch (IOException e)
{
System.exit(1);
}
while (!stop)
{
try
{
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
SelectionKey key = null;
while (it.hasNext())
{
key = it.next();
it.remove();
try {
handleInput(key);
}catch (Exception e)
{
key.cancel();
key.channel().close();
}
}
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
}
}
AIO是NIO2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。异步通道提供两种方式获取操作结果。
通过java.util.concurrent.Future类来表示异步操作结果;
在执行异步操作的时候传入一个java.nio.channels;
NIO2.0的异步套接字通道是真正的异步非阻塞I/O,它不需要通过多路复用器(selector)来注册的通道进行轮询操作即可实现异步读写,从而简化了NIO的编程模型。
不同I/O的对比
1,异步非阻塞I/O
JDK1.4提供的NIO框架称为异步非阻塞I/O,但是,如果严格按照UNIX网络编程模型和JDK的实现进行区分,实际上它只能被称为非阻塞I/O,不能叫做异步非阻塞I/O。
JDK1.5优化了Selector的实现,它在底层使用epoll替换了select/poll。
JDK1.7提供的NIO2.0,新增了异步的套件字通道,它是真正的异步I/O,通过传递信号变量完成操作,当操作完成之后会回调相关方法,异步I/O也被称为AIO。
2,多路复用器Selector
多路复用的核心就是通过Selector来轮询注册在其上的Channel,当发现某个或者多个Channel处于就绪状态后,从阻塞状态返回就绪的Channel的选择集合,进行I/O操作。
3,伪异步I/O
完全来源于实践。在通信线程和业务线程之间做个缓冲区,用于隔离I/O线程和业务线程间的直接访问。
同步阻塞I/O(BIO) | 伪异步I/O | 非阻塞I/O(NIO) | 异步I/O(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使用难度 | 简单 | 简单 | 非常复杂 | 复杂 |
调试难度 | 简单 | 简单 | 复杂 | 复杂 |
可靠性 | 非常差 | 差 | 高 | 高 |
吞吐量 | 低 | 中 | 高 | 高 |