• Netty入门指南之NIO Channel详解


    作者简介:☕️大家好,我是Aomsir,一个爱折腾的开发者!
    个人主页Aomsir_Spring5应用专栏,Netty应用专栏,RPC应用专栏-CSDN博客
    当前专栏Netty应用专栏_Aomsir的博客-CSDN博客

    参考文献

    前言

    从本篇文章开始,我们将深入学习 NIO(非阻塞I/O)编程的相关内容,从头到尾详尽分析,包括Selector和Reactor模型,这将为我们后续学习Netty等更高层次的网络编程库打下坚实的基础。NIO编程的核心元素主要包括ChannelBuffer

    NIO编程使用Channel来进行通信。在服务端,我们还引入了Selector选择器,它帮助我们主动监控客户端的Channel,确保这些Channel能够正常通信(即正常连接且没有阻塞)。通过监控客户端的请求链路,Selector的作用是,一旦发现某个客户端阻塞,它可以将分配给该客户端的线程重新分配给其他可用客户端,这样一个线程就能为多个客户端提供服务。需要注意的是,每个客户端都拥有自己独立的Channel,不共享一个Channel。这篇文章将深入学习和理解Channel的概念,为后续的内容打下坚实的基础。

    Channel

    简介

    Channel是一种用于IO通信的管道,类似于传统的InputStreamOutputStream。然而,与传统IO流不同,其中有输入流和输出流的方向性,NIO中的Channel是 无方向性。在传统IO开发中,为了读取文件并在JVM中进行操作后将结果写回文件,我们需要一个InputStream输入流将文件读入JVM,然后使用OutputStream输出流将结果写回文件。这些流是单向的,每个用于特定的目的。而在NIO中,Channel既可以用于读取数据,也可以用于写入数据,这为通用的双向数据传输提供了更大的灵活性
    在这里插入图片描述

    常见Channel

    • 文件IO操作
      • FileChannel:读和写文件中的数据
    • 网络IO操作
      • ServerSocketChannel:监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接创建一个SocketChannel
      • SocketChannel:通过TCP读写网络中的数据
      • DatagramChannel:通过UDP读写网络中的数据

    Channel获取方式

    • 文件IO操作
      • FileInputStream/FileOutputStream获取
      • RandonAccessFile获取
    • 网络通信
      • Socket获取
      • ServerSocket获取
      • DatagramSocket获取
    FileChannel channel = new FileInputStream(TestNIO1.class.getClassLoader().getResource("data.txt").getFile()).getChannel();
    
    FileChannel channel = new RandomAccessFile("data.txt", "rw").getChannel();
    
    • 1
    • 2
    • 3

    Buffer简介

    因在下一篇文章中,我们将详细讲解Buffer,因为它是NIO中的一个非常核心的概念。掌握好Buffer对于理解后续的内容,包括Netty,至关重要。

    Buffer可以被看作是JVM内存中的一块区域,它类似于一个缓冲区。在传统的流通信中,我们通常使用字节数组来装载接收到的数据。Buffer也类似于这个字节数组,但不同之处在于它具有读写指针,用于标识内存中的数据是用于读取还是写入。这一特性使得Buffer非常适用于NIO中的数据处理

    演示案例

    IO流

    如下是使用Stream流的方式的进行读操作

    public class TestNIO1 {
        private static final Logger log = LoggerFactory.getLogger(TestNIO1.class);
    
        public static void main(String[] args) throws IOException {
            // 创建输入流
            InputStream inputStream = TestNIO1.class.getClassLoader().getResourceAsStream("data.txt");
    
            // 创建缓冲区
            byte[] bytes = new byte[1024];
    
            // 读取数据到缓冲区
            while (inputStream.read(bytes) != -1){
                String s = new String(bytes);
                log.info("s = {}", s);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    Channel

    错误案例

    在下面的案例中,我们设置Buffer的大小为10个字节。但是,如果我们尝试读取一个13字节的数据流,那么后面的3个字节将一直保留在缓冲区中,无法读取。动态调整Buffer的大小可能不是一个明智的选择,因为这会引入复杂性。

    为了解决这个问题,我们可以采用改进案例中的方法。不再固定Buffer的大小,而是使用循环不断读取,直到所有数据被消耗。这种方式可以有效地处理不定长度的数据,而不需要频繁地调整缓冲区大小

    public class TestNIO1 {
        private static final Logger log = LoggerFactory.getLogger(TestNIO1.class);
    
        public static void main(String[] args) throws IOException {
           // 1.创建Channel通道 - FileChannel
           FileChannel channel = new FileInputStream("/Users/aomsir/MyStudyProject/Java/Netty/netty-basic-01/data.txt").getChannel();
    
           // 2.创建Buffer缓冲区,容量为10字节,具体根据文件编码来定
           ByteBuffer buffer = ByteBuffer.allocate(10);
    
           while (true) {
               // 3.把channel读取的数据放入buffer,读完以后返回的是-1
               int read = channel.read(buffer);
               if (-1 == read) {
                   break;
               }
    
               // 4.设置buffer为读模式,代表程序从buffer中读取数据
               buffer.flip();
    
               // 5.循环读取缓冲区数据
               while (buffer.hasRemaining()) {
                   byte b = buffer.get();
                   System.out.println((char) b);
               }
    
               // 6.操作之后将buffer设置为写模式
               buffer.clear();
           }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    改进案例

    改进案例非常简单,我们可以通过循环复用Buffer来处理未读取的数据。这意味着我们不需要不断调整Buffer的大小,而是在一个循环中不断读取数据,直到所有数据都被处理。这种方法能够有效地解决数据流长度不确定的情况,确保不会漏掉任何数据。同时上一个错误案例没有做异常处理,此改进版本做了异常处理。

    public class TestNIO2 {
        private static final Logger log = LoggerFactory.getLogger(TestNIO2.class);
    
        public static void main(String[] args) {
    
            FileChannel channel = null;
            try {
    
                // 1.通过FileInputStream获取对应的FileChannel管道
                channel = new FileInputStream(TestNIO1.class.getClassLoader().getResource("data.txt").getFile()).getChannel();
    
                // 2.创建Buffer缓冲区,容量为10字节,具体根据文件编码来定
                ByteBuffer buffer = ByteBuffer.allocate(10);
    
                while (true) {
                    // 3.让buffer从channel中读取数据,如果没有读到数据则返回-1
                    int read = channel.read(buffer);
                    if (-1 == read) {
                        break;
                    }
    
                    // 4.设置buffer为读模式,代表程序从buffer中读取数据
                    buffer.flip();
    
                    // 5.循环读取缓冲区数据
                    while (buffer.hasRemaining()) {
                        byte b = buffer.get();
                        log.info("(char)b = {}", (char)b);
                    }
    
                    // 6.操作之后将buffer设置为写模式,方便下一次写入数据
                    buffer.clear();
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (channel != null) {
                    try {
                        channel.close();
                    } catch (IOException e) {
                        throw new RuntimeException();
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    注意

    在Buffer中,通过使用flip()方法,我们可以切换为读操作的模式。这表示其他程序可以从Buffer中读取数据。另一方面,要切换为写操作模式,我们可以使用clear()方法。这表示后续的程序可以向Buffer中写入数据,或者从Channel中读取数据并放入Buffer中进行进一步处理。

    总结

    在本篇文章中,我们将深入研究NIO中的Channel,探讨其双向可读可写的特性,介绍一些常见的Channel类型以及它们的创建方式。我们还将详细演示FileChannel的使用,而在后续的内容中,我们将逐渐学习SocketChannel、ServerSocketChannel等更多内容。这些知识将有助于我们更好地理解和充分利用NIO中的通道,为后续的学习和应用奠定坚实的基础。

  • 相关阅读:
    Elasticsearch—(MacOs)
    Leecode-SQL 1393. 股票的资本损益
    秋招每日一题T2——统计子矩阵
    GitHub:黑客盗用 OAuth 令牌,导致数十个组织数据泄露
    C++ STL简介
    Pytorch实现线性回归
    MTK RILD 无法启动问题分析
    【大规模 MIMO 检测】基于ADMM的大型MU-MIMO无穷大范数检测研究(Matlab代码实现)
    Spring是什么?
    1.5-39:与7无关的数
  • 原文地址:https://blog.csdn.net/qq_43266723/article/details/134274916