NIOを使ってServerプログラムを書く時には、ServerSocketChannelとSelectorが登場することと思います。
try (ServerSocketChannel serverChannel = ServerSocketChannel.open();
Selector selector = Selector.open()) {
このSelectorの実装ですが、環境ごとに実装が異なり、それぞれ実行時に選択されます。
JDK 8で、Solaris用にSelectorが追加されていたみたいですね。
デフォルト(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を見ると、確認することができます。
デフォルトの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の実装を出力するようにしています。
<?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
こんにちは、世界
こんにちは、世界
こんなところで。