IO是构建程序的一个非常重要的一环。一个好的IO能够大大的提高程序的运行效率。IO分为BIO、NIO、AIO三种。接下来我们挨个详细讲解。
IO详解
1、BIO
BIO的全名是Blocking IO,就是阻塞式IO。BIO是Java中最古老的IO工具,Java出来的时候就有了。
服务器对应的是一个ServerSocket,它可以监听客户端发来的连接请求,然后为客户端创建一个Socket,客户端本身也有一个Socket,客户端和服务器就利用这一对Socket来传递数据,读取和写入是通过数据流实现的。
由于利用Socket来接收数据是会阻塞的,因为不是所有时间都会有数据传来的,没有数据传来的时候,socket会阻塞,这时服务器端对于一个客户端请求会分配一个线程Thread,然后在线程中阻塞的获取客户端发来的数据。这样的话有三个问题:
1、线程的创建和销毁是一个重量级操作。除了为线程分配内存、初始化等重量级操作外,还牵扯到用户态切换到内核态,需要安全检测以及用户栈运行状态信息的保存等操作,比较消耗资源。
2、线程本身比较占内存,线程对应的有虚拟机栈、本地方法栈等信息,如果系统中存在成千上万的线程,会极大的占用JVM的内存。
3、线程之间的切换的成本高,需要保存线程的运行状态信息,同时还会导致用户态切换到内核态。
代码:
public class EchoServer {
public static void main(String[] args) throws IOException {
//客户端ServerSocket用于接收客户端的申请
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
//阻塞的接受客户端连接
Socket socket = serverSocket.accept();
//为每一个客户端新建thread来获取数据
new Thread(()->{
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg;
//阻塞的获取数据,客户端没有发送数据就阻塞
while ((msg = reader.readLine()) != null) {
System.out.println("receive msg: " + msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
2、NIO
NIO的全称是New IO,是jdk1.4更新中引入的。NIO利用多路复用技术,改用单线程来处理多任务,改善了BIO的多线程浪费资源的问题。
NIO也是阻塞的。
NIO有三大核心:
1、Selector
Selector是事件分发器,客户端发送的写数据、读数据、连接等操作都是发送到Selector上的。Seletor调用select()方法来获取对应的所有客户端的操作,如果不存在就阻塞。
2、Channel
Channel分为ServerSocketChannel、SocketChannel。ServerSocketChannel是针对连接请求的,用于接收客户端的连接请求,然后为客户端初始化对应的SocketChannel。客户端本身也拥有一个SocketChannel,客户端和服务器就利用这一对SocketChannel来传递信息。
ServerSocketChannel和SocketChannel都需要注册到Selector中,这样Selector才会将其事件读取过来。
ServerSocketChannel注册的是ACCEPT类型的事件,而SocketChannel注册的是READABLE的事件。
3、Buffer
当客户端和服务器发送和读取数据的时候,将数据保存到哪呢?和BIO不同的是,NIO采用的是Buffer,缓冲池,将读取的数据和写入的数据存储在Buffer中。
Tip
1、一个Selector可以同时注册多个ServerSocketChannel。
代码
public class EchoServer {
public static void main(String[] args) throws IOException {
//事件分发器
Selector selector = Selector.open();
//服务器Channel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口号
serverSocketChannel.bind(new InetSocketAddress(8080));
//是否阻塞的进行read()、write()
serverSocketChannel.configureBlocking(false);
// 将ServerSocketChannel利用accept事件绑定到selector上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 阻塞的获取客户端发来的事件
selector.select();
//获取事件set,SelectionKey对应的就是客户端发来的事件信息
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 遍历selectKeys
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 如果是客户端连接事件
if (selectionKey.isAcceptable()) {
//客户端想要连接的ServerSocketChannel
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
//为客户端创建SocketChannel
SocketChannel socketChannel = ssc.accept();
System.out.println("accept new conn: " + socketChannel.getRemoteAddress());
//非阻塞的read()和write()
socketChannel.configureBlocking(false);
//将SocketChannel注册到selector上,类型为read
socketChannel.register(selector, SelectionKey.OP_READ);
//如果是客户端发来数据
} else if (selectionKey.isReadable()) {
// 获取对应的SocketChannel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将数据读入到buffer中
int length = socketChannel.read(buffer);
if (length > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
// 将数据读入到byte数组中
buffer.get(bytes);
String content = new String(bytes, "UTF-8").replace("\r\n", "");
if (content.equalsIgnoreCase("quit")) {
selectionKey.cancel();
socketChannel.close();
} else {
System.out.println("receive msg: " + content);
}
}
}
//将事件从set中移除,因为已经处理过了
iterator.remove();
}
}
}
}
NIO通过事件分发机制,达到了单线程就可以处理多任务的效果,节省了资源。但是NIO也有缺点:
1、如果并发量大的话,响应时间会变慢,因为是单线程,需要一个一个遍历。
2、如果有一个客户端的处理过程较慢,会影响其他客户端的操作,因为是串行处理的。
3、AIO
AIO的全称是Asynchronous IO,是jdk1.7更新引入的。AIO是异步IO,采用回调函数来异步调用,调用完IO后线程会继续执行。当IO完成后会自动调用回调函数。
由于linux系统底层不支持AIO,所以AIO被用的很少,不在仔细介绍了。