本篇博客打算总结一下BIO,NIO和AIO的简单区别,其中对于AIO不会重点介绍,只会简单提及部分,不做实例演示。本文博客中的实例参照《Netty 权威指南》一书,关于BIO与NIO的理论描述,参照了《Netty、Redis、Zookeeper高并发实战》一书。
底层IO的读写,均会调用底层的read和write两大系统调用(不同系统中IO读写的系统调用名称不同,但基本功能都差不多)。
read——把数据从内核缓冲区复制到进程缓冲区(这里涉及一个概念,内核缓冲区和进程缓冲区,可以参看这篇博客: 用户进程缓冲区和内核缓冲区)。
例如:如果read操作是从一个socket中读取数据,则分为如下两个阶段:
第一个阶段:等待数从网络中达到网卡。当所等待的分组到达时,数据会从socket中复制到内核缓冲区中,这个操作由操作系统底层自己完成,对应用程序是无感知的。
第二个阶段:这个就是把数据从内核缓冲区复制到进程缓冲区。
总的来说read操作,分为两个步骤,第一个步骤就是内核等待外部文件的数据达到,如果数据完整之后,会自动复制到内核缓冲区。第二个步骤,就是数据从内核缓冲区复制到进程缓冲区。
write——把数据从进程缓冲区读到内核缓冲区。
操作系统只有一个内核缓冲区,每个进程有自己独立的缓冲区,这个就是进程缓冲区。基本的通信交互模型比较简单,如下所示
在正式介绍各个IO类型之前,还是先说一下阻塞IO和异步IO的区别
阻塞IO,其实指的是应用程序需要内核的IO操作完成之后,才能开始处理用户的操作。阻塞指的是用户程序的执行状态。在Java中,默认创建的Socket都是阻塞的。
非阻塞IO,与阻塞IO相对而言,非阻塞IO指的是用户空间的程序(应用程序)不需要等待内核IO操作完成,可以立即返回用户空间执行用户的操作,与此同时内核会立即返回用户程序一个状态。
关于同步IO与异步IO,《Netty、Redis、Zookeeper高并发实战》一书中介绍的是应用程序与内核空间IO发起方式不同。但是我个人并不能理解这个,我个人认为,其实就是普通的同步与异步的概念,只是对象变成了应用程序与操作系统内核而已。
默认情况下Socket是同步阻塞的,在阻塞模式中,Java应用程序从IO调用开始,直到系统调用返回,在这段时间内,Java进程都是阻塞的。直到返回成功,程序才能开始下一步操作。总之,阻塞IO的特点是,在内核进行IO执行的两个阶段,整个用户线程都被阻塞了。
如果单独讨论网络编程,BIO绝对是每一个学习网络编程的Helloworld程序,通过Socket与ServerSocket完成服务器与客户端的消息通信,这里依旧参照《Netty 权威指南》一书,以一个简单的时间回显的实例来进行说明。
具体代码如下
客户端的代码相对而言比较简单,无非就是实例化socket,根据socket获取输入输出流,然后将命令写入到输出中,然后从服务端读取客户端输入的数据。
/**
* autor:liman
* createtime:2020/8/11
* comment:Time client的客户端
*/
@Slf4j
public class TimeClient {
public static void main(String[] args) {
int port = 8999;
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
//初始化socket
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");
log.info("send time:{},send order to server succeed",LocalDateTime.now().toString());
//读取服务端的响应
String responseMessage = in.readLine();
log.info("server time is :{}", responseMessage);
} catch (Exception e) {
log.error("error ,error message:{}",e);
}finally {
//一些关闭流的操作
if(out!=null){
out.close();
}
if(in!=null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
服务端代码相对复杂点,通过一个while(true)循环,不断监听来自客户端的连接请求,然后在循环中处理客户端的数据,并将返回数据通过流的形式返回。
/**
* autor:liman
* createtime:2020/8/11
* comment:
* 参照Netty权威指南一书,BIO实例
*/
@Slf4j
public class TimeServer {
public static void main(String[] args) {
int port = 8999;
ServerSocket serverSocket = null;
try{
serverSocket = new ServerSocket(port);
log.info("Time server start in port:{}",port);
Socket socket = null;
//服务端不断循环,通过accept建立与客户端的连接,整个过程是阻塞的。
while (true) {
socket = serverSocket.accept();//建立连接
//构建输入输出流
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
String currentTime = null;
String body = null;
body = in.readLine();
if (body == null)//如果没有收到客户端的请求,则退出当前循环。
break;
log.info("this time server receive order:{}", body);
if ("QUERY TIME".equalsIgnoreCase(body)) {
Thread.sleep(10000);//睡眠10秒,模拟客户端的请求。
currentTime = LocalDateTime.now().toString();
} else {
currentTime = "BAD ORDER";
}
out.println(currentTime);
}
}catch (Exception e){
log.error("服务端读取信息异常,异常信息为:{}",e);
}finally {
//关闭serverSocket,这里省略其他流的关闭操作。
if(serverSocket!=null){
log.info("the time server close!");
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
启动一个服务端,然后启动两个客户端,运行日志如下:
服务端正常收到两次请求
第一个启动的客户端日志为
第二个启动的客户端日志为
从两个客户端日志可以看出,第二个客户端处理耗时接近20秒,而我们服务端每次请求会休眠10秒,可以看出,每次服务端处理客户端请求是阻塞式的,这就是传说中的阻塞式IO,也就称为BIO。
这里需要说明一下,同步非阻塞IO,这里可以简称为NIO,但是并不对应于Java中的NIO,虽然他们的英文缩写是一样的,但是并不是同一个事情,Java中的NIO是基于另一种模型——IO多路复用模型。
Socket连接模式是阻塞模式,在Linux系统下,可以通过设置将Socket变成非阻塞模式。
在内核缓冲区中没有数据的情况下,系统调用会立即返回,返回一个调用失败的信息。
在内核缓冲区中有数据的情况下,这个时候,客户端是阻塞的,直到获取数据的系统调用返回成功,应用程序才会开始处理内核的数据。
这种模式引入了一种新的系统滴啊用,查询IO的就绪状态,在Linux系统中,对应的系统调用为select/epoll系统调用,通过这个系统调用,一个进程可以监视多个文件描述符。一旦其中之一变成可读,则会将状态返回给应用程序。通过使用select/epoll系统调用,单个应用程序的线程,可以不断的轮询成百上千的socket连接。
Java中的NIO才对应了该IO模型,关于Java NIO,其实有三个关键的主键——selector,buffer,channel的部分,其中最为关键的其实是buffer的读写部分,这一部分在之前的博客中有过总结,可以参考这篇博客——NIO的三个关键组件。
启动类
/**
* autor:liman
* createtime:2020/8/12
* comment:启动类
*/
@Slf4j
public class TimeClient {
public static void main(String[] args) {
int port = 8999;
new Thread(new TimeClientHandler("127.0.0.1",port),"TimeClientThread-001").start();
}
}
真正的handler处理类
/**
* autor:liman
* createtime:2020/8/12
* comment:
*/
@Slf4j
public class TimeClientHandler implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop;
//构造函数中需要实例化SocketChannel,只有实例化完成的SocketChannel才能注册到Selector上。
public TimeClientHandler(String host, int port) {
this.host = host;
this.port = port;
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
} catch (Exception e) {
log.error("客户端初始化异常,异常信息为:{}",e);
}
}
@Override
public void run() {
try {
//先建立连接,没有建立连接,一切都免谈,毕竟这里是基于TCP的协议。
doConnect();
} catch (IOException e) {
e.printStackTrace();
}
//如果线程没有被中断,则一直轮询去遍历在selector上注册的事件
//然后根据不同的事件类型调用不同的处理逻辑。
while(!stop){
try {
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> selectorKeyIterator = selectionKeys.iterator();
SelectionKey key = null;
while(selectorKeyIterator.hasNext()){
key = selectorKeyIterator.next();
try{
handleInput(key);
}catch (Exception e){
if(key!=null){
key.cancel();
if(key.channel()!=null){
key.channel().close();
}
}
}
selectorKeyIterator.remove();
}
} catch (IOException e) {
e.printStackTrace();
}
}
if(selector!=null){
try{
selector.close();
}catch (Exception e){
log.error("流关闭异常,异常信息为:{}",e);
}
}
}
//selector上的注册事件处理逻辑。
private void handleInput(SelectionKey key) throws IOException {
//
if (key.isValid()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
if (key.isConnectable()) {
//如果连接建立
if (socketChannel.finishConnect()) {
//注册可读事件
socketChannel.register(selector, SelectionKey.OP_READ);
log.info("client connect to server ,client time is {}", LocalDateTime.now().toString());
//channel中写入数据
doWrite(socketChannel);
} else {
System.exit(1);
}
}
if (key.isReadable()) {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = socketChannel.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes,"UTF-8");
log.info("receive server message,Now is : {}",body);
this.stop = true;
}else if(readBytes < 0){
key.cancel();
}
socketChannel.close();
}
}
}
//与客户端建立连接,连接成功之后,需要将SocketChannel的可读事件注册到selector上。
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);
}
}
//往channel中写入数据
private void doWrite(SocketChannel socketChannel) throws IOException {
byte[] req = "QUERY TIME".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
socketChannel.write(writeBuffer);
if (!writeBuffer.hasRemaining()) {
log.info("send order 2 server succeed.");
}
}
}
服务端入口代码
/**
* autor:liman
* createtime:2020/8/12
* comment:时间服务器
*/
@Slf4j
public class TimeServer {
public static void main(String[] args) {
int port = 8999;
MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
new Thread(timeServer,"NIO-Time-HandlerServer-001").start();
}
}
服务端业务处理代码,这个代码和客户端的业务处理差不多。
/**
* autor:liman
* createtime:2020/8/12
* comment:
*/
@Slf4j
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注册到selector上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
log.info("The time server is start in port : {}", port);
} catch (IOException e) {
log.error("服务启动出行异常,异常信息为:{}", e);
System.exit(1);
}
}
public void stop() {
this.stop = true;
}
@Override
public void run() {
while (!stop) {
try {
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> keysIterator = selectionKeys.iterator();
SelectionKey key = null;
while (keysIterator.hasNext()) {
key = keysIterator.next();
try {
handleInput(key);
} catch (Exception e) {
log.error("通道出现异常,异常信息为:{}",e);
if(key!=null){
key.cancel();
if(key.channel()!=null){
key.channel().close();
}
}
}
keysIterator.remove();
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 处理Selector上的注册事件
* @param key
* @throws IOException
*/
private void handleInput(SelectionKey key) throws IOException {
String currentTime = null;
if (key.isValid()) {
if(key.isAcceptable()){
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
//服务端的SocketChannel需要通过serverSocketChannel.accept()来获取。
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
}
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = socketChannel.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
log.info("the time server receive order:{}", body);
if ("QUERY TIME".equalsIgnoreCase(body)) {
currentTime = LocalDateTime.now().toString();
} else {
currentTime = "BAD ORDER";
}
doWrite(socketChannel, currentTime);
} else if (readBytes < 0) {
key.channel();
}
socketChannel.close();
}
}
}
//往channel中写入buffer
private void doWrite(SocketChannel socketChannel, 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();
socketChannel.write(writeBuffer);
}
}
}
上述代码运行结束,不会出现BIO中阻塞的问题
AIO操作是对用户程序最友好的IO操作,用户线程在通过系统调用,向内核注册某个IO操作,内核在整个IO操作完成之后,主动通知用户程序。整个过程用户程序无需关注任何IO的状态,只需要等待操作系统内核告知IO处理完成即可。
Netty依旧使用的是IO多路复用的模型,在5.0左右的版本中曾经打算采用AIO,但是后来发现AIO可维护性并没有想象中的优秀,于是放弃了AIO的方式,目前Netty依旧采用的是IO的多路复用模型。因此关于AIO本篇博客也没有做过多的总结。