NIO(5) Selector

  • IO多路复用:Select模型
           服务器上所有Channel(包括ServerSocketChannel和SocketChannel)都需要向Selector注册,而该Selector则负责监视这些Socket的IO状态,当其中任意一个或多个Channel具有可用的IO操作时,该Selector的select()方法将会返回大于0的整数,该整数值就表示该Selector上有多少个Channel具有可用的IO操作,并提供了selectedKeys()方法来返回这些Channel对应的SelectionKey集合。正是通过Selector,使得服务器端只需要不断地调用Selector实例的select()方法即可知道当前所有Channel是否有需要处理的IO操作。

  • Selector(选择器)
           是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为读、写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。

  • SelectableChannel(可选择通道)
           这个抽象类提供了通道的可选择性所需要的公共方法。FileChannel对象不是可选择的,因为没继承SelectableChannel。所有SocketChannel都是可选择的,包括从管道(Pipe)对象获得的通道。SelectableChannel可以被注册到Selector对象上,一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。

  • SelectionKey(选择键)
           SelectionKey封装了Channel与Selector的注册关系。选择键对象被SelectableChannel.register返回并提供一个表示这种注册关系的标记。通道在被注册到一个选择器上之前,必须先设置为非阻塞模式。
            调用可选择通道的register()方法会将他注册到一个选择器上。若试图注册一个处于阻塞状态下的通道,register()将抛出未检查的IllegalBlockingModeException异常。此外通道一旦被注册,就不能回到阻塞状态。
            键的interest(感兴趣的操作)集合和ready(已经准备好的操作)集合是和特定的通道相关的。每个通道的实现,将定义它自己的选择键类。在register()方法中可以构造它并将它传递给所提供的选择器对象。
            使用非阻塞I/O编写服务器处理程序,大致步骤如下:
                  1、向Selector对象注册感兴趣的事件。
                  2、从selector中获取感兴趣的事件。
                  3、根据不同的事件进行相应的处理。

  • 多人聊天示例

package com.zz.selectorChannel;

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

/**
 * Select模型服务器
 */
public class NIOServer {
    // 通道管理器(选择器:检测哪些通道已经做好了read、write准备)
    private Selector selector;

    private ServerSocketChannel server = null;

    // 定义实现编码、解码的字符集对象
    private Charset charset = Charset.forName("UTF-8");

    /**
     * 获得通道选择器selector、ServerSocket通道,并对该通道做一些初始化的工作
     * @param port 绑定的端口号
     * @throws IOException IOException
     */
    public void initServer(int port) throws IOException {
        // 1、获得一个通道管理器(选择器)
        this.selector = Selector.open();
        // 2、获得一个ServerSocket通道
        this.server = ServerSocketChannel.open();
        // 3、将该通道对应的ServerSocket绑定到port端口
        this.server.socket().bind(new InetSocketAddress(port));
        // 4、设置ServerSocket通道为非阻塞
        this.server.configureBlocking(false);
        // 5、将channel注册到selector上,当有客户端连接时触发
        this.server.register(selector, SelectionKey.OP_ACCEPT);
    }

    /**
     * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
     * 
     * @throws IOException IOException
     */
    public void listen() throws IOException {
        System.out.println("server start...");
        // 轮询selector
        while (true) {
            /** 获取(是否有感兴趣事件)int值个的读写准备已经就绪的通道
             * (若某个时刻没有任Channel准备就绪,则会阻塞在这里,直到有任何一个通道准备就绪了才会往下执行)
             */
            int keys = this.selector.select();
            /**
             * 访问"已选择键集(selected key set)"中的就绪通道
             *   当Selector注册Channel时,Channel.register()方法会返回一个SelectionKey 对象。
             *   这个对象代表了注册到该Selector的通道。可以通过SelectionKey的selectedKeySet()方法访问这些对象
             */
            if (0 < keys) {
                Iterator<SelectionKey> it = this.selector.selectedKeys()
                        .iterator();
                // 循环遍历已选择键集中的每个键,并检测各个键所对应的通道的就绪事件
                while (it.hasNext()) {
                    SelectionKey selectKey = it.next();
                    /**
                     * Selector不会自己从已选择键集中移除SelectionKey实例。
                     * 必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中
                     */
                    it.remove();

                    this.handler(selectKey);
                }
            } else {
                System.out.println("Select finished, no any key...");
            }
        }
    }

    /**
     * 处理请求
     * 
     * @param key 当前注册到该Selector的通道
     * @throws IOException IOException
     */
    public void handler(SelectionKey key) throws IOException {
        // 如果SelectionKey对应的Channel包含客户端的连接请求
        if (key.isAcceptable()) {
            this.handlerAccept(key);
            // 获得了可读的事件
        } else if (key.isReadable()) {
            this.handelerRead(key);
        }
    }

