介绍 Java NIO Selector

介绍 Java NIO Selector

文本我们探讨java NIO引入Selector组件。

selector提供了监控多个NIO channel机制,并识别何时有一个或多个channel可用来传输数据。通过这种方式,单个线程可以用于管理多个channel,从而管理多个网络连接。

为什么使用Selector?

使用selector,让一个线程可以管理多个channel,而不是多个线程。多个线程的上下文切换对操作系统来说是昂贵的,另外每个线程也会占用内存。

因此,使用线程越少就越好。然而重要的是,现代操作系统和cpu在处理多任务上越来越好,因此多线程耗费时间会越来越少。

这里我们会演示如何通过selector使用单线程处理多个channel。还要注意,选择器不仅帮助您读取数据;它们还可以侦听传入的网络连接,并跨慢速通道写入数据。

开发说明

为了使用selector,无需任何特殊的依赖。我们需要所有类都在java.nio包中,仅需要导入相应的类。

然后,我们能使用selector注册多个channel。当任何channel上有I/O活动发生时,selector会通知我们。这就是我们如何从一个线程读取多个数据源的方法。

任何使用selector注册的channel必须是SelectableChannel类的子类。这些特殊类型channel能设置非阻塞模式。

创建Selector

可以通过Selector类的静态方法创建selector对象,其会使用系统缺省selector提供程序创建新的selector对象:

Selector selector = Selector.open();

注册SelectableChannel

为了让selector监控channel,必须使用selector注册channel。可以通过执行channel的register方法实现。但是channel被注册之前,必须被设置为非阻塞模式:

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

这意味着不能使用FileChannel注册selector,因为其不能被切换至非阻塞模式,如socket channel的方式。

第一个参数是我们之前创建的selector,第二个参数定义感兴趣活动集,即通过selector监听channel感兴趣的事件。可以监听四种不同类型的事件,每种通过SelectionKey类的一个常量表示:

Connect – 当客户端尝试连接到服务器,使用SelectionKey.OP_CONNECT表示。
Accept – 当服务器端接收客户端连接,使用SelectionKey.OP_ACCEPT表示。
Read – 当服务器端准备从channel读数据,使用SelectionKey.OP_READ表示。
Write – 当服务器端准备些数据至channel,使用SelectionKey.OP_WRITE表示。

返回对象SelectionKey表示使用selector注册的channel,下面我们会涉及。

SelectionKey对象

前面我们看到,当使用selector注册channel是,会获得SelectionKey对象,其包含的数据表示通道注册的数据。一些重要属性,我们必须要很好地理解,以便能够在channel上使用selector。下面依次说明这些属性。

兴趣集

兴趣集定义了我们希望selector在channel上监控的事件集。它是一个整数值;我们可以用下面的方法得到这个信息。

首先,我们有SelectionKey的interestOps方法返回信息集。然后我们在SelectionKey中有事件常量,我们之前看过。当我们使用and与两个值时,会获得boolean值,其表示事件是否被监控:

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;

就绪集

就绪集定义channel准备好处理的事件集。它也是一个整数值;我们可以用下面的方法得到该信息。
我们已经通过SelectionKey的readyOps方法获得就绪集。与前面一样,把该值使用AND与事件常量,会获得boolean值表示channel是否准备就绪。另外一种简化方法,直接使用SelectionKey的方法实现相同目的:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWriteable();

获得Channel

通过SelectionKey对象访问channel很简单,直接调用channel()方法:

Channel channel = key.channel();

获取Selector

和获得channel一样,通过SelectionKey对象获得Selector对象也很容易:

Selector selector = key.selector();

附件其他对象

我们可以给SelectionKey附加其他对象。有时我们可能希望给channel一个自定义ID或附加用于跟踪的任何类型的java对象。附加对象方法很简单。下面示例展示如何附加对象,并从SelectionKey获取:

key.attach(Object);
 
Object object = key.attachment();

另外,我们也可以在channel注册时附加对象。仅需要把对象作为register方法的第三个参数:

SelectionKey key = channel.register(
  selector, SelectionKey.OP_ACCEPT, object);

channel 键选择

到目前为止,我们已经看了如何创建Selector,把其注册至channel并检测其返回SelectionKey对象。这仅是一般过程,现在我们完成后续过程,选择前面提到的就续集,使用select方法:

int channels = selector.select();

该方法会阻塞,直到至少一个channel就绪进行操作。返回的integer表示channel就绪进行操作的键的数量。接下来,我们通常返回一组选择键进行处理:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

获得集合是SelectionKey对象,每个键代表已注册channel,其准备进行操作。之后,通常迭代该集合的每个键,获得channel并执行任何感兴趣的操作。

在channel的生命周期中,当键出现在不同事件的就续集中时,可以多次选择它。这就是为什么我们必须有一个连续的循环来捕获和处理channel发生的事件。

完整示例

为了巩固我们前面所学的知识,下面搭建一个完整的客户端-服务器程序示例。
为了更容易测试我们的代码,示例程序包括echo服务器和echo客户端。客户端连接至服务器,然后启动发送消息。服务器返回消息给相应发送消息的客户端。

当服务器接收到特定消息时,如end,作为结束通讯标志,关闭与客户端的连接。

服务器端

这里是EchoServer.java代码:

public class EchoServer {
 
