🐔

Netty の Socket で SO_LINGER を設定する

に公開

はじめに

ちょっと Netty で SO_LINGER の設定をしたかったんだけどドキュメントが雑過ぎて最初良く分からなかったので[1]メモ。

Linux の SO_LINGER の設定方法

Linux では(Un*x はみんなそうなんじゃないかな?)setSockOpt(3p) で SO_LINGER の設定ができる。Linux の man だと setSockOpt には詳しく書いてなくて、socket(7) に SO_LINGER の設定が書いてあるので、見ると以下のような構造体で設定することになっている。

struct linger {
    int l_onoff;    /* linger active */
    int l_linger;   /* how many seconds to linger for */
};

l_onoff が有効無効の指定、l_linger が有効だった場合の待ち時間だ。l_onoff はオンオフなので 2 値だが、_Bool とか bool ではなく伝統的な int による表現を使っている[2]。まぁ C 言語使うヤツなら注釈など無くてもわかるだろう。

Netty におけるソケットオプションの設定方法

Netty では Bootstrap(クライアントの場合)、あるいは、ServerBootstrap(サーバの場合) を使って TCP 接続を開始するが、こいつらには option、あるいは、childOption(ServerBootstrap だけ)というメソッドがあって、コイツでソケットオプションを設定する。ちなみに、ServerBootstrap にメソッドが 2 つあるのは、リスニングソケットへの設定用とアクセプト後のソケットへの設定用があるから。見ての通り引数は ChannelOption<T> 型と T 型だ。

で、ChannelOption<T> にはちゃんと SO_LINGER がある。分かりやすくて大変よろしい。

が、型引数 T が Integer だ。つまり設定値は Integer だけってことだ。オンオフどこ行った。

で、ドキュメントを見ると null を指定するとオプションを削除、それ以外でオプションを設定と書かれている。SO_LINGER はデフォルトでは無効なので、有効にしたければ Integer を、無効のままにしたければ null を指定しろっていうことなんだろうか?

いや、でもわざわざ「オプションを削除」って書いてあるってことは、何かから削除するってことだよな?SO_LINGER を無効化することを普通「削除」とは呼ばないよな?これは一体どういうことよ?

「オプションを削除」の意味を調べる

と言うわけで、頑張って追っていくと以下のような流れになっていることが分かった。

  1. Bootstrap.option メソッド、あるいは ServerBootstrap.option メソッドが呼ばれると、AbstractBootstrap.options という Map<ChannelOption<?>, Object> 型のプライベートフィールドに設定値が保存される。値が null なら設定値が削除される。
  2. チャネルが初期化される際[3]、options に保存されている設定が以下の流れでソケットに設定される。
    1. AbstractBootstrap.setChannelOptions
    2. AbstractBootstrap.setChannelOption
    3. SocketChannelConfig.setOption
    4. SocketChannelConfig.setSoLinger
    5. Socket.setSoLinger

上記 2 の流れの中で、3, 4 にある SocketChannelConfig や 5 にある Socket というのは、実際のクラスはチャネルクラスによって異なる。実際のクラスは以下の通り。

チャネルクラス チャネル設定クラス ソケットクラス
EpollSocketChannel EpollSocketChannelConfig LinuxSocket[4]
IoUringSocketChannel IoUringSocketChannelConfig[5] LinuxSocket[6]
KQueueSocketChannel KQueueSocketChannelConfig BsdSocket[7]
NioSocketChannel NioSocketChannelConfig[8] java.net.Socket

ちなみに、ServerBootstrap.childOption の場合、ServerBootstrap.childOptions というMap<ChannelOption<?>, Object> 型のプライベートフィールドに保存されることを除いて、ほぼ同じ流れで設定される[9]。

ここで重要なのは、2 はチャネル初期化時までは単に Map に保存されてるだけなので、一旦設定した後でも、チャネル初期化前なら null を指定することによって設定をなかったことにできる、ということだ。なるほど、たしかに「オプションを削除」だ。

ここまででまぁ流れは分かったし、デフォルトで無効なんだから l_linger に指定する秒数を引数として指定すれば有効になるだろ JK、ということで SO_LINGER を設定する[10]という当初の目的は果たすことができた。

目的を果たすことはできたし null の意味も分かったんだが、せっかくここまで調べたのでもうちょっと先まで調べることにした。

Socket の setSoLinger メソッド

上に述べた通り、最終的には Socket.setSoLinger メソッドが呼ばれていることが分かったので、その先を調べる。

Netty の setSoLinger メソッド

まずは Netty の io.netty.channel.unix.Socket.setSoLinger メソッドから。シグネチャは以下の通り。

public final void setSoLinger​(int soLinger) throws java.io.IOException

