Bio演变的NIO的过程
Bio是一个阻塞式的io,不能够支持并发请求访问;可以多线程优化代码
这种方式也存在缺点:如果每个请求过来都使用一个线程,这时候非常浪费CPU的资源。
所以在网络编程服务器中,是否使用单线程提高响应的效率问题,所以nio出现;
|
public class ServerTcpSocket {
|
NIO非阻塞式代码
|
public class ServerNioTcpSocket {
|
|
public class ClientTcpSocket {
|
五种IO模型
Linux下的五种I/O模型
1)阻塞I/O(blocking I/O)
2)非阻塞I/O (nonblocking I/O)
3) I/O复用(select 和poll) (I/O multiplexing)
4)信号驱动I/O (signal driven I/O (SIGIO))
5)异步I/O (asynchronous I/O (the POSIX aio_functions))
前面四种都是同步io、第五种是异步IO;
阻塞I/O
专业话术:
当我们在调用一个io函数的时候,如果没有获取到数据的情况下,那么就会一直等待;等待的过程中会导致整个应用程序一直是一个阻塞的过程,无法去做其他的实现。
非阻塞I/O
专业数据:
不管是否有获取到数据,都会立马获取结果,如果没有获取数据的话、那么就不间断的循环重试,但是我们整个应用程序不会实现阻塞。
白话文:
小军安排黄牛帮我们代替排队,每次间隔一段时间咨询黄牛 是否轮到自己呢。那么这个时候小军可以实现做其他的事情
这种非阻塞式的io还存在那些缺点?也是非常消耗cpu的资源,但是程序不会阻塞。
Cas、乐观锁与悲观锁
IO复用(select,poll,epoll)
IO复用机制:IO实际指的就是网络的IO、多路也就是多个不同的tcp连接;复用也就是指使用同一个线程合并处理多个不同的IO操作,这样的话可以减少CPU资源。
白话文:单个线程可以同时处理多个不同的io操作,应用场景非常广泛:redis原理。Mysql连接原理
信号驱动I/O
发出一个请求实现观察监听,当有数据的时候直接走我们异步回调;
白话文:小军在排队的时候 只需要去领取一个排队的号码,等到叫到了小军的时候才开始处理业务,这时候小军实际上还是可以去做其他的事情。 银行架构
异步I/O(Aio)
异步io也就是发出请求数据之后,剩下的事情完全实现异步完成
Nio与Bio的核心区别
什么是NIO
Java的nio是在Jdk1.4版本之后推出了一套新的io方案,这种io方案对原有io做了一次性能上的升级
NIO翻译成英文 no blocking io 简称为 nio 非阻塞io,不是new io。
比传统的io支持了面向缓冲区、基于通道实现的io的方案。
NIO与BIO区别
|
IO |
Nio |
|
面向流(Stream oriented) |
面向缓冲区(Buffer oriented) |
|
阻塞式(Blocking IO) |
非阻塞式(Non blocking IO) |
|
|
选择器(Selectors) |
传统的bio(同步阻塞 )是面向与流传输的,而NIO(同步非阻塞io)是面向与缓冲区非阻塞式的io,其中最大的亮点就是多路io复用机制。
阻塞式io:当我们没有获取到数据的时候,整个应用程序会实现阻塞等待,不能实现做其他的事情。
非阻塞式io:不管是否有获取到数据,都必须立马获取到结果,如果没有获取数据的情况下,就会不断的重试获取数据
类似于之前学习:cas、悲观和乐观锁
NIO的核心组件
通道(Channel)
通常我们nio所有的操作都是通过通道开始的,所有的通道都会注册到统一个选择器(Selector)上实现管理,在通过选择器将数据统一写入到 buffer中。
缓冲区(Buffer)
Buffer本质上就是一块内存区,可以用来读取数据,也就先将数据写入到缓冲区中、在统一的写入到硬盘上。
选择器(Selector)
Selector可以称做为选择器,也可以把它叫做多路复用器,可以在单线程的情况下可以去维护多个Channel,也可以去维护多个连接;
NIO设计思想伪代码
public class SocketNioTcpServer { private static List<SocketChannel> listSocketChannel = new ArrayList<>(); private static ByteBuffer byteBuffer = ByteBuffer.allocate(512); public static void main(String[] args) { try { // 1.创建一个ServerSocketChannel ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 2. 绑定地址 ServerSocketChannel bind = serverSocketChannel.bind(new InetSocketAddress(8080)); serverSocketChannel.configureBlocking(false); while (true) { SocketChannel socketChannel = serverSocketChannel.accept(); if (socketChannel != null) { socketChannel.configureBlocking(false); listSocketChannel.add(socketChannel); } for (SocketChannel scl : listSocketChannel) { try { int read = scl.read(byteBuffer); if (read > 0) { byteBuffer.flip(); Charset charset = Charset.forName("UTF-8"); String receiveText = charset.newDecoder().decode (byteBuffer.asReadOnlyBuffer()).toString(); System.out.println("receiveText:" + receiveText); } } catch (Exception e) { e.printStackTrace(); } } } } catch (Exception e) { e.printStackTrace(); } } }
|
使用jdk原生api实现nio
public class NIOServer { /** * 创建一个选择器 */ private Selector selector; public void initServer(int port) throws IOException { // 获得一个ServerSocketChannel通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 设置通道为非阻塞 serverSocketChannel.configureBlocking(false); // 将该通道对应的ServerSocket绑定到port端口 serverSocketChannel.bind(new InetSocketAddress(port)); // 获得一个通道管理器 this.selector = Selector.open(); // 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后, // 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); } public void listen() throws IOException { System.out.println("服务端启动成功!"); // 轮询访问selector while (true) { // 当注册的事件到达时,方法返回;否则,该方法会一直阻塞 int select = selector.select(); if (select == 0) { continue; } // 获得selector中选中的项的迭代器,选中的项为注册的事件 Iterator<SelectionKey> ite = this.selector.selectedKeys().iterator(); while (ite.hasNext()) { SelectionKey key = (SelectionKey) ite.next(); // 删除已选的key,以防重复处理 ite.remove(); if (key.isAcceptable()) {// 客户端请求连接事件 ServerSocketChannel server = (ServerSocketChannel) key.channel(); // 获得和客户端连接的通道 SocketChannel channel = server.accept(); // 设置成非阻塞 channel.configureBlocking(false); // 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。 channel.register(this.selector, SelectionKey.OP_READ); } else if (key.isReadable()) {// 获得了可读的事件 read(key); } } } } public void read(SelectionKey key) throws IOException { // 服务器可读取消息:得到事件发生的Socket通道 SocketChannel channel = (SocketChannel) key.channel(); // 创建读取的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(512); channel.read(buffer); byte[] data = buffer.array(); String msg = new String(data).trim(); System.out.println("服务端收到信息:" + msg); ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes("utf-8")); channel.write(outBuffer);// 将消息回送给客户端 } public static void main(String[] args) throws IOException { NIOServer server = new NIOServer(); server.initServer(8000); server.listen(); } }
|
Redis是如何单线程且又能够非常好支持并发
Redis官方没有windows版本redis,只有linux版本的reids
Redis的底层是采用nio 多路io复用机制实现对多个不同的连接(tcp)实现io的复用;能够非常好的支持高并发,同时能够先天性支持线程安全的问题。
备注:使用一个线程维护多个不同的io操作 原理使用nio的选择器,将多个不同的Channel统一交给我们的selector(选择器管理)
但是nio的实现在不同的操作系统上存在差别:在我们windows操作系统上使用select实现轮训机制、在linux操作系统使用epoll
备注:windows操作系统是没有epoll
在windows操作系统中使用select实现轮训机制时间复杂度是为 o(n),而且这种情况也会存在空轮训的情况,效率非常低、其次默认对我们的轮训有一定限制,所以这样的话很难支持上万tcp连接。
所以在这时候linux操作就出现epoll实现事件驱动回调形式通知,不会存在空轮训的情况,只是对活跃的socket实现主动回调,这样的性能有很大的提升 所以时间复杂度为是o(1)
注意:windows操作系统没有epoll、只有linux操作系统有。
所以为什么Nginx、redis能够支持非常高的并发 最终都是靠的linux版本的 io多路复用机制epoll