《Java高并发核心卷一》NIO核心详解

1.Java NIO简介

从JDK1.4版本开始,引进了新的异步IO库,被称为Java New IO类库,简称为Java NIO。NIO弥补了原来面向流的OIO同步阻塞的不足,为标准Java代码提供了高速、面向缓冲区的IO。

1.1 NIO和OIO的对比

在Java中,NIO和OIO的区别主要体现在三个方面:
(1)OIO是面向流(Stream Oriented)的,NIO是面向缓冲区(Buffer Oriented)的。
         在一般的OIO操作中,面向字节流或字符流的IO操作总是以流式的方式顺序地从一个流(Stream)中读取一个或多个字节,因此,我们不能随意改变读取指针的位置。
         在NIO操作中则不同,NIO中引入了Channel和Buffer的概念。面向缓冲区的读取和写入只需要从通道读取数据到缓冲区中,或将数据从缓冲区写入通道中。
         NIO不像OIO那样是顺序操作,它可以随意读取Buffer中任意位置的数据。

(2)OIO的操作是阻塞的,而NIO的操作是非阻塞的。
OIO操作都是阻塞的。例如,我们调用一个read方法读取一个文件的内容,调用read的线程就会被阻塞,直到read操作完成。
在NIO模式中,当我们调用read方法时,如果此时有数据,则read读取数据并返回;如果此时没有数据,则read也会直接返回,而不会阻塞当前线程。
NIO的非阻塞是如何做到的呢?其实在上一章已经揭晓答案,即NIO使用了通道和通道的多路复用技术。

(3)OIO没有选择器(Selector)的概念,而NIO有选择器的概念。
NIO的实现是基于底层选择器的系统调用的,所以NIO需要底层操作系统提供支持;而OIO不需要用到选择器。

IO多路复用
      目前支持IO多路复用的系统调用有select、epoll等。几乎所有的操作系统都支持select系统调用,它具有良好的跨平台特性。epoll是在Linux 2.6内核中提出的,是select系统调用的Linux增强版本。
      在IO多路复用模型中通过select/epoll系统调用,单个应用程序的线程可以不断地轮询成百上千的socket连接的就绪状态,当某个或者某些socket网络连接有IO就绪状态时就返回这些就绪的状态(或者说就绪事件)。
     (1)选择器注册。首先,将需要read操作的目标文件描述符(socket连接)提前注册到Linux的select/epoll选择器中,在Java中所对应的选择器类是Selector类。然后,开启整个IO多路复用模型的轮询流程。
     (2)就绪状态的轮询。通过选择器的查询方法,查询所有提前注册过的目标文件描述符(socket连接)的IO就绪状态。通过查询的系统调用,内核会返回一个就绪的socket列表。当任何一个注册过的socket中的数据准备好或者就绪了就说明内核缓冲区有数据了,内核将该socket加入就绪的列表中,并且返回就绪事件。
     (3)用户线程获得了就绪状态的列表后,根据其中的socket连接发起read系统调用,用户线程阻塞。内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区。
     (4)复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行。
     IO多路复用模型的特点是:IO多路复用模型的IO涉及两种系统调用,一种是IO操作的系统调用,另一种是select/epoll就绪查询系统调用。IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用select/epoll。
     和NIO模型相似,多路复用IO也需要轮询。负责select/epoll状态查询调用的线程,需要不断地进行select/epoll轮询,以找出达到IO操作就绪的socket连接。
     IO多路复用模型的优点是一个选择器查询线程可以同时处理成千上万的网络连接,所以用户程序不必创建大量的线程,也不必维护这些线程,从而大大减少了系统的开销。与一个线程维护一个连接的阻塞IO模式相比,这一点是IO多路复用模型的最大优势。
     IO多路复用模型的缺点是,本质上select/epoll系统调用是阻塞式的,属于同步IO,需要在读写事件就绪后由系统调用本身负责读写,也就是说这个读写过程是阻塞的。要彻底地解除线程的阻塞,就必须使用异步IO模型。

2.NIO三大核心组件介绍

Java NIO类库包含以下三个核心组件:
Channel(通道)
Buffer(缓冲区)
Selector(选择器)

         Java NIO属于第三种模型——IO多路复用模型。只不过,Java-NIO组件提供了统一的API,为大家屏蔽了底层的操作系统的差异。

2.1 通道(Channel)

         在OIO中,同一个网络连接会关联到两个流:一个是输入流(Input-Stream),另一个是输出流(Output-Stream)。Java应用程序通过这两个流不断地进行输入和输出的操作。
         在NIO中,一个网络连接使用一个通道表示,所有NIO的IO操作都是通过连接通道完成的。一个通道类似于OIO中两个流的结合体,既可以从通道读取数据,也可以向通道写入数据。

2.2 选择器(Selector)

首先回顾一下前面介绍的基础知识——IO多路复用指的是一个进程/线程可以同时监视多个文件描述符(含socket连接),
一旦其中的一个或者多个文件描述符可读或者可写,该监听进程/线程就能够进行IO就绪事件的查询。
         选择器可以理解为一个IO事件的监听与查询器。通过选择器,一个线程可以查询多个通道的IO事件的就绪状态。
由于一个选择器只需要一个线程进行
         与OIO相比,NIO使用选择器的最大优势是系统开销小。系统不必为每一个网络连接(文件描述符)创建进程/线程,从而大大减少了系统的开销。
         总之,一个线程负责多个连接通道的IO处理是非常高效的,这种高效来自Java的选择器组件Selector及其底层的操作系统IO多路复用技术的支持。监控,因此我们可以很简单地使用一个线程,通过选择器去管理多个连接通道。

2.3缓冲区(Buffer)

         应用程序与通道的交互主要是进行数据的读取和写入。为了完成NIO的非阻塞读写操作,NIO为大家准备了第三个重要的组件——Buffer。
         所谓通道的读取,就是将数据从通道读取到缓冲区中;所谓通道的写入,就是将数据从缓冲区写入通道中。
缓冲区的使用是面向流进行读写操作的OIO所没有的,也是NIO非阻塞的重要前提和基础之一。

3.NIO三大核心组件详解

3.1缓冲区(Buffer)

         NIO的Buffer本质上是一个内存块,既可以写入数据,也可以从中读取数据。Java-NIO中代表缓冲区的Buffer类是一个抽象类,位于java.nio包中。
         NIO的Buffer内部是一个内存块(数组),
         与普通的内存块(Java数组)不同的是:NIO-Buffer对象提供了一组比较有效的方法,用来进行写入和读取的交替访问。
说明:Buffer类是一个非线程安全类。

         Buffer类是一个抽象类,对应于Java的主要数据类型。在NIO中,有8种缓冲区类,不同的Buffer子类可以操作的数据类型能够通过名称进行判断,实际上,使用最多的是ByteBuffer(二进制字节缓冲区)类型,
         Buffer的子类会拥有一块内存,作为数据的读写缓冲区,ByteBuffer子类就拥有一个byte[]类型的数组成员final byte[] hb
为了记录读写的状态和位置,Buffer类额外提供了一些重要的属性,其中有三个重要的成员属性:capacity(容量)、position(读写位置)和limit(读写的限制)。

capacity属性

Buffer类的capacity属性表示内部容量的大小。一旦写入的对象数量超过了capacity,缓冲区就满了,不能再写入了。
Buffer类的capacity属性一旦初始化,就不能再改变。

position属性

         Buffer类的position属性表示当前的位置。position属性的值与缓冲区的读写模式有关。
         在不同的模式下,position属性值的含义是不同的,在缓冲区进行读写的模式改变时,position值会进行相应的调整。

Buffer的读写模式具体的切换
         当新建了一个缓冲区实例时,缓冲区处于写模式,这时是可以写数据的。
         在数据写入完成后,如果要从缓冲区读取数据,就要进行模式的切换,可以调用flip()方法将缓冲区变成读模式,flip为翻转的意思。
在从写模式到读模式的翻转过程中,position和limit属性值会进行调整,具体的规则是:
(1)limit属性被设置成写模式时的position值,表示可以读取的最大数据位置。
(2)position由原来的写入位置变成新的可读位置,也就是0,表示可以从头开始读。

limit属性

         Buffer类的limit属性表示可以写入或者读取的数据最大上限,
         其属性值的具体含义也与缓冲区的读写模式有关。在不同的模式下,limit值的含义是不同的,具体分为以下两种情况:
(1)在写模式下,limit属性值的含义为可以写入的数据最大上限。
(2)在读模式下,limit值的含义为最多能从缓冲区读取多少数据。

3个属性描述

3.11 详解NIO Buffer类的重要方法

allocate()

获取一个Buffer实例对象时,调用子类的allocate()方法获取Buffer子类的实例对象,并且分配内存空间。

IntBuffer intBuffer = IntBuffer.allocate(20);
/**
[DEBUG] ------------after allocate------------------
position=0
limit=20
capacity=20
*/
put()

返回了实例对象后,缓冲区实例对象处于写模式,可以写入对象,如果要把对象写入缓冲区,就需要调用put()方法。

for (int i = 0; i < 5; i++) {
      //写入一个整数到缓冲区
       intBuffer.put(i);
}
 /**
[DEBUG] ------------after putTest------------------
[DEBUG] position=5
[DEBUG] limit=20
[DEBUG] capacity=20
*/
flip()

缓冲区还处于写模式,如果需要读取数据,需要调用flip()翻转方法将写模式翻转成读模式。

//翻转缓冲区,从写模式翻转成读模式
        intBuffer.flip();
/**
[INFO] ------------after flip ------------------
[INFO] position=0
[INFO] limit=5
[INFO] capacity=20

首先,设置可读上限limit的属性值。将写模式下的缓冲区中内容的最后写入位置position值作为读模式下的limit上限值。
其次,把读的起始位置position的值设为0,表示从头开始读。
最后,清除之前的mark标记,因为mark保存的是写模式下的临时位置,发生模式翻转后,如果继续使用旧的mark标记,就会造成位置混乱。
*/        

在读取完成后,如何再一次将缓冲区切换成写模式呢?
答案是:可以调用**Buffer.clear()清空或者Buffer.compact()**压缩方法,它们可以将缓冲区转换为写模式。
在这里插入图片描述

get()

调用flip()方法将缓冲区切换成读模式之后,就可以开始从缓冲区读取数据了。
读取数据的方法很简单,可以调用get()方法每次从position的位置读取一个数据,并且进行相应的缓冲区属性的调整。

 //先读2个数据
        for (int i = 0; i< 2; i++)
        {
            int j = intBuffer.get();
            Logger.info("j = " + j);
        }
/**
[INFO] j = 0
[INFO] j = 1
[INFO] ---------after get 2 int --------------
[INFO] position=2
[INFO] limit=5
[INFO] capacity=20
*/        

有一个问题:缓冲区是不是可以重复读呢?答案是可以的,既可以通过倒带方法rewind()去完成,也可以通过mark()和reset()两个方法组合实现。

rewind()

已经读完的数据,如果需要再读一遍,可以调用rewind()方法。

intBuffer.rewind();
/**
[INFO] ------------after rewind ------------------
[INFO] position=0
[INFO] limit=5
[INFO] capacity=20
*/

rewind ()方法主要是调整了缓冲区的position属性与mark属性,具体的调整规则如下:
(1)position重置为0,所以可以重读缓冲区中的所有数据。
(2)limit保持不变,数据量还是一样的,仍然表示能从缓冲区中读取的元素数量。
(3)mark被清理,表示之前的临时位置不能再用了。

mark()和reset()

mark()和reset()两个方法是配套使用的:Buffer.mark()方法将当前position的值保存起来放在mark属性中,
让mark属性记住这个临时位置;然后可以调用Buffer.reset()方法将position的值恢复成mark。
例如,在前面重复读取的示例代码中,在读到第三个元素(i为2时)时,可以调用mark()方法,把当前位置position的值保存到mark属性中,这时mark属性的值为2。
接下来可以调用reset()方法将mark属性的值恢复到position中,这样就可以从位置2(第三个元素)开始重复读取了。

intBuffer.reset();
//调用reset()方法之后,position的值为2,此时去读取缓冲区,输出后面的三个元素2、3、4。
 for (int i =2; i< 5; i++) {
    int j = intBuffer.get();
    Logger.info("j = " + j);
}
/**
[INFO] ------------after reset------------------
[INFO] position=2
[INFO] limit=5
[INFO] capacity=20
[INFO] j = 2
[INFO] j = 3
[INFO] j = 4
*/
        
clear() 和 compact()

在读模式下,调用clear()方法将缓冲区切换为写模式。此方法的作用是:
(1)将position清零。(2)limit设置为capacity最大容量值,可以一直写入,直到缓冲区写满。

//intBuffer.clear();
intBuffer.compact();
/**
[INFO] ------------after clear / compact------------------
[INFO] position=0
[INFO] limit=20
[INFO] capacity=20
*/

总体来说,使用Java NIO Buffer类的基本步骤如下:
(1)使用创建子类实例对象的allocate()方法创建一个Buffer类的实例对象。
(2)调用put()方法将数据写入缓冲区中。
(3)写入完成后,在开始读取数据前调用Buffer.flip()方法,将缓冲区转换为读模式。
(4)调用get()方法,可以从缓冲区中读取数据。
(5)读取完成后,调用Buffer.clear()方法或Buffer.compact()方法,将缓冲区转换为写模式,可以继续写入。

3.2通道(Channel类)

         Java NIO中一个socket连接使用一个Channel来表示。从更广泛的层面来说,一个通道可以表示一个底层的文件描述符,例如硬件设备、文件、网络连接等。然而,远不止如此,Java-NIO的通道可以更加细化。
例如,不同的网络传输协议类型,在Java中都有不同的NIO-Channel实现。
         这里不对Java-NIO的全部通道类型进行过多的描述,仅着重介绍其中最为重要的四种Channel实现:
(1)FileChannel:文件通道,用于文件的数据读写。
(2)SocketChannel:套接字通道,用于套接字TCP连接的数据读写。
(3)ServerSocketChannel:服务器套接字通道(或服务器监听通道),允许我们监听TCP连接请求,为每个监听到的请求创建一个SocketChannel通道。
(4)DatagramChannel:数据报通道,用于UDP的数据读写。

在大部分应用场景中,从通道读取数据都会调用通道的int-read(ByteBuffer-buf)方法,
它把从通道读取的数据写入ByteBuffer缓冲区,并且返回读取的数据量。

FileChannel

(1)获取FileChannel
 //创建一个文件输入流
 FileInputStream fis = new FileInputStream(srcFile);
 //获取文件流的通道
 FileChannel inChannel = fis.getChannel();
 //创建一个文件输出流
 FileOutputStream fos = new FileOutputStream(srcFile);
 //获取文件流的通道
 FileChannel outchannel = fos.getChannel();
 
 //也可以通过RandomAccessFile(文件随机访问)类来获取FileChannel实例,
 RandomAccessFile rFile = new RandomAccessFile("filename.txt","rw");
 //获取文件流的通道(可读可写)
 FileChannel channel = rFile.getChannel();
(2) 读取FileChannel

在大部分应用场景中,调用通道的read(ByteBuffer buf)方法,它把从通道读取的数据写入ByteBuffer缓冲区,并且返回读取的数据量。

  RandomAccessFile aFile = new RandomAccessFile("filename.txt", "rw");
        //获取通道(可读可写)
        FileChannel channel1 = aFile.getChannel();
        //获取一个字节缓冲区
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        int length = -1;

        //调用通道的read()方法,读取数据并写入字节类型的缓冲区
        while ((length = channel1.read(readBuffer))>0) {
            System.out.println("-------" + length);
            readBuffer.flip();
            byte[] bytes = new byte[readBuffer.remaining()];
            readBuffer.get(bytes);
            System.out.println(new String(bytes, "UTF-8"));
        }

(3) 写入FileChannel

write(ByteBuffer)方法的作用是从ByteBuffer缓冲区中读取数据,然后写入通道自身,而返回值是写入成功的字节数。

 RandomAccessFile aFile = new RandomAccessFile("filename.txt", "rw");
        //获取通道(可读可写)
        FileChannel outchannel = aFile.getChannel();
        String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        byte[] bytes = date.getBytes();
        ByteBuffer buffer = ByteBuffer.allocate(date.length());
        buffer.put(bytes);
        //如果buf处于写模式(如刚写完数据),需要翻转buf,使其变成读模式
        buffer.flip();
        int outlength = 0;
        //调用write()方法,将buf的数据写入通道
        while ((outlength = outchannel.write(buffer)) != 0) {//覆盖写入
            System.out.println("写入的字节数:" + outlength);
            //强制刷新到磁盘
            outchannel.force(true);
        }
(4) 强制刷新到磁盘 force()

在将缓冲区写入通道时,出于性能的原因,操作系统不可能每次都实时地将写入数据落地(或刷新)到磁盘,完成最终的数据保存。
在将缓冲区数据写入通道时,要保证数据能写入磁盘,可以在写入后调用一下FileChannel的force()方法。

outchannel.force(true);
(5) 关闭通道
 outchannel.close();

SocketChannel

         在NIO中,涉及网络连接的通道有两个:一个是SocketChannel,负责连接的数据传输;另一个是ServerSocketChannel,负责连接的监听。
         其中,NIO中的SocketChannel传输通道与OIO中的Socket类对应,NIO中的ServerSocketChannel监听通道对应于OIO中的ServerSocket类。
         ServerSocketChannel仅应用于服务端,而SocketChannel同时处于服务端和客户端。所以,对于一个连接,两端都有一个负责传输的SocketChannel。
         在非阻塞模式下,通道的操作是异步、高效的,这也是相对于传统OIO的优势所在。下面详细介绍在非阻塞模式下通道的获取、读写和关闭等操作。

(1) 获取SocketChannel传输通道
 		//获得一个套接字传输通道
		SocketChannel socketChannel = SocketChannel.open();
        //设置为非阻塞模式
        socketChannel.configureBlocking(false);
        //对服务器的IP和端口发起连接
        socketChannel.connect(new InetSocketAddress("127.0.0.1",80));
        //在非阻塞情况下,与服务器的连接可能还没有真正建立,socketChannel.connect()方法就返回了,因此需要不断地自旋,检查当前是否连接到了主机:
         while(! socketChannel.finishConnect() ){
            //不断地自旋、等待,或者做一些其他的事情
        }
         //新连接事件到来,首先通过事件获取服务器监听通道
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        //获取新连接的套接字通道
        SocketChannel socketChannel = server.accept();
        //设置为非阻塞模式
        socketChannel.configureBlocking(false);

说明:NIO套接字通道主要用于非阻塞的传输场景。所以,基本上都需要调用通道的configureBlocking(false)方法,将通道从阻塞模式切换为非阻塞模式。

(2) 读取SocketChannel传输通道

在读取时,因为是异步的,所以我们必须检查read()的返回值,以便判断当前是否读取到了数据。
read()方法的返回值是读取的字节数,如果是-1,那么表示读取到对方的输出结束标志,即对方已经输出结束,准备关闭连接。
实际上,通过read()方法读数据本身是很简单的,比较困难的是在非阻塞模式下如何知道通道何时是可读的。

  ByteBuffer buffer = ByteBuffer.allocate(1024);
  int bytesRead = socketChannel.read(buffer);
(3) 写入SocketChannel传输通道

和前面把数据写入FileChannel一样,大部分应用场景都会调用通道的int write(ByteBufferbuf)方法

 //写入前需要读取缓冲区,要求ByteBuffer是读模式
  buffer.flip();
  socketChannel.write(buffer);
(4) 关闭SocketChannel传输通道

         在关闭SocketChannel传输通道前,如果传输通道用来写入数据,则建议调用一次shutdownOutput()终止输出方法,向对方发送一个输出的结束标志(-1)。然后调用socketChannel.close()方法,关闭套接字连接。

//客户端调用终止输出方法,向对方发送一个输出的结束标志
socketChannel.shutdownOutput();
//关闭套接字连接
closeQuietly(socketChannel);

DatagramChannel

         在Java中使用UDP传输数据比TCP更加简单。和socket的TCP不同,UDP不是面向连接的协议。
         使用UDP时,只要知道服务器的IP和端口就可以直接向对方发送数据。在Java-NIO中,使用DatagramChannel来处理UDP的数据传输。

@Test
    public void test_DatagramChannel() throws IOException {
        //获取DatagramChannel
        DatagramChannel channel = DatagramChannel.open();
        //设置为非阻塞模式
        channel.configureBlocking(false);

        /**
         * 如果需要接收数据,还需要调用bind()方法绑定一个数据报的监听端口,具体如下:
         */
        //调用bind()方法绑定一个数据报的监听端口
        channel.socket().bind(new InetSocketAddress(18080));

        /**
         * 2. 从DatagramChannel读取数据
         *
         * 当DatagramChannel通道可读时,可以从DatagramChannel读取数据。和前面的SocketChannel读取方式不同,这里不调用read()方法,
         * 而是调用receive(ByteBufferbuf)方法将数据从DatagramChannel读入,再写入ByteBuffer缓冲区中。
         */
        //创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //从DatagramChannel读入,再写入ByteBuffer缓冲区
        SocketAddress clientAddr= channel.receive(buffer);
        /**
         * @通道读取receive(ByteBufferbuf)方法虽然读取了数据到buf缓冲区,但是其返回值是SocketAddress类型,表示返回发送端的连接地址(包括IP和端口)。
         * @通过receive方法读取数据非常简单,但是在非阻塞模式下如何知道DatagramChannel通道何时是可读的呢?和SocketChannel一样,
         * @同样需要用到NIO的新组件——Selector通道选择器。
         */

        /**
         * 3. 写入DatagramChannel
         *
         * 向DatagramChannel发送数据,和向SocketChannel通道发送数据的方法是不同的。这里不是调用write()方法,而是调用send()方法。示例代码如下:
         */
        //把缓冲区翻转为读模式
        buffer.flip();
        //调用send()方法,把数据发送到目标IP+端口
        channel.send(buffer,  new InetSocketAddress("127.0.0.1",18899));
        //清空缓冲区,切换到写模式
        buffer.clear();

        /**
         * @由于UDP是面向非连接的协议,因此在调用send()方法发送数据时需要指定接收方的地址(IP和端口)。
         */

        /**
         * 4. 关闭DatagramChannel
         */
        //简单关闭即可
        channel.close();
    }

链接: 使用DatagramChannel发送数据的实战案例

3.3选择器(Selector)

选择器是什么?选择器和通道的关系又是什么?
         简单地说,选择器的使命是完成IO的多路复用,其主要工作是通道的注册、监听、事件查询。
         一个通道代表一条连接通路,通过选择器可以同时监控多个通道的IO(输入输出)状况。
         选择器提供了独特的API方法,能够选出(select)所监控的通道已经发生了哪些IO事件,包括读写就绪的IO操作事件。
         在极端情况下(数万个连接),只用一个线程就可以处理所有的通道,这样会大量地减少线程之间上下文切换的开销。

通道和选择器之间的关联通过register(注册)的方式完成。,register方法有两个参数:第一个参数指定通道注册到的选择器实例;第二个参数指定选择器要监控的IO事件类型。
可供选择器监控的通道IO事件类型包括以下四种:
(1)可读:SelectionKey.OP_READ。
(2)可写:SelectionKey.OP_WRITE。
(3)连接:SelectionKey.OP_CONNECT。
(4)接收:SelectionKey.OP_ACCEPT。

什么是IO事件?

这里的IO事件不是对通道的IO操作,而是通道处于某个IO操作的就绪状态,表示通道具备执行某个IO操作的条件。
例如,某个SocketChannel传输通道如果完成了和对端的三次握手过程,就会发生“连接就绪”(OP_CONNECT)事件;
某个ServerSocketChannel服务器连接监听通道,在监听到一个新连接到来时,则会发生“接收就绪”(OP_ACCEPT)事件;
一个SocketChannel通道有数据可读,就会发生“读就绪”(OP_READ)事件;
一个SocketChannel通道等待数据写入,就会发生“写就绪”(OP_WRITE)事件。

并不是所有的通道都是可以被选择器监控或选择的。例如,FileChannel就不能被选择器复用。SelectableChannel类提供了-实现通道可选择性-所需要的公共方法。

SelectionKey

SelectionKey和IO的关系可以简单地理解为SelectionKey就是被选中了的IO事件。通过select()方法,选择器可以不断地选择通道中所发生操作的就绪状态,返回注册过的那些感兴趣的IO事件。换句话说,一旦在通道中发生了某些IO事件(就绪状态达成),并且是在选择器中注册过的IO事件,就会被选择器选中,并放入SelectionKey(选择键)的集合中。
(1)获取选择器实例。

//调用静态工厂方法open()来获取Selector实例
Selector selector = Selector.open();

(2)将通道注册到选择器实例。要实现选择器管理通道,需要将通道注册到相应的选择器上,简单的示例代码如下:

  //获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //绑定连接
        serverSocketChannel.bind(new InetSocketAddress(18899));
        //将通道注册到选择器上,并指定监听事件为“接收连接”
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

这里需要注意:注册到选择器的通道必须处于非阻塞模式下,否则将抛出IllegalBlockingModeException异常。其次,一个通道并不一定支持所有的四种IO事件。例如,服务器监听通道ServerSocketChannel仅支持Accept(接收到新连接)IO事件

//轮询,选择感兴趣的IO就绪事件(选择键集合)
        while (selector.select() > 0) {
            Set selectedKeys = selector.selectedKeys();
            Iterator keyIterator = selectedKeys.iterator();
            while (keyIterator.hasNext()) {
                SelectionKey key = (SelectionKey) keyIterator.next();
                //根据具体的IO事件类型执行对应的业务操作
                if (key.isAcceptable()) {
                    //IO事件:ServerSocketChannel服务器监听通道有新连接
                } else if (key.isConnectable()) {
                    //IO事件:传输通道连接成功
                } else if (key.isReadable()) {
                    //IO事件:传输通道可读
                } else if (key.isWritable()) {
                    //IO事件:传输通道可写
                }
                //处理完成后,移除选择键
                keyIterator.remove();
            }
        }

处理完成后,需要将选择键从SelectionKey集合中移除,以防止下一次循环时被重复处理。
SelectionKey集合不能添加元素,如果试图向SelectionKey中添加元素,则将抛出java.lang.UnsupportedOperationException异常。
用于选择就绪的IO事件的select()方法有多个重载的实现版本,具体如下:
(1)select():阻塞调用,直到至少有一个通道发生了注册的IO事件。
(2)select(long timeout):和select()一样,但最长阻塞时间为timeout指定的毫秒数。
(3)selectNow():非阻塞,不管有没有IO事件都会立刻返回。

select()方法的返回值是整数类型(int),表示发生了IO事件的数量,即从上一次select到这一次select之间有多少通道发生了IO事件,
更加准确地说是发生了选择器感兴趣(注册过)的IO事件数。
下面简单案例 仅仅是为了学习NIO的知识,所以没有为了解决“粘包”和“半包”问题。

链接: 使用NIO发送数据的简单案例

4.使用SocketChannel在服务端接收文件的实战案例

链接: 案例

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值