Netty框架(一)IO&BIO&NIO

本文详细介绍了Java的IO模型,特别是NIO(非阻塞IO)的工作原理。NIO的核心组件包括通道(Channel)、缓冲区(Buffer)和选择器(Selector)。通道是双向的,可以读写数据,而缓冲区用于在通道与程序间高效传输数据。选择器允许单个线程处理多个通道的IO事件,提高了系统的并发性能。此外,文章还讨论了Epoll空轮询问题及其解决方案。
摘要由CSDN通过智能技术生成

提示:基于操作系统进行相关记录


前言(IO、BIO)

IO

一、IO简介

  • io即输入/输出,就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。

二、IO通道:

IO通道(摘至os第三版):
1.虽然在cpu与IO设备之间增加了设备控制器后,能大大减少CPU对IO的干预,但当主机所配置的外设(如键盘)很多时,CPU的负担仍然很重。为此,在cpu和设备控制器之间又增设了通道。其主要目的是为了建立独立的IO操作,不仅使数据的传输能独立于cpu,而且也希望有关对IO操作的组织、管理以及结束处理尽量独立,以保证CPU有更多的时间去进行数据处理;即目的是使一些原来由CPU处理的IO任务转由通道来承担。
2.在设置了通道后,CPU只需向通道发送一条IO指令。通道在收到该指令后,便从内存中取出本次要执行的通道程序,然后执行该通道程序,仅当通道完成了规定的IO任务后,才向CPU发出中断信号。
3.注意:通道没有自己的内存,通道所执行的通道程序 是放在主机的内存中的。换言之,是通道与CPU共享内存。

三、IO控制方式

  • 程序io方式:忙-等待方式,不断循环等待测试状态位busy,直到空闲才进行后续IO任务。
  • 中断驱动IO控制方式:当进程要启动某个IO设备工作时,便有cpu向相应的设备控制器发出一条IO指令,然后立即返回继续执行原来的任务
  • 直接存储器访问(DMA)

1.DMA是一种完全由硬件执行IO交换的工作方式。DMA控制器从CPU完全接管对总线的控制,数据交换不经过CPU,而直接在内存和IO设备之间进行。
2. DMA一般用于高速传送成组数据。DMA方式的主要优点是速度快。由于CPU根本不用参加传送操作,因此就省去了CPU取指令、取数、送数等操作。
3.但是CPU每发出一条IO指令,也只能去读写一个连续的数据块,而当我们需要一次去读多个数据块且将他们分别传送到不同的内存区域,或者相反时,则必须由CPU分别发出多条IO指令及进行多次中断处理才能完成。(即有大量的IO请求,CPU的干预还是会很多),所以后续IO通道则是为了解决该问题。

  • IO通道方式:CPU只需要发出一条IO指令,就可以完成多种IO操作,之后被中断。

当CPU要完成一组相关的读写操作及有关控制时,只需向IO通道发送一条IO指令,以给出其所要执行的通道程序的首地址和要访问的IO设备,通道接到该指令后,通过执行通道程序便可以完成CPU指定的IO任务。

四、IO模式

  • BIO
  • NIO
  • AIO

BIO

一、简介

  • BIO模型:同步并阻塞,即一对一模式:一个线程对一个客户端连接,如果这个连接不做任何事情会造成不必要的线程开销。

二、使用流程

  • 服务器端启动一个 Server。
  • 客户端启动 Socket 对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯。
  • 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝。
  • 如果有响应,客户端线程会等待请求结束后,再继续执行。

提示:以下是本篇文章正文内容,下面案例可供参考

NIO

一、定义

  • 同步非阻塞模式:即一对多模型(一个服务器处理多个客户端请求),服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理。如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
    在这里插入图片描述

二、NIO的核心组件

1.简介

  • channel通道:Buffer和Channel之间的数据流向是双向的
  • buffer缓冲区:面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。(而BIO是基于字节流和字符流)
  • selector选择器:即多路复用 (HTTP 2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP 1.1 大了好几个数量级。)

在这里插入图片描述

