NIO

攻破JAVA NIO技术壁垒
Java NIO 的前生今世 之四 NIO Selector 详解

1. 基本介绍

Java NIO :同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理

在这里插入图片描述
NIO非阻塞式IO

--使用Channel代替Stream
--使用Selector监控多条Channel,由于Channel的读的方法可以是非阻塞的,所以我一直监控Channel的数据是否准备好
--可以在一个线程处理Channel I/O

selector可以选择一个通道,channelbuffer进行交互,socketbuffer进行交互

  • NIO 有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)
  • NIO是 面向缓冲区 ,或者面向 块 编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
  • Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情
  • 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个
  • 如果通道有事情发生就去处理这个通道, 没有就去处理其他的通道,如果所有通道都没有事情,那么甚至可以去处理自己的事情

NIO vs BIO

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

2. 三大组件

NIO
在这里插入图片描述

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 , 底层的操作系统通道就是双向的.

对应BIO来说,完成程序与磁盘的读写需要在磁盘到程序建立一个管道,就像现实生活中的水管,这样源节点的字节数据就像水流一样直接流到程序

而NIO的Channel替代BIO的流,我们可以通过Channel读写,但是真正完成读写我们需要一个Buffer,他对应的是内存中一块可以读写的区域,也就是说Channel就是程序和磁盘中间的通道,我们可以把他理解为生活当作的铁路,只是用于连接,铁路自己本身不能完成运输,想完成运输要依赖于火车,也就是NIO中的缓冲区了,把数据装到缓冲区从铁路传输缓冲区到目的地,channel是双向的,Buffer可以双向移动(可读可写)

2.1 Buffer缓冲区

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

根据数据类型的不同(boolean除外),提供了相应类型的缓冲区ByteBuffer/ CharBuffer/ ShortBuffer/ IntBuffer/ LongBuffer/ FloatBuffer/ DoubleBuffer,缓冲区的管理方式几乎一致,通过allocate()获取缓冲区

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
① 缓冲区的四个核心属性

capacity 容量,表示缓冲区中最大存储数据的容量,一但声明不能改变。(因为底层是数组,数组一但被创建就不能被改变)

limit 界限,表示缓冲区中可以操作数据的大小。(limit后数据不能进行读写)

position 位置,表示缓冲区中正在操作数据的位置position <= limit <= capacity

mark 标记,表示记录当前position的位置,可以通过reset()恢复到mark的位置

② 基本方法
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
 }

allocate():分配缓冲区

ByteBuffer byteBuffer = ByteBuffer.allocate(8);

在这里插入图片描述
put():将数据存入缓冲区

byte[] data = new byte[] {'H','E','L','L','O'};
byteBuffer.put(data);

在这里插入图片描述
由于Buffer是双向的,可以读可以写,所以他有读写两种模式

flip(): 切换到读取数据的模式

byteBuffer.flip();

在这里插入图片描述
get():读取数据

 byte[] data1 = new byte[3];
 byteBuffer.get(data1);

读的最远位置就是limit的位置,保证了读到的数据就是刚刚写入的数据
在这里插入图片描述
clear():清空缓冲区,但是缓冲区中的数据依然存在,只是处于一种“被遗忘“的状态,所以的指针回到了最初的位置

compact():如果我上一次没有读完,读到一半就切换到了写模式,我想把没读的那一部分不清除,而是保存起来
compact会把未读的数据拷贝到缓冲区的头部,position指向移动后未读取的数据的后一个位置,然后再他后面进行写
在这里插入图片描述
rewind():重复读,使position归0

mark():标记。mark会记录当前的position之后使用reset将此缓冲区的位置重置为先前标记的位置

reset():position恢复到mark记录的位置

hasRemaining(),判断缓冲区是不是还要没读的,告诉当前位置和极限之间是否存在任何元素

remaining(),还有几个没读的,返回当前位置和限制之间的元素数

buffer的分散和聚合
前面的读写操作,都是通过一个Buffer完成的,NIO还支持通过多个Buffer(Buffer数组)完成读写操作,即Scattering和Gathering

2.2 Channel

Channel用于源节点与目标节点的连接。在Java NIO中负责缓冲区中数据的传输。Channel本身并不存储数据,因此需要配合Buffer一起使用,每一个Channel也可以向另一个Channel进行数据交换

  • 我们可以在同一个 Channel 中执行读和写操作, 然而同一个 Stream 仅仅支持读或写.
  • Channel 可以异步地读写, 而 Stream 是阻塞的同步读写.
  • Channel 总是从 Buffer 中读取数据, 或将数据写入到 Buffer
java.nio.channels.Channel接口:

用于本地数据传输:
​ |-- FileChannel

用于网络数据传输:
​ |-- SocketChannel //TCP 操作|-- ServerSocketChannel //TCP 操作, 使用在服务器端 每个客户端先通过ServerSocketChannel和服务端进行通讯要建立连接,之后服务端会和客户端之间建立一个SocketChannle进行之后的通信|-- DatagramChannel //UDP 操作
FileChannel
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),把数据从当前通道复制给目标通道

打开Channel

Java 针对支持通道的类提供了一个 getChannel() 方法

FileChannel fin = null;//与源文件之间的通道
fin = new FileInputStream(source).getChannel();//通过输出流得到一个channel

在这里插入图片描述
nio实际上是对java原生IO的一个包装,实际上底层还是通过原生IO的操作

文件大小

我们可以通过 channel.size()获取关联到这个 Channel 中的文件的大小. 注意, 这里返回的是文件的大小, 而不是 Channel 中剩余的元素个数.

channel.size()

FileChannel 中读取数据

因为Buffer是在管道运输,所以从FileChannel读实际上是把读的数据写到缓冲区

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);//如果 read()返回 -1表示读完了

写入数据

写入实际上是从buffer中读

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    channel.write(buf);
}
使用NIO完成文件的拷贝
   public void nioBufferCopy(File source, File target){
        //通道
        FileChannel fin = null;//与源文件之间的通道
        FileChannel fout = null;//与目标文件之间的通道
        try {
            fin = new FileInputStream(source).getChannel();
            fout = new FileOutputStream(target).getChannel();
            //一个Buffer又读又写
            ByteBuffer buffer = ByteBuffer.allocate(1024);//最大容量为1024
            //fin-->buffer-->fout
            while (fin.read(buffer)!=-1){//写到buffer中
                //buffer转换为读模式
                buffer.flip();
                while(buffer.hasRemaining()){//只要buffer里面有可以读的就一直读
                    fout.write(buffer);
                }
                //清空转换为写模式
                buffer.clear();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                fin.close();
                fout.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
使用NIO直接让两个Channel交互完成文件的拷贝
 public void nioTransferCopy(File source, File target){
        //通道
        FileChannel fin = null;//与源文件之间的通道
        FileChannel fout = null;//与目标文件之间的通道
        try {
            fin = new FileInputStream(source).getChannel();
            fout = new FileOutputStream(target).getChannel();
            //直接让两个Channel交互完成文件的拷贝
            long transfer = 0;
            long size = fin.size();
            while (transfer!=size) {
                transfer += fin.transferTo(0, fin.size(), fout);//返回结果是这次一共传输了多少字节
                
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                fin.close();
                fout.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

对比所以的文件拷贝方法,包括以前的流的方法,使用缓冲区的效率要远远大于不使用缓冲区,NIO的性能实际上和传统IO差不多,因为在JDK1.4以后,传统IO的底层也进行了改进,所以性能和NIO差不多

SocketChannel

SocketChannel 是一个客户端用来进行 TCP 连接的 Channel.
创建一个 SocketChannel 的方法有两种:

  • 打开一个 SocketChannel, 然后将其连接到某个服务器中

  • 当一个 ServerSocketChannel 接受到连接请求时, 会返回一个 SocketChannel 对象.

API

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();//关闭通道
}

打开 SocketChannel

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));

读取数据
如果 read()返回 -1, 那么表示连接中断了.

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);

写入数据

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    channel.write(buf);
}
非阻塞模式

我们可以设置 SocketChannel 为异步模式, 这样我们的 connect, read, write 都是异步的了

连接

socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://example.com", 80));

while(! socketChannel.finishConnect() ){
    //wait, or do something else...    
}

在异步模式中, 或许连接还没有建立, connect 方法就返回了, 因此我们需要检查当前是否是连接到了主机, 因此通过一个 while 循环来判断.

读写
在异步模式下, 读写的方式是一样的.
在读取时, 因为是异步的, 因此我们必须检查 read 的返回值, 来判断当前是否读取到了数据

ServerSocketChannel

ServerSocketChannel 顾名思义, 是用在服务器为端的, 可以监听客户端的 TCP 连接, 例如:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
    SocketChannel socketChannel =
            serverSocketChannel.accept();

    //do something with socketChannel...
}

API

public abstract class ServerSocketChannel extends AbstractSelectableChannel    implements NetworkChannel{
	public static ServerSocketChannel open()//得到一个 ServerSocketChannel 通道
	public final ServerSocketChannel bind(SocketAddress local)//设置服务器端端口号
	public final SelectableChannel configureBlocking(boolean block)//设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
	public SocketChannel accept()//接受一个连接,返回代表这个连接的通道对象
	public final SelectionKey register(Selector sel, int ops)//注册一个选择器并设置监听事件
}

非阻塞模式

在非阻塞模式下, accept()是非阻塞的, 因此如果此时没有连接到来, 那么 accept()方法会返回null:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);

while(true){
    SocketChannel socketChannel =
            serverSocketChannel.accept();

    if(socketChannel != null){
        //do something with socketChannel...
        }
}

2.3 Selector

Java 的 NIO,用非阻塞的 IO 方式,可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)

Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求

  • 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
  • 避免了多线程之间的上下文切换导致的开销
  • 但是因为在一个线程中使用了多个 Channel, 因此也会造成了每个 Channel 传输效率的降低.
public abstract class Selector implements Closeable { 
	public static Selector open();//得到一个选择器对象
	public int select(long timeout);//监控所有注册的通道,当其中有 IO 操作可以进行时,将对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
	public Set<SelectionKey> selectedKeys();//从内部集合中得到所有的 SelectionKey	
}

创建选择器

通过 Selector.open()方法, 我们可以创建一个选择器

Selector selector = Selector.open();
Channel 注册到选择器中

为了使用选择器管理 Channel, 我们需要将 Channel 注册到选择器中:

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

注意, 如果一个 Channel 要注册到 Selector 中, 那么这个 Channel 必须是非阻塞的, 即channel.configureBlocking(false);

因为 Channel 必须要是非阻塞的, 因此 FileChannel 是不能够使用选择器的, 因为 FileChannel 都是阻塞的.

注意到, 在使用 Channel.register()方法时, 第二个参数指定了我们对 Channel 的什么类型的事件感兴趣,也就是我们让Selector监听Channel的什么状态,这些事件有:

Connect, 即连接事件(TCP 连接), 对应于SelectionKey.OP_CONNECT

Accept, 即确认事件, 对应于SelectionKey.OP_ACCEPT

Read, 即读事件, 对应于SelectionKey.OP_READ, 表示 buffer 可读.

Write, 即写事件, 对应于SelectionKey.OP_WRITE, 表示 buffer 可写.

可以使用或运算|来组合多个事件
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey

如上所示, 当我们使用 register 注册一个 Channel 时, 会返回一个 SelectionKey 对象, 这个对象包含了如下内容:

interest set:即我们感兴趣的事件集, 即在调用 register 注册 channel 时所设置的 interest set.

我们可以通过如下方式获取 interest set:

int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE; 

--------------------------------------------------------------------------------------   
ready set:代表了 Channel 所准备好了的操作

int readySet = selectionKey.readyOps();

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
--------------------------------------------------------------------------------------   
channel: 获取相对应的 Channel 

Channel  channel  = selectionKey.channel();
--------------------------------------------------------------------------------------   
selector:获取相对应的 Selector:

Selector selector = selectionKey.selector();  
--------------------------------------------------------------------------------------   
attached object, 可以在selectionKey中附加一个对象:

基本API

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();//是否可以写
}

通过 Selector 选择 Channel

我们可以通过 Selector.select()方法获取有多少个 Channel 处在感兴趣状态

我们可以通过 Selected key set 访问这个 Channel

Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
}

注意, 在每次迭代时, 我们都调用 "keyIterator.remove()" 将这个 key 从迭代器中删除, 因为 select() 方法仅仅是简单地将就绪的 IO 操作放到 selectedKeys 集合中, 因此如果我们从 selectedKeys 获取到一个 key, 但是没有将它删除, 那么下一次 select 时, 这个 key 所对应的 IO 事件还在 selectedKeys 中.

例如此时我们收到 OP_ACCEPT 通知, 然后我们进行相关处理, 但是并没有将这个 Key 从 SelectedKeys 中删除, 那么下一次 select() 返回时 我们还可以在 SelectedKeys 中获取到 OP_ACCEPT 的 key.

注意, 我们可以动态更改 SekectedKeys 中的 keyinterest set. 例如在 OP_ACCEPT 中, 我们可以将 interest set 更新为 OP_READ, 这样 Selector 就会将这个 Channel 的 读 IO 就绪事件包含进来了

Selector 的基本使用流程

通过 Selector.open() 打开一个 Selector.

Channel 注册到 Selector 中, 并设置需要监听的事件(interest set)

不断重复以下步骤
① 调用 select() 方法
② 调用 selector.selectedKeys() 获取 selected keys
③ 迭代每个 selected key:
④ 从 selected key 中获取 对应的 Channel 和附加信息(如果有的话)
⑤ 判断是哪些 IO 事件已经就绪了, 然后处理它们. 如果是 OP_ACCEPT 事件, 则调用 "SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept()" 获取 SocketChannel, 并将它设置为 非阻塞的, 然后将这个 Channel 注册到 Selector 中.
⑥ 根据需要更改 selected key 的监听事件.
⑦ 将已经处理过的 keyselected keys 集合中删除

关闭 Selector:当调用了 Selector.close()方法时, 我们其实是关闭了 Selector 本身并且将所有的 SelectionKey 失效, 但是并不会关闭 Channel

public class NioEchoServer {
    private static final int BUF_SIZE = 256;
    private static final int TIMEOUT = 3000;

    public static void main(String args[]) throws Exception {
        // 打开服务端 Socket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 打开 Selector
        Selector selector = Selector.open();
        // 服务端 Socket 监听8080端口, 并配置为非阻塞模式
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);
        // 将 channel 注册到 selector 中.
        // 通常我们都是先注册一个 OP_ACCEPT 事件, 然后在 OP_ACCEPT 到来时, 再将这个 Channel 的 OP_READ
        // 注册到 Selector 中.
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            // 通过调用 select 方法, 阻塞地等待 channel I/O 可操作
            if (selector.select(TIMEOUT) == 0) {
                System.out.print(".");
                continue;
            }
            // 获取 I/O 操作就绪的 SelectionKey, 通过 SelectionKey 可以知道哪些 Channel 的哪类 I/O 操作已经就绪.
            Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                // 当获取一个 SelectionKey 后, 就要将它删除, 表示我们已经对这个 IO 事件进行了处理.
                keyIterator.remove();
                if (key.isAcceptable()) {
                    // 当 OP_ACCEPT 事件到来时, 我们就有从 ServerSocketChannel 中获取一个 SocketChannel,
                    // 代表客户端的连接
                    // 注意, 在 OP_ACCEPT 事件中, 从 key.channel() 返回的 Channel 是 ServerSocketChannel.
                    // 而在 OP_WRITE 和 OP_READ 中, 从 key.channel() 返回的是 SocketChannel.
                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                    clientChannel.configureBlocking(false);
                    //在 OP_ACCEPT 到来时, 再将这个 Channel 的 OP_READ 注册到 Selector 中.
                    // 注意, 这里我们如果没有设置 OP_READ 的话, 即 interest set 仍然是 OP_CONNECT 的话, 那么 select 方法会一直直接返回.
                    clientChannel.register(key.selector(), OP_READ, ByteBuffer.allocate(BUF_SIZE));
                }

                if (key.isReadable()) {
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    ByteBuffer buf = (ByteBuffer) key.attachment();
                    long bytesRead = clientChannel.read(buf);
                    if (bytesRead == -1) {
                        clientChannel.close();
                    } else if (bytesRead > 0) {
                        key.interestOps(OP_READ | SelectionKey.OP_WRITE);
                        System.out.println("Get data length: " + bytesRead);
                    }
                }

                if (key.isValid() && key.isWritable()) {
                    ByteBuffer buf = (ByteBuffer) key.attachment();
                    buf.flip();
                    SocketChannel clientChannel = (SocketChannel) key.channel();

                    clientChannel.write(buf);

                    if (!buf.hasRemaining()) {
                        key.interestOps(OP_READ);
                    }
                    buf.compact();
                }
            }
        }
    }
}

3. NIO网络编程基本流程

① 当客户端连接时,会通过ServerSocketChannel 得到 SocketChannel
Selector 进行监听 select 方法, 返回有事件发生的通道的个数.
③ 将socketChannel注册到Selector上 一个selector上可以注册多个SocketChannel
④ 注册后返回一个 SelectionKey, 会和该Selector 关联(集合)
⑤ 进一步得到各个 SelectionKey (有事件发生)
⑥ 在通过 SelectionKey 反向获取 SocketChannel
⑦ 可以通过 得到的 channel , 完成业务处理

服务端

public class NIOServer {
    public static void main(String[] args) throws Exception{
        //创建ServerSocketChannel -> ServerSocket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //得到一个Selecor对象
        Selector selector = Selector.open();
        //绑定一个端口6666, 在服务器端监听
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //把 serverSocketChannel 注册到  selector 关心 事件为 OP_ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("注册后的selectionkey 数量=" + selector.keys().size()); // 1

        //循环等待客户端连接
        while (true) {

            //这里我们等待1秒,如果没有事件发生, 返回
            if(selector.select(1000) == 0) { //没有事件发生
                System.out.println("服务器等待了1秒,无连接");
                continue;
            }

            //如果返回的>0, 就获取到相关的 selectionKey集合
            //1.如果返回的>0, 表示已经获取到关注的事件
            //2. selector.selectedKeys() 返回关注事件的集合
            //   通过 selectionKeys 反向获取通道
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            System.out.println("selectionKeys 数量 = " + selectionKeys.size());

            //遍历 Set<SelectionKey>, 使用迭代器遍历
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();

            while (keyIterator.hasNext()) {
                //获取到SelectionKey
                SelectionKey key = keyIterator.next();
                //根据key 对应的通道发生的事件做相应处理
                if(key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接
                    //该该客户端生成一个 SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode());
                    //将  SocketChannel 设置为非阻塞
                    socketChannel.configureBlocking(false);
                    //将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel
                    //关联一个Buffer
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4..
                }
                if(key.isReadable()) {  //发生 OP_READ
                    //通过key 反向获取到对应channel
                    SocketChannel channel = (SocketChannel)key.channel();
                    //获取到该channel关联的buffer
                    ByteBuffer buffer = (ByteBuffer)key.attachment();
                    channel.read(buffer);
                    System.out.println("form 客户端 " + new String(buffer.array()));

                }
                //手动从集合中移动当前的selectionKey, 防止重复操作
                keyIterator.remove();

            }

        }

    }
}

客户端

public class NIOClient {
    public static void main(String[] args) throws Exception{

        //得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //提供服务器端的ip 和 端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
        //连接服务器
        if (!socketChannel.connect(inetSocketAddress)) {

            while (!socketChannel.finishConnect()) {
                System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作..");
            }
        }

        //...如果连接成功,就发送数据
        String str = "hello~";
        //Wraps a byte array into a buffer
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        //发送数据,将 buffer 数据写入 channel
        socketChannel.write(buffer);
        System.in.read();

    }
}

4. 零拷贝

什么是Zero-Copy?
零拷贝(Zero-copy)及其应用详解

4.1 什么是零拷贝

  • 我们说零拷贝,是从操作系统的角度来说的,零拷贝内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据)
  • 零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算
  • 零拷贝从操作系统角度,是没有cpu 拷贝
① 传统io数据读写

在这里插入图片描述

DMA: direct memory access 直接内存拷贝(不使用CPU)

这个过程一共发生了四次上下文切换(严格来说是模式的切换),并且数据也来回拷贝了4次

  1. JVM向OS发出read()系统调用,触发上下文切换,从用户态切换到内核态。
  2. 从外部存储(如硬盘)读取文件内容,通过直接内存访问(DMA)存入内核地址空间的缓冲区。
  3. 将数据从内核缓冲区拷贝到用户空间缓冲区,read()系统调用返回,并从内核态切换回用户态。
  4. JVM向OS发出write()系统调用,触发上下文切换,从用户态切换到内核态。
  5. 将数据从用户缓冲区拷贝到内核中与目的地Socket关联的缓冲区。
  6. 数据最终经由Socket通过DMA传送到硬件(如网卡)缓冲区,write()系统调用返回,并从内核态切换回用户态。

在这里插入图片描述

数据白白从kernel模式到user模式走了一圈,浪费了2次copy(第一次,从kernel模式拷贝到user模式;第二次从user模式再拷贝回kernel模式,)。而且上面的过程中kernel和user模式的上下文的切换也是4次

mmap优化

mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数
在这里插入图片描述

  • DMA拷贝还在(一定在)
  • 没有kernel buffer到user buffer的拷贝(他们共享数据)
  • 所有数据可以直接将在kernal buffer修改
  • 但是用户态和内核态的切换没有变
sendFile优化

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

在这里插入图片描述
Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝
在这里插入图片描述
这里其实有 一次cpu 拷贝kernel buffer -> socket buffer
但是,拷贝的信息很少,比如lenght , offset , 消耗低,可以忽略

④ 零拷贝总结

可见确实是消除了从内核空间到用户空间的来回复制,因此“zero-copy”这个词实际上是站在内核的角度来说的,并不是完全不会发生任何拷贝

在Java NIO包中提供了零拷贝机制对应的API,即FileChannel.transferTo()方法

SocketAddress socketAddress = new InetSocketAddress(HOST, PORT);
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(socketAddress);

File file = new File(FILE_PATH);
FileChannel fileChannel = new FileInputStream(file).getChannel();
fileChannel.transferTo(0, file.length(), socketChannel);

fileChannel.close();
socketChannel.close();

mmapsendFile 的区别

  • mmap 适合小数据量读写,sendFile 适合大文件传输。
  • mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝
  • sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值