netty学习(一)java原生IO

本文介绍了Java中的IO模型,包括BIO、NIO和AIO,重点讲解了NIO的工作原理,如Buffer、Channel和Selector。通过示例展示了NIO如何实现非阻塞I/O,最后探讨了为何在NIO基础上还需要Netty。
摘要由CSDN通过智能技术生成

netty学习(一)JAVA中的IO方式

1. 为什么要学习netty?

别问,必学

2. BIO、NIO、AIO

这里再复习一下java中的三种IO模型,要将java中的IO模型,首先离不开操作系统中的IO模型

2.1. 操作系统中的IO模型

一次IO的过程包括两部分: 1. 内核空间接收到外来数据 2. 将内核空间的数据拷贝到用户空间供应用使用

由此诞生了操作系统中的5种IO模型

同步并阻塞

在该模型下,每一个线程在调用读取函数之后就一直进入阻塞,直到读取到数据

同步非阻塞

与上面不同,每一个线程在调用读取函数时马上返回,并不断询问数据是否到达,在这个阶段是非阻塞的,但当进行到第二个阶段时会进入阻塞

信号驱动

在此种模型下,不需要不断询问数据是否到达,而是注册一个函数,等待数据到达内核空间后进入阻塞,进行数据的复制

异步非阻塞

在异步情况下,每一个线程在调用读取函数后会注册一个回调函数,然后处理其他事情直到两个阶段都完成再回调进行处理

多路复用

与前三种不同的是,在多路复用模型下,每一个线程可以同时处理多个连接,然后根据各连接的情况进行处理,这里就不详细讲啦

2.2 java中的IO模型

而在java中实现了三种IO模型

阻塞IO BIO

非阻塞IO NIO

异步IO AIO

注意的是java中的NIO对应的实际上是操作系统中的多路复用

每种IO都有各自的优缺点,而当下使用最多的是NIO模型

各种IO使用场景

BIO: 连接数目标少且固定的场景

NIO:连接数多且连接比较短的场景(聊天服务器、弹幕系统、服务期间通讯)

AIO:连接数目比较多且连接比较长

2.2.1 BIO

java原生BIO的实现包括以下部分:

  1. 建立一个线程池
  2. 创建一个ServerSocket
  3. 调用accpet等待客户端连接,获取Socket对象
  4. 取一个线程进行处理,利用Socket进行传输
  5. 关闭Socket

代码儿

public class BIOServer {
    public static void main(String[] args) throws Exception {
        // 1. 创建线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        // 2. 创建ServerSocket
        ServerSocket serverSocket = new ServerSocket(5555);
        System.out.println("服务器启动!");
        while(true){
            // 3. 等待客户端的连接
            final  Socket socket = serverSocket.accept();
            System.out.println("有客户端连接了");
            // 4. 取一个线程执行IO
            executorService.execute(new Runnable() {
                public void run() {
                    try {
                        handler(socket);
                    }catch (Exception e){
                        e.printStackTrace();
                    }finally {
                        System.out.println("连接关闭");
                        try {
                            socket.close(); // 5. 关闭Socket
                        }catch (Exception e){
                            e.printStackTrace();
                        }
                    }
                }
            });
        }
    }
    public static void handler(Socket socket) throws Exception{
        InputStream inputStream = socket.getInputStream();
        byte[] bytes = new byte[1024];
        while(true){
            int read = inputStream.read(bytes);
            if(read!=-1){
                System.out.println(new String(bytes));
            }else{
                break;
            }
        }
    }
}
2.2.2 NIO

上面说到java中的NIO其实就是多路复用的技术,为什么多路复用可以实现高并发呢?

首先,操作系统的最大线程数量是有限的,若有1w个请求到达,而操作系统只支持1000个线程,则此时另外9000个请求就会进入等待,而正在进行的线程不是时时刻刻都在执行的,大部分都在等待数据的传输而进入阻塞,这就造成了资源的浪费,且操作系统需要不停的切换线程来执行任务,这也造成了运行的缓慢。

而是用多路复用技术,则大大减少了线程的数量,让每一个线程同时处理多个请求也减少了阻塞情况的发生。

