Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO

第 3 章 JavaNIO 编程

一、简介

1、基本概念

  • JavaNIO 全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的 输入/输出的新特性,被统称为 NIO(也称为 New IO),是同步非阻塞的
  • NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。
  • NIO 有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)
  • NIO 是 面向缓冲区 ,或者面向块编程的。数据读取到的缓冲区,需要时可在缓冲区中前后
    移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
  • JavaNIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果
    目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可
    以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情
  • 通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配 50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个。

2、 NIO 的 Buffer 基本使用

示例代码:

public static void main(String[] args) {

    //创建一个容量为5的int型buffer
    IntBuffer intBuffer = IntBuffer.allocate(5);

    //向buffer中存放数据
    for (int i = 0; i < intBuffer.capacity(); i++) {
        intBuffer.put(i * 2);
    }

    //转换buffer的读写状态(必须加!)
    /**
         *     public final Buffer flip() {
         *         limit = position;
         *         position = 0;
         *         mark = -1;
         *         return this;
         *     }
         */
    intBuffer.flip();

    //从buffer中获取数据
    while (intBuffer.hasRemaining()) {
        System.out.print(intBuffer.get() + " ");
    }
}

结果:

0 2 4 6 8 

3、NIO 和 BIO 的比较

  • BIO 以的方式处理数据,而 NIO 以的方式处理数据,块 I/O 的效率比流 I/O 高很多
  • BIO 是阻塞的,NIO 则是非阻塞
  • BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道 读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道

二、 NIO 三大核心组件详解

1、三大组件(Selector 、 Channel 和 Buffer )之间的关系

  • 每个 channel 都会对应一个 Buffer
  • Buffer 就是一个内存块 , 底层是有一个数组
  • Selector 对应一个线程, 一个线程对应多个 channel(连接)
  • 程序切换到哪个 channel 是有事件(Event )决定的,Event 就是一个重要的概念
  • Selector 会根据不同的事件,在各个通道上切换
  • 数据的读取写入是通过 Buffer, 这个和 BIO ,BIO 中要么是输入流,或者是 输出流, 不能双向,但是 NIO 的 Buffer 是可以读也可以写, 需要 flip 方法切换。
  • channel 是双向的, 可以返回底层操作系统的情况, 比如 Linux , 底层的操作系统 通道就是双向的.

Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO_第1张图片

2、缓冲区(Buffer)

1)基本介绍

缓冲区(Buffer)本质上是一个可以读写数据的内存块,可以理解成一个容器对象(含数组),该对象提供了一组方法,可以更轻松使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer

Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO_第2张图片

2)Buffer 类及其子类

  • 在 NIO 中,Buffer 是一个顶层抽象父类。Buffer类的子类有ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer 。其中这些子类各自还有自己的子类
    • ByteBuffer,存储字节数据到缓冲区
    • ShortBuffer,存储字符串数据到缓冲区
    • CharBuffer,存储字符数据到缓冲区
    • IntBuffer,存储整数数据到缓冲区
    • LongBuffer,存储长整型数据到缓冲区
    • DoubleBuffer,存储小数到缓冲区
    • FloatBuffer,存储小数到缓冲区

Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO_第3张图片

  • Buffer类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息:
属性 描述
capacity 容量,即可以容纳的最大数据量;在缓存区创建时被设定并且不能改变
Limit 表示缓冲区当前的终点,不能对缓冲区中超过Limit的部分进行读写(相当于哨兵)。而且Limit是可以修改的
Position 当前的读/写位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写作准备
Mark 标记

Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO_第4张图片

  • Buffer类相关方法
public abstract class Buffer {
    //JDK1.4时,引入的api
    public final int capacity()//返回此缓冲区的容量
    public final int position()//返回此缓冲区的位置
    public final Buffer position (int newPositio)//设置此缓冲区的位置
    public final int limit()//返回此缓冲区的限制
    public final Buffer limit (int newLimit)//设置此缓冲区的限制
    public final Buffer mark()//在此缓冲区的位置设置标记
    public final Buffer reset()//将此缓冲区的位置重置为以前标记的位置
    public final Buffer clear()//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并没有真正擦除, 后面操作会覆盖
    public final Buffer flip()//反转此缓冲区
    public final Buffer rewind()//重绕此缓冲区
    public final int remaining()//返回当前位置与限制之间的元素数
    public final boolean hasRemaining()//告知在当前位置和限制之间是否有元素
    public abstract boolean isReadOnly();//告知此缓冲区是否为只读缓冲区
 
