微服务架构-高性能Netty服务器-062:NIO多路IO复用底层实现原理

1 IO多路复用课程原理介绍

课程内容:
1.Bio到Nio架构模式演变的过程
2.为什么Redis单线程也能够支持非常高的并发
3.NIO架构模式如何使用选择器Selector轮询
4.IO复用机制、信号驱动IO、异步IO的区别
5.选择器 Linux Epoll与windows select有哪些不同

2 什么是Bio阻塞式io

阻塞式io:
在没有获取到数据的时候,整个应用可能会产生阻塞,放弃了cpu执行权,无法去做其他的事情。
非阻塞式io:
不管是否有获取到数据,都必须立马返回一个结果,如果没有获取到数据的情况下返回一个错误标记,根据错误的标记不断的轮询(重试)。
在这里插入图片描述
Bio就是一个阻塞式io操作。

3 使用多线程实现伪异步io优缺点

在这里插入图片描述
使用多线程技术实现伪异步io有哪些缺点?
非常消耗服务器资源

4 NIO模式核心设计技术思想

Java语言在jdk1.4版本推出一个新的io方案,对原来的bio(阻塞式)实现了优化。
Nio翻译成英文:no blocking io 同步、非阻塞式io
核心:面向于缓冲区、基于通道实现、多路io复用实现(选择器)

Bio与Nio区别:
Bio是一个阻塞式的io,它是面向于流传输也就是根据每个字节实现传输,效率非常低;Nio是面向于缓冲区的非阻塞io,其中最大的亮点:io多路复用机制。

5 Scoekt技术实现非阻塞式的IO操作

IO多路复用
多路实际指的就是多个不同的tcp连接;
复用指一个线程可以维护多个不同的io操作。
好处:占用cpu资源非常小、保证线程安全问题

public class NioServerTcp {

    static ByteBuffer byteBuffer = ByteBuffer.allocate(512);

    public static void main(String[] args) {
        // 实现多路io复用 要求:必须非阻塞式
        try {
            // 1.创建ServerSocketChannel
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // 2.绑定端口号
            serverSocketChannel.bind(new InetSocketAddress(8080));
            // 3.非阻塞式
            serverSocketChannel.configureBlocking(false);
            SocketChannel socketChannel = serverSocketChannel.accept();
            if (socketChannel != null) {
                int read = socketChannel.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);
                }
            }
            System.out.println("程序已经结束..");//程序已经结束..
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

6 Nio技术多路IO复用底层实现原理

在这里插入图片描述
服务器端

public class NioServerTcp {

    static ByteBuffer byteBuffer = ByteBuffer.allocate(512);
    // 存放多个不同的tcp连接
    private static List<SocketChannel> socketChannelList = new ArrayList<>();

    public static void main(String[] args) {
        // 实现多路io复用 要求:必须非阻塞式 一个线程实现维护多个不同的io操作
        try {
            // 1.创建ServerSocketChannel
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // 2.绑定端口号
            serverSocketChannel.bind(new InetSocketAddress(8080));
            // 3.非阻塞式
            serverSocketChannel.configureBlocking(false);
            while (true) {
                SocketChannel socketChannel = serverSocketChannel.accept();
                // 非阻塞式:不管是否获取数据,立马返回结果,如果没有数据返回错误标记
                // socketChannel==null,不断循环重试
                if (socketChannel != null) {
                    socketChannel.configureBlocking(false);
                    socketChannelList.add(socketChannel);
                }
                // 主动轮询连接是否有数据 一个线程维护多个不同的io操作
                for (SocketChannel scl : socketChannelList) {
                    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(Thread.currentThread().getName() + " receiveText:" + receiveText);
socketChannelList.remove(scl);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

客户端

public class ClientTcpSocket {
    public static void main(String[] args) {
        try {
            Socket socket = new Socket();
            SocketAddress address = new InetSocketAddress(InetAddress.getLocalHost(), 8080);
            socket.connect(address);
            while (true) {
                Scanner scanner = new Scanner(System.in);
                socket.getOutputStream().write(scanner.next().getBytes());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

测试结果:
在这里插入图片描述

7 Java原生Api实现NIO的操作

NIO核心组件:
Chancel(通道):数据传输都是经过管道;
Selector(选择器):也叫做多路复用器,可以在单线程的情况下去维护多个Channel,也可以去维护多个连接;
缓冲区(Buffer):本质上是一块内存区,可以用来读取数据,先将数据写入到缓冲区中,再统一的写入到硬盘上。
在这里插入图片描述
通常nio所有的操作都是通过通道开始,所有的通道都会统一注册到一个选择器(Selector)上实现管理,再通过选择器将数据统一写入到 buffer中。

使用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(10);
            if (select == 0) {
                continue;
            }
            // 发出tcp连接有两种方式:建立连接、发送消息
            // 获得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(8080);
        server.listen();
    }
}

以上代码主要懂得多路复用机制的思想,nio在最后都是被netty封装且netty性能更好。

8 Redis为什么单线程能够支持高并发

Redis是单线程为什么能够支持高并发?
Redis官方没有windows版本redis,只有linux版本的reids。
Redis的底层是采用nio的多路io复用机制对多个不同的连接(tcp)实现io的复用,能够非常好的支持高并发,同时能够先天性保证线程安全的问题。
多路io复用:使用一个线程维护多个不同的io操作。
原理:使用nio的选择器,将多个不同的Channel统一交给Selector(选择器)管理。

但是nio的实现在不同的操作系统上存在差别:在windows操作系统上使用select实现轮询机制、在linux操作系统使用epoll。(windows操作系统是没有epoll)
在windows操作系统中使用select实现轮询机制时间复杂度为o(n),而且会存在空轮询的情况,效率非常低;其次默认对轮询有一定限制(默认1024),所以这样的话很难支持上万tcp连接。
linux操作系统采用epoll实现事件驱动回调形式通知,不会存在空轮询的情况,只是对活跃的socket实现主动回调,时间复杂度为是o(1),性能有很大的提升。
注意:windows操作系统没有epoll、只有linux操作系统有。
所以为什么Nginx、redis能够支持非常高的并发?最终都是靠linux版本的io多路复用机制(epoll)。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值