    private static final String POISON_PILL = "POISON_PILL";
 
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.bind(new InetSocketAddress("localhost", 5454));
        serverSocket.configureBlocking(false);
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        ByteBuffer buffer = ByteBuffer.allocate(256);
 
        while (true) {
            selector.select();
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectedKeys.iterator();
            while (iter.hasNext()) {
 
                SelectionKey key = iter.next();
 
                if (key.isAcceptable()) {
                    register(selector, serverSocket);
                }
 
                if (key.isReadable()) {
                    answerWithEcho(buffer, key);
                }
                iter.remove();
            }
        }
    }
 
    private static void answerWithEcho(ByteBuffer buffer, SelectionKey key)
      throws IOException {
  
        SocketChannel client = (SocketChannel) key.channel();
        client.read(buffer);
        if (new String(buffer.array()).trim().equals(POISON_PILL)) {
            client.close();
            System.out.println("Not accepting client messages anymore");
        }
 
        buffer.flip();
        client.write(buffer);
        buffer.clear();
    }
 
    private static void register(Selector selector, ServerSocketChannel serverSocket)
      throws IOException {
  
        SocketChannel client = serverSocket.accept();
        client.configureBlocking(false);
        client.register(selector, SelectionKey.OP_READ);
    }
 
    public static Process start() throws IOException, InterruptedException {
        String javaHome = System.getProperty("java.home");
        String javaBin = javaHome + File.separator + "bin" + File.separator + "java";
        String classpath = System.getProperty("java.class.path");
        String className = EchoServer.class.getCanonicalName();
 
        ProcessBuilder builder = new ProcessBuilder(javaBin, "-cp", classpath, className);
 
        return builder.start();
    }
}

通过调用静态open方法创建Selector对象。然后通过静态open方法创建channel,ServerSocketChannel的实例。这是因为ServerSocketChannel是可选择的,适合面向流的侦听套接字。

然后绑定至一个端口,还记得之前说的,注册可选择的channel至Selector,必须先设置为非阻塞模式。所以接下来设置并注册channel至Selector。

在这个阶段,我们不需要channel的SelectionKey实例,所以我们先不用提及。

java nio使用面向缓存模式,而不是面向流模式。所以socket通信通常通过读写buffer来进行。因此,我们创建一个新的ByteBuffer,服务器端利用其进行读写。其初始化为256字节,当然可以是任意值,这依赖我们打算传输数据的大小。

最后,我们执行选择过程。我们准备就绪channel,返回其选择键,迭代所有的键并执行每个就绪channel的操作。

我们把这个过程放在无限循环中,因为无论是否有活动事件都需要保持服务器端处于运行状态。ServerSocketChannel仅能处理的操作是ACCEPT操作。当我们接受从客户端的连接时,则会获得SocketChannel对象,并在其上进行读写操作。我们设置其为非阻塞模式,并将其注册至Selector监控READ操作。

在随后的选择中,这个新channel将变为就绪读状态。我们检索它并将其内容读入缓冲区。作为echo服务器,我们必须将这些内容写回客户端。

当我们想要写入处于读取状态的缓冲区时,必须调用flip()方法。最后,通过flip方法设置缓冲区至写模式。

这里定义的start方法,是为了服务器端程序在单元测试中可以作为独立运行。

客户端程序

请看EchoClient.java代码:

public class EchoClient {
    private static SocketChannel client;
    private static ByteBuffer buffer;
    private static EchoClient instance;
 
    public static EchoClient start() {
        if (instance == null)
            instance = new EchoClient();
 
        return instance;
    }
 
    public static void stop() throws IOException {
        client.close();
        buffer = null;
    }
 
    private EchoClient() {
        try {
            client = SocketChannel.open(new InetSocketAddress("localhost", 5454));
            buffer = ByteBuffer.allocate(256);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    public String sendMessage(String msg) {
        buffer = ByteBuffer.wrap(msg.getBytes());
        String response = null;
        try {
            client.write(buffer);
            buffer.clear();
            client.read(buffer);
            response = new String(buffer.array()).trim();
            System.out.println("response=" + response);
            buffer.clear();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return response;
 
    }
}

客户端程序比服务器端简单。这里使用单例模式,通过静态start方法内实例化,其内部调用私有的构造函数。
在私有构造函数中,在服务器端相同的主机和端口上打开连接。

下面创建缓冲区进行读写。我们有sendMessage方法,它读取我们传递给它的任何字符串,并将其封装到字节缓冲区中,字节缓冲区通过通道传输到服务器。最后我们从客户端channel读取以获得服务器发送的消息。我们返回这个消息作为echo。

单元测试

我们定义测试类EchoTest,我们打算创建一个测试用例,启动服务器,非服务器发送消息,客户端接收并显示消息。最后停止服务器。

public class EchoTest {
 
    Process server;
    EchoClient client;
 
    @Before
    public void setup() throws IOException, InterruptedException {
        server = EchoServer.start();
        client = EchoClient.start();
    }
 
    @Test
    public void givenServerClient_whenServerEchosMessage_thenCorrect() {
        String resp1 = client.sendMessage("hello");
        String resp2 = client.sendMessage("world");
        assertEquals("hello", resp1);
        assertEquals("world", resp2);
    }
 
    @After
    public void teardown() throws IOException {
        server.destroy();
        EchoClient.stop();
    }
}

总结

本文我们讨论了java nio selector组件。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值