引用

https://developer.aliyun.com/article/726698
https://www.cnblogs.com/anker/p/3265058.html
https://zhuanlan.zhihu.com/p/69554144
https://cloud.tencent.com/developer/article/1005481

IO流即输入流和输出流

BIO(blocking-io)同步阻塞
数据的读(写)在一个线程内,当暂不可用时阻塞等待。

NIO(non-blocking-io)同步非阻塞
线程轮询IO是否就绪,当不可用时结束等待(去做其他事),一定时间后再询问。JAVA NIO(new io)指的是一个线程访问轮询访问一堆是否就绪。

AIO(synchronnous-non-blocking-IO)异步非阻塞
线程发出请求后不等待,而是当缓冲区就绪后,通知线程或者执行线程交给予的回调函数(又被称为new io 2.0)

同步和异步

同步优点:

  1. 同步流程对结果处理通常更为简单,可以就近处理。
  2. 同步流程对结果的处理始终和前文保持在一个上下文内。
  3. 同步流程可以很容易捕获、处理异常。
  4. 同步流程是最天然的控制过程顺序执行的方式。

异步优点:

  1. 异步流程可以立即给调用方返回初步的结果。
  2. 异步流程可以延迟给调用方最终的结果数据,在此期间可以做更多额外的工作,例如结果记录等等。
  3. 异步流程在执行的过程中,可以释放占用的线程等资源,避免阻塞,等到结果产生再重新获取线程处理。
  4. 异步流程可以等多次调用的结果出来后,再统一返回一次结果集合,提高响应效率

什么时候使用异步

  1. 不涉及共享资源,或对共享资源只读,即非互斥操作
  2. 没有时序上的严格关系
  3. 不需要原子操作,或可以通过其他方式控制原子性
  4. 常用于IO操作等耗时操作,因为比较影响客户体验和使用性能
  5. 不影响主线程逻辑
    剩余自然是同步QWQ

非阻塞目前理论上是要优于阻塞的,但非阻塞更麻烦一点,在文件传输这种少链接多质量的时候可以考虑。

BIO 模型

采用 BIO 通信模型的服务端,通常有一个独立的 Acceptor 线程负责监听客户端的连接,它接收到客户端的连接请求之后,为每个客户端创建一个新的线程进行链路处理,处理完之后,通过输出流返回应答客户端,线程销毁。这就是典型的一请求一应答通信模型。这个是在多线程情况下执行的。当在单线程环境条件下时,在 while 循环中服务端会调用 accept 方法等待接收客户端的连接请求,一旦收到这个连接请求,就可以建立 socket,并在 socket 上进行读写操作,此时不能再接收其他客户端的连接请求,只能等待同当前服务端连接的客户端的操作完成或者连接断开。

伪异步 I/O 编程

为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化,后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N,通过线程池可以灵活的调配线程资源。设置线程的最大值,防止由于海量并发接入导致线程耗尽。
采用线程池和任务队列可以实现一种叫做伪异步的I/O通信框架。

