Java网编之:多路复用NIO 以及一些感悟

目录

一、同步阻塞:BIO

二、同步非阻塞:多路复用NIO

1、NIO概念

2、NIO三大核心:Buffer、Channel、Selector

三、进一步理解NIO

1、缓冲区:Buffer

(1)缓冲区对象的创建

(2)缓冲区对象添加数据

(3)缓冲区对象读取数据

(4)常用的ByteBuffer类

                堆外内存:DirectByteBuffer类

                堆内内存:HeapByteBuffer类

2、通道:Channel

(1)服务器端实现:ServerSocketChannel

(2)客户端实现:SocketChannel

3、选择器+多路复用器:Selector

(1)Selector类

(2)SelectionKey类

(3)Selector编码:服务器端实现

四、异步非阻塞:AIO

五、一些有感而发的废话


一、同步阻塞:BIO

昨天学习了Socket编程。它的本质是一种阻塞IO,即BIO,是常用的IO流。BIO的使用弊端如下:

  1. 使用BIO时,主线程会进入阻塞状态(影响程序性能,不能充分利用机器资源);
  2. 即使是多线程,在高并发状态下,创建很多线程会占用系统内存,线程间的切换也会浪费资源开销。

二、同步非阻塞:多路复用NIO

1、NIO概念

NIO是一种非阻塞IO。它的运行机制为事件驱动(事件可以看作是 程序的输入、输出、读或写 等一系列行为),所以NIO只有在真正有读写事件驱动时才会进行读写。

此时一个服务器会处理多个请求,也就是说客户端发送的连接请求都会注册到多路复用器(Selector选择器)上,然后多路复用器轮询到有IO请求就进行处理:

 NIO机制的好处在于:

  1. 不必为每个连接都创建一个对应的线程;
  2. 也不必去维护多个线程,减少系统开销;
  3. 同时也避免了多个线程上下文切换导致的资源浪费。

2、NIO三大核心:Buffer、Channel、Selector

缓冲区Buffer存储数据
通道Channel运输
选择器Selector控制器

 在NIO中所有数据都使用Buffer处理(读模式和写模式),这就是和传统BIO的区别:BIO面向Stream流,而NIO面向缓冲区Buffer

我们可以把Buffer理解为一个内存块,它的本质是一个数组,用来存储数据;而Channel提供从网络读取数据的渠道(但数据的读写都必须经由Buffer),所有的Channel都会注册到选择器、也就是多路复用器Selector上。它们之间的关系如下:

  

我们会发现:

  • 对应关系如下:
  • 每个通道Channel都对应一个缓冲区Buffer, 一个选择器Selector对应一个线程,一个线程又对应多个连接,同时每个通道Channel都会注册到多路复用器Selector上
  • 运作过程如下:
  • 多路复用器Selector会不断轮询查看通道Channel上的事件,然后根据不同的事件完成不同的处理操作
  •  数据读取写入机制如下:
  • NIO中对数据的读取写入主要依靠缓冲区Buffer。和BIO的输入输出流不同,NIO中的Buffer可读可写,Channel是双向的。

三、进一步理解NIO

1、缓冲区:Buffer

前面也说了,Buffer的本质是一个可读可写数据的内存块,可理解为是一个数组。

Buffer类是一个抽象类,有七种类型分别对应不同数据类型:

在学习缓冲区的创建、添加和读取数据之前,要先知道三个关键量:

  • capacity --> 容量,也就是总长度
  • limit --> 读写界限
  • position --> 位置

(1)缓冲区对象的创建

static ByteBuffer allocate(int howLong)创建byte类型的指定长度的缓冲区
static ByteBuffer wrap(byte[] array)创建一个有内容的byte类型缓冲区

(2)缓冲区对象添加数据

一个Buffer具体类的对象可以直接调用以下方法实现向缓冲区添加数据:

int position()

position(int newPosition)

获得当前要操作的索引

修改当前要操作的索引位置

int limit()

limit(int newLimit)

最多能操作到哪个索引

修改最多能操作的索引位置

int capacity()返回缓冲区的总长度

int remaining()

boolean hasRemaining()

返回能操作索引的个数

是否还能继续操作

put(byte b)

put(byte[] src)

添加一个字节

添加字节数组

(3)缓冲区对象读取数据

flip()切换写-->读模式(limit设为position位置,position设为0)
clear()切换读-->写模式(limit设为capacity,psitiong设为0)

get()

get(byte[] dst)

get(int index)

一个字节

读多个字节

读指定索引的字节

rewind()重复读取(将position设为0)
array()将缓冲区转为字节数组返回

(4)常用的ByteBuffer类

以上的七种Buffer类型中常用的是ByteBuffer(将数据转为字节处理),它实质上就是一个byte[ ]数组。像个俄罗斯套娃,ByteBuffer又有HeapByteBuffer和DirectByteBuffer之分,简单了解下吧:

  • 堆外内存:DirectByteBuffer类

