IO是什么
IO可以简单地理解为是计算机与硬盘之间进行通讯一种方式,通过输入输出流来进行数据的交互。在java中存在了几种IO的接口,如常见的字节流和字符流。
- InputStream :字节输入流
- OutputStream :字节输出流
- Reader :字符输入流
- Writer :字符输出流
在java应用程序层面通过调用native方法调用操作系统,从硬盘中或者internet中进行对数据的存取操作。
IO的几种类型
在IO的发展史中大致存在了以下几种类型的IO操作。
在java发展之初性能并不需要维持很大的并发量,因此原始的IO是一种同步并阻塞的IO。在程序=>操作系统=>硬盘层面只能保证一个线程的操作,并不允许多个线程同时进行连接。
随着业务的发展与性能要求的提升,逐渐演变出了一种通过线程池创建的多线程连接,即同步非阻塞的IO,此时会有一个守护线程持续不断的访问硬件层面是否返回成功,返回成功则告诉应用程序。
后来由于同步仍然存在部分的阻塞,又演变为一种异步非阻塞的IO,即NIO,此时会将所有连接都阻塞在一个单线程的队列中,开启一个线程来判断是否需要通信,如果需要则修改连接状态进行通讯,并异步返回。
同步并阻塞的BIO
首先我们来看看初始版本的BIO,只通过一个线程进行同步并阻塞的BIO。
public class BioServer {
public static void main(String[] args) {
try(ServerSocket serverSocket = new ServerSocket(8888)){
System.out.println("BioServer has started。Listener on port:"+serverSocket.getLocalSocketAddress());
while (true){
Socket clientSocket = serverSocket.accept();
System.out.println("Connection from "+clientSocket.getRemoteSocketAddress());
try (Scanner input = new Scanner(clientSocket.getInputStream())){
while (true){
String request = input.nextLine();
if("quit".equals(request)){
break;
}
System.out.println(String.format("From %s : %s",clientSocket.getRemoteSocketAddress(),request));
String response = "From BioServer Hello "+request+". \n";
clientSocket.getOutputStream().write(response.getBytes());
}
}
}
}catch (Exception e){
}
}
}
此时当一个连接占据时其他连接都无法获取socket连接,造成的极大地不便,当多个用户进行操作时效率会变得很低。
通过线程池实现同步非阻塞的BIO
此时为了解决多用户的连接请求,演化出线程池实现多个socket连接。
//处理客户端请求数据
public class RequestHandler {
public String handle(String request){
return "From BioServer Hello "+request+". \n";
}
}
public class BioServerThreadPool {
public static void main(String[] args) {
ExecutorService executor= Executors.newFixedThreadPool(3);
RequestHandler requestHandler = new RequestHandler();
try(ServerSocket serverSocket = new ServerSocket(9999)){
System.out.println("BioServer has started。Listener on port:"+serverSocket.getLocalSocketAddress());
while (true) {
Socket clientSocket = serverSocket.accept();
executor.submit(new ClientHandler(clientSocket,requestHandler));
System.out.println("Connection from " + clientSocket.getRemoteSocketAddress());
}
} catch (Exception e) {
}
}
}
public class ClientHandler implements Runnable {
private final Socket clientSocket;
private final RequestHandler requestHandler;
public ClientHandler(Socket clientSocket, RequestHandler requestHandler) {
this.clientSocket = clientSocket;
this.requestHandler = requestHandler;
}
@Override
public void run() {
try (Scanner input = new Scanner(clientSocket.getInputStream())){
while (true){
String request = input.nextLine();
if("quit".equals(request)){
break;
}
System.out.println(String.format("From %s : %s",clientSocket.getRemoteSocketAddress(),request));
String response = requestHandler.handle(request);
clientSocket.getOutputStream().write(response.getBytes());
}
}catch (Exception e){
throw new RuntimeException();
}
}
}
此时虽然使用了多线程,但是线程池的数量仍然是一个不好处理的东西,多了可能系统硬件承受不住,少了可能会造成溢出,或者无法合理运用系统资源,而选择一个合适的线程池数量则是一个头疼的问题,与此同时线程持续的进行频繁交互,并且在读取数据时会造成部分阻塞,也会造成性能问题,怎么样解决这些问题呢?
异步非阻塞的NIO的实现
基于以上讨论,出现了一种异步非阻塞的NIO模式。
这种方式会将所有连接阻塞在一个叫Channel的队列中,并将状态设置为accept,此时会用一个守护线程selector持续轮询,如果某个连接需要进行IO操作,则会将此连接状态修改为read或者write进行读写,同时从队列中弹出符合条件的连接进行IO操作,以此达到异步非阻塞的效果。
public class NioServer {
public static void main(String[] args) throws Exception {
//01、创建一个服务端Channel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
//设置为非阻塞
serverChannel.configureBlocking(false);
//serverchannel需要绑定一个端口
serverChannel.bind(new InetSocketAddress(6666));
System.out.println("BioServer has started。Listener on port:"+serverChannel.getLocalAddress());
//02、Selector:专门用来进行轮询,判断socket的状态
Selector selector = Selector.open();
//将一个个channel注册到Selector,channel初始状态
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
//ByteBuffer进行数据临时存储
ByteBuffer buffer = ByteBuffer.allocate(1024);
RequestHandler requestHandler = new RequestHandler();
//对selector里面的channel进行轮询,判断谁需要后续的IO操作
while(true){
int select = selector.select();
if (select==0){
continue;
}
//selector中有channel
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
//selectionKey中保存了channel的各种信息
SelectionKey key = iterator.next();
//假如channel的状态是readable
if(key.isAcceptable()){
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = channel.accept();
System.out.println("Connection from"+clientChannel.getRemoteAddress());
clientChannel.configureBlocking(false);
//将channel改变状态:read、write
clientChannel.register(selector,SelectionKey.OP_READ);
}
if (key.isReadable()){
//数据交互
SocketChannel channel = (SocketChannel) key.channel();
//将客户端的数据读取到buffer中
channel.read(buffer);
String request = new String(buffer.array()).trim();
buffer.clear();
System.out.println(String.format("From %s : %s",channel.getRemoteAddress(),request));
String response = requestHandler.handle(request);
channel.write(ByteBuffer.wrap(response.getBytes()));
}
iterator.remove();
}
}
}
}
NIO的使用场景
因为NIO的性能比较高效,一些框架会基于NIO进行api的封装,典型的有Netty,而Dubbo的rpc协议又是基于Netty进行封装的。因此NIO在一些频繁IO操作高性能框架中使用的还是挺多的。