引数が int だ。Integer ですらない。上で調べた結果から予想はしていたが、やはり null の場合は呼び出されないってことだ。上で調べた内容とも整合性がとれている。

で、さらに先を追うとネイティブメソッドで、ここ にあった。

static void netty_unix_socket_setSoLinger(JNIEnv* env, jclass clazz, jint fd, jint optval) {
    struct linger solinger;
    if (optval < 0) {
        solinger.l_onoff = 0;
        solinger.l_linger = 0;
    } else {
        solinger.l_onoff = 1;
        solinger.l_linger = optval;
    }
    netty_unix_socket_setOption(env, fd, SOL_SOCKET, SO_LINGER, &solinger, sizeof(solinger));
}

要は値が負なら無効、0 以上なら有効ってことだ。あ~、言われてみりゃあ、そりゃそうか…orz

つまり、null じゃなくて -1 とかを指定すれば、たとえチャネル初期化時に SO_LINGER を有効にしていたとしても、後から(たとえばクローズする直前とかに)SO_LINGER を無効化できるってことだ。なるほど。

標準ライブラリの setSoLinger メソッド

一方、由緒正しい標準ライブラリの Socket#setSoLinger のシグネチャは以下の通り。

public void setSoLinger(boolean on, int linger) throws SocketException

こちらは非常に素直で、struct linger をそのまま引数にしたような感じだ。

で、ここで呼び出していた。

    @Override
    public SocketChannelConfig setSoLinger(int soLinger) {
        try {
            if (soLinger < 0) {
                javaSocket.setSoLinger(false, 0);
            } else {
                javaSocket.setSoLinger(true, soLinger);
            }
        } catch (SocketException e) {
            throw new ChannelException(e);
        }
        return this;
    }

あ~、こっち見ればもっとすぐわかったじゃん。普通に引数が負の場合は無効化してるじゃん。

まぁ、整合性も取れてるし、さっきのが間違った結論ということもなさそうだ。

先に標準ライブラリの getSoLinger 見ときゃあよかった

とろこで、標準ライブラリでは引数 2 つだけど値の取得はどうなってるんだろうと、ふと気になって Socket#getSoLinger の Javadoc を見てみた。

public int getSoLinger() throws SocketException

見ての通り int しか返ってこない。で、ちょっと読んだら書いてあった。

Returns setting for SO_LINGER.戻り値 -1は、このオプションが無効になっていることを意味します。 設定はソケットを閉じる場合にだけ影響します。

微妙に英語なままなのは置いておいて、-1 なら無効って書いてあった。これ見てたらきっと気づいただろうな…

ちなみに、Netty の Socket#getSoLinger もシグネチャはほぼ一緒(例外だけ違う)だが、例によって何も書いてなかった。

public final int getSoLinger() throws java.io.IOException

たのむ…

ちなみに Reactor Netty でも同じだよ

実はそもそも Reactor Netty で設定したかったんだが、調べていったら Netty 本体にたどり着いただけ。

いずれも引数は ChannelOption<T> 型、つまり Netty を直接使った場合と一緒だ。

おわりに

思わずむきになって調べてしまった。負の数値を無効の意味で使うのは API としてはあまり筋が良くないとは思うんだが、伝統的な手法(?)でもあるから仕方ないかな。
オレ的には、Netty のソケットオプション設定の流れが分かってまぁ良かったかなと思う[11]。

それでは、皆さんよい Netty ライフを。

脚注
  1. 雑なのはオレの頭かもしれない… ↩︎

  2. 導入時期から考えれば当たり前ではある。 ↩︎

  3. Bootstrap なら connect、ServerBootstrap なら bind。 ↩︎

  4. メソッドの実体は io.netty.channel.unix.Socket から継承している。 ↩︎

  5. パッケージプライベート(デフォルトアクセス)なため、Javadoc は公開されていない。 ↩︎

  6. 凶悪なことに、Epoll 用の LinuxSocket と同名だがパッケージが異なる別クラスだ。パッケージプライベート(デフォルトアクセス)なため、Javadoc は公開されていないが、メソッドの実体は io.netty.channel.unix.Socket から継承している。 ↩︎

  7. パッケージプライベート(デフォルトアクセス)なため、Javadoc は公開されていないが、メソッドの実体は io.netty.channel.unix.Socket から継承している。 ↩︎

  8. NioSocketChannel にネストしたプライベートなクラスなため、Javadoc は公開されていないが、setSoLinger メソッドの実体は DefaultSocketChannelConfig から継承している。 ↩︎

  9. もちろん、チャネルが初期化されるのは accept の時だ。 ↩︎

  10. より正確に言うと、SO_LINGER を有効にしたうえで待ち時間を 0 にして、切断時に即 RST 投げて欲しかった ↩︎

  11. 自己弁護。 ↩︎

Discussion