首页 NIO基础
文章
取消

NIO基础

NIO三大组件

Channel和Buffer

Channel是读写数据的双向通道,可以从Channel将数据读入Buffer,也可以将Buffer的数据写入Channel,而Java中的Stream,如InputStream、OutputStream要么输入,要么输出,Channel比Stream更加底层

常见的Channel

  • FileChannel
  • DatagramChannel,UDP式的数据传输通道
  • SocketChannel,TCP式的,可用于服务端和客户端
  • ServerSocketChannel,TCP式的,专用于服务端

常见的Buffer

  • ByteBuffer,以字节为单位存储数据的Buffer,是抽象类,下面是实现类
    • MappedByteBuffer
    • DirectByteBuffer
    • HeapByteBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • CharBuffer

Selector

多线程设计服务器

需求:每个服务器可能同时需要处理来自多个客户端的请求,这就需要使用多线程

处理:每个客户端与服务端的连接开一个线程处理数据传输,如下图所示
每个Socket连接对应一个线程

存在的问题:
1.CPU能同时开启的线程数是有限的,如16核CPU最多开启16个线程,也就是能够同时处理的连接也就16个,如果有新的连接就会被阻塞
2.每个线程会占用一定的内存(记忆状态,名词变量等等),线程一多内存占用就会很高
3.线程上下文切换成本高,一旦线程被换下,它的状态是需要被记住的,以便下次处理该线程的时候继续上次的进度,因此这样会有许多线程的状态需要被记住
这种多线程设计只适合少连接的情况

线程池设计服务器

需求:多线程版本线程数量不受管束,容易超出内存

处理:线程池限制最高线程数,由于最高线程数被限制,那么一旦连接数过多,每个线程就要处理多条连接,但是同一时刻线程只能处理一条连接,其他连接会被阻塞,线程池的工作模式如下:
线程池的工作模式

缺点:
1.不管正在被处理的连接干不干事,线程都会被占用,其他线程只能等待,这个模式被称为阻塞模式,也就是每个线程仅能处理一个socket连接
2.仅适合短链接,http请求下服务端返回响应,连接就可以被断开了,当下线程便可继续处理其他线程,这被称为短链接,非常适合线程池模式

总结:线程池模式下,线程利用率不高

Selector设计服务器

需求:如果连接不进行任何读写事件,就不占用线程,线程可以处理其他发出读写事件的连接
注意:在Selector中,Channel就相当于一次Socket连接

处理:Selector负责监听所有连接,一旦有连接发出读写事件,就通知Thread进行处理,如果有多个线程同时发出读写事件,就轮流处理;模式图如下:
Selector工作模式

适用多连接和流量低的场景,如果一次连接的流量过高,比如某个连接需要处理大量的数据,这样就要耗费大量的时间,在轮流处理模式下其他连接其实就相当与被阻塞了,甚至可能永远不会被处理到,比如在当前连接处理当中,有新增了需要处理的连接,这样等待处理的连接队伍就会越来越长;之所以适合多连接是因为Selector模式就是为了在多连接下提高线程的利用率

ByteBuffer的基本使用

读取本地文件,并在控制台打印内容

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
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class Demo {
  public static void main(String[] args) {
    // 1.文件输入流获取FileChannel 或 2.RandomAccessFile获取
    try (FileChannel fileChannel = new FileInputStream("E:\\Netty\\NIO-demo1\\src\\main\\resources\\file1.txt").getChannel()) {
      // 准备ByteBuffer缓冲区
      ByteBuffer byteBuffer = ByteBuffer.allocate(10);
      while (true) {
        // Channel数据写入ByteBuffer
        int count = fileChannel.read(byteBuffer);
        if (count < 0) {
          break;
        }
        // 开启读模式
        byteBuffer.flip();
        // 打印数据
        while (byteBuffer.hasRemaining()) {
          byte b = byteBuffer.get();
          System.out.println((char) b);
        }
        // 切换写模式
        byteBuffer.clear();
      }
    } catch (IOException e) {
    }
  }
}

ByteBuffer正确使用姿势

1.文件输入流获取Channel
2.准备ByteBuffer缓冲区,Channel数据写入缓冲区(ByteBuffer刚创建默认为写模式)
3.ByteBuffer开启读模式,byteBuffer.flip()
4.byteBuffer读出数据,如byteBuffer.get()
5.如果要继续写入byteBuffer,请开启写模式,byteBuffer.clear(),或byteBuffer.compact()

ByteBuffer内部结构

本文由作者按照 CC BY 4.0 进行授权