此类能直接操作操作系统本地代码创建的内存缓冲数组,底层的数据维护在内核缓存中,而不是JVM里。

应用场景如下:

  1. 程序与本地磁盘、socket传输数据
  2. 大文件对象使用,不会受到堆内存大小限制
  3. 不需要频繁创建、生命周期较长的情况
  4. 能重复使用的情况

  • 堆内内存:HeapByteBuffer类

此类创建的字节缓冲区在JVM堆内,也就是JVM内部所维护的字节数组。因为维护在JVM里,所以把内容写进Buffer中的速度会快一点。应用场景就是除了上面对外内存使用的四种情况以外的其他情况。

当向NIO Channel写入数据时,堆内内存HeapByteBuffer需要先把数据拷贝后才发出去;而堆外内存DirectByteBuffer写入时无需拷贝,直接发送。由此也可以看出二者性能上的差异。

2、通道:Channel

其实可以把通道看作是流,只不过区别在于--> 流是单向的(只能读或只能写),而Channel支持双向(读写都可以);并且Channel总是基于Buffer读写。

Channel是一个接口。其常用的实现类主要有:

  • FileChannel:文件数据读写
  • DategramChannel:UDP数据读写
  • ServerSocketChannelTCP数据读写(类似于ServerSocket)
  • SocketChannel:TCP数据读写(类似于Socket)

着重学习一下TCP数据的读写。其实原理和昨天学的BIO Socket通信一样,同样是两个类两个端,因为都是TCP通信机制,所以实现的思路也都一样,只不过Socket是建立一个socket,Channel是建立一个channel(废话,然后要把Channel设为非阻塞模式。

(1)服务器端实现:ServerSocketChannel

实现思路大致如下:

  1. 先打开一个服务端Channel(open()),绑定对应的端口号(bind());
  2. 此时Channel默认是阻塞的,需要设为非阻塞(configureBlocking(false));
  3. 接着检查是否有客户端连接(accept()),如果有就返回对应的Channel;
  4. 然后获取到客户端发来的数据,将其存在byteBuffer缓冲区中(allocate()创建缓冲区);
  5. 接着给客户端回写数据;
  6. 结束后释放资源。
/**
 * 服务端
 */
public class NIOServer {
    public static void main(String[] args) throws IOException,InterruptedException {
        //1. 打开一个服务端通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //2. 绑定对应的端口号
        serverSocketChannel.bind(new InetSocketAddress(9999));
        //3. 通道默认是阻塞的,需要设置为非阻塞(true 为通道阻塞,false 为非阻塞)
        serverSocketChannel.configureBlocking(false);

        System.out.println("服务端启动成功..........");

        while (true) {
            //4. 检查是否有客户端连接(有返回对应的通道 , 否则返回null)
            SocketChannel socketChannel = serverSocketChannel.accept();
            if (socketChannel == null) {
                System.out.println("没有客户端连接...我去做别的事情");
                Thread.sleep(2000);
                continue;
            }

            //5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);  // 建立缓冲区数组
            /**
             * read返回值:
             * 正数:表示本次读到的有效字节个数.
             * 0表示本次没有读到有效字节.
             * -1表示读到了末尾
             **/
            int read = socketChannel.read(byteBuffer);
            System.out.println("客户端消息:" + new String(byteBuffer.array(), 0, read,StandardCharsets.UTF_8));
            //6. 给客户端回写数据
            socketChannel.write(ByteBuffer.wrap("知道拉!".getBytes(StandardCharsets.UTF_8)));
            //7. 释放资源
            socketChannel.close();
        }
    }
}

(2)客户端实现:SocketChannel

相比于服务端,客户端的实现思路就简单了不少,大致如下:

  1. 打开Channel(open()
  2. 连接对应的IP和端口号(connet())
  3. 写--> 数据
  4. 读--> 服务器端写回的数据
  5. 结束后释放资源
/**
 * 客户端
 */
public class NIOClient {
    public static void main(String[] args) throws IOException {
        //1.打开通道
        SocketChannel socketChannel = SocketChannel.open();
        //2.设置连接IP和端口号
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999));
        //3.写出数据
        socketChannel.write(ByteBuffer.wrap("我来给你发消息拉!".getBytes(StandardCharsets.UTF_8)));

        //4.读取服务器写回的数据
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        int read=socketChannel.read(readBuffer);
        System.out.println("服务端消息:" + new String(readBuffer.array(), 0, read,
                StandardCharsets.UTF_8));
        //5.释放资源
        socketChannel.close();
    }
}

3、选择器+多路复用器:Selector

用一个线程处理多个的客户端连接就要用到Selector,它能检测多个注册的服务端Channel上是否有事件发生。如果有,就获取事件,然后处理。这样就可以只用一个单线程去管理多个Channel,也就是管理多个连接和请求:

 

(1)Selector类

常用方法如下:

open()得到一个Selector对象

select()或(阻塞time)

阻塞,监控所有注册的通道。

