Netty指南之IO和NIO的介绍(一)

一、前言

在学习 Netty 框架之前,我们需要先理解 Java 的 IO 和 NIO 模型。这两种模型在网络编程中扮演着重要角色,而 Netty 则是基于 NIO 模型构建的高性能网络应用框架。本文将详细介绍 IO 和 NIO 的概念、特点以及它们之间的区别,为后续学习 Netty 打下坚实基础。

二、IO 和 NIO

下表总结了 Java IO 和 NIO 之间的主要区别:

IO 与 NIO 的对比
特性IONIO
处理方式阻塞非阻塞
数据处理面向流面向缓冲
传输方式单向双向(通过 Channel)
并发处理多线程单线程可处理多连接
编程难度简单相对复杂
适用场景连接数少,流程简单高并发,大量连接
2.1. 阻塞与非阻塞

阻塞式操作:当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有数据可读或数据完全写入。

非阻塞操作:线程可以发起 IO 请求后立即返回,做其他事情。
Java IO是属于阻塞式,NIO属于非阻塞式操作。阻塞式最大的缺点就是导致资源利用率不高。

2.2.面向流(Stream)和面向缓冲(Buffer)

面向流:IO 是面向流的,每次从流中读取一个或多个字节,直至读取所有字节。

面向缓冲:NIO 是面向缓冲区的,数据被读取到一个缓冲区中,便于处理。

下表总结了 面向流 和 面向缓冲的主要区别:

特性面向流面向缓冲
数据处理单位字节流缓冲区
数据读写方式从流中顺序读取从缓冲区中读取或写入
处理灵活性只能顺序读写可以随机访问缓冲区的任何位置
数据缓存不直接缓存数据数据缓存在缓冲区中
适用场景简单的顺序读写需要灵活处理的数据
读写操作read()或write()方法flip()、clear()等缓冲区操作
处理效率相对较低较高,特别是在大量数据处理时
内存使用通常较少可能较多,需要分配缓冲区
编程复杂度相对简单较复杂,需要管理缓冲区状态
数据转换需要为不同数据类型创建不同的流可以使用不同的缓冲区类型(如ByteBuffer、CharBuffer)

三、同步和异步

在这里提一嘴同步和异步。

同步:在同步操作中,发起调用的线程会等待操作完成后才继续执行。

异步:在异步操作中,发起调用的线程不会等待操作完成,而是继续执行其他任务。操作完成后通过回调、通知等机制来通知调用方。

在这里用我们熟悉的表格来总结一下IO同步异步的差别:

特性同步阻塞 IO同步非阻塞 IO异步 IO
阻塞性阻塞非阻塞非阻塞
CPU 利用高(轮询消耗 CPU)
编程复杂度简单复杂较复杂
适用场景连接数少高并发高并发,长连接

四、小试牛刀

光练不说傻把式,又练又说真把式,那么接下来我们搭建一下来试试IO和NIO的差别,在实操的感受过程中,再想一下为什么有了NIO还有Netty的诞生呢?

IO初体验

4.1. 服务端代码


/**
 * @Author: Panjiangfeng
 * @CreateTime: 2024-10-11  23:23
 * @Description: IOClient
 * @Version: 1.0
 */
public class IOClient {
    public static void main(String[] args) {

        new Thread(() -> {

            try {
                Socket socket = new Socket("127.0.0.1", 8000);

                while (true) {
                    try {
                        // 发送消息到服务端
                        socket.getOutputStream().write((new Date() + ": hello world").getBytes());

                        // 休眠2秒
                        Thread.sleep(2000);

                    } catch (Exception e) {
                        e.printStackTrace(); // 打印异常
                    }
                }

            } catch (IOException e) {
                e.printStackTrace(); // 打印异常
            }

        }).start();
    }

}

4.2客户端代码

/**
 * @Author: Panjiangfeng
 * @CreateTime: 2024-10-11  23:22
 * @Description: IOServer
 * @Version: 1.0
 */
public class IOServer {

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

        ServerSocket serverSocket = new ServerSocket(8000);

        // 接收新连接的线程
        new Thread(() -> {
            while (true) {
                try {
                    // (1) 阻塞方法获取新连接
                    Socket socket = serverSocket.accept();

                    // (2) 为每一个新连接都创建一个新线程,负责读取数据
                    new Thread(() -> {
                        try {
                            int len;
                            byte[] data = new byte[1024];
                            InputStream inputStream = socket.getInputStream();

                            // (3) 按字节流方式读取数据
                            while ((len = inputStream.read(data)) != -1) {
                                System.out.println(new String(data, 0, len));
                            }

                        } catch (IOException e) {
                            e.printStackTrace(); // 打印异常
                        }
                    }).start();

                } catch (IOException e) {
                    e.printStackTrace(); // 打印异常
                }
            }
        }).start();
    }
}

