资讯详情

浅谈(Java)NIO通信

??

?

??

??

??相遇是缘分,既然来了就拿着小板凳坐下来聊一会儿,如果在文中有所收获,请不要忘记一键三连,??,你的鼓励是我创作的动力!


文章目录

  • Java NIO - 基础详解
    • 流与块
    • 通道及缓冲区
      • 1. 通道
      • 2. 缓冲区
    • 缓冲区状态变量
    • 文件 NIO 实例
    • 选择器
      • 1. 创建选择器
      • 2. 将通道注册到选择器上
      • 3. 监听事件
      • 4. 获取到达事件
      • 5. 事件循环
    • 套接字 NIO 实例
    • 内存映射文件
    • 对比

Java NIO - 基础详解

新的输入/输出 (NIO) 库是在 JDK 1.4 中引入弥补了原来的 I/O 不足提供了面向块的高速和高速 I/O。

Standard IO是对字节流的读写IO在此之前,首先创建一个流对象,流对象的读写操作是按字节进行的 ,读或写字节。NIO把IO抽象成块,类似于磁盘的读写,每次IO操作单位是块,块读入内存后是块byte[],NIO多个字节可以一次读写。

流与块

I/O 与 NIO 最重要的区别是数据包装和传输,I/O 以流的方式处理数据, NIO 数据以块的形式处理。

面向流的 I/O 处理一个字节数据: 输入流产生字节数据,输出流消耗字节数据。为流量数据创建过滤器非常容易,链接几个过滤器,这样每个过滤器只负责复杂处理机制的一部分。缺点是面向流量 I/O 通常很慢。

面向块的 I/O 一次处理数据块,按块处理数据比按流处理数据快得多。但是面向块的 I/O 缺少一些面向流 I/O 优雅简约。

I/O 包和 NIO 集成良好,java.io.* 已经以 NIO 基础已经重新实现,现在可以用了 NIO 一些特点。java.io.* 包中的一些类别包括以块的形式读写数据的方法,这使得即使在面向流系统中,处理速度也会更快。

通道及缓冲区

1. 通道

通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。

通道和流的区别在于流只能向一个方向移动(一个流必须是 InputStream 或者 OutputStream 子类),通道是双向的,可以同时用于读写或读写。

通道包括以下类型:

  • FileChannel: 从文件中读写数据;
  • DatagramChannel: 通过 UDP 读写网络中的数据;
  • SocketChannel: 通过 TCP 读写网络中的数据;
  • ServerSocketChannel: 可以监控新进来的 TCP 连接将为每每个新的连接 SocketChannel。

2. 缓冲区

所有发送道的所有数据都必须首先放在缓冲区。同样,从通道中读取的任何数据都必须先读到缓冲区。换句话说,通道不会直接读写数据,而是通过缓冲区。

缓冲区本质上是一个数组,但它不仅仅是一个数组。缓冲区提供结构化访问数据,并跟踪系统的读写过程。

缓冲区包括以下类型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

缓冲区状态变量

  • capacity: 最大容量;
  • position: 目前已读写的字节数;
  • limit: 还可以读写字节数。

例如:状态变量的变化过程:

① 大小新建 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略了。

image.png

② 读取输入通道 5 此时,个字节数据写入缓冲区 position 移动设置为 5,limit 保持不变。

