nio 01 核心组件

线程是非常宝贵的资源!!

线程是非常宝贵的资源!!

线程是非常宝贵的资源!!

NIO 就是要解决线程浪费的问题!

BIO\NIO\AIO

BIO 同步阻塞式 (Blocking I/O)

所谓的阻塞就是在客户端读写过程,此时线程会出于等待状态,是一种浪费

在java1.4之前 这种传输的实现 只能通过inputStream和OutputStream 实现。这是一种阻塞式IO模型,应对连接数不多的文件系统还好,如果应对的是成千上万的网络服务,这种阻塞式模型就会造成大量的线程占用,造成服务器无法承载更高的并发。

NIO 同步非阻塞式(Non Blocking I/O)

为解决这个问题 java1.4之后引入了NIO ,它通过双向管道进行通信,并且支持以非阻塞式的方式进行,就解决了网络传输导致线程占用的问题。Netty其在底层就是采用这种通信模型。

AIO 异步非阻塞式(Asynchronous Blocking I/O)

NIO的非阻塞的实现是依赖选择器 对管道状态进行轮循实现,如果同时进行的管道较多,性能必会受影响,所以java1.7引入了 异步非阻塞式IO,通过异步回调的方式代替选择器。这种改变在windows下是很明显,在linux系统中不明显。现大部分JAVA系统都是通过linux部署,所以AIO直正被应用的并不广泛。所以我们接下学的学习重点更关注到BIO与NIO的对比。

TOMCAT

tomcat6.0开始支持bio\nio,但是默认bio

tomcat7.0默认nio

tomcat8.0开始aio

NIO 基础组件 Buffer 与 Channel

缓冲区 Buffer

Buffer 是数据容器,里面 只能存储byte、int、char三种类型,不具备线程安全能力

数据存储结构上可以理解成数组

但它比数组复杂较多,里面有 几个概念

  • capacity:整个缓冲区的容量大小,它由开发者定义的一个固定值,

  • position:当前指针读取的位置

  • limit:对空间的限制

除了上述几个概念,还有 几个方法值得关注

  • put\get 设置\读取数据,此时 position 会 +1
  • flip 为读取做好准备,将 position 置为 0,limit 置为原 position
  • clear 回到缓冲的初始状态(但不会删除数据,结合下一条理解)
  • buffer 没有清空数据的概念
  • mark 在当前 position 位置添加标记
  • reset 将 position 回到 mark 的位置,用于替换内容的场景
  • rewind 将 position、mark 初始化

理清楚几个关键点

0 <= mark <= position <= limit <= capacity

下面我们结合代码理解

package com.shar.netty.buffer;

import org.junit.Test;

import java.nio.IntBuffer;
import java.util.Arrays;

public class IntBufferTest {

    @Test
    public void test1(){

        //定义长度 capacity 为 6 的缓冲空间
        //此时,position=0、limit=6
        IntBuffer allocate = IntBuffer.allocate(6);

        //放置数据,然后 position 会加1
        allocate.put(1);
        allocate.put(2);
        System.out.println(Arrays.toString(allocate.array()));
        //[1, 2, 0, 0, 0, 0]

        //为读取数据做准备,position 回到 0,limit 置为原 position
        allocate.flip();

        //获取数据,然后 position 会加1
        allocate.get();
        allocate.get();

        // 回到缓冲的初始状态
        // Buffer 没有删除\清空数据这个说法
        // 循环利用空间
        allocate.clear();
        System.out.println(Arrays.toString(allocate.array()));
        //[1, 2, 0, 0, 0, 0]

        for (int i = 0; i < allocate.array().length; i++) {
            allocate.put(i+1);
        }
        allocate.flip();

        // mark 方法,添加标记,以便后续调用 reset ,将 position 回到标记,用于替换内容的场景
        allocate.get();
        System.out.println(Arrays.toString(allocate.array()));
        //[1, 2, 3, 4, 5, 6]
        allocate.mark();
        allocate.reset();
        allocate.put(888);
        System.out.println(Arrays.toString(allocate.array()));
        //[1, 888, 3, 4, 5, 6]
    }
}

NIO 之 普通管道 FileChannel

下面我们测试下 FileChannel 的读写

package com.shar.netty.channel;