    //JDK1.6时引入的api
    public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
    public abstract Object array();//返回此缓冲区的底层实现数组
    public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
    public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区
}

3)ByteBuffer介绍

从前面可以看出对于 Java 中的基本数据类型(boolean除外),都有一个 Buffer 类型与之相对应。最常用的是ByteBuffer 类(二进制数据),该类的主要方法如下:

public abstract class ByteBuffer {
    //缓冲区创建相关api
    public static ByteBuffer allocateDirect(int capacity)//创建直接缓冲区
    public static ByteBuffer allocate(int capacity)//设置缓冲区的初始容量
    public static ByteBuffer wrap(byte[] array)//把一个数组放到缓冲区中使用
    //构造初始化位置offset和上界length的缓冲区
    public static ByteBuffer wrap(byte[] array,int offset, int length)
     //缓存区存取相关API
    public abstract byte get( );//从当前位置position上get,get之后,position会自动+1
    public abstract byte get (int index);//从绝对位置get
    public abstract ByteBuffer put (byte b);//从当前位置上添加,put之后,position会自动+1
    public abstract ByteBuffer put (int index, byte b);//从绝对位置上put
 }

3、通道(Channel)

1)基本介绍

  • NIO 的通道(Channel)类似于流(Stream),但有些区别如下:
    • 通道可以同时进行读写,而流只能读或者只能写
    • 通道可以实现异步读写数据
  • BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)是双向的,可以读操作,也可以写操作。
  • Channel 在 NIO 中是一个接口 public interface Channel extends Closeable{}
  • 常 用 的 Channel 类 有 : FileChannel 、 DatagramChannel 、 ServerSocketChannelSocketChannel
    • FileChannel 用于文件的数据读写
    • DatagramChannel 用于 UDP 的数据读写
    • ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。 ServerSocketChanne 类似 ServerSocket ,SocketChannel 类似 Socket

Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO_第5张图片

2)FileChannel 类

FileChannel 主要用来对本地文件进行 IO 操作,常见的方法有:

  • public int read(ByteBuffer dst),从通道读取数据并放到缓冲区中
  • public int write(ByteBuffer src),把缓冲区的数据写到通道中
  • public long transferFrom(ReadableByteChannel src,long position,long count),从目标通道中复制数据到当前通道
  • public long transferTo(long position,long count,WritableByteChannel target),把数据从当前通道复制给目标通道

3)应用实例 1-本地文件写数据

使用前面学习后的 ByteBuffer(缓冲) 和 FileChannel(通道), 将 “hello,world” 写入到 hello.txt 中

    public static void main(String[] args) throws IOException {
        //创建字符串
        String hello = "Hello World!";

        //创建一个文件输出流
        FileOutputStream stream = new FileOutputStream("C:\\hello.txt");

        //通过FileOutputStream获取到对应的 channel
        //channel的实际类型是FileChannelImpl
        FileChannel channel = stream.getChannel();

        //创建一个缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //将str放入byteBuffer
        byteBuffer.put(hello.getBytes());
        //引入等下要从头读,所以要进行反转
        byteBuffer.flip();

        //将byteBuffer中的内容写入到channel
        channel.write(byteBuffer);
        //关闭流
        stream.close();
    }

4)应用实例 2-本地文件读数据

使用前面学习后的 ByteBuffer(缓冲) 和 FileChannel(通道), 将 hello.txt 中的数据读入到程序,并显示在控制
台屏幕

public static void main(String[] args) throws IOException {
    //创建文件输入流
    File file = new File("C:\\hello.txt");
    FileInputStream stream = new FileInputStream(file);

    //从输入流获取 FileChannel
    FileChannel fileChannel = stream.getChannel();

    //创建 ByteBuffer 缓冲区用于存储数据
    ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());

    //将从 fileChannel 读取的数据放入 ByteBuffer
    fileChannel.read(byteBuffer);

    //将byteBuffer的字节数据转成String输出
    System.out.println(new String(byteBuffer.array()));
    
    //关闭输入流
    stream.close();
}

5)应用实例 3-使用一个 Buffer 完成文件读取、写入

