java io模型

目录

一、五种IO模型

二、NIO,BIO,AIO选型

1.同步阻塞IO(BIO)

2.同步非阻塞IO(NIO)

3.异步非阻塞 I/O(AIO)

1.什么是AIO

2.应用场景

1.为什么大多数公司并没有使用AIO,而是使用了netty?

2.为什么Netty使用NIO而不是AIO?

4.各种I/O的对比

三、java NIO

1.java NIO和IO对比

2.NIO的核心组件

3.nio交互架构设计

1.poll机制

2.epoll机制

3.poll和epoll 对比

4.Redis线程模型

5.NIO空轮询解决方案

四、java AIO

1. 基础概念

2.java aio api

五、Netty基础

1.为什么需要Netty

1.nio API 繁杂

2.netty 网络通信功能全面

2.Netty的使用场景

 3.Netty编码解码机制

1.编解码器实现

2.自定义序列化框架

4.Netty心跳检测机制

1.心跳检测机制配置

2.心跳检测机制实现原理

3.心跳检测机制应用

1.断线重连或者告警

 2.客户端主动释放资源

5.TCP参数配置

6.ByteBuf组件

1.内存管理

2.扩容机制

3.读写原理

五、Netty架构设计

1.Netty模块组件

1.Bootstrap(启动引导类)

2.Selector

3.Channel

ChannelHandler

ChannelPipline 

4.NioEventLoop

NioEventLoopGroup

5.Future(监听)

2.Netty线程模型

3.Netty异步非阻塞处理流程

4.Netty零拷贝技术应用

1.直接内存引用

2.ByteBuf内存池设计

5.Netty无锁串行化设计

六、Netty源码设计

1.服务端设计

2.客户端设计

参考资料


一、五种IO模型

在《Unix网络编程》一书中提到了五种IO模型,分别是:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。

而Reactor模式实现了同步非阻塞模型,而Proactor模式实现了异步非阻塞模型

具体方面请参考我的另一篇博客

网络io模型-优快云博客

二、NIO,BIO,AIO选型

1.同步阻塞IO(BIO)

我们熟知的Socket编程就是BIO,一个socket连接一个处理线程(这个线程负责这个Socket连接的一系列数据传输操作)。
在这里插入图片描述

阻塞的原因在于:操作系统允许的线程数量是有限的,多个socket申请与服务端建立连接时,服务端不能提供相应数量的处理线程,没有分配到处理线程的连接就会阻塞等待或被拒绝。

缺点:

1、IO代码里read操作是阻塞操作,如果连接不做数据读写操作会导致线程阻塞,浪费资源
2、如果线程很多,会导致服务器线程太多,压力太大,比如C10K问题

应用场景:

BIO 方式适用于连接数目比较小且固定的架构, 这种方式对服务器资源要求比较高, 但程序简单易理解。

2.同步非阻塞IO(NIO)

NIO(New IO)或者NIO(Non Blocking IO)是对BIO的改进,基于Reactor模型。

同步非阻塞,服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理,JDK1.4开始引入。

应用场景:

NIO方式适用于连接数目多且连接比较短(轻操作) 的架构, 比如聊天服务器, 弹幕系统, 服务器间通讯,编程比较复杂。

3.异步非阻塞 I/O(AIO)

1.什么是AIO

AIO是对NIO的改进(所以AIO又叫NIO.2),它是基于Proactor模型的。

异步非阻塞 I/O 模型读请求会立即返回,说明 read 请求已经成功发起了,在后台完成读操作时,应用程序然后会执行其他处理操作。当 read 的响应到达时,就会产生一个信号或执行一个基于线程的回调函数来完成这次 I/O 处理过程。

所以异步最大的特点是,应用程序不需要自己去触发数据从内核空间到用户空间的拷贝。

为什么是应用程序去“触发”数据的拷贝,而不是直接从内核拷贝数据呢?

这是因为应用程序是不能访问内 核空间的,因此数据拷贝肯定是由内核来做,关键是谁来触发这个动作。

是内核主动将数据拷贝到用户空间并通知应用程序。还是等待应用程序通过Selector来查询,当数据就绪 后,应用程序再发起一个read调用,这时内核再把数据从内核空间拷贝到用户空间。

需要注意的是,数据从内核空间拷贝到用户空间这段时间,应用程序还是阻塞的。所以你会看到异步的效率 是高于同步的,因为异步模式下应用程序始终不会被阻塞。

下面我以网络数据读取为例,来说明异步模式的工作过程。

首先,应用程序在调用read API的同时告诉内核两件事情:数据准备好了以后拷贝到哪个Buffer,以及调用 哪个回调函数去处理这些数据。 之后,内核接到这个read指令后,等待网卡数据到达,数据到了后,产生硬件中断,内核在中断程序里把数 据从网卡拷贝到内核空间,接着做TCP/IP协议层面的数据解包和重组,再把数据拷贝到应用程序指定的 Buffer,最后调用应用程序指定的回调函数。

你可能通过下面这张图来回顾一下同步与异步的区别:

我们可以看到在异步模式下,应用程序当了“甩手掌柜”,内核则忙前忙后,但最大限度提高了I/O通信的 效率。