import org.junit.Test;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelTest  {

    String file_name = "c://javatest/李志歌词.txt";

    @Test
    public void test1() throws IOException {

        //1. 打开文件管道
        FileChannel channel = new RandomAccessFile(file_name,"rw").getChannel();
        // 把缓冲区数据写入到管道
        channel.write(ByteBuffer.wrap("飞机飞过天空22".getBytes()));
        channel.close();

    }

    @Test
    public void test2() throws IOException {

        //1. 打开文件管道
        FileChannel channel = new RandomAccessFile(file_name,"r").getChannel();

        // 声明1024个空间
        ByteBuffer buffer=ByteBuffer.allocate(1024);

        // 从文件中 读取数据并写入管道 在写入缓冲
        channel.read(buffer);

        //准备就绪,为读取做准备
        buffer.flip();

        byte[] bytes= new byte[buffer.remaining()];
        int i =0;
        while (buffer.hasRemaining()){
            bytes[i++]= buffer.get();
        }
        System.out.println(new String(bytes));
    }
}

有意思的来了

如果你已经运行了test1,那么,请把test1中的中文 “飞机飞过天空22” 改成 “飞机飞过天空

你觉得这个txt会变成什么样?

txt 没变化!

原因我没深究,我的猜测是前面学习的 Buffer 的 position 机制~

对比传统IO

传统的io,需要建立两条通道

NIO 体系下,走的是 双向管道 Channel -> 缓冲区 机制

下面我使用 传统IO普通管道 做下读写性能测试

这块的效率我没对比出个所以然来,不用太纠结,Netty 的重点在网络编程~

package com.shar.netty.channel;

import org.junit.Test;
import sun.misc.IOUtils;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ChannelTest {

    String file_name = "c://javatest/02.杭州 (Live)_李志.wav";
    String copy_name = "c://javatest/02.杭州 (Live)_李志_备份.wav";
    //1.基于普通文件流读写
    @Test
    public void streamReadTest() throws IOException {

        FileInputStream inputStream = new FileInputStream(file_name);
        long begin = System.currentTimeMillis();
        byte[] bytes = IOUtils.readFully(inputStream, -1, false);
        System.out.println((System.currentTimeMillis() - begin) );

        begin = System.currentTimeMillis();
        Files.write(Paths.get(copy_name), bytes);
        System.out.println((System.currentTimeMillis() - begin) );

        inputStream.close();
    }
    // 2.基于普通管道读写
    @Test
    public void channelTest() throws IOException {

        long begin = System.currentTimeMillis();
        FileChannel channel = new RandomAccessFile(file_name,"rw").getChannel();
        ByteBuffer dst=ByteBuffer.allocate((int) channel.size());
        channel.read(dst);
        byte[] bytes=new byte[(int) channel.size()];
        dst.flip();
        dst.get(bytes);
        System.out.println((System.currentTimeMillis() - begin) );

        // 写入性能
        begin = System.currentTimeMillis();
        ByteBuffer wrap = ByteBuffer.wrap(bytes);
        channel.write(wrap);
        System.out.println((System.currentTimeMillis() - begin) );

        channel.close();
    }
}

零拷贝技术

@Test
public void test3() throws IOException {
    FileChannel source = new FileInputStream(file_name).getChannel();
    FileChannel target = new FileOutputStream(copy_name).getChannel();
    source.transferTo(0, source.size(), target);
    source.close();
    target.close();
}

在上面的复制过程 效率是极高 的。原因是 channel 具体实现了 零copy机制

其核心思想是为了在主存数据搬移到io设备的过程中,避免用户态内存的参与

它的重要前提是 不对数据进行重新计算,而只是单纯的搬移数据

在 NIO 包中借助 buffer 的直接内存 特性实现了 零copy技术模型

关于 buffer 的直接内存:

在 java 中直接内存不受jvm管理,在 jdk1.4 版本之后出现的 nio 接口的引入的 channel 和 buffer 的读取 IO 的方式中,nio 可以直接通过 native 函数库在核空间直接分配堆外内存,然后通过在 java 堆中的 DirectByteBuffer 对象引用堆外内存的地址,并通过这个引用间件地实现与 IO 设备之间的数据交换,避免了java 堆内内存与核堆外内存之间的 copy 操作,提高了执行效率。

NIO 之 UPD管道 DatagramChannel

DatagramChannel 是一个 UDP 管道,我们用它来模拟一个UDP 服务器