当有新的客户端接入时,将客户端的 Socket 封装成一个 Task(该任务实现 Java.lang.Runnablle 接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和N个活跃线程对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

由于线程池和消息队列都是有界的,因此,无论客户端并发连接数多大,它都不会导致线程个数过于膨胀或者内存溢出,相对于传统的一连接一线程模型,是一种改良。

伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。

BIO 实现原理

  • 同步并阻塞 IO,服务器实现模式一个连接一个线程,即客户端有连接请求时服务端就需要启动一个线程进行处理,如果这个连接不做任何事情,就会造成不必要的线程开销,当然可以通过线池(Thread-Pool)程机制改善。
  • 本地进程间通讯,通常只需绑定进程Pid 即可,而网络中通常采用ip地址+协议+端口号唯一标示网络中的一个进程
    image.png

客户端

public class Client {
public static void main(String[] args) {
        try {
            Socket socket = new Socket("127.0.0.1", 8080);
            //下面的代码也会阻塞
            Scanner scanner = new Scanner(System.in);
            String txt = scanner.next();
            socket.getOutputStream().write(txt.getBytes());
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务端

public class QQServer {
    static byte[] bytes = new byte[1024];
    public static void main(String[] args) {
        try {
            //用于监听
            ServerSocket serverSocket = new ServerSocket();
            //绑定服务器的ip和端口,ip为本机,所以省略
            serverSocket.bind(new InetSocketAddress(8080));
            while(true) {
                System.out.println("wait conn");
                //监听,会阻塞->当前线程回放弃CPU线程,就意味着不会再向下执行了
                //这个socket是专门用于与客户端通信的socket
                Socket socket = serverSocket.accept();

                System.out.println("conn success");
                System.out.println("wait data");

                //专门用于接受客户端发来的byte数组,返回一个int类型的值,用于表示返回多少字节
                int read = socket.getInputStream().read(bytes);

                System.out.println("data success");
                String content = new String(bytes);
                System.out.println(content);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

NIO模型

与 Socket 类和 ServerSocket 类对应,NIO 也提供了 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,在 JDK1.4 中引入。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式非常简单,但性能和可靠性都不好,非阻塞模式正好相反。我们可以根据自己的需求来选择合适的模式,一般来说,低负载、低并发的应用程序可以选择同步阻塞 IO 以降低编程复杂度,但是对于高负载、高并发的网络应用,需要使用 NIO 的非阻塞模式进行开发。
尽管 NIO 编程难度确实比同步阻塞 BIO 大很多,但是我们要考虑到它的优点:

(1)客户端发起的连接操作是异步的,可以通过在多路复用器注册 OP_CONNECT 等后续结果,不需要像之前的客户端那样被同步阻塞。

(2)SocketChannel 的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样IO通信线程就可以处理其它的链路,不需要同步等待这个链路可用。

(3)线程模型的优化:由于 JDK 的 Selector 在 Linux 等主流操作系统上通过 epoll 实现,它没有连接句柄数的限制(只受限于操作系统的最大句柄数或者对单个进程的句柄限制),这意味着一个 Selector 线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降,因此,它非常适合做高性能、高负载的网络服务器。

NIO 实现原理

  • 同步非阻塞,服务器实现模式一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程处理。用户进程也需要时不时地询问IO操作是否就绪,这就要求用户进程不停的去询问。
    image.png

NIO 使用

服务端

public class Server { 
    private Selector selector; 
    private ByteBuffer readBuffer = ByteBuffer.allocate(1024);//调整缓存的大小可以看到打印输出的变化 
    private ByteBuffer sendBuffer = ByteBuffer.allocate(1024);//调整缓存的大小可以看到打印输出的变化 
 
    String str;
    public void start() throws IOException {
        // 打开服务器套接字通道 
        ServerSocketChannel ssc = ServerSocketChannel.open(); 
        // 服务器配置为非阻塞 
        ssc.configureBlocking(false); 
        // 进行服务的绑定 
        ssc.bind(new InetSocketAddress("localhost", 8001)); 
        
        // 通过open()方法找到Selector
        selector = Selector.open(); 
        // 注册到selector,等待连接
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        
        while (!Thread.currentThread().isInterrupted()) { 
            selector.select(); 
            Set<SelectionKey> keys = selector.selectedKeys(); 
            Iterator<SelectionKey> keyIterator = keys.iterator(); 
            while (keyIterator.hasNext()) { 
                SelectionKey key = keyIterator.next(); 
                if (!key.isValid()) { 
                    continue; 
                } 
                if (key.isAcceptable()) { 
                    accept(key); 
                } else if (key.isReadable()) { 
                    read(key); 
                } else if (key.isWritable()) {
                    write(key);
                }
                keyIterator.remove(); //该事件已经处理,可以丢弃
            } 
        } 
    }

    private void write(SelectionKey key) throws IOException, ClosedChannelException {
        SocketChannel channel = (SocketChannel) key.channel();
        System.out.println("write:"+str);
        
        sendBuffer.clear();
        sendBuffer.put(str.getBytes());
        sendBuffer.flip();
        channel.write(sendBuffer);
        channel.register(selector, SelectionKey.OP_READ);
    } 
 
    private void read(SelectionKey key) throws IOException { 
        SocketChannel socketChannel = (SocketChannel) key.channel(); 
 
        // Clear out our read buffer so it's ready for new data 
        this.readBuffer.clear(); 
//        readBuffer.flip();
        // Attempt to read off the channel 
        int numRead; 
        try { 
            numRead = socketChannel.read(this.readBuffer); 
        } catch (IOException e) { 
            // The remote forcibly closed the connection, cancel 
            // the selection key and close the channel. 
            key.cancel(); 
            socketChannel.close(); 
            
            return; 
        } 
        
        str = new String(readBuffer.array(), 0, numRead);
        System.out.println(str);
        socketChannel.register(selector, SelectionKey.OP_WRITE);
    } 
 
    private void accept(SelectionKey key) throws IOException { 
        ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); 
        SocketChannel clientChannel = ssc.accept(); 
        clientChannel.configureBlocking(false); 
        clientChannel.register(selector, SelectionKey.OP_READ); 
        System.out.println("a new client connected "+clientChannel.getRemoteAddress()); 
    } 
 
    public static void main(String[] args) throws IOException { 
        System.out.println("server started..."); 
        new Server().start(); 
    } 
} 

客户端

public class Client { 
 
    ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    
    public void start() throws IOException { 
        // 打开socket通道  
        SocketChannel sc = SocketChannel.open(); 
        //设置为非阻塞
        sc.configureBlocking(false); 
        //连接服务器地址和端口
        sc.connect(new InetSocketAddress("localhost", 8001)); 
        //打开选择器
        Selector selector = Selector.open(); 
        //注册连接服务器socket的动作
        sc.register(selector, SelectionKey.OP_CONNECT); 
        
        Scanner scanner = new Scanner(System.in); 
        while (true) { 
            //选择一组键,其相应的通道已为 I/O 操作准备就绪。  
            //此方法执行处于阻塞模式的选择操作。
            selector.select();
            //返回此选择器的已选择键集。
            Set<SelectionKey> keys = selector.selectedKeys(); 
            System.out.println("keys=" + keys.size()); 
            Iterator<SelectionKey> keyIterator = keys.iterator(); 
            while (keyIterator.hasNext()) { 
                SelectionKey key = keyIterator.next(); 
                keyIterator.remove(); 
                // 判断此通道上是否正在进行连接操作。 
                if (key.isConnectable()) { 
                    sc.finishConnect(); 
                    sc.register(selector, SelectionKey.OP_WRITE); 
                    System.out.println("server connected..."); 
                    break; 
                } else if (key.isWritable()) { //写数据 
                    System.out.print("please input message:"); 
                    String message = scanner.nextLine(); 
                    //ByteBuffer writeBuffer = ByteBuffer.wrap(message.getBytes()); 
                    writeBuffer.clear();
                    writeBuffer.put(message.getBytes());
                    //将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
                    writeBuffer.flip();
                    sc.write(writeBuffer); 
                    
                    //注册写操作,每个chanel只能注册一个操作,最后注册的一个生效
                    //如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来
                    //int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
                    //使用interest集合
                    sc.register(selector, SelectionKey.OP_READ);
                    sc.register(selector, SelectionKey.OP_WRITE);
                    sc.register(selector, SelectionKey.OP_READ);
                    
                } else if (key.isReadable()){//读取数据
                    System.out.print("receive message:");
                    SocketChannel client = (SocketChannel) key.channel();
                    //将缓冲区清空以备下次读取 
                    readBuffer.clear();
                    int num = client.read(readBuffer);
                    System.out.println(new String(readBuffer.array(),0, num));
                    //注册读操作,下一次读取
                    sc.register(selector, SelectionKey.OP_WRITE);
                }
            } 
        } 
    } 
 
    public static void main(String[] args) throws IOException { 
        new Client().start(); 
    } 
}
  • Java NIO 由以下几个核心部分组成:
    Channels,Buffers,Selectors

Buffer

Buffer是一个抽象的对象,下面还有ByteBuffer,IntBuffer,LongBuffer等子类,相比老的IO将数据直接读/写到Stream对象,NIO是将所有数据都用到缓冲区处理,相当于批量操作数据,等数据累计到一定值,再读取/写入,从而提高性能。

Channel

如自来水管一样,支持网络数据从Channel中读写,通道和流最大的不同是:通道是双向的,而流是一个方向上移动(InputStream/OutputStream),通道可用于读/写或读写同时进行,它还可以和下面要讲的selector结合起来,有多种状态位,方便selector去识别.常见的channel如下

  • FileChannel 从文件中读写数据。
  • DatagramChannel 能通过UDP读写网络中的数据。
  • SocketChannel 能通过TCP读写网络中的数据。
  • ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel

Selector 多路复用选择器

多路复用选择器Selector会不断轮询注册在其上的通道(Channel),如果某个通道发生了读写操作,这个通道处于就绪状态,会被selector轮询出来,然后通过selectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。
一个多路复用器(Selector)可以负责成千上万个Channel,没有上限,这也是JDK使用epoll代替了传统的selector实现,获得连接句柄没有限制。这也意味着我们只要一个线程负责selector的轮询,就可以接入成千上万个客户端,这是JDK,NIO库的巨大进步。

这里解释一下IO 多路复用就是我们说的select,poll,epoll

select,poll,epoll 大致(因为不太了解)

image.png
具体参考
https://cloud.tencent.com/developer/article/1005481

select

5个参数,后面4个参数都是in/out类型(值可能会被修改返回)

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select的几大缺点

  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  3. select支持的文件描述符数量太小了,默认是1024

poll

select遗留的三个问题中,问题(3)是用法限制问题,问题(1)和(2)则是性能问题。poll和select非常相似,poll并没着手解决性能问题,poll只是解决了select的问题(1)fds集合大小1024限制问题。

poll的较select改变

  1. poll改变了fds集合的描述方式,使用了pollfd结构而不是select的fd_set结构,使得poll支持的fds集合限制远大于select的1024。

epoll

待学

Linux操作系统就将权限等级分为了2个等级,分别就是内核态和用户态

内核态可以执行特权指令:只能由操作系统内核部分使用,不允许用户直接使用的指令。如,I/O指令、置终端屏蔽指令、清内存、建存储保护、设置时钟指令(这几种记好,属于内核态)

用户态只能执行非特权指令:所有程序均可直接使用

从用户态到内核态切换可以通过三种方式:

  1. 系统调用:其实系统调用本身就是中断,但是软件中断,跟硬中断不同。
  2. 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,就会触发切换。例如:缺页异常。
  3. 外设中断:当外设完成用户的请求时,会向CPU发送中断信号。
    存在问题

AIO模型

JDK1.7 升级了 NIO 类库,升级后的 NIO 类库被称为NIO2.0。也就是我们要介绍的 AIO。NIO2.0 引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。异步通道提供两种方式获取操作结果。

(1)通过 Java.util.concurrent.Future 类来表示异步操作的结果;

(2)在执行异步操作的时候传入一个Java.nio.channels.CompletionHandler接口的实现类作为操作完成的回调。

NIO2.0 的异步套接字通道是真正的异步非阻塞 IO,它对应 UNIX 网络编程中的事件驱动 IO(AIO),它不需要通过多路复用器(Selector)对注册的通道进行轮询操作即可实现异步读写,从而简化了 NIO 的编程模型。

我们可以得出结论:异步 Socket Channel是被动执行对象,我们不需要想NIO编程那样创建一个独立的IO线程来处理读写操作。对于AsynchronousServerSocketChannel和AsynchronousSocketChannel,它们都由 JDK 底层的线程池负责回调并驱动读写操作。正因为如此,基于 NIO2.0 新的异步非阻塞 Channel 进行编程比 NIO 编程更为简单。

AIO

异步非阻塞,此种方式下,用户进程只需要发起一个IO操作便立即返回,等 IO 操作真正完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据处理就好了,不需要进行实际的 IO 读写操作,因为真正的 IO 操作已经由操作系统内核完成了。
image.png

bio,nio,aio使用场景

BIO 适用于连接数目比较小且固定的结构。它对服务器资源要求比较高,并发局限于应用中,JDK1.4之前唯一选择,但程序直观简单易理解,如之前在 Apache 中使用。

NIO 适用于连接数目多且连接比较短的架构,比如聊天服务器,并发局限于应用中,变成比较复杂。JDK1.4开始支持,如在 Nginx、Netty 中使用。

AIO 适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持,在成长中,Netty 曾经使用过,后来放弃。

使用建议

在大多数场景下,不建议直接使用 JDK 的 NIO 类库(门槛很高),除非精通 NIO 编程或者有特殊的需求。在绝大多数的业务场景中,可以使用 NIO 框架 Netty 来进行 NIO 编程,其既可以作为客户端也可以作为服务端,且支持 UDP 和异步文件传输,功能非常强大。

NIO 比 BIO 把一些无效的连接挡在了启动线程之前,减少了这部分资源的浪费。因为我们都知道每创建一个线程,就要为这个线程分配一定的内存空间。

AIO 比 NIO 进一步改善是,将一些暂时可能无效的请求挡在了启动线程之前,比如在 NIO 的处理方式中,当一个请求来的话,开启线程进行处理,但这个请求所需要的资源还没有就绪,此时必须等待后端的应用资源,这时线程就被阻塞了。