IO模型
io模型就是各种数据使用相应通道进行发送和接收。Java共支持三种网络编程IO模式 BIO、NIO、AIO
BIO (Blocking IO)
同步阻塞模型,一个客户端连接对应一个处理线程。
缺点
1.IO代码里read是阻塞操作,如果连接不做读写操作会导致线程阻塞,浪费资源
2.如果读写很多,会导致服务器线程过多,压力太大。
应用场景
BIO适用于连接数目较小且固定的架构,这种方式对服务器资源的要求比较高,但是程序简单易理解。
示例代码
public class SocketServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9000); while (true){ System.out.println("等待连接..."); final Socket socket = serverSocket.accept(); System.out.println("有客户端连接..."); new Thread(new Runnable() { public void run() { try{ handler(socket); }catch (Exception e){ e.printStackTrace(); } } }).start(); } } public static void handler(Socket socket) throws IOException{ System.out.println("当前线程:"+Thread.currentThread().getId()); byte[] bytes = new byte[1024]; System.out.println("准备read..."); int read = socket.getInputStream().read(bytes); if(read!=-1){ System.out.println("接收到客户端的信息为:"+new String(bytes,0,read)); System.out.println("当前线程:"+Thread.currentThread().getId()); } socket.getOutputStream().write("hello client".getBytes()); socket.getOutputStream().flush(); } }
|
public class SocketClient { public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1",9000); socket.getOutputStream().write("Hello BIO".getBytes()); socket.getOutputStream().flush(); System.out.println("数据发送结束!"); byte[] bytes = new byte[1024]; socket.getInputStream().read(bytes); System.out.println("接收到的信息为:"+new String(bytes)); socket.close(); } }
|
NIO (Non Blocking IO)
同步非阻塞模型,服务实现模型为一个线程可以处理多个连接(请求),客户端的连接都会注册到多路复用器selector上面,多路复用轮询到连接用IO请求就进行处理,I/O 多路复用底层一般用的是Linux API (select,poll,epoll)来实现,区别见下面表格:
|
select |
poll |
epoll(jdk1.5及以上) |
操作方式 |
遍历 |
遍历 |
回调 |
底层实现 |
数组 |
链表 |
哈希表 |
IO效率 |
每次调用进行线性遍历,时间复杂度为O(n) |
每次调用进行线性遍历,时间复杂度为O(n) |
事件通知方式,每当有io事件就绪,系统注册的回调函数就会被调用,时间复杂度为O(1) |
最大连接 |
有上限 |
无上限 |
无上限 |
应用场景
NIO 适用于连接数目多且连接时间短(轻量级)的架构,比如聊天服务器,弹幕系统,服务间通信。编程比较复杂。jdk1.4开始支持。
NIO 有三大组件: Channel(通道)、Buffer(缓冲区),Selector(选择器)
- channel 类似于流,每个channel对应一个buffer缓冲区,buffer底层是个数组
- channel 会注册到selector上面,由selector 根据channel读写事件的发生将其交给空闲的线程处理。
- selector 可以对应一个或多个线程
- NIO 的Buffer和Channel都是既可以读也可以写的
NIO 服务端程序分析
- 创建一个ServerSocketChannel和Selector,将serverSocketChannel注册到Selector上
- selector通过select()方法监听channel事件,当客户端连接时selector监听到连接事件,获取到ServerSocketChannel注册时绑定的selectionKey
- selectionKey通过channel()方法可以获取绑定的ServerSocketChannel
- ServerSocketChannel通过accept()方法得到SocketChannel
- 将SocketChannel注册到Selector上,关心read事件
- 注册后返回一个SelectionKey,会和该SocketChannel关联
- selector继续通过select()方法监听事件,当客户端发送数据给服务端,selector监听到read事件,获取到SocketChannel注册时绑定的selectionKey
- selectionKey通过channel()方法可以获取绑定的socketChannel
- 将socketChannel里的数据读取出来
- 用socketChannel将服务端数据写回客户端
*总结:NIO模型的selector 就像一个大总管,负责监听各种IO事件,然后转交给后端线程去处理 。
NIO相对于BIO非阻塞的体现就在,BIO的后端线程需要阻塞等待客户端写数据(比如read方法),如果客户端不写数据线程就要阻塞, NIO把等待客户端操作的事情交给了大总管 selector,selector 负责轮询所有已注册的客户端,发现有事件发生了才转交给后端线程处 理,后端线程不需要做任何阻塞等待,直接处理客户端事件的数据即可,处理完马上结束,或返回线程池供其他客户端事件继续使用。还 有就是 channel 的读写是非阻塞的。
Redis就是典型的NIO线程模型,selector收集所有连接的事件并且转交给后端线程,线程连续执行所有事件命令并将结果写回客户端*
示例代码
public class NioServer {
public static void main(String[] args) throws IOException { ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.configureBlocking(false); ssc.socket().bind(new InetSocketAddress(9000)); Selector selector = Selector.open(); ssc.register(selector, SelectionKey.OP_ACCEPT); while (true){ System.out.println("等待事件发生.."); selector.select(); System.out.println("有事件发生了.."); Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()){ SelectionKey key = it.next(); it.remove(); handler(key); } } }
public static void handler(SelectionKey key) throws IOException{ if(key.isAcceptable()){ System.out.println("有客户端连接事件发生了.."); ServerSocketChannel ssc =(ServerSocketChannel) key.channel(); SocketChannel sc= ssc.accept(); sc.configureBlocking(false); sc.register(key.selector(), SelectionKey.OP_READ); }else if(key.isReadable()){ System.out.println("有客户端可读数据事件发生.."); SocketChannel sc =(SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int len = sc.read(byteBuffer); if(len!=-1){ System.out.println("接收到了客户端的消息:"+new String(byteBuffer.array(),0,len)); } ByteBuffer bufferWrite = ByteBuffer.wrap("hello client".getBytes()); sc.write(bufferWrite); key.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE); sc.close(); } } }
|
public class NioClient { Selector selector;
public static void main(String[] args) throws IOException{ NioClient nioClient = new NioClient(); nioClient.initClient("127.0.0.1",9000); nioClient.connection(); }
public void initClient(String ip,int port) throws IOException { SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); this.selector = Selector.open(); socketChannel.connect(new InetSocketAddress(ip,port)); socketChannel.register(this.selector, SelectionKey.OP_CONNECT); }
public void connection() throws IOException{ while(true){ this.selector.select(); Iterator<SelectionKey> it = this.selector.selectedKeys().iterator(); while (it.hasNext()){ SelectionKey key = it.next(); it.remove(); if(key.isConnectable()){ SocketChannel socketChannel =(SocketChannel) key.channel(); if(socketChannel.isConnectionPending()){ socketChannel.finishConnect(); } socketChannel.configureBlocking(false); ByteBuffer byteBuffer = ByteBuffer.wrap("hello server".getBytes()); socketChannel.write(byteBuffer); socketChannel.register(this.selector,SelectionKey.OP_READ); }else if(key.isReadable()){ read(key); } } } }
public void read(SelectionKey key) throws IOException{ SocketChannel channel = (SocketChannel)key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int len = channel.read(byteBuffer); if(len!=-1){ System.out.println("接收到服务端信息:"+new String(byteBuffer.array(),0,len)); } } }
|
AIO(NIO 2.0)
异步非阻塞模型,由操作系统完成后回调通知服务端程序启用线程去处理,一般使用于连接数较多且连接时间长的应用。
应用场景
AIO方式适用于连接数目多且连接比较长(重操作)的架构,jdk7开始支持。
代码示例
public class AIOServer { public static void main(String[] args) throws Exception { final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000)) ; serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() { @Override public void completed(AsynchronousSocketChannel socketChannel, Object attachment) { try{ serverChannel.accept(attachment,this); System.out.println(socketChannel.getRemoteAddress()); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); socketChannel.read(byteBuffer, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer attachment) { byteBuffer.flip(); System.out.println("接收到客户端消息:"+new String(byteBuffer.array(),0,result)); socketChannel.write(ByteBuffer.wrap("hello client".getBytes())); }
@Override public void failed(Throwable exc, ByteBuffer attachment) { exc.printStackTrace(); } });
}catch (IOException e){
}
}
@Override public void failed(Throwable exc, Object attachment) { exc.printStackTrace(); } }); Thread.sleep(Integer.MAX_VALUE); } }
|
public class AIOClient {
public static void main(String... args) throws Exception { AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open(); socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000)).get(); socketChannel.write(ByteBuffer.wrap("HelloServer".getBytes())); ByteBuffer buffer = ByteBuffer.allocate(512); Integer len = socketChannel.read(buffer).get(); if (len != -1) { System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len)); } } }
|
BIO、NIO、AIO 对比
|
BIO |
NIO |
AIO |
IO模型 |
同步阻塞 |
同步非阻塞(多路复用) |
异步非阻塞 |
编程难度 |
简单 |
复杂 |
复杂 |
可靠性 |
低 |
高 |
高 |
吞吐量 |
低 |
高 |
高 |
网络段子
老张爱喝茶,废话不说,煮开水。 出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
老张把水壶放到火上,立等水开。(同步阻塞) 老张觉得自己有点傻
老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞) 老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~的噪音。
老张把响水壶放到火上,立等水开。(异步阻塞) 老张觉得这样傻等意义不大
老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞) 老张觉得自己聪明了。
所谓同步异步,只是对于水壶而言。
普通水壶,同步;
响水壶,异步。
虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。
这是普通水壶所不能及的。
同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。
所谓阻塞非阻塞,仅仅对于老张而言。
立等的老张,阻塞;看电视的老张,非阻塞。