那么java中的NIO是怎么实现的呢?换句话说,java是怎么实现一个线程处理多个连接的呢?

image-20210401195830847

如上图,NIO模型由三大组件组成

selector负责监视channel提供给线程

channel负责管理服务端的IO传输

buffer负责跟客户端的IO传输

在这种结构下,不同于BIO使用流式的传输,将数据传输到buffer中可以实现块式传输,从而能让线程不必实时监听一个连接。

而channel跟selector的互动则可以满足各个连接的切换

要注意的是,buffer和channel都是双向传输的,即可以读也可以写

2.2.2.1 buffer(缓冲区)

buffer是怎样实现双向读写的呢?

我们跟着源码看一下

buffer是一个抽象类,对于每一个基本数据类型(除了boolean),都有一个子类的实现,我们以byteBuffer为例,该类维护了一个byte数组:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UKXbbnFU-1619023129199)(uploads/image-20210401214038619.png)]

这个数据即数据存放的结构

buffer维护了4个重要的属性

image-20210401213813885

其中:

  • mark是一个标记

  • position代表我们进行操作时对应元素的位置

  • limit代表最大访问的位置

  • capacity代表了数组的容量

每一次get和put操作,即将position+1

那怎么实现同时读写呢?

每次进行读写切换时,我们需要显示的调用flip方法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QwCLytsS-1619023129201)(uploads/image-20210401214350776.png)]

可以看到,该方法将limit设置成了position的位置,然后position从0开始。

这样就很巧妙的实现了读写的切换

2.2.2.2 channel(通道)

同样,为了能和buffer进行交互,channel也是能支持双向传输的。

channel是一个接口,其也有很多实现类:

image-20210401235024412

比较常用的为FileChannel、ServerSocketChannel和SocketChannel

对于FileChannel,我们可以理解为其对标准输入输出的一层封装(但其实channel是写在标准IO里面的),使用该通道可以将文件中的内容和buffer中的内容进行读写:

栗子,复制1.txt中的内容到2.txt

public class Channel {
    public static void main(String[] args) throws Exception{
        FileInputStream fileInputStream = new FileInputStream("1.txt");  // 创建两个文件流
        FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
        FileChannel channel = fileInputStream.getChannel();  // 获取文件流的channel
        FileChannel channel1 = fileOutputStream.getChannel();
        ByteBuffer bb = ByteBuffer.allocate(1024);  // 创建buffer
        while(true){  
            int read = channel.read(bb);  // 将文件中的一部分读入buffer中
            if(read!=-1){
                bb.flip();
                channel1.write(bb);  // 将buffer写入2.txt
                bb.flip();
            }else{
                break;
            }
        }
    }
}

ServerSocketChannel和SocketChannel的关系和ServerSocket和Socket的关系类似,可以实现客户端和服务器的连接,这也是完成NIO的主要类,将在后面详细讨论。

2.2.2.3 Selector(选择器)

作为NIO中最为核心的组件,Selector实现了对连接的监听然后返回给线程,那么它是怎么实现跟SocketChannel联系的呢,我们看看源码:

我们知道ServerSocketChannel就好比ServerSocket,调用accpt方法之后就会产生一个SocketChannel对象,在SocketChannel的抽象类中,我们找到了一个register方法:

image-20210402173948976

可以看到,该方法是传入一个selector对象,然后返回一个selectionKey对象,该方法中做了什么呢?

我们一路追踪,找到了如下关键:

  1. 创建了一个selectionKey对象:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kdlw8Bkw-1619023129202)(uploads/image-20210402174940144.png)]

  1. 将这个selectionKey对象放入到了一个Keys集合中:
image-20210402213447073
  1. 返回这个selectionKey

这个Keys集合里其实就是我们注册的所有连接,之后我们我们就可以通过这个selectionKey来反向找到我们的SocketChannel。

既然channel注册进来了,那么怎么进行监听呢?

这就来到了我们的selector类里面进行解释:

首先,Selector是一个抽象类,其中有3个核心的方法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dti0iioF-1619023129204)(uploads/image-20210402172108087.png)]