Windows的IOCP和Linux内核2.6的AIO都提供了异步I/O的支持,Java的NIO.2 API就是对操作系统异 步I/O API的封装。

AIO与NIO的区别:AIO是发出IO请求后,由操作系统自己去获取IO权限并进行IO操作;NIO则是发出IO请求后,由线程不断尝试获取IO权限,获取到后通知应用程序自己进行IO操作。

2.应用场景

一般适用于连接数较多且连接时间较长的应用,但实际上aio使用的地方并不多。

1.为什么大多数公司并没有使用AIO,而是使用了netty?

AIO的底层实现仍使用Epoll,并没有很好的实现异步,在性能上对比NIO没有太大优势

AIO的代码逻辑比较复杂,且Linux上AIO还不够成熟

Netty在NIO上做了很多异步的封装,是异步非阻塞框架

2.为什么Netty使用NIO而不是AIO?

在Linux系统上,AIO的底层实现仍使用Epoll,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化,Linux上AIO还不够成熟。

Netty是异步非阻塞框架,Netty在NIO上做了很多异步的封装。

4.各种I/O的对比

这里写图片描述

三、java NIO

1.java NIO和IO对比

下表总结了Java NIO和IO之间的主要差别,我会更详细地描述表中每部分的差异。

IO NIO
面向流 面向缓冲
阻塞IO 非阻塞IO
选择器

1、面向流与面向缓冲

Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。

Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。

Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

即:一个流中读写数据,一个从缓冲读写数据

2、阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。

Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

3、选择器(Selectors)

Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以在一个选择器中注册多个通道,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

2.NIO的核心组件

NIO 有三大核心组件: Channel(通道), Buffer(缓冲区),Selector(多路复用器)

1、channel 类似于流,每个 channel 对应一个 buffer缓冲区,buffer 底层就是个数组

首先说一下Channel,国内大多翻译成“通道”。Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,譬如:InputStream, OutputStream.而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。

NIO中的Channel的主要实现有:

FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel

2、channel 会注册到 selector 上,由 selector 根据 channel 读写事件的发生将其交由某个空闲的线程处理

3、NIO 的 Buffer 和 channel 都是既可以读也可以写

在这里插入图片描述

3.nio交互架构设计

1.poll机制

package com.gitee.io.nio.core;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class NioServer {

    // 保存客户端连接
    static List<SocketChannel> channelList = new ArrayList<>();

    public static void main(String[] args) throws IOException, InterruptedException {

        // 创建NIO ServerSocketChannel,与BIO的serverSocket类似
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));
        // 设置ServerSocketChannel为非阻塞
        serverSocket.configureBlocking(false);
        System.out.println("服务启动成功");

        while (true) {
            // 非阻塞模式accept方法不会阻塞,否则会阻塞
            // NIO的非阻塞是由操作系统内部实现的,底层调用了linux内核的accept函数
            SocketChannel socketChannel = serverSocket.accept();
            if (socketChannel != null) { // 如果有客户端进行连接
                System.out.println("连接成功");
                // 设置SocketChannel为非阻塞
                socketChannel.configureBlocking(false);
                // 保存客户端连接在List中
                channelList.add(socketChannel);
            }
            // 遍历连接进行数据读取
            Iterator<SocketChannel> iterator = channelList.iterator();
            while (iterator.hasNext()) {
                SocketChannel sc = iterator.next();
                ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                // 非阻塞模式read方法不会阻塞,否则会阻塞
                int len = sc.read(byteBuffer);
                // 如果有数据,把数据打印出来
                if (len > 0) {
                    System.out.println("接收到消息:" + new String(byteBuffer.array()));
                } else if (len == -1) { // 如果客户端断开,把socket从集合中去掉
                    iterator.remove();
                    System.out.println("客户端断开连接");
                }
            }
        }
    }
}

NIO底层在JDK1.4版本是用linux的内核函数select()或poll()来实现,跟上面的NioServer代码类似,selector每次都会轮询所有的sockchannel看下哪个channel有读写事件,有的话就处理,没有就继续遍历。

总结:如果连接数太多的话,会有大量的无效遍历,假如有10000个连接,其中只有1000个连接有写数据,但是由于其他9000个连接并没有断开,我们还是要每次轮询遍历一万次,其中有十分之九的遍历都是无效的,这显然不是一个让人很满意的状态。

2.epoll机制

nio组件之间的工作流程如下
在这里插入图片描述

1.创建ServerSocketChannel并绑定端口

2.创建Selector多路复用器,并注册Channel

3.循环监听是否有感兴趣的事件发生selector.select();

4.获得事件的句柄,并进行处理

JDK1.5开始引入了epoll基于事件响应机制来优化NIO。

package com.gitee.io.nio.core.epoll;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NioSelectorServer {

    public static void main(String[] args) throws IOException, InterruptedException {

        // 创建NIO ServerSocketChannel
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));
        // 设置ServerSocketChannel为非阻塞
        serverSocket.configureBlocking(false);
        // 打开Selector处理Channel,即创建epoll
        Selector selector = Selector.open();
        // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务启动成功");

        while (true) {
            // 阻塞等待需要处理的事件发生
            selector.select();

            // 获取selector中注册的全部事件的 SelectionKey 实例
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = se
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值