接下来先启动服务端代码,再启动客户端代码就可以每隔2秒给服务端发送消息,这也很简单,看代码大家都能看懂吧。其实Java IO的代码还是很简单的吧
服务端接受的消息
NIO初体验
想必大家其实对NIO可能还是不是很懂,所以这里初体验当然是先初以下再体验咯,大家先学习下相关概念哈,很重要,很重要,很重要

Channel
通常来说, 所有的 NIO 的 I/O 操作都是从 Channel 开始的. 一个 channel 类似于一个 stream。

Channel 类型有:

FileChannel, 文件操作

DatagramChannel, UDP 操作

SocketChannel, TCP 操作

ServerSocketChannel, TCP 操作, 使用在服务器端。

这些通道涵盖了 UDP 和 TCP网络 IO以及文件 IO。

Buffer
当我们需要与 NIO Channel 进行交互时, 我们就需要使用到 NIO Buffer, 即数据从 Buffer读取到 Channel 中, 并且从 Channel 中写入到 Buffer 中.
实际上, 一个 Buffer 其实就是一块内存区域, 我们可以在这个内存区域中进行数据的读写. NIO Buffer 其实是这样的内存块的一个封装, 并提供了一些操作方法让我们能够方便地进行数据的读写。

Buffer 类型有:

ByteBuffer

CharBuffer

DoubleBuffer

FloatBuffer

IntBuffer

LongBuffer

ShortBuffer

这些 Buffer 覆盖了能从 IO 中传输的所有的 Java 基本数据类型。

selector

selector 是 NIO 中才有的概念, 它是 Java NIO 之所以可以非阻塞地进行 IO 操作的关键。

通过 Selector, 一个线程可以监听多个 Channel 的 IO 事件, 当我们向一个 Selector 中注册了 Channel 后, Selector 内部的机制就可以自动地为我们不断地查询(select) 这些注册的 Channel 是否有已就绪的 IO 事件(例如可读, 可写, 网络连接完成等). 通过这样的 Selector 机制, 我们就可以很简单地使用一个线程高效地管理多个 Channel 了。

相信大家看完上面的玩意,脑袋里都是懵逼的,这啥玩意啊,没事,先顺着看下去,咱们把代码先跑起来,下一章会对NIO详细深入了解。

服务端代码:


public class NIOServer {

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

        Selector serverSelector = Selector.open();
        Selector clientSelector = Selector.open();

        // 服务端监听线程
        new Thread(() -> {
            try {
                // 对应IO编程中的服务端启动
                ServerSocketChannel listenerChannel = ServerSocketChannel.open();
                listenerChannel.socket().bind(new InetSocketAddress(8000));
                listenerChannel.configureBlocking(false);
                listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);

                while (true) {
                    // 监测是否有新连接,这里的1指阻塞的时间为1ms
                    if (serverSelector.select(1) > 0) {
                        Set<SelectionKey> set = serverSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();

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

                            if (key.isAcceptable()) {
                                try {
                                    // 每来一个新连接,不需要创建一个线程,而是直接注册到 clientSelector
                                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                                    clientChannel.configureBlocking(false);
                                    clientChannel.register(clientSelector, SelectionKey.OP_READ);
                                } finally {
                                    keyIterator.remove();
                                }
                            }
                        }
                    }
                }
            } catch (IOException ignored) {
                ignored.printStackTrace();
            }
        }).start();

        // 客户端读写线程
        new Thread(() -> {
            try {
                while (true) {
                    // 批量轮询哪些连接有数据可读,这里的1指阻塞的时间为1ms
                    if (clientSelector.select(1) > 0) {
                        Set<SelectionKey> set = clientSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();

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

                            if (key.isReadable()) {
                                try {
                                    SocketChannel clientChannel = (SocketChannel) key.channel();
                                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

                                    // 面向Buffer的读取操作
                                    clientChannel.read(byteBuffer);
                                    byteBuffer.flip();
                                    System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());
                                } finally {
                                    keyIterator.remove();
                                    key.interestOps(SelectionKey.OP_READ);
                                }
                            }
                        }
                    }
                }
            } catch (IOException ignored) {
                ignored.printStackTrace();
            }
        }).start();
    }
}

是不是特别复杂呢小伙伴们,这就是为什么引入Netty的原因,先不考虑这个问,我们先来分析一下NIO的代码吧

  • NIO模型一般会绑定2个线程,每个线程都绑定一个轮训器Seletor。

    serverSelector:负责轮训是否有新连接。

    clientSelector:负责轮训是否有数据能读。

  • 服务端监测到新连接之后,不再创建一个新线程,而是直接将新连接绑定到clientSelector上,这样就不用IO模型中的1万个while循环死等。

  • clientSelector被一个while死循环包裹着,如果在某一时刻有多个连接有数据可读,那么通过clientSelector.select(1)方法可以轮询出来,进而批量处理。

结语

如果这一章NIO这一块的代码不太清晰,大家先看下一章对于NIO的详解,然后回过头来复习一下一定会收获满满。

文中: 源代码点我跳转

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值