0%

Netty-NIO-Buffer&Channel&Selector

Netty-NIO-Buffer&Channel&Selector

基本介绍

  1. Java NIO全称Java non-blocking IO,是指JDK提供的新API。从JDK1.4开始,Java提供了一系列改进的输入/输出的新特性,被统称为NIO(即New IO),是同步非阻塞

  2. NIO相关类都被放在java.nio包及子包下,并对原java.io包中的很多类进行改写

  3. NIO有三大核心部分:Channel(通道)Buffer(缓冲区)Selector(选择器)

    NIO-detail)

  4. NIO是面向缓冲区的,或者说是面向 块 编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络

  5. Java NIO的非阻塞模式,使一个线程从某通道发送请求或读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直到数据变的可以读取之前,该线程可以继续做其它的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情

  6. 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或100个线程来处理。不像之前的阻塞IO那样,必须分配10000个

  7. HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个不,而且并发请求的数量比HTTP1.1大了好几个数量级

Buffer测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class BasicBuffer {
public static void main(String[] args) {
// Buffer的使用
// 创建一个Buffer,大小为5,即可以存放5个int
IntBuffer buffer = IntBuffer.allocate(5);
// 向buffer存放数据
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put(i*2);
}
// 从buffer读取数据
// 将buffer转换,读写切换
buffer.flip();

while(buffer.hasRemaining()){
System.out.println(buffer.get());
}
}
}

NIO和BIO的比较

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

Selector、Channel和Buffer的关系

  1. 每个Channel都会对应一个Buffer
  2. Selector对应一个线程,一个线程对应多个Channel(连接)
  3. 多个Channel注册到一个Selector
  4. 程序切换到哪个Channel,是由事件(Event)决定的
  5. Selector会根据不同的事件,在各个通道上切换
  6. Buffer 就是一个内存块,底层是一个数组
  7. 数据的读取/写入是通过Buffer,这和BIO有本质不同,BIO中要么是输入流,要么是输出流,不能双向,而NIO的Buffer是可读可写的,需要flip()切换
  8. Channel是双向的,可以反映底层操作系统的情况,如Linux底层操作系统通道就是双向的

Buffer(缓冲区)

基本介绍

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

buffer
buffer

Buffer类及其子类

  1. 在NIO中,Buffer是一个顶层父类,它是一个抽象类

    常用Buffer子类

    • ByteBuffer:存储字节数据到缓冲区
    • ShortBuffer:存储短整数数据到缓冲区
    • CharBuffer:存储字符数据到缓冲区
    • IntBuffer:存储整数数据到缓冲区
    • LongBuffer:存储长整数数据到缓冲区
    • DoubleBuffer:存储小数到缓冲区
    • FloatBuffer:存储小数到缓冲区
  2. Buffer类定义的所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息

    属性 描述
    capacity 容量,即可以容纳的最大数据量,在级次区创建时被设定并且不能改变
    limit 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作,且极限是可以修改的
    position 位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变该值,为下次读写作准备
    mark 标记
  3. Buffer类相关方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 返回此缓冲区的容量
    public final int capacity();
    // 返回此缓冲区的位置
    public final int position();
    // 设置此缓冲区的位置
    public finall Buffer position(int newPosition);
    // 返回此缓冲区的限制
    public final int limit();
    // 设置此缓冲区的限制
    public final Buffer limit(inte new Limit);
    //清除此缓冲区,即将各个标记恢复到初始状态,但数据并没有真正擦除
    public final Buffer clear();
    // 反转此缓冲区
    public final Buffer flip();
    // 告知在当前位置和限制之间是否有元素
    public final boolean hasRemaining();
    // 告知此缓冲区是否为只读缓冲区
    public abstract boolean isReadOnly();
    // 告知此缓冲区是否具有可访问的底层实现数组
    public abstract boolean hasArray();
    // 返回此缓冲区的底层实现数组
    public abstract Object array();

ByteBuffer

从前面可以看出,对于Java中的基本数据类型(boolean除外),都有一个Buffer类型与之相对应,最常用的自然是ByteBuffer类(二进制数据),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 常用方法
// 创建直接缓冲区
public static ByteBuffer allocateDirect(int capacity);
// 设置缓冲区的初始容量
public static ByteBuffer allocate(int capacity);
// 缓冲区存取相关API
// 从当前位置position上get,get之后,position会自动+1
public abstract byte get();
// 从绝对位置get
public abstract byte get(int index);
// 从当前位置上put,put之后,position会自动+1
public abstract ByteBuffer put(byte b);
// 从绝对位置上put
public abstract ByteBuffer put(int index,byte b);

Channel(通道)

基本介绍

  1. NIO的通道类似于流,但有些区别:

    • 通道可以同时进行读写,而流只能读或只能写
    • 通道可以实现异步读写数据
    • 通道可以从缓冲读数据,也可以写数据到缓冲
  2. BIO中的stream是单向的,如FileInputStream对旬只能进行读取数据的操作,而NIO的通道(Channel)是双向的,可以读操作,也可以写操作

  3. Channel在NIO中是一个接口

    1
    public interface Channel extends Closeable{}
  4. 常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel

  5. FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写

FileChannel类

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

1
2
3
4
5
6
7
8
// 从通道读取数据并放到缓冲区中
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,WriteableByteChannel target);

Selector(选择器)

基本介绍

  1. Java的NIO,用非阻塞的IO方式。可以用一个线程,处理多个客户端连接,就会使用到Selector(选择器)
  2. Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求
  3. 只有在连接真正有读写事件发生时,才会进行读写,就大大减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
  4. 避免了多线程之间的上下文切换导致的开销

Selector特点说明

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

Selector类相关方法

1
2
3
4
5
6
// 得到一个选择器对象
public static Selector open();
// 监控所有注册的通道,当其中有IO操作进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间
public int select(long timeout);
// 从内部集合中得到所有的SelectionKey
public Set<SelectionKey> selectedKeys();

注意事项

  1. NIO中的ServerSocketChannel功能类似于ServerSocket,SocketChannel功能类似Socket

  2. selector相关方法说明

    1
    2
    3
    4
    5
    6
    7
    8
    // 阻塞
    selector.select();
    // 阻塞1000毫秒,在1000毫秒后返回
    selector.select(1000);
    // 唤醒selector
    selector.wakeup();
    // 不阻塞,立刻返还
    selector.selectNow();

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

  1. 当客户端连接时,会通过ServerSocketChannel得到对应的SocketChannel
  2. 将SocketChannel注册到Selector上register(Selector sel,int ops),一个Selector上可以注册多个SocketChannel
  3. 注册后返回一个SelectionKey,会和该Selector关联(以集合的方式)
  4. Selector进行监听(select()方法),返回有事件发生的通道的个数
  5. 进一步得到各个SelectionKey(有事件发生)
  6. 再通过SelectionKey反向获取SocketChannel,方法是channel()
  7. 可以通过得到的channel,完成业务处理