    /**
     * 处理连接请求
     * 
     * @param key 当前注册到该Selector的通道
     * @throws IOException
     */
    public void handlerAccept(SelectionKey key) throws IOException {
        // 获取服务器通道
        // ServerSocketChannel serverChannel = (ServerSocketChannel)
        // key.channel();
        // 调用accept方法接受连接,产生服务器端的SocketChannel
        // SocketChannel channel = serverChannel.accept();

        // 以上两行代码可替换为:使用服务端的ServerSocketChannel直接获取
        SocketChannel socketChannel = this.server.accept();
        // 设置成非阻塞
        socketChannel.configureBlocking(false);
        // 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
        socketChannel.register(this.selector, SelectionKey.OP_READ);
        // 将SelectionKey对应的Channel设置成准备接受其他请求
        key.interestOps(SelectionKey.OP_ACCEPT);
    }

    /**
     * 处理读的事件
     * 
     * @param key 当前注册到该Selector的通道
     * @throws IOException
     */
    public void handelerRead(SelectionKey skey) throws IOException {
        // 获取该SelectionKey对应的Channel,该Channel中有可读的数据
        SocketChannel socketChannel = (SocketChannel) skey.channel();
        // 创建读取的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        String content = "";
        // 开始读取数据
        try {
            while (socketChannel.read(buf) > 0) {
                buf.flip();
                content += charset.decode(buf);
            }
            // 打印从该sk对应的Channel里读取到的数据
            System.out.println("读取的数据:" + content);
            // 将sk对应的Channel设置成准备下一次读取
            skey.interestOps(SelectionKey.OP_READ);
        }
        // 如果捕捉到该sk对应的Channel出现了异常,即表明该Channel
        // 对应的Client出现了问题,所以从Selector中取消skey的注册
        catch (IOException ex) {
            // 从Selector中删除指定的SelectionKey
            skey.cancel();
            if (skey.channel() != null) {
                skey.channel().close();
            }
        }

        // 如果content的长度大于0,即聊天信息不为空
        if (content.length() > 0) {
            // 遍历该selector里注册的所有SelectionKey
            for (SelectionKey key : selector.keys()) {
                // 获取该key对应的Channel
                Channel targetChannel = key.channel();
                // 如果该channel是SocketChannel对象
                if (targetChannel instanceof SocketChannel) {
                    // 将读到的内容写入该Channel中
                    SocketChannel dest = (SocketChannel) targetChannel;
                    dest.write(charset.encode(content));
                }
            }
        }
    }

    /**
     * 启动服务端测试
     * 
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        NIOServer server = new NIOServer();
        server.initServer(9999);
        server.listen();
    }
}
package com.zz.selectorChannel;

import java.io.IOException;
import java.net.InetAddress;
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.nio.charset.Charset;
import java.util.Scanner;
/**
 * Select模型客服端
 */
public class NioClient2 {
    // 定义检测SocketChannel的Selector对象
    private Selector selector = null;
    // 定义处理编码和解码的字符集
    private Charset charset = Charset.forName("UTF-8");
    // 客户端SocketChannel
    private SocketChannel sc = null;

    private String addr;
    private int port;

    public NioClient2(String addr, int port) {
        super();
        this.addr = addr;
        this.port = port;
    }

    public void init() throws IOException {
        this.selector = Selector.open();
        InetSocketAddress isa = new InetSocketAddress(addr, port);
        // 调用open静态方法创建连接到指定主机的SocketChannel
        this.sc = SocketChannel.open(isa);
        // 设置该sc以非阻塞方式工作
        this.sc.configureBlocking(false);
        // 将SocketChannel对象注册到指定Selector
        this.sc.register(this.selector, SelectionKey.OP_READ);
        // 启动读取服务器端数据的线程
        new ClientThread().start();
        // 创建键盘输入流
        Scanner scan = new Scanner(System.in);
        while (scan.hasNextLine()) {
            // 读取键盘输入
            String line = scan.nextLine();
            // 将键盘输入的内容输出到SocketChannel中
            this.sc.write(this.charset
                    .encode(InetAddress.getLocalHost() + ": " + line));
        }
    }

    // 定义读取服务器数据的线程
    private class ClientThread extends Thread {
        public void run() {
            try {
                while (selector.select() > 0) {
                    // 遍历每个有可用IO操作Channel对应的SelectionKey
                    for (SelectionKey sk : selector.selectedKeys()) {
                        // 删除正在处理的SelectionKey
                        selector.selectedKeys().remove(sk);
                        // 如果该SelectionKey对应的Channel中有可读的数据
                        if (sk.isReadable()) {
                            // 使用NIO读取Channel中的数据
                            SocketChannel sc = (SocketChannel) sk.channel();
                            ByteBuffer buff = ByteBuffer.allocate(1024);
                            String content = "";
                            while (sc.read(buff) > 0) {
                                sc.read(buff);
                                buff.flip();
                                content += charset.decode(buff);
                            }
                            // 打印输出读取的内容
                            System.out.println("聊天信息:" + content);
                            // 为下一次读取作准备
                            sk.interestOps(SelectionKey.OP_READ);
                        }
                    }
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        new NioClient2("127.0.0.1", 9999).init();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值