使用 FileChannel(通道) 的方法 read和write完成文件的拷贝。其中inputChannel将文件内容写入到ByteBuffer中,而outputChannel将ByteBuffer写入到文件中。
Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO_第6张图片
代码示例:

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

        //创建输入流
        File file = new File("C:\\hello.txt");
        FileInputStream inputStream = new FileInputStream(file);
        //获得输入channel
        FileChannel inputChannel = inputStream.getChannel();

        //创建输出流
        FileOutputStream outputStream = new FileOutputStream("C:\\hello_copy.txt");
        //获得输出channel
        FileChannel outputChannel = outputStream.getChannel();


        //-----------------------拷贝方法1-----------------------
        //创建缓冲区 byteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);

        while (true) {

            //重置position和limit的值,
            // 否则下次循环position==limit,
            // 导致无法读取内容,且read==0无法退出
            byteBuffer.clear();

            //将 inputChannel 中的数据读取到 byteBuffer中
            int read = inputChannel.read(byteBuffer);
            if (read == -1){
                //read == -1表示读取完毕
                break;
            }

            //进行flip准备写入
            byteBuffer.flip();
            // 将byteBuffer中内容写入到outputChannel
            outputChannel.write(byteBuffer);
        }

        //-----------------------拷贝方法2-----------------------
//        //创建缓冲区 byteBuffer
//        ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//
//        //将 inputChannel 中的数据读取到 byteBuffer中
//        inputChannel.read(byteBuffer);
//
//        //需要对byteBuffer进行flip操作,准备进行读取
//        byteBuffer.flip();
//
//        //将byteBuffer中的内容写入到outputChannel
//        outputChannel.write(byteBuffer);
//
        //关闭输入输出流
        inputStream.close();
        outputStream.close();
    }

6)应用实例 4-拷贝文件 transferFrom 方法

使用 FileChannel(通道) 的方法 transferFrom ,完成文件的(零)拷贝

    public static void main(String[] args) throws IOException {
        //创建输入流
        FileInputStream inputStream = new FileInputStream("C:\\hello.txt");
        //创建输入channel
        FileChannel inputChannel = inputStream.getChannel();

        //创建输入流
        FileOutputStream outputStream = new FileOutputStream("C:\\hello_copy.txt");
        //创建输入channel
        FileChannel outputChannel = outputStream.getChannel();

        //使用transferTo或者transferFrom进行拷贝
//        inputChannel.transferTo(0,inputChannel.size(),outputChannel);
        outputChannel.transferFrom(inputChannel, 0, inputChannel.size());

        //关闭资源
        inputChannel.close();
        outputChannel.close();
        inputStream.close();
        outputStream.close();
    }

7)Buffer注意事项和细节

  • ByteBuffer 支持类型化的put 和 get(putInt, getInt,…), put 放入的是什么数据类型,get就应该使用相应的数据类型来取出(并且不能仅仅用get),否则可能有 BufferUnderflowException 异常。
public static void testPutGetByType() {
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

    byteBuffer.putChar('A');
    byteBuffer.putInt(42);
    byteBuffer.putLong(10L);
    byteBuffer.putDouble(2.33);

    byteBuffer.flip();

    System.out.println(byteBuffer.getChar());
    System.out.println(byteBuffer.getInt());
    System.out.println(byteBuffer.getLong());
    System.out.println(byteBuffer.getDouble());
}
  • 可以将一个普通Buffer 转成只读Buffer
public static void toReadOnlyBuffer() {
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

    byteBuffer.putInt(42);

    //获取该byteBuffer的只读版本
    ByteBuffer readOnlyBuffer = byteBuffer.asReadOnlyBuffer();

    //注意position和limit的值也和byteBuffer一样
    readOnlyBuffer.flip();

    System.out.println(readOnlyBuffer.getInt());

    readOnlyBuffer.putInt(10);
}

结果:

42
Exception in thread "main" java.nio.ReadOnlyBufferException
	at java.nio.HeapByteBufferR.putInt(HeapByteBufferR.java:375)
	at Netty.NIO.ByteBufferTest.toReadOnlyBuffer(ByteBufferTest.java:23)
	at Netty.NIO.ByteBufferTest.main(ByteBufferTest.java:8)
  • NIO 还提供了 MappedByteBuffer, 可以让文件直接在内存(堆外的内存)中进行修改, 而如何同步到文件 由 NIO 来完成
    • MappedByteBuffer 可让文件直接在内存(堆外内存)修改, 操作系统不需要拷贝一次
    public static void main(String[] args) throws IOException {

        //第一个参数是文件位置,第二个参数是操作类型(r:读,w:写)
        RandomAccessFile randomAccessFile =
                new RandomAccessFile("C:\\Users\\韩壮\\Desktop\\hello.txt", "rw");
        //获得channel
        FileChannel channel = randomAccessFile.getChannel();

        /**
         * 通过channel的map方法获得MappedByteBuffer,实际类型是其子类 DirectByteBuffer
         *      参数 mode:操作类型(读、写、读写)
         *      参数 position:文件映射到内存的起始地址,可以直接修改的起始位置
         *      参数 size:映射到内存的大小(不是索引位置),即将文件的多少个字节映射到内存
         */
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);

        //对文件进行修改,第一参数是要修改位置的下标,第二个是内容
        mappedByteBuffer.put(0, (byte) 'A');
        mappedByteBuffer.put(4, (byte) 'B');
//        mappedByteBuffer.put(5, (byte) 'C');//IndexOutOfBoundsException

        randomAccessFile.close();
    }
  • 前面我们讲的读写操作,都是通过一个 Buffer 完成的,NIO 还支持 通过多个 Buffer(即 Buffer 数组) 完成读 写操作,即 Scattering 和 Gathering
public static void main(String[] args) throws IOException {

    //使用 ServerSocketChannel 和 SocketChannel 进行网络通信

    //创建ServerSocketChannel
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    //创建端口对象
    InetSocketAddress inetSocketAddress = new InetSocketAddress(6666);
    //绑定端口到socket,并启动
    serverSocketChannel.socket().bind(inetSocketAddress);

    //创建缓冲数组
    ByteBuffer[] buffers = new ByteBuffer[2];
    buffers[0] = ByteBuffer.allocate(5);
    buffers[1] = ByteBuffer.allocate(3);

    //等待客户端连接(telnet)
    SocketChannel socketChannel = serverSocketChannel.accept();

    int messageLength = 8; //假定从客户端接收 8 个字节
    while (true) {
        int byteRead = 0;

        while (byteRead < messageLength) {
            long l = socketChannel.read(buffers);
            byteRead += l;
            System.out.println("byteRead" + byteRead);
            for (ByteBuffer buffer : buffers) {
                System.out.println("position=" + buffer.position() + " limit=" + buffer.limit());
            }
        }

        //对所有buffer进行flip
        for (ByteBuffer buffer : buffers) {
            buffer.flip();
        }

        int byteWrite = 0;

        while (byteWrite < messageLength) {
            long l = socketChannel.write(buffers);
            byteWrite += l;
        }

        //对所有buffer进行flip
        for (ByteBuffer buffer : buffers) {
            buffer.clear();
        }

        System.out.println("byteRead" + byteRead + ", byteWrite" + byteWrite + ", messageLength" + messageLength);
    }
}

4、 Selector(选择器)

1)基本介绍

  • Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)
  • Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管
    理多个通道,也就是管理多个连接和请求。
  • 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都
    创建一个线程,不用去维护多个线程 。避免了多线程之间的上下文切换导致的开销
    Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO_第7张图片

2)Selector 特点

  • Netty 的 IO 线程 NioEventLoop 聚合了 Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客
    户端连接。
  • 当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
  • 线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出
    通道。
  • 由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂
    起。
  • 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线
    程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升

3)Selector类相关方法

public abstract class Selector implements Closeable { 
    //得到一个选择器对象
    public static Selector open();

    //监控所有注册的通道,当其中有 IO 操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,
    public int select();
    //带超时时间的select
    public int select(long timeout);
    //唤醒正在阻塞selector
    public void wakeup();
    //不阻塞,立马返还
    public int selectNow();

    //从内部集合中得到所有的 SelectionKey	
    public Set<SelectionKey> selectedKeys();
}

5、NIO 非阻塞 网络编程原理分析