package com.shar.netty.channel;

import org.junit.Test;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;

public class DatagramChannelTest {

    @Test
    public void test1() throws IOException {

        //创建一个 datagram 通道
        DatagramChannel channel=DatagramChannel.open();
        //绑定端口
        channel.bind(new InetSocketAddress(6784));
        //声明缓存空间
        ByteBuffer buffer=ByteBuffer.allocate(111);

        while (true){
            //清空还原
            buffer.clear();
            //阻塞,等待客户端发消息过来
            channel.receive(buffer);
            buffer.flip();
            byte[] bytes=new byte[buffer.remaining()];
            buffer.get(bytes);
            System.out.println(new String(bytes));
        }
    }
}

此时服务器出于阻塞状态,等待客户端发数据上来

我们打开 cmd,用 nc 命令测试

如果你不会 nc ,请看这篇 netcat 网络测试工具

nc -u 127.0.0.1 6784

此时你可以通过控制台发送字符串给后台了!

那么,数据能不能从服务端写回给客户端呢?

这是不行的,因为 UDP 是单向的。

物联网开发喜欢用 UDP 服务器做数据采集,也正是因为单向通行的机制高效!

NIO 之 TCP管道 ServerSocketChannel

ServerSocketChannel 如何使用呢?

ServerSocketChannel 简单示范

我们先用一段简单的 test 做下示范

@Test
public void test1() throws IOException {
    // 打开管道
    ServerSocketChannel channel = ServerSocketChannel.open();
    channel.bind(new InetSocketAddress(8080));
    // 建立连接
    SocketChannel socketChannel = channel.accept();
    // 创建缓冲区
    ByteBuffer buffer = ByteBuffer.allocate(8192);
    // 管道数据读入buffer
    socketChannel.read(buffer);
    // 从buffer 当中读出来
    buffer.flip();
    byte[] bytes = new byte[buffer.remaining()];
    buffer.get(bytes);
    String message = new String(bytes);
    System.out.println(message);
    // 写回给客户端
    buffer.rewind();
    socketChannel.write(buffer);
    socketChannel.close();
    channel.close();
}

打开 cmd 执行 telnet localhost 8080,此时我们可以在控制台输入字符

服务器会接收到并且打印(因为没有while循环,接收一次前端请求后程序会自动终止)!

模拟 tomcat bio 阻塞模型

接下来我们给上面的程序加入 while 循环,简单的模拟下 tomcat bio 的阻塞模型

@Test
public void test2() throws IOException {
    // 打开管道
    ServerSocketChannel channel = ServerSocketChannel.open();
    channel.bind(new InetSocketAddress(8080));
    while (true) {
        // 建立连接
        handle(channel.accept());
    }
}

public void handle(final SocketChannel socketChannel) throws IOException {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            // 创建缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(8192);
            while (true) {
                try {
                    buffer.clear();
                    // 管道数据读入buffer
                    socketChannel.read(buffer);
                    // 从buffer 当中读出来
                    buffer.flip();
                    byte[] bytes = new byte[buffer.remaining()];
                    buffer.get(bytes);
                    String message = new String(bytes);
                    System.out.println(message);
                    // 写回给客户端
                    buffer.rewind();
                    socketChannel.write(buffer);
                    if (message.trim().equals("exit")) {
                        break;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            try {
                socketChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    });
    thread.start();
}

我们结合 bio 模型图理解下

bio 模式下,前端请求进来后 tomcat 会先分配线程,然后在线程中读取前端数据

如果前端数据过大(大文件)或者网速过慢的话,线程就会一直处于工作状态,显然你的业务代码还没执行

那么,能不能优化下数据读取的过程,在数据读取完毕后才开始分配线程呢?

选择器

nio 就是要解决这个问题,它里面有个核心概念:选择器

socket、datagram等都可以注册到选择器中,交由选择器托管!

所有的管道都可以托管吗?不是的

如下类图展示了,你必须实现了 SelectableChannel 接口才可以

下面章节我们开始掌握选择器!


转载请注明来源。 欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。 可以在下面评论区评论,也可以邮件至 sharlot2050@foxmail.com。

文章标题:nio 01 核心组件

字数:2.6k

本文作者:夏来风

发布时间:2020-07-11, 10:00:42

原始链接:http://www.demo1024.com/blog/nio/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。