即在上面图基础上,在读写过程中补充通道与缓冲区的关系:
1.即每个channel会对应一个缓冲区,而Selector 对应一个线程,一个线程对应多个 Channel(连接),即channel注册到selector中。而selector会根据不同的事件在各个通道上进行切换,保证各个任务的完成。
2.Buffer就是个内存块,底层是一个数组。在NIO中Buffer 是可以读也可以写,但需要 flip 方法切换。后续buffer目录处说明。

2.Buffer

目的:提高CPU和IO设备之间的并行性;缓和CPU和IO设备之间速度不匹配矛盾

本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer,后续通过程序回传给客户端。

buffer类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息

public abstract class Buffer {
    static final Unsafe UNSAFE = Unsafe.getUnsafe();
    static final int SPLITERATOR_CHARACTERISTICS = 16464;
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
    long address;
}

capacity: 容量
Limit:表示缓冲区的当前的终点,不能对缓冲区超过集显的位置进行读写操作。且极限是可以修改的
Position:位置,下一个要被读或者写的元素的索引,每次读写缓冲区数据时都会改变值,为下次读写做准备
mark:标记

针对读写切换的flip():Buffer有两种模式,写模式和读模式。在写模式下调用flip()之后,Buffer从写模式变成读模式。那么limit就设置成了position当前的值(即当前写了多少数据),postion会被置为0,以表示读操作从缓存的头开始读,mark置为-1。

3.channel

目的:类似于传统IO中的流,是直接对接操作系统(java中的unSafe类)的,当我们处理好缓冲区后,就可以通过缓冲区对通道进行数据的输入输出,完成整个NIO的过程

channel通道是双向的,而流是单向的。即通道可以实现异步读写数据,以从缓冲读数据,也可以写数据到缓冲。

4.selector

目的:主要作用就是使用一个线程来对多个通道中的已就绪通道进行选择,然后就可以对选择的通道进行数据处理,属于一对多的关系。这种机制在NIO技术中心称为“IO多路复用”。其优势是可以节省CPU资源。

1.Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
2.只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
3.避免了多线程之间的上下文切换导致的开销。

public abstract class Selector implements Closeable {
    //得到一个选择器对象
    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }
    //判断selector是否处于工作状态 
    public abstract boolean isOpen();
    //从内部稽核中得到所有的selectorkey
    public abstract Set<SelectionKey> selectedKeys();
    //监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectorKey加入内部集合中并返回,参数用来设置超时时间
    public abstract int select(long var1) throws IOException;

相关说明:
1.为什么netty支持高并发呢? 大致就是利用了多路复用,netty的IO线程NioEventLoop聚合了Selector(多路复用器),可以同时并发处理成百上千个客户端连接。即比如假设有 10000 个请求过来,根据实际情况,可以分配 50 或者 100 个线程来生成对应selelctor来处理即可。不像传统的阻塞 IO 那样,非得分配 10000 个。

在这里插入图片描述

1.当客户端连接时,会通过ServerSocketChannel得到一个socketChannel。
2.后续将socktChannel注册到selector上,register(Selector sel, int ops),一个 Selector 上可以注册多个 SocketChannel。
3.注册后返回一个 SelectionKey,会和该 Selector 关联(集合)。
在通过 SelectionKey 反向获取 SocketChannel,方法 channel()。
可以通过得到的 channel,完成业务处理。