selectorNow(): 非阻塞的selector方法,该方法执行后不论怎样都会立即返回

selector(long):有最大等待时间的selector方法,若没有事件发生,该方法会在等待超时后才返回

selector(): 前面两种方法都是调用了selector方法,我们来看其具体实现

一路追踪,我们找到了processSelectedKeys(long updateCount)这个方法

image-20210402172444065

可以发现该方法根据POLLIN、POLLOUT、POLLCONN执行了不同的操作,看到这里,学过操作系统中多路复用的朋友可能就有点印象了,这和select多路复用是不是很像?

然后我们继续看processFDSet()方法:

image-20210402172723468 image-20210402212715104

代码太长我们看到前半段和中间就懂了,该方法在遍历一个fds数组,然后若有事件发生的则将返回的int类型+1,并且将该事件加入到selectedKeys集合中,

selector中维护了多个selectionKey的集合:

image-20210402183547352

通过注释得知 keys为注册的事件集合, selectedKeys为发生事件的集合,而后两个则为前两个集合的public版本,所以我们通过selector的selectKeys()方法即可获得发生事件的集合。

前面原理可能有点点乱,我们再来总结一下一个连接进行处理的流程:

  1. 将ServerSocketChannel注册到selector中,ServerSocketChannel调用accpet() 等待客户端连接
  2. 客户端连接后返回一个SocketChannel,然后调用register()方法将该通道注册进selector中并得到一个selectionKey用于反向取回通道
  3. selector调用select方法遍历所有的selectionKey,并统计发生事件的连接数量
  4. 再进一步调用selectKeys()方法得到发生事件的selectionKey,然后取得通道Channel进行业务处理

这样一来是不是清晰明了?当然这只是大概的流程,里面还有很多很多的细节

我们用代码的形式来演示上面的流程

先写一个服务端:

public class SelectorServer {
    public static void main(String[] args) throws Exception{
        // 创建selector

        Selector selector = Selector.open();
        // 创建serverSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 绑定端口
        // 设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(5555));
        // 1. 将serverSocketChannel注册到selector中, 将事件定义为accept事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 等待客户端连接
        while(true){
            // 3. 统计发生时间的数量
            if(selector.select(1000)==0){
                System.out.println("服务端等待了一秒,没有事件发生");
            }
            // 4. 遍历发生的事件进行业务处理
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while(iterator.hasNext()){
                SelectionKey selectionKey = iterator.next();
                // 若为accept事件,则获取连接
                if(selectionKey.isAcceptable()){
                    // 2. 获取SocketChannel,注册进selector中
                    ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel)selectionKey.channel();
                    SocketChannel channel = serverSocketChannel1.accept();
                    // 同样要设置成非阻塞模式
                    channel.configureBlocking(false);
                    // 这里同时可以创建并绑定一个buffer对象进行传输
                    channel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                // 若为可读事件
                if(selectionKey.isReadable()){
                    // 反向获取channel
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    // 获取对应的buffer对象
                    ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                    channel.read(buffer);
                    // 打印读取到的值
                    System.out.println(new String(buffer.array()));
                }
                // 这里很重要,处理后记得把事件删除
                iterator.remove();
            }


        }

    }
}

再来一个客户端:

public class SelectorClient {
    public static void main(String[] args) throws Exception{
        // 创建一个连接
        SocketChannel channel = SocketChannel.open();
        // 绑定端口
        channel.connect(new InetSocketAddress("127.0.0.1", 5555));
        String str = "hello, 树先生";
        // 创建一个buffer传输数据
        ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
        // 设置为非阻塞模式
        channel.configureBlocking(false);
        channel.write(byteBuffer);
    }
}

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P1mk2eIW-1619023129205)(uploads/image-20210402210554015.png)]

可以看到,服务器在读了一次数据之后并没有阻塞,而是继续监听等待别的事件,所以达到了非阻塞的效果

到这里,java中的NIO原理和使用应该都大致了解了,那么有了NIO为什么还要使用netty呢?,下一章我们继续学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值