CLOVER🍀

That was when it all began.

NIOのSelectorの実装を切り替える(SelectorProviderを指定する)

NIOを使ってServerプログラムを書く時には、ServerSocketChannelとSelectorが登場することと思います。

        try (ServerSocketChannel serverChannel = ServerSocketChannel.open();
             Selector selector = Selector.open()) {

このSelectorの実装ですが、環境ごとに実装が異なり、それぞれ実行時に選択されます。

JDK 8で、Solaris用にSelectorが追加されていたみたいですね。

Java I/Oの拡張機能

デフォルト(JDK 8)だと、以下のようになっています。

  • Linux … sun.nio.ch.EPollSelectorProvider
  • Windows … sun.nio.ch.WindowsSelectorProvider
  • MacOS X … sun.nio.ch.KQueueSelectorProvider

これは、java.nio.channels.spi.SelectorProvider#providerのJavadocに記載のある通り、システムプロパティか
Service Providerの仕組みで切り替えることができます。

https://docs.oracle.com/javase/jp/8/docs/api/java/nio/channels/spi/SelectorProvider.html#provider--

注意〜)
…とはいうものの、ふつうはこれらを切り替えることはないと思います。epollやkqueueを使える環境下で、そこから
別のシステムコールに変更することはあんまり考えられないかな、と。

また、Selector#openは、Selector#providerから得られるSelectorProvider#openへのショートカットであるので、
SelectorProviderから直接Selectorをopenしても良い、という切り替え方もあります。

これは、Selector#openが返すSelectorの実装について、単純に切り替え方の手段の確認をしたくて書いている
エントリになります。
〜注意)

切り替え方は、ドキュメントの通り以下のようになります。

  • システムプロパティ「java.nio.channels.spi.SelectorProvider」で、SelectorProviderの実装クラス名を指定する
  • ファイル「META-INF/services/java.nio.channels.spi.SelectorProvider」を作成し、その中にSelectorProviderの実装クラス名を指定する

この選択の流れは、SelectorProvider#runを見ると、確認することができます。

http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/dddb1b026323/src/share/classes/java/nio/channels/spi/SelectorProvider.java#l170

デフォルトのSelectorProviderは、OSによって異なりますが、以下で選択されています。

http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/dddb1b026323/src/solaris/classes/sun/nio/ch/DefaultSelectorProvider.java#l62
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/dddb1b026323/src/windows/classes/sun/nio/ch/DefaultSelectorProvider.java#l45

他にどのようなSelectorProviderがあるかは、以下のソースツリーを見ることになります。SelectorProviderというワードが含まれているクラスを
探してみましょう。

http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/dddb1b026323/src/share/classes/sun/nio/ch
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/dddb1b026323/src/solaris/classes/sun/nio/ch
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/dddb1b026323/src/windows/classes/sun/nio/ch
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/dddb1b026323/src/macosx/classes/sun/nio/ch

まあ、solarisのツリー以外だと、選択肢がないのですが…。

サンプル

それでは、ここで実際にSelectorProviderを切り替えてみましょう。

環境は、次の通り。

## OS
$ lsb_release -idrc
Distributor ID:	Ubuntu
Description:	Ubuntu 18.04.1 LTS
Release:	18.04
Codename:	bionic

## JDK
$ java -version
openjdk version "1.8.0_171"
OpenJDK Runtime Environment (build 1.8.0_171-8u171-b11-0ubuntu0.18.04.1-b11)
OpenJDK 64-Bit Server VM (build 25.171-b11, mixed mode)

## Maven
$ mvn -version
Apache Maven 3.5.3 (3383c37e1f9e9b3bc3df5050c29c8aff9f295297; 2018-02-25T04:49:05+09:00)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 1.8.0_171, vendor: Oracle Corporation
Java home: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-29-generic", arch: "amd64", family: "unix"

お題としては、以下のようなEchoServerを用意。ポート5000でリッスンする、Echo Serverです。
src/main/java/org/littlewings/nio/EchoServer.java

package org.littlewings.nio;

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.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Iterator;

public class EchoServer {
    public static void main(String... args) throws IOException {
        EchoServer server = new EchoServer();
        server.start();
    }

    public void start() throws IOException {
        int port = 5000;

        try (ServerSocketChannel serverChannel = ServerSocketChannel.open();
             Selector selector = Selector.open()) {
            log("Selector Implementation = " + selector.getClass().getName());

            serverChannel.configureBlocking(false);
            serverChannel.bind(new InetSocketAddress(port));

            serverChannel.register(selector, serverChannel.validOps());

            log("startup echo server.");

            while (selector.keys().size() > 0) {
                selector.select();
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();

                    iterator.remove();

                    if (!key.isValid()) {
                        continue;
                    }

                    log("accept key");

                    if (key.isAcceptable()) {
                        log("accept acceptable");
                        handleAcceptable(key);
                    }

                    if (key.isReadable()) {
                        log("accept readable");
                        handleRequest(key);
                    }
                }
            }
        }
    }

    void handleAcceptable(SelectionKey key) throws IOException {
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        SocketChannel channel = serverChannel.accept();
        channel.configureBlocking(false);

        ByteBuffer buffer = ByteBuffer.allocate(8192);
        channel.register(key.selector(), SelectionKey.OP_READ, buffer);
    }

    void handleRequest(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();

        int size = channel.read(buffer);

        buffer.flip();

        log("received message = " + StandardCharsets.UTF_8.decode(buffer));

        if (size > 0) {
            while (buffer.position() > 0) {
                buffer.flip();
                channel.write(buffer);
                buffer.compact();
            }

            key.cancel();
            channel.close();
        }
    }

    void log(String message) {
        System.out.printf("[%s] %s%n", LocalDateTime.now(), message);
    }
}

起動時に、Selectorの実装を出力するようにしています。

pom.xmlは、これだけです。
pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.littlewings</groupId>
    <artifactId>nio-selector-provider</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
</project>

とりあえず、起動。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.nio.EchoServer

EPollSelectorImplが使われています。

[2018-07-29T21:52:59.782] Selector Implementation = sun.nio.ch.EPollSelectorImpl
[2018-07-29T21:52:59.784] startup echo server.

動作確認。

$ nc localhost 5000
こんにちは、世界
こんにちは、世界

EchoServerを1度停止して、今度はSelectorProviderの実装に「sun.nio.ch.PollSelectorProvider」を使うようにして
起動してみます。
※ですが、ふつうはこの切り替えはやらないと思います…

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.nio.EchoServer -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider

Selectorの実装が、PollSelectorImplに切り替わりました。

[2018-07-29T21:54:24.173] Selector Implementation = sun.nio.ch.PollSelectorImpl
[2018-07-29T21:54:24.177] startup echo server.

確認。

$ nc localhost 5000
こんにちは、世界
こんにちは、世界

こんなところで。