[外链图片存储失败,源站可能有防盗链机制,建议保存图片直接上传(img-S0BYWuh1-1651325208397)(https://s2.51cto.com/images/20220424/1650785830867472.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)]

③ 在将缓冲区的数据写入输出通道之前,需要调用 flip() 方法,这个方法会 limit 设置为当前 position,并将 position 设置为 0。

④ 从缓冲区取出 4 个字节到输出缓冲中,此时 position 设为 4。

⑤ 最后需要调用 clear() 方法清空缓冲区,此时 position 和 limit 都被设置为初始位置。

文件 NIO 实例

使用情况如下 NIO 快速复制文件的例子:

public static void fastCopy(String src, String dist) throws IOException { 
              /* 输入字节流获取源文件 */     FileInputStream fin = new FileInputStream(src);      /* 获取输入字节流的文件通道 */     FileChannel fcin = fin
       
        .
        getChannel
        (
        )
        ; 
        /* 获取目标文件的输出字节流 */ 
        FileOutputStream fout 
        = 
        new 
        FileOutputStream
        (dist
        )
        ; 
        /* 获取输出字节流的通道 */ 
        FileChannel fcout 
        = fout
        .
        getChannel
        (
        )
        ; 
        /* 为缓冲区分配 1024 个字节 */ 
        ByteBuffer buffer 
        = 
        ByteBuffer
        .
        allocateDirect
        (
        1024
        )
        ; 
        while 
        (
        true
        ) 
        { 
          
        /* 从输入通道中读取数据到缓冲区中 */ 
        int r 
        = fcin
        .
        read
        (buffer
        )
        ; 
        /* read() 返回 -1 表示 EOF */ 
        if 
        (r 
        == 
        -
        1
        ) 
        { 
          
        break
        ; 
        } 
        /* 切换读写 */ buffer
        .
        flip
        (
        )
        ; 
        /* 把缓冲区的内容写入输出文件中 */ fcout
        .
        write
        (buffer
        )
        ; 
        /* 清空缓冲区 */ buffer
        .
        clear
        (
        )
        ; 
        } 
        } 
       

选择器

NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。

NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。

通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。

因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件具有更好的性能。

应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。

1. 创建选择器

Selector selector = Selector.open();   

2. 将通道注册到选择器上

ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);  

通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。

在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

它们在 SelectionKey 的定义如下:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;  

可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;   

3. 监听事件

int num = selector.select();

使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。

4. 获取到达的事件

Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) { 
        
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()) { 
        
        // ...
    } else if (key.isReadable()) { 
        
        // ...
    }
    keyIterator.remove();
}

5. 事件循环

因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。

while (true) { 
        
    int num = selector.select();
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = keys.iterator();
    while (keyIterator.hasNext()) { 
        
        SelectionKey key = keyIterator.next();
        if (key.isAcceptable()) { 
        
            // ...
        } else if (key.isReadable()) { 
        
            // ...
        }
        keyIterator.remove();
    }
} 

套接字 NIO 实例

public class NIOServer { 
        

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

        Selector selector = Selector.open();

        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.configureBlocking(false);
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

        ServerSocket serverSocket = ssChannel.socket();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
        serverSocket.bind(address);

        while (true) { 
        

            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = keys.iterator();

            while (keyIterator.hasNext()) { 
        

                SelectionKey key = keyIterator.next();

                if (key.isAcceptable()) { 
        

                    ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();

                    // 服务器会为每个新连接创建一个 SocketChannel
                    SocketChannel sChannel = ssChannel1.accept();
                    sChannel.configureBlocking(false);

                    // 这个新连接主要用于从客户端读取数据
                    sChannel.register(selector, SelectionKey.OP_READ);

                } else if (key.isReadable()) { 
        

                    SocketChannel sChannel = (SocketChannel) key.channel();
                    System.out.println(readDataFromSocketChannel(sChannel));
                    sChannel.close();
                }

                keyIterator.remove();
            }
        }
    }

    private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException { 
        

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        StringBuilder data = new StringBuilder();

        while (true) { 
        

            buffer.clear();
            int n = sChannel.read(buffer);
            if (n == -1) { 
        
                break;
            }
            buffer.flip();
            int limit = buffer.limit();
            char[] dst = new char[limit];
            for (int i = 0; i < limit; i++) { 
        
                dst[i] = (char) buffer.get(i);
            }
            data.append(dst);
            buffer.clear();
        }
        return data.toString();
    }
}
public class NIOClient { 
        

    public static void main(String[] args) throws IOException { 
        
        Socket socket = new Socket("127.0.0.1", 8888);
        OutputStream out = socket.getOutputStream();
        String s = "hello world";
        out.write(s.getBytes());
        out.close();
    }
}

内存映射文件

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。

向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。

下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。

MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);

对比

NIO 与普通 I/O 的区别主要有以下两点:

  • NIO 是非阻塞的
  • NIO 面向块,I/O 面向流

Java系列文章摘自Java全栈知识体系,这是一个非常棒的Java网站,知识体系全面,由浅入深的讲解知识点,官网地址:https://pdai.tech/ 文章转载已取得站长本人同意,仅用于学习和知识分享,切勿商用,违者必究。网站上站长的联系方式,进群有福利哟~


标签: rp5n连接电缆

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

 锐单商城 - 一站式电子元器件采购平台  

 深圳锐单电子有限公司