package com.atguigu.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {
    public static void main(String[] args) throws Exception{

        //创建ServerSocketChannel -> ServerSocket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //得到一个Selecor对象
        Selector selector = Selector.open();
        //绑定一个端口6666, 在服务器端监听
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //把 serverSocketChannel 注册到  selector 关心 事件为 OP_ACCEPT       pos_1
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //循环等待客户端连接
        while (true){
            //如果返回的>0, 就获取到相关的 selectionKey集合
            //1.如果返回的>0, 表示已经获取到关注的事件
            //2. selector.selectedKeys() 返回关注事件的集合
            //   通过 selectionKeys 反向获取通道
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            System.out.println("selectionKeys 数量 = " + selectionKeys.size());
            //遍历 Set<SelectionKey>, 使用迭代器遍历
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while (keyIterator.hasNext()) {
                //获取到SelectionKey
                SelectionKey key = keyIterator.next();
                //根据key 对应的通道发生的事件做相应处理
                if(key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接
                    //该该客户端生成一个 SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode());
                    //将  SocketChannel 设置为非阻塞
                    socketChannel.configureBlocking(false);
                    //将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel
                    //关联一个Buffer
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if(key.isReadable()) {  //发生 OP_READ
                    //通过key 反向获取到对应channel
                    SocketChannel channel = (SocketChannel)key.channel();
                    //获取到该channel关联的buffer
                    ByteBuffer buffer = (ByteBuffer)key.attachment();
                    channel.read(buffer);
                    System.out.println("form 客户端 " + new String(buffer.array()));
                }
                //手动从集合中移动当前的selectionKey, 防止重复操作
                keyIterator.remove();
            }
        }
    }
}

补充

1.Epoll 导致Selecotor空轮询问题
  • 问题:若Selector的轮询结果为空,也没有wakeup或新消息处理,使用selector的selelct方法来轮询当前是否有IO时间,而select方法会一直阻塞,知道IO时间达到或者超时。则发生空轮询,CPU使用率100%,
  • 解决

Netty的解决办法: 利用一个变量和阈值比较

private void select(boolean oldWakenUp) throws IOException {
        Selector selector = this.selector;
        try {
            int selectCnt = 0;
            long currentTimeNanos = System.nanoTime();
            long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);

                //
                int selectedKeys = selector.select(timeoutMillis);
                selectCnt ++;  关键在这里 每次select则进行计数+1
                  .
                  .
                  . 
                   后续这里会判断
                long time = System.nanoTime();
                if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {  
                    如果是超时则跳出阻塞
                    selectCnt = 1;
                } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                        selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                    selector = selectRebuildSelector(selectCnt);
                    selectCnt = 1;
                    break;            //如果不是超时则重新构造selelctor
                }
                currentTimeNanos = time;
            }
        }
    }

1.每次调用后,selectCnt + 1
2.若超时(正常 跳出阻塞),重置selectCnt的值
3.若 未超时(非正常 跳出阻塞),重新构造一个selector,并 重置selectCnt的值

重建Selector相关源码

private void rebuildSelector0() {
        final Selector oldSelector = selector;   记录原来的Selector
        final SelectorTuple newSelectorTuple;

        if (oldSelector == null) {
            return;
        }

        try {
            newSelectorTuple = openSelector();   新建一个Selector
        } catch (Exception e) {
            logger.warn("Failed to create a new Selector.", e);
            return;
        }

        // Register all channels to the new Selector.
        int nChannels = 0;
        for (SelectionKey key: oldSelector.keys()) {
            Object a = key.attachment();
            try {
                if (!key.isValid() || key.channel().keyFor(newSelectorTuple.unwrappedSelector) != null) {
                    continue;
                }

                int interestOps = key.interestOps();
                key.cancel();

                关键在这里,将旧的Selector上主的的key全部注册到新的SelectorSelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
                if (a instanceof AbstractNioChannel) {
                    // Update SelectionKey
                    ((AbstractNioChannel) a).selectionKey = newKey;
                }
                nChannels ++;
            } catch (Exception e) {
                logger.warn("Failed to re-register a Channel to the new Selector.", e);
                if (a instanceof AbstractNioChannel) {
                    AbstractNioChannel ch = (AbstractNioChannel) a;
                    ch.unsafe().close(ch.unsafe().voidPromise());
                } else {
                    @SuppressWarnings("unchecked")
                    NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                    invokeChannelUnregistered(task, key, e);
                }
            }
        }

        然后将当前Selector指向新的Selector

        selector = newSelectorTuple.selector;
        unwrappedSelector = newSelectorTuple.unwrappedSelector;

        try {
            // time to close the old selector as everything else is registered to the new one
            oldSelector.close();         关闭旧的Selector
        } catch (Throwable t) {
            if (logger.isWarnEnabled()) {
                logger.warn("Failed to close the old Selector.", t);
            }
        }

        if (logger.isInfoEnabled()) {
            logger.info("Migrated " + nChannels + " channel(s) to the new Selector.");
        }
    }

1.新建一个 Selector
2.将旧的Selector 上注册的key,全部注册 到 新的Selector 上
3.将当前Selector 指向新的Selector
4.关闭旧的Selector

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值