java网络编程中的BIO,NIO,AIO
BIO,是指Blocking IO阻塞的io,是针对socket编程从网络上读写操作而言的。
对于传统的IO,在socket编程中,read,write,connect服务端的accept都会产生阻塞。
因此,在jdk1.4之前,如果遇到并发请求。那么就需要开启多个线程,每个线程处理一个网络请求。
这就会造成问题
线程的增多,会造成cpu压力增大
线程增大,会导致线程栈增多,因此会占用大量的内存。
然而这些线程,并不是都在请求,所以会造成cpu,内存的浪费。
利用线程池确实可以解决,但是线程池也是有数量的。假设有5个线程,那如果这5个线程同时阻塞了。那么另外的线程就又在等待了。
基于上诉问题。NIO出现了。
NIo首先设计了Channel,使得socket的accept方法不在阻塞,然后将所有的通道(这里是socket在NIO中是通道)注册在一个多路复用器中,Selector。这个多路复用器会阻塞调用select方法。这个方法会返回所有有事件的通道个数,如果个数为0,则阻塞。直到有活跃通道。这里的事件是指,连接,读,写等等。然后再根据事件,处理通道。因此,这个可以单线程处理所有客户端。第一,服务器监听客户端连接,不在阻塞。第二,采用了事件触发。只有当有事件的通道,cpu才会去执行。没有事件,就一直轮训。这里的事件包括客户端连接服务器。即accept。
但是这模式也有弊端。第一,如果有长连接,单线程下,在处理这个长连接的同时,是不能处理其他通道的读写事件和监听事件的。如果通道的连接处理时间过长,仍然会产生阻塞。这时就产生了多线程NIO,将通道处理的逻辑放在线程池中去做。
这种方法仍然有弊端,就是select轮询所有的注册通道,因为有的通道注册了,但是并没有事件,如果连接数很多,依然会造成性能浪费。依然不能最大化的压榨服务器性能。所以产生了另外的react方式。
bio是传统的阻塞,等待通信,实现多客户端需要采用循环监听加多线程读写。子线程阻塞不影响主线程监听。主线程监听阻塞不影响子线程读写。否则,监听的阻塞,在没监听到客户端连接的情况下,程序阻塞,将无法读取已监听的客户端的读写。或者在读写已经连接的客户端时候,程序阻塞,因此无法监听新的客户端
nio是采用了事件触发模式。首先监听不在阻塞。此时在没有监听到客户端连接的情况下,不影响已连接客户端的通信。第二,把客户端通道设为非阻塞,在客户端没有读到内容时,将不再阻塞,因此程序会继续循环监听。所以,如果直接读取内容,一旦客户端没有发送数据,程序将继续进行,不在阻塞等待。此时如果另外的客户端连接上来,那么前一个通道将被覆盖。因此,需要不断的循环监听同时,将连接到的客户端放入List。然后轮询list,每当拿出一个channel,就read,如果读到内容就处理,读不到就继续下一个,因为是非阻塞的。这时就实现了多路复用。
上面的多路复用是我们自己实现的。效率比较低。jvm采用了poll,select,epoll来让操作系统实现这个轮询。一旦发现有读写操作的channel,就会返回活跃的通道个数,并且有selectedkeys返回活跃通道集合,然后我们根据通道注册的事件,来进行下一步的处理。就不用轮询所有的channel了。
package NIO; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; public class ServerTest { private Selector selector=null; private int port=9999; public void init(){ try { selector=Selector.open(); System.out.println("多路复用器准备就绪。。。"); ServerSocketChannel serverSocketChannel=ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(port)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("服务端通道已注册。。。"); } catch (IOException e) { e.printStackTrace(); } } public void run(){ while (true) { try { if (!(selector.select()>0)) { System.out.println("没有活跃通道!"); break; } System.out.println("有"+selector.select()+"条活跃通道"); for(SelectionKey sk:selector.selectedKeys()){ selector.selectedKeys().remove(sk); if(sk.isAcceptable()){ System.out.println("服务端通道有连接事件"); SocketChannel socketChannel=((ServerSocketChannel)sk.channel()).accept(); socketChannel.configureBlocking(false); socketChannel.register(selector,SelectionKey.OP_READ); System.out.println("客户端通道注册完成!"); } if(sk.isReadable()){ System.out.println("有活跃通道读事件"); ByteBuffer byteBuffer=ByteBuffer.allocate(1024); String content=""; try { while (((SocketChannel) sk.channel()).read(byteBuffer) > 0) { byteBuffer.flip(); content += Charset.defaultCharset().decode(byteBuffer); } System.out.println(content); }catch (Exception e){ sk.cancel(); if(sk.channel()!=null){ sk.channel().close(); } } } } } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) { ServerTest ser=new ServerTest(); ser.init(); ser.run(); } }
代码中的
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
的open方法,
而产生provider需要调用create方法,create方法实现如下
最终调了poll0方法,poll0在操作系统中调用了Windows的select方法。这是在Windows系统下的
如果是在linux下,则会掉epoll。
这两个方法,可以将注册在多路复用器中的channel,交给操作系统内核,然后他的轮询就交由操作系统来做。因此效率比较高。
另外NIO还有一个特性是0拷贝,也是效率比较高。
上面介绍的BIO和NIO都是同步IO,因为IO分为两部分,1,程序发出IO请求。2完成实际的IO操作。对于前面的两种阻塞和非阻塞,是针对于第一步,划分的。如果发出请求会阻塞线程,则为阻塞IO,如果没有阻塞线程,则为非阻塞IO。但是同步IO与异步IO的区别在第二步,如果实际的IO操作由操作系统完成,将结果返回给应用程序,这就是异步IO。如果实际的IO需要应用程序本身区执行,会阻塞线程,那就是同步IO。BIO和NIO都是同步IO,NIO是同步非阻塞IO
异步IO是采用
AsynchronousServerSocketChannel ,
AsynchronousSocketChannel
两个通道来完成。服务端通道的accept方法有两种一种是普通,另一种需要传递
AsynchronousChannelGroup
例子如下
public class server02 { static List<AsynchronousSocketChannel>channelList=new ArrayList<AsynchronousSocketChannel>(); public static void main(String[] args) { ByteBuffer byteBuffer=ByteBuffer.allocate(1024); ExecutorService executorService= Executors.newFixedThreadPool(80); try { AsynchronousChannelGroup channelGroup=AsynchronousChannelGroup.withThreadPool(executorService); AsynchronousServerSocketChannel serverSocketChannel= AsynchronousServerSocketChannel.open(channelGroup); serverSocketChannel.bind(new InetSocketAddress(8888)); System.out.println("开启监听。。。。"); serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() { @Override public void completed(AsynchronousSocketChannel sc, Object o) { System.out.println("连接成功,接收数据。。。"); channelList.add(sc); serverSocketChannel.accept(null,this); sc.read(byteBuffer, null, new CompletionHandler<Integer, Object>() { @Override public void completed(Integer integer, Object o) { byteBuffer.flip(); System.out.println("接收到数据:"); System.out.println(Charset.defaultCharset().decode(byteBuffer)); System.out.println("发送给其他客户端"); for (var ch:channelList ) { try { ch.write(byteBuffer).get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } byteBuffer.clear(); sc.read(byteBuffer,null,this); } @Override public void failed(Throwable throwable, Object o) { System.out.println("读取数据失败:"+throwable); channelList.remove(sc); } }); } @Override public void failed(Throwable throwable, Object o) { System.out.println("连接失败:"+throwable+"!"); } }); Scanner scanner=new Scanner(System.in); scanner.nextInt(); } catch (Exception e) { e.printStackTrace(); } }