当有对应的事件操作时,会将SelectionKey放入集合内部并返回事件数量

selectedKeys() 返回所有事件集合

(2)SelectionKey类

常用方法如下:

isAcceptable()是否是连接继续事件
isConnectable()是否是连接就绪事件
isReadable()是否是读就绪事件
isWritable()是否是写就绪事件

定义的4种事件(和方法的调用一样,用.调用),一般用于判断使用:

OP_ACCEPT

连接继续事件

表示服务器监听到了客户连接,服务器可以接收这个连接了

OP_CONNECT

连接就绪事件

表示客户端与服务器的连接已经建立成功

OP_READ

读就绪事件

表示通道中已经有了可读的数据,可以执行读操作

OP_WRITE

写就绪事件

表示可以向通道写数据

(3)Selector编码:服务器端实现

步骤如下:

  1.  打开一个服务端Channel
  2.  绑定对应的端口号
  3.  Channel默认是阻塞的,需设置为非阻塞
  4.  创建Selector对象(同样使用open())
  5.  将服务端Channel注册到Selector上,并指定注册监听的事件为OP_ACCEPT
    register(selector, SelectionKey.OP_ACCEPT)
  6.  检查Selector是否有事件(selector.select()是否为0,0即无事件
  7.  获取事件集合(selector.selectedKeys()
  8.  判断事件是否是客户端连接事件--> SelectionKey.isAcceptable()
  9.  得到客户端Channel,将其注册到Selector上(
    serverSocketChannel.accept()
    ),并指定监听事件为OP_READ(同样为register方法)
  10.  判断是否是客户端读就绪事件--> SelectionKey.isReadable()
  11.  得到客户端Channel,读取数据到Buffer
  12.  给客户端回写数据
  13.  从集合中删除对应的事件(防止二次处理)
/**
 * 服务端-选择器
 */
public class NIOSelectorServer {
    public static void main(String[] args) throws IOException,
            InterruptedException {
        //1. 打开一个服务端通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //2. 绑定对应的端口号
        serverSocketChannel.bind(new InetSocketAddress(9999));
        //3. 通道默认是阻塞的,需要设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //4. 创建选择器
        Selector selector = Selector.open();
        //5. 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("服务端启动成功...");

        while (true) {
            //6. 检查选择器是否有事件
            int select = selector.select(2000);
            if (select == 0) {
                continue;
            }

            //7. 获取事件集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 迭代器重出江湖!!!
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();

                //8. 判断事件是否是客户端连接事件 --> SelectionKey.isAcceptable()
                if (key.isAcceptable()) {
                    //9. 得到客户端通道,并将通道注册到选择器上
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("客户端已连接......" + socketChannel);

                    //必须设置通道为非阻塞, 因为selector需要轮询监听每个通道的事件
                    socketChannel.configureBlocking(false);
                    //并指定监听事件为OP_READ
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }

                //10. 判断是否是客户端读就绪事件 --> SelectionKey.isReadable()
                if (key.isReadable()) {
                    //11.得到客户端通道
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int read = socketChannel.read(byteBuffer); // 读取数据到缓冲区
                    if (read > 0) {
                        System.out.println("客户端消息:" + new String(byteBuffer.array(), 0, read, StandardCharsets.UTF_8));
                        //12.给客户端回写数据
                        socketChannel.write(ByteBuffer.wrap("知道拉!".getBytes(StandardCharsets.UTF_8)));
                        socketChannel.close();
                    }
                }
                //13.从集合中删除对应的事件, 因为防止二次处理.
                iterator.remove();
            }
        }
    }
}

四、异步非阻塞:AIO

AIO采用了Proactor模式(消息异步通知的设计模式)。它通知的不是事件,而是操作完成事件。先由操作系统完成后,才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。


五、一些有感而发的废话

绝了,一个NIO以及其他衍生知识我整理了快七个小时,终于梳顺畅了。想了一下和昨天的BIO相比,连接机制整体没变,但是多了很多细节,也正是因为这些细节提升了IO性能,节省了系统开销。

最近状态超不错,一股干劲的同时效率也很高(陶白白狠准!并且学久了就会发现,万物间真理都是相通的。当知识进入你的脑子,就会在生活中给你反馈。今天中午一个人在取快递的路上听书,发现竟然可以将计算机的体系原理和我们人体的思维模式巧妙联系起来!真的很神奇,把所学东西和感兴趣的哲学联系起来也是真的意外之喜。

仅仅是留校第三天,我却觉得时间过得无比漫长。这种漫长不是空虚导致的漫长,反而或许是好的改变在短时间内呈现给我带来的满足感:原理社交媒体、每天十个小时的专注学习补充我的知识漏洞、健康饮食、规律作息、偶尔运动让我有踏实感以及真实感,一个人的相处时间让我去思考很多东西....都是一些难以用言语去表达的积极情绪和改变。

跑题了==大晚上突然有感而发,希望继续保持状态,冲冲冲!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

颜 然

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值