JavaIO详解--尽可能将BIO、NIO、AIO讲得通俗易懂
一、BIO概述
BIO即同步阻塞IO,实现模型为一个连接就需要一个线程去处理。这种方式简单来说就是当有客户端来请求服务器时,服务器就会开启一个线程去处理这个请求,即使这个请求不干任何事情,这个线程都一直处于阻塞状态。
BIO模型有很多缺点,最大的缺点就是资源的浪费。想象一下如果QQ使用BIO模型,当有一个人上线时就需要一个线程,即使这个人不聊天,这个线程也一直被占用,那再多的服务器资源都不管用
二、BIO代码实践
首先建立Server,建立一个ServerSocket对象,绑定端口,然后等待连接,如果连接成功就新建一个线程去处理连接。
public class server {
private static Socket socket=null;
public static void main(String[] args) {
try {
//绑定端口
ServerSocket serverSocket=new ServerSocket();
serverSocket.bind(new InetSocketAddress(8080));
while (true){
//等待连接 阻塞
System.out.println("等待连接");
socket = serverSocket.accept();
System.out.println("连接成功");
//连接成功后新开一个线程去处理这个连接
new Thread(new Runnable() {
@Override
public void run() {
byte[] bytes=new byte[1024];
try {
System.out.println("等待读取数据");
//等待读取数据 阻塞
int length=socket.getInputStream().read(bytes);
System.out.println(new String(bytes,0,length));
System.out.println("数据读取成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Client {
public static void main(String[] args) {
Socket socket= null;
try {
socket = new Socket("127.0.0.1",8080);
socket.getOutputStream().write("一条数据".getBytes());
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这样就实现了一个BIO,但是BIO的缺点实在太明显了,因此在JDK1.4的时候,NIO出现了。
三、NIO概述
BIO是阻塞的,如果没有多线程,BIO就需要一直占用CPU,而NIO则是非阻塞IO,NIO在获取连接或者请求时,即使没有取得连接和数据,也不会阻塞程序。NIO有几个知识点需要掌握,Channel(通道),Buffer(缓冲区), Selector(选择器(多路复用))。
Channel既可以用来进行读操作,又可以用来进行写操作。NIO中常用的Channel有FileChannel
、SocketChannel、ServerSocketChannel、DatagramChannel。
Selector 一般称为选择器或者多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。在javaNIO中使用Selector往往是将Channel注册到Selector中。
四、NIO代码实践
在BIO的代码中,有两处位置发生了阻塞:1.等待请求时,2.等待数据时
socket = serverSocket.accept();
int length=socket.getInputStream().read(bytes);
而NIO的代码就是要让这两处的阻塞变为非阻塞,Socket本身并不支持NIO,因为java创建了支持NIO的socket,SocketChannel和ServerSocketChannel。通过configureBlocking方法设置为非阻塞。
ServerSocketChannel是NIO中的通道,ByteBuffer是获取数据的缓冲区,多路复用机制我在这里使用for循环代替,而多路复用机制实际上是通过select/epoll/poll实现的,这需要涉及到底层的知识,当一个socket连接成功时,就将他放入集合中,并不断遍历这个集合观察是否有哪个socket发送了数据。
public class Server {
//list用来存放已经连接的socket
static List<SocketChannel> list=new ArrayList<>();
//缓冲区,用来获取数据
static ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
public static void main(String[] args) {
try {
//创建一个socketchannel,并绑定端口
ServerSocketChannel server=ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
//设置这个通道为非阻塞,对应于BIO的第一处阻塞
server.configureBlocking(false);
while (true){
//模拟多路复用器,不断查询是否有数据发过来
for (SocketChannel socketChannel1:list){
byteBuffer.clear();
int length = socketChannel1.read(byteBuffer);
if (length>0){
byteBuffer.flip();
System.out.println(new String(byteBuffer.array()));
}
}
//非阻塞的获取连接
SocketChannel socketChannel = server.accept();
//如果没有,则等待
if (socketChannel==null){
System.out.println("等待连接");
Thread.sleep(1000);
}else {
//如果获取到了一个连接,则把这个连接也设置为非阻塞,对应于BIO的第二处阻塞
socketChannel.configureBlocking(false);
//将设置连接放进list中,不断轮询是否有消息发出
list.add(socketChannel);
System.out.println("连接成功");
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class client {
public static void main(String[] args) {
try {
Socket socket=new Socket("127.0.0.1",8080);
Scanner scanner=new Scanner(System.in);
String str=scanner.next();
socket.getOutputStream().write(str.getBytes());
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
当启动服务器端时,不断输出等待连接,当启动客户端后,输出了连接成功,但此时并未阻塞程序,而是继续等待。
当有数据发送给服务器端时,则接收到了ByteBuffer的数据,程序的所有执行过程都处于非阻塞状态。
五、AIO概述
AIO是在JDK1.7中推出的新的IO方式--异步非阻塞IO,也被称为NIO2.0,AIO在进行读写操作时,直接调用API的read和write方法即可,这两种均是异步的方法,且完成后会主动调用回调函数。简单来讲,当有流可该取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当保作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。
Java提供了四个异步通道:AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel。
六、AIO代码实践
服务器端代码:AIO的创建方式和NIO类似,先创建通道,再绑定,再监听。只不过这里的监听方式用了类似递归的监听方式。在读取数据的操作中,read方法也和NIO有区别。
public class AIOServer {
public static void main(String[] args) {
try {
//创建异步通道
AsynchronousServerSocketChannel serverSocketChannel=AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
System.out.println("等待连接中");
//在AIO中,accept有两个参数,
// 第一个参数是一个泛型,可以用来控制想传递的对象
// 第二个参数CompletionHandler,用来处理监听成功和失败的逻辑
// 如此设置监听的原因是因为这里的监听是一个类似于递归的操作,每次监听成功后要开启下一个监听
serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
//请求成功处理逻辑
@Override
public void completed(AsynchronousSocketChannel result, Object attachment) {
System.out.println("连接成功,处理数据中");
//开启新的监听
serverSocketChannel.accept(null,this);
handledata(result);
}
@Override
public void failed(Throwable exc, Object attachment) {
System.out.println("失败");
}
});
try {
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void handledata(AsynchronousSocketChannel result) {
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//通道的read方法也带有三个参数
//1.目的地:处理客户端传递数据的中转缓存,可以不使用
//2.处理客户端传递数据的对象
//3.处理逻辑,也有成功和不成功的两个写法
result.read(byteBuffer, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
if (result>0){
attachment.flip();
byte[] array = attachment.array();
System.out.println(new String(array));
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("失败");
}
});
}
}
客户端代码基本上没有太多差别,可以使用同步的方式也可以使用异步方式,这里使用异步处理的方式:
public class AIOClient {
public static void main(String[] args) {
try {
AsynchronousSocketChannel socketChannel=AsynchronousSocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1",8080));
Scanner scanner=new Scanner(System.in);
String next = scanner.next();
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
byteBuffer.put(next.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}