本文收录于JavaStarter ,里面有我完整的Java系列文章,学习或面试都可以看看
(一)引言
IO流是Java中比较难理解的一个知识点,但是IO流在实际的开发场景中经常会使用到,比如Dubbo底层就是NIO进行通讯。本文将介绍Java发展过程中出现的三种IO:BIO、NIO以及AIO,重点介绍NIO。
(二)什么是BIO
BIO即同步阻塞IO,实现模型为一个连接就需要一个线程去处理。这种方式简单来说就是当有客户端来请求服务器时,服务器就会开启一个线程去处理这个请求,即使这个请求不干任何事情,这个线程都一直处于阻塞状态。
BIO模型有很多缺点,最大的缺点就是资源的浪费。想象一下如果QQ使用BIO模型,当有一个人上线时就需要一个线程,即使这个人不聊天,这个线程也一直被占用,那再多的服务器资源都不管用。
(三)BIO代码实践
我们通过socket模拟BIO的实现逻辑
首先建立Server,建立一个ServerSocket对象,绑定端口,然后等待连接,如果连接成功就新建一个线程去处理连接。
publicclassserver { privatestaticSocketsocket=null; publicstaticvoidmain(String[] args) { try { //绑定端口ServerSocketserverSocket=newServerSocket(); serverSocket.bind(newInetSocketAddress(8080)); while (true){ //等待连接 阻塞System.out.println("等待连接"); socket=serverSocket.accept(); System.out.println("连接成功"); //连接成功后新开一个线程去处理这个连接newThread(newRunnable() { publicvoidrun() { byte[] bytes=newbyte[1024]; try { System.out.println("等待读取数据"); //等待读取数据 阻塞intlength=socket.getInputStream().read(bytes); System.out.println(newString(bytes,0,length)); System.out.println("数据读取成功"); } catch (IOExceptione) { e.printStackTrace(); } } }).start(); } } catch (IOExceptione) { e.printStackTrace(); } } }
接着建立Client代码
publicclassClient { publicstaticvoidmain(String[] args) { Socketsocket=null; try { socket=newSocket("127.0.0.1",8080); socket.getOutputStream().write("一条数据".getBytes()); socket.close(); } catch (IOExceptione) { e.printStackTrace(); } } }
客户端的代码就连接一个服务器,然后发出一条数据即可。
这样就实现了一个BIO,但是BIO的缺点实在太明显了,因此在JDK1.4的时候,NIO出现了。
(四)什么是NIO
BIO是阻塞的,如果没有多线程,BIO就需要一直占用CPU,而NIO则是非阻塞IO,NIO在获取连接或者请求时,即使没有取得连接和数据,也不会阻塞程序。NIO的服务器实现模式为一个线程可以处理多个请求(连接)。
NIO有几个知识点需要掌握,Channel(通道),Buffer(缓冲区), Selector(多路复用选择器)。
Channel既可以用来进行读操作,又可以用来进行写操作。NIO中常用的Channel有FileChannel 、SocketChannel、ServerSocketChannel、DatagramChannel。
Buffer缓冲区用来发送和接受数据。
Selector 一般称为选择器或者多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。在javaNIO中使用Selector往往是将Channel注册到Selector中。
下面我通过代码的方式模拟javaNIO的运行流程。
(五)NIO代码实践
首先贴上NIO的实践代码:
NIO服务端详细的执行过程是这样的:
1、创建一个ServerSocketChannel和Selector,然后将ServerSocketChannel注册到Selector上
2、Selector通过select方法去轮询监听channel事件,如果有客户端要连接时,监听到连接事件。
3、通过channel方法将socketchannel绑定到ServerSocketChannel上,绑定通过SelectorKey实现。
4、socketchannel注册到Selector上,关心读事件。
5、Selector通过select方法去轮询监听channel事件,当监听到有读事件时,ServerSocketChannel通过绑定的SelectorKey定位到具体的channel,读取里面的数据。
publicclassNioServer { publicstaticvoidmain(String[] args) throwsIOException { //创建一个socket通道,并且设置为非阻塞的方式ServerSocketChannelserverSocketChannel=ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.socket().bind(newInetSocketAddress(9000)); //创建一个selector选择器,把channel注册到selector选择器上Selectorselector=Selector.open(); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true){ System.out.println("等待事件发生"); selector.select(); System.out.println("有事件发生了"); Iterator<SelectionKey>iterator=selector.selectedKeys().iterator(); while (iterator.hasNext()){ SelectionKeykey=iterator.next(); iterator.remove(); handle(key); } } } privatestaticvoidhandle(SelectionKeykey) throwsIOException { if (key.isAcceptable()){ System.out.println("连接事件发生"); ServerSocketChannelserverSocketChannel= (ServerSocketChannel) key.channel(); //创建客户端一侧的channel,并注册到selector上SocketChannelsocketChannel=serverSocketChannel.accept(); socketChannel.configureBlocking(false); socketChannel.register(key.selector(),SelectionKey.OP_READ); }elseif (key.isReadable()){ System.out.println("数据可读事件发生"); SocketChannelsocketChannel= (SocketChannel) key.channel(); ByteBufferbuffer=ByteBuffer.allocate(1024); intlen=socketChannel.read(buffer); if (len!=-1){ System.out.println("读取到客户端发送的数据:"+newString(buffer.array(),0,len)); } //给客户端发送信息ByteBufferwrap=ByteBuffer.wrap("hello world".getBytes()); socketChannel.write(wrap); key.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE); socketChannel.close(); } } }
客户端代码:NIO客户端代码的实现比BIO复杂很多,主要的区别在于,NIO的客户端也需要去轮询自己和服务端的连接情况。
publicclassNioClient { publicstaticvoidmain(String[] args) throwsIOException { //配置基本的连接参数SocketChannelchannel=SocketChannel.open(); channel.configureBlocking(false); Selectorselector=Selector.open(); channel.connect(newInetSocketAddress("127.0.0.1",9000)); channel.register(selector, SelectionKey.OP_CONNECT); //轮询访问selectorwhile(true){ selector.select(); Iterator<SelectionKey>iterator=selector.selectedKeys().iterator(); while (iterator.hasNext()){ SelectionKeykey=iterator.next(); iterator.remove(); //连接事件发生if (key.isConnectable()){ SocketChannelsocketChannel= (SocketChannel) key.channel(); //如果正在连接,则完成连接if (socketChannel.isConnectionPending()){ socketChannel.finishConnect(); } socketChannel.configureBlocking(false); ByteBufferbuffer=ByteBuffer.wrap("客户端发送的数据".getBytes()); socketChannel.write(buffer); socketChannel.register(selector,SelectionKey.OP_READ); }elseif (key.isReadable()){ //读取服务端发送过来的消息read(key); } } } } privatestaticvoidread(SelectionKeykey) throwsIOException { SocketChannelsocketChannel= (SocketChannel) key.channel(); ByteBufferbuffer=ByteBuffer.allocate(512); intlen=socketChannel.read(buffer); if (len!=-1){ System.out.println("客户端收到信息:"+newString(buffer.array(),0,len)); } } }
效果大概是这样的:首先服务端等待事件发生,当客户端启动时,服务器端先接受到连接的请求,接着接受到数据读取的请求,读完数据后继续等待。
客户端发送数据后,获取到了来自服务端的回复。
(六)NIO总结
NIO通过一个Selector,负责监听各种IO事件的发生,然后交给后端的线程去处理。NIO相比与BIO而言,非阻塞体现在轮询处理上。BIO后端线程需要阻塞等待客户端写数据,如果客户端不写数据就一直处于阻塞状态。而NIO通过Selector进行轮询已注册的客户端,当有事件发生时才会交给后端去处理,后端线程不需要等待。
(七)什么是AIO
AIO是在JDK1.7中推出的新的IO方式--异步非阻塞IO,也被称为NIO2.0,AIO在进行读写操作时,直接调用API的read和write方法即可,这两种均是异步的方法,且完成后会主动调用回调函数。简单来讲,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。
Java提供了四个异步通道:AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel。
(八)AIO代码实践
服务器端代码:AIO的创建方式和NIO类似,先创建通道,再绑定,再监听。只不过AIO中使用了异步的通道。
publicclassAIOServer { publicstaticvoidmain(String[] args) { try { //创建异步通道AsynchronousServerSocketChannelserverSocketChannel=AsynchronousServerSocketChannel.open(); serverSocketChannel.bind(newInetSocketAddress(8080)); System.out.println("等待连接中"); //在AIO中,accept有两个参数,// 第一个参数是一个泛型,可以用来控制想传递的对象// 第二个参数CompletionHandler,用来处理监听成功和失败的逻辑// 如此设置监听的原因是因为这里的监听是一个类似于递归的操作,每次监听成功后要开启下一个监听serverSocketChannel.accept(null, newCompletionHandler<AsynchronousSocketChannel, Object>() { //请求成功处理逻辑publicvoidcompleted(AsynchronousSocketChannelresult, Objectattachment) { System.out.println("连接成功,处理数据中"); //开启新的监听serverSocketChannel.accept(null,this); handledata(result); } publicvoidfailed(Throwableexc, Objectattachment) { System.out.println("失败"); } }); try { TimeUnit.SECONDS.sleep(Integer.MAX_VALUE); } catch (InterruptedExceptione) { e.printStackTrace(); } } catch (IOExceptione) { e.printStackTrace(); } } privatestaticvoidhandledata(AsynchronousSocketChannelresult) { ByteBufferbyteBuffer=ByteBuffer.allocate(1024); //通道的read方法也带有三个参数//1.目的地:处理客户端传递数据的中转缓存,可以不使用//2.处理客户端传递数据的对象//3.处理逻辑,也有成功和不成功的两个写法result.read(byteBuffer, byteBuffer, newCompletionHandler<Integer, ByteBuffer>() { publicvoidcompleted(Integerresult, ByteBufferattachment) { if (result>0){ attachment.flip(); byte[] array=attachment.array(); System.out.println(newString(array)); } } publicvoidfailed(Throwableexc, ByteBufferattachment) { System.out.println("失败"); } }); } }
客户端代码基本上没有太多差别,主要还是实现数据的发送功能
publicclassAIOClient { publicstaticvoidmain(String[] args) { try { AsynchronousSocketChannelsocketChannel=AsynchronousSocketChannel.open(); socketChannel.connect(newInetSocketAddress("127.0.0.1",8080)); Scannerscanner=newScanner(System.in); Stringnext=scanner.next(); ByteBufferbyteBuffer=ByteBuffer.allocate(1024); byteBuffer.put(next.getBytes()); byteBuffer.flip(); socketChannel.write(byteBuffer); } catch (IOExceptione) { e.printStackTrace(); } } }
观察结果: