Java NIO学习二

Posted 想作会飞的鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java NIO学习二相关的知识,希望对你有一定的参考价值。

一、NIO的异步方式

异步 I/O 是一种 没有阻塞地 读写数据的方法。通常,在代码进行 read() 调用时,代码会阻塞直至有可供读取的数据。同样,write() 调用将会阻塞直至数据能够写入。另一方面,异步 I/O 调用不会阻塞。相反您将注册对特定 I/O 事件的兴趣,包括可读的数据的到达、新的套接字连接,等等,而在发生这样的事件时,系统将会告诉您。

异步 I/O 的一个优势在于,它允许您同时根据大量的输入和输出执行 I/O。同步程序常常要求助于轮询,或者创建许许多多的线程以处理大量的连接。使用异步 I/O,您可以监听任何数量的通道上的事件,不用轮询,也不用额外的线程。

NIO实现异步IO的方法是通过Selectors。

二、Selectors

异步 I/O 中的核心对象名为 Selector。Selector 就是您注册对各种 I/O 事件的兴趣的地方,而且当那些事件发生时,就是这个对象告诉您所发生的事件。选择器提供选择执行已经就绪的任务的能力,这使得多元I/O成为可能,就绪选择和多元执行使得单线程能够有效率的同时管理多个I/O通道(channels),简单言之就是selector充当一个监视者,您需要将之前创建的一个或多个可选择的通道注册到选择器对象中。一个表示通道和选择器的键将会被返回。选择键会记住您关心的通道。它们也会追踪对应的通道是否已经就绪当您调用一个选择器对象的 select( )方法时,相关的键会被更新,用来检查所有被注册到该选择器的通道。您可以获取一个键的集合,从而找到当时已经就绪的通道。通过遍历这些键,您可以选择出每个从上次您调用 select( )开始直到现在,已经就绪的通道。

1、新建Selector

我们需要做的第一件事就是创建一个 Selector:

Selector selector = Selector.open();

对于网络NIO,为了接收连接,我们需要一个 ServerSocketChannel。事实上,我们要监听的每一个端口都需要有一个 ServerSocketChannel 。对于每一个端口,我们打开一个 ServerSocketChannel,如下所示:

ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking( false );

ServerSocket ss = ssc.socket();
InetSocketAddress address = new InetSocketAddress( ports[i] );
ss.bind( address );

第一行创建一个新的 ServerSocketChannel ,最后三行将它绑定到给定的端口。第二行将 ServerSocketChannel 设置为 非阻塞的 。我们必须对每一个要使用的套接字通道调用这个方法,否则异步 I/O 就不能工作。

2、注册Channel

我们将对不同的通道对象调用 register() 方法,以便注册我们对这些对象中发生的 I/O 事件的兴趣。register() 的第一个参数总是这个 Selector。

下一步是将新打开的 ServerSocketChannels 注册到 Selector上。为此我们使用 ServerSocketChannel.register() 方法,如下所示

SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );

register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT,这里它指定我们想要监听 accept 事件,也就是在新的连接建立时所发生的事件。这是适用于 ServerSocketChannel 的唯一事件类型。

需要注意register()方法的第二个参数,它是一个“interest set”,意思是注册的Selector对Channel中的哪些时间感兴趣,事件类型有四种:Connect、Accept、Read和Write。通道触发了一个事件意思是该事件已经 Ready(就绪)。所以,某个Channel成功连接到另一个服务器称为 Connect Ready。一个ServerSocketChannel准备好接收新连接称为 Accept Ready,一个有数据可读的通道可以说是 Read Ready,等待写数据的通道可以说是Write Ready。上面这四个事件对应到SelectionKey中的四个常量:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

如果你对多个事件感兴趣,可以通过or操作符来连接这些常量:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; 

请注意对 register() 的调用的返回值为SelectionKey类型。 SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。

3、SelectionKey

SelectionKey表示SelectableChannel 在 Selector 中的注册的标记/句柄。register()方法返回一个SelectinKey对象,这个对象包含一些你感兴趣的属性。通过调用某个SelectionKey的cancel()方法,关闭其通道,或者通过关闭其选择器来取消该Key之前,它一直保持有效。取消某个Key之后不会立即从Selector中移除它,相反,会将该Key添加到Selector的已取消key set,以便在下一次进行选择操作的时候移除它。

  • interest集合
    感兴趣的事件集合,可以通过SelectionKey读写interest集合,
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = (interestSet & Selection.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectioKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
  • ready集合 —— 是通道已经准备就绪的操作的集合,在一个选择后,你会是首先访问这个ready set

int readySet = selectionKey.readyOps();

可以向检测interet集合那样的方法,来检测channel中什么事件或操作已经就绪,也可以使用一下四个方法,


selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
  • 从SelectionKey获取对应的Channel和Selector:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
  • 附加的对象 —— 可以将一个对象或者更多的信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。例如,可以附加与通道一起使用的Buffer,或是包含聚集数据的某个对象。
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

4、通过Selector选择就绪的通道

一旦向Selector注册了一个或多个通道,就可以调用几个重载的select()方法。这些方法返回你所感兴趣的事件(连接,接受,读或写)已经准备就绪的那些通道。换句话说,如果你对”读就绪“的通道感兴趣,select()方法会返回读事件已经就绪的那些通道。

select() —— 阻塞到至少有一个通道在你注册的事件上就绪了
select(long timeout) —— 和select()一样,除了最长会阻塞timeout毫秒
selectNow() —— 不会阻塞,不管什么通道就绪都立刻返回;此方法执行非阻塞的选择操作,如果自从上一次选择操作后,没有通道变成可选择的,则此方法直接返回0

select()方法返回的Int值表示多少通道就绪。一旦调用了select()方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用selector的selectorKeys()方法,访问”已选择键集“中的就绪通道,

Set selectedKeys = selector.selectedKeys();

可以遍历这个已选择的集合来访问就绪的通道

int num = selector.select();
Set selectedKeys = selector.selectedKeys();
Iterator 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 eatablished 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();

首先,我们调用 Selector 的 select() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时,select() 方法将返回所发生的事件的数量num。
接下来,我们调用 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个 集合 。
我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。
在处理 SelectionKey 之后,我们几乎可以返回主循环了。但是我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活的键出现,这会导致我们尝试再次处理它。我们调用迭代器的remove() 方法来删除处理过的 SelectionKey。

某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。只要让其他线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。阻塞在select()方法上的线程会立马返回。

三、通过NIO实现Socket通信

1、服务端

package com.kang;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

/**
 * NIO服务端
 * 
 */
public class Nioserver 
    // 通道管理器
    private Selector selector;

    /**
     * 获得一个ServerSocket通道,并对该通道做一些初始化的工作
     * 
     * @param port
     *            绑定的端口号
     * @throws IOException
     */
    public void initServer(int port) throws IOException 
        // 获得一个ServerSocket通道
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        // 设置通道为非阻塞
        serverChannel.configureBlocking(false);
        // 将该通道对应的ServerSocket绑定到port端口
        serverChannel.socket().bind(new InetSocketAddress(port));
        // 获得一个通道管理器
        this.selector = Selector.open();
        // 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
        // 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    

    /**
     * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
     * 
     * @throws IOException
     * @throws InterruptedException 
     */
    public void listen() throws IOException, InterruptedException 
        System.out.println("服务端启动成功!");
        // 轮询访问selector
        while (true) 
            // 当注册的事件到达时,方法返回;否则,该方法会一直阻塞
            selector.select();
            // 获得selector中选中的项的迭代器,选中的项为注册的事件
            Iterator ite = this.selector.selectedKeys().iterator();
            while (ite.hasNext()) 
                SelectionKey key = (SelectionKey) ite.next();
                // 删除已选的key,以防重复处理
                ite.remove();
                // 客户端请求连接事件
                if (key.isAcceptable()) 
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    // 获得和客户端连接的通道
                    SocketChannel channel = server.accept();
                    // 设置成非阻塞
                    channel.configureBlocking(false);

                    // 在这里可以给客户端发送信息Hello World!
                    channel.write(ByteBuffer.wrap(new String("Hello World!").getBytes()));
                    // 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。注册读就绪事件
                    channel.register(this.selector, SelectionKey.OP_READ);

                    // 获得了可读的事件
                 else if (key.isReadable()) 
                    Thread.sleep(4000);
                    read(key);
                

            

        
    

    /**
     * 处理读取客户端发来的信息 的事件
     * 
     * @param key
     * @throws IOException
     */
    public void read(SelectionKey key) throws IOException 
        // 服务器可读取消息:得到事件发生的Socket通道
        SocketChannel channel = (SocketChannel) key.channel();
        // 创建读取的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        channel.read(buffer);//读取数据到缓存区
        byte[] data = buffer.array();
        String msg = new String(data).trim();//还原信息
        System.out.println("服务端收到信息:" + msg);
        ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
        channel.write(outBuffer);// 将消息回送给客户端
    

    /**
     * 启动服务端测试
     * 
     * @throws IOException
     * @throws InterruptedException 
     */
    public static void main(String[] args) throws IOException, InterruptedException 
        NIOServer server = new NIOServer();
        server.initServer(8000);
        server.listen();
    

2、客户端

package com.kang;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

/**
 * NIO客户端
 * 
 */
public class NIOClient 
    // 通道管理器
    private Selector selector;

    /**
     * 获得一个Socket通道,并对该通道做一些初始化的工作
     * 
     * @param ip
     *            连接的服务器的ip
     * @param port
     *            连接的服务器的端口号
     * @throws IOException
     */
    public void initClient(String ip, int port) throws IOException 
        // 获得一个Socket通道
        SocketChannel channel = SocketChannel.open();
        // 设置通道为非阻塞
        channel.configureBlocking(false);
        // 获得一个通道管理器
        this.selector = Selector.open();

        // 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
        // 用channel.finishConnect();才能完成连接
        channel.connect(new InetSocketAddress(ip, port));
        // 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
        channel.register(selector, SelectionKey.OP_CONNECT);
    

    /**
     * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
     * 
     * @throws IOException
     * @throws InterruptedException 
     */
    public void listen() throws IOException, InterruptedException 
        // 轮询访问selector
        while (true) 
            selector.select();
            // 获得selector中选中的项的迭代器
            Iterator ite = this.selector.selectedKeys().iterator();
            while (ite.hasNext()) 
                SelectionKey key = (SelectionKey) ite.next();
                // 删除已选的key,以防重复处理
                ite.remove();
                // 连接事件发生
                if (key.isConnectable()) 
                    SocketChannel channel = (SocketChannel) key.channel();
                    // 如果正在连接,则完成连接
                    if (channel.isConnectionPending()) 
                        channel.finishConnect();

                    
                    // 设置成非阻塞
                    channel.configureBlocking(false);

                    // 在这里可以给服务端发送信息
                    channel.write(ByteBuffer.wrap(new String("OK").getBytes()));
                    // 在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
                    channel.register(this.selector, SelectionKey.OP_READ);

                    // 获得了可读的事件
                 else if (key.isReadable()) 
                    Thread.sleep(2000);
                    read(key);
                

            

        
    

    /**
     * 处理读取服务端发来的信息 的事件
     * 
     * @param key
     * @throws IOException
     */
    public void read(SelectionKey key) throws IOException 
        SocketChannel channel = (SocketChannel) key.channel();
        // 创建读取的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        channel.read(buffer);
        byte[] data = buffer.array();
        String msg = new String(data).trim();
        System.out.println("客户端收到信息:" + msg);
        ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
        channel.write(outBuffer);// 将消息回送给服务端
    

    /**
     * 启动客户端测试
     * 
     * @throws IOException
     * @throws InterruptedException 
     */
    public static void main(String[] args) throws IOException, InterruptedException 
        NIOClient client = new NIOClient();
        client.initClient("localhost", 8000);
        client.listen();
    

上面完成了服务端和客户端的信息交互,服务端把接收到的数据重新传给客户端;客户端也是如此,这样就形成了两者消息的交替打印。运行结果如下:
服务端:

客户端:

以上是关于Java NIO学习二的主要内容,如果未能解决你的问题,请参考以下文章

Java学习笔记6.3.4 文件操作 - Path接口和Files工具类

杂谈 : 聊一聊NIO

java BIO/NIO/AIO 学习

Java NIO 学习--FileChannel

Java IO/NIO的基本概念学习

NIO流的学习