网络编程相关组件(Selector、SelectionKey、ServerScoketChannel和SocketChannel) 的关系梳理:

  • 当客户端连接时,会通过ServerSocketChannel 得到 SocketChannel
  • Selector 进行监听 (select 方法), 对于有事件发生的通道,将对应的SelectionKey加入到内部集合中并返回
  • 将socketChannel注册到Selector上(register(Selector sel, int ops)方法), 一个selector上可以注册多个SocketChannel
  • 注册后返回一个 SelectionKey, 会和该Selector 关联(集合)
  • 有事件发生时,得到SelectionKey
  • 在通过 SelectionKey 反向获取 SocketChannel (方法 channel)
  • 可以通过得到的 channel , 完成业务处理

Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO_第8张图片

服务器端实现代码:

public class NIOServer {

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

        //创建ServerSocketChannel对象,类似于BIO中的ServerSocket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //创建Selector对象
        Selector selector = Selector.open();

        //绑定端口6666在服务器端监听
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //将serverSocketChannel设置为非阻塞
        serverSocketChannel.configureBlocking(false);

        //把serverSocketChannel注册到selector,设置关心时间为OP_ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {


            if (selector.select(2000) == 0) {
                System.out.println("服务器等待2秒,无连接");
                continue;
            }


            //select的返回值>0,说明有事件发生。获取到相关的事件的selectionKeys
            //selector.selectedKeys()返回关注事件的集合
            //可以通过SelectionKey反向获取channel
            Set<SelectionKey> selectionKeys = selector.selectedKeys();

            Iterator<SelectionKey> iterator = selectionKeys.iterator();

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

                //根据key,对应通道发生的事件做相应的处理
                if (key.isAcceptable()) { //如果是OP_ACCEPT,说明有新的客户端连接

                    //为该客户端分配一个SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //设置为非阻塞
                    socketChannel.configureBlocking(false);
                    System.out.println("客户端连接成功,对应的socketChannel为:" + socketChannel.hashCode());

                    //将SocketChannel注册到selector上,关心的事件为OP_READ
                    //同时给该socketChannel关联一个buffer
                    socketChannel.register(selector,
                            SelectionKey.OP_READ, ByteBuffer.allocate(1024));

                }
                if (key.isReadable()) { //OP_READ
                    //根据SelectionKey获取到对应的channel
                    SocketChannel channel = (SocketChannel) key.channel();
                    //获取到该 Channel 关联的 buffer
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    //从channel中读取数据放入到buffer中
                    channel.read(buffer);
                    System.out.println("客户端:" + new String(buffer.array()));
                }

                //手动移除当前的SelectionKey,防止重复操作
                iterator.remove();
            }
        }
    }

}

客户端实现代码:

public class NIOClient {

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

        //得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //创建一个连接地址
        InetSocketAddress inetSocketAddress =
                new InetSocketAddress("127.0.0.1", 6666);

        //连接服务器
        if (!socketChannel.connect(inetSocketAddress)) {
            while (!socketChannel.finishConnect()){
                System.out.println("连接需要时间,如果连接失败,客户端不会阻塞");
            }
        }

        //如果连接成功,就发送数据
        String str = "Hello World";
        //将字符串的内容放入Buffer
        ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
        //将byteBuffer的内容写入到socketChannel
        socketChannel.write(byteBuffer);

        //暂停
        System.in.read();
    }
}

运行结果:

服务器等待2秒,无连接
服务器等待2秒,无连接
服务器等待2秒,无连接
客户端连接成功,对应的socketChannel为:94438417
客户端:Hello World                                                                     服务器等待2秒,无连接
服务器等待2秒,无连接

6、相关组件分析

1)SelectionKey

SelectionKey,表示 Selector 和网络通道的注册关系, 共四种:

  • OP_ACCEPT:有新的网络连接可以 accept,值为 16
  • OP_CONNECT:代表连接已经建立,值为 8
  • OP_READ:代表读操作,值为 1
  • OP_WRITE:代表写操作,值为 4

源码:

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;

SelectionKey相关方法有:

public abstract class SelectionKey {
    public abstract Selector selector();//得到与之关联的 Selector 对象

    public abstract SelectableChannel channel();//得到与之关联的通道

    public final Object attachment();//得到与之关联的共享数据

    public abstract SelectionKey interestOps(int ops);//设置或改变监听事件

    public final boolean isAcceptable();//是否可以 accept

    public final boolean isReadable();//是否可以读

    public final boolean isWritable();//是否可以写
}

2)ServerSocketChannel

ServerSocketChannel 在服务器端监听新的客户端 Socket 连接

相关方法如下:

public abstract class ServerSocketChannel
    extends AbstractSelectableChannel
    implements NetworkChannel{
	
    public static ServerSocketChannel open();//得到一个 ServerSocketChannel 通道

    public final ServerSocketChannel bind(SocketAddress local);//设置服务器端端口号

    //设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
    public final SelectableChannel configureBlocking(boolean block);

    public SocketChannel accept();//接受一个连接,返回代表这个连接的通道对象

    public final SelectionKey register(Selector sel, int ops);//注册一个选择器并设置监听事件
}

3)SocketChannel

SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。

相关方法如下:

public abstract class SocketChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel{

    public static SocketChannel open();//得到一个 SocketChannel 通道

    public final SelectableChannel configureBlocking(boolean block);//设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式

    public boolean connect(SocketAddress remote);//连接服务器

    public boolean finishConnect();//如果上面的方法连接失败,接下来就要通过该方法完成连接操作

    public int write(ByteBuffer src);//往通道里写数据

    public int read(ByteBuffer dst);//从通道里读数据

    public final SelectionKey register(Selector sel, int ops, Object att);//注册一个选择器并设置监听事件,最后一个参数可以设置共享数据

    public final void close();//关闭通道
}

6、NIO 网络编程应用实例-群聊系统

实例要求:

  • 编写一个 NIO 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
  • 实现多人群聊
  • 服务器端:可以监测用户上线,离线,并实现消息转发功能
  • 客户端:通过channel 可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(有服务器转发得到)
  • 目的:进一步理解NIO非阻塞网络编程机制

服务器端代码:

public class GroupChatServer {

    //Selector
    private Selector selector;
    //定义ServerSocketChannel
    private ServerSocketChannel listenChannel;
    //定义端口
    private static final int PORT = 6666;

    //定义构造方法,进行初始化工作
    public GroupChatServer(){
        try {
            //创建Selector
            selector = Selector.open();
            //创建ServerSocketChannel
            listenChannel = ServerSocketChannel.open();
            //设置端口
            listenChannel.socket().bind(new InetSocketAddress(PORT));
            //设置非阻塞
            listenChannel.configureBlocking(false);
            //将listenChannel注册到selector中
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        GroupChatServer server = new GroupChatServer();
        server.listen();
    }


    //进行监听
    public void listen(){
        try {
            while (true) {
                int count = selector.select();
                if (count > 0) { //说明有事件发生
                    //获得事件的SelectionKey
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();

                    for (SelectionKey key : selectionKeys) { //对每个事件进行处理

                        if (key.isAcceptable()) { //如果是连接事件
                            //为客户端分配一个SocketChannel
                            SocketChannel socketChannel = listenChannel.accept();
                            //设置非阻塞
                            socketChannel.configureBlocking(false);

                            //将SocketChannel注册到selector上,关心的事件为OP_READ
                            socketChannel.register(selector,SelectionKey.OP_READ);

                            System.out.println(socketChannel.getRemoteAddress() + " 上线 ");
                        } else if (key.isReadable()) { //是read事件,即通道是可读事件
                            //处理读操作
                            readData(key);
                        }
                        selectionKeys.remove(key);
                    }
                } else {
                    System.out.println("等待中....");
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //从SocketChannel读取客户端发送的数据输出并转发
    private void readData(SelectionKey key) {
        SocketChannel channel = null;
        try {
            //从SelectionKey获取SocketChannel
            channel = (SocketChannel) key.channel();
            //定义缓冲器
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            //将SocketChannel中的数据读取到buffer
            int read = channel.read(buffer);
            String msg = new String(buffer.array());
            if (read > 0) {
                //在服务器端输出
                System.out.println(msg);
                //转发到其他客户端
                redirectMsgToOtherClient(msg, channel);
            }
        } catch (IOException e) {
            try {
                //发生异常说明有客户端离线
                System.out.println(channel.getRemoteAddress() + " 离线了 ");
                //取消注册
                key.cancel();
                //关闭通道
                channel.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }

    //将一个客户端发送的信息转发到其他客户端(排除自己)
    private void redirectMsgToOtherClient(String msg, SocketChannel self) throws IOException {

        System.out.println("服务转发消息中...");

        //遍历所有注册到selector上的SocketChannel,并排除self
        for (SelectionKey key : selector.keys()) {

            Channel channel = key.channel();

            //不对自己转发
            if (channel instanceof SocketChannel && channel != self) {

                //转型
                SocketChannel sc = (SocketChannel) channel;
                //创建缓冲区并赋值
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                //将缓冲区的值写入SocketChannel
                sc.write(buffer);
            }
        }
    }
}

客户端代码:

public class GroupCharClient {

    //定义属性
    private static final String HOST = "127.0.0.1";
    private static final int PORT = 6666;
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    //初始化属性
    public GroupCharClient() throws IOException {
        selector = Selector.open();
        //连接服务器
        socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //将socketChannel注册到Selector
        socketChannel.register(selector, SelectionKey.OP_READ);
        //设置username为本地地址
        username = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(username + " 初始化完成");
    }

    public static void main(String[] args) throws IOException {
        GroupCharClient client = new GroupCharClient();

        new Thread(()->{
            //每隔一秒读取一次从服务器端转发的消息
            while (true) {
                try {
                    client.getMsg();
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        //用于读取输入数据
        Scanner scanner = new Scanner(System.in);

        //发送输入的数据
        while (scanner.hasNextLine()) {
            String s = scanner.nextLine();
            client.sendMsg(s);
        }
    }

    //向服务器发送消息
    public void sendMsg(String msg) {
        msg = username + " : " + msg;
        try {
            socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //读取服务器转发的消息
    public void getMsg() {
        try {
            int count = selector.select();
            if (count > 0) {//有可以用的通道

                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                for (SelectionKey key : selectionKeys) {
                    if (key.isReadable()) {
                        //根据key获取SocketChannel
                        SocketChannel sc = (SocketChannel) key.channel();

                        ByteBuffer buffer = ByteBuffer.allocate(1024);

                        //将数据从SocketChannel读到buffer
                        int read = sc.read(buffer);
                        if (read > 0) {
                            System.out.println(new String(buffer.array()));
                        }
                    }
                    //删除当前的 selectionKey, 防止重复操作
                    selectionKeys.remove(key);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

测试结果:

客户端 a 输入输出内容:

Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO_第9张图片

客户端 b 输入输出内容:
Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO_第10张图片
服务器端输出内容:
Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO_第11张图片

三、NIO与零拷贝

零拷贝是网络编程的关键,很多性能优化都离不开。在 Java 程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile。

注意:零拷贝从操作系统角度,是没有cpu 拷贝

1、传统IO数据读写

下面是Java 传统 IO 和 网络编程的一段代码:

File file = new File("test.txt");
RandomAccessFile raf = new RandomAccessFile(file, "rw");

byte[] arr = new byte[(int) file.length()];
raf.read(arr);

Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);

Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO_第12张图片
DMA: direct memory access 直接内存拷贝(不使用CPU)

2)mmap 优化

mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数。如下图
Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO_第13张图片

3)sendFile 优化

  • Linux 2.1 版本 提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换

Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO_第14张图片

  • Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。具体如下图和小结:
    Netty学习笔记(二):NIO简介、缓冲区(Buffer)、通道(Channel)、选择器(Selector)、NIO编程、零拷贝、AIO_第15张图片

这里其实有 一次cpu 拷贝kernel buffer -> socket buffer。但是,拷贝的信息很少,比如 lenght , offset , 消耗低,可以忽略

总结

  • 我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据)
  • 零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。
  • mmap 适合小数据量读写,sendFile 适合大文件传输。
  • mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
  • sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。

四、 Java AIO 基本介绍

  • JDK 7 引入了 Asynchronous I/O,即 AIO。在进行 I/O 编程中,常用到两种模式:Reactor和 Proactor。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理
  • AIO 即 NIO2.0,叫做异步不阻塞的 IO。AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用

BIO、NIO、AIO比较:

BIO NIO AIO
IO 模型 同步阻塞 同步非阻塞(多路复用) 异步非阻塞
编程难度 简单 复杂 复杂
可靠性
吞吐量
  • 同步阻塞:到理发店理发,就一直等理发师,直到轮到自己理发。

  • 同步非阻塞:到理发店理发,发现前面有其它人理发,给理发师说下,先干其他事情,一会过来看是否轮到自己.

  • 异步非阻塞:给理发师打电话,让理发师上门服务,自己干其它事情,之后理发师会打电话通知你

你可能感兴趣的:(Netty,后端)