Groovyに続いて、KotlinPcap4Jを使ってパケットキャプチャしてみた。

KotlinからでもPcap4Jちゃんと動くよということを実証するのが主な目的。 また、今後JavaなアプリはKotlinで書こうかと思っているので、その予習も兼ねている。

Kotlinとは

KotlinはJVM言語、つまりJavaのバイトコードにコンパイルされてJavaの実行環境で動くプログラミング言語のひとつ。 IntelliJ IDEAで有名なJetBrains社によってOSSとして開発されている。

2011年に生まれた新しめな言語で、2016/2/17にv1がリリースされ、主にAndroidアプリの開発用として注目されている。

「実用的」であることを売りにしていて、つまり少ない学習コストで導入でき、既存のJavaコードやMavenなどのツールとの相互運用性を持つとされている。 IntelliJ IDEA、Android StudioEclipseといった主要なIDEのサポートもあり、開発環境は整っている。 v1以降の後方互換性の維持も表明されていて、長期サポートが必要な製品開発にも堪える。

さらに、厳格な静的型付けやNullable/Non-Null型などにより安全性を確保しつつ、型推論やラムダ式などで生産性を高めている。

Javaのバイトコードだけでなく、JavaScriptを生成するバックエンドを持っているのも大きな特徴。 ユースケースがよく分からないが。

GitHubにホストされているKotlinプロジェクトは、2016/4/15現在、全体の 0.1% (34933215549) しかない。 v1のリリースは結構注目を集めたので、この割合は今後増えていくと期待される。

Kotlinのインストール

チュートリアルに従えば、IDEやテキストエディタ+コマンドラインの環境を整えてHello Worldを書いて実行するところまで簡単にできる。 筆者はEclipse(Mars)とコマンドラインの環境をWindows 7上で作った。 Kotlinのバージョンは1.0.1-2。

コマンドラインについては、GitHub Releasesからアーカイブをダウンロードして、適当なところに展開してbinフォルダにパスを通すだけ。 前提となるJavaについては、環境変数JAVA_HOMEを設定するか、javaコマンドにパスを通せばいい模様。

因みにKotlinの書き方は、Kotlin Koansという例題集をオンラインのIDEで解きながらを学ぶことができる。

パケットキャプチャ with Pcap4J in Java

Pcap4Jでパケットキャプチャするコードを普通にJavaで書くと以下の様になる。 (Groovyの時のと一緒。)

import java.io.IOException;
import org.pcap4j.core.BpfProgram.BpfCompileMode;
import org.pcap4j.core.NotOpenException;
import org.pcap4j.core.PacketListener;
import org.pcap4j.core.PcapHandle;
import org.pcap4j.core.PcapNativeException;
import org.pcap4j.core.PcapNetworkInterface;
import org.pcap4j.core.PcapNetworkInterface.PromiscuousMode;
import org.pcap4j.packet.Packet;
import org.pcap4j.util.NifSelector;
public class Pcap4jLoop {
public static void main(String [] args) throws PcapNativeException, IOException, NotOpenException, InterruptedException {
String filter = null;
if (args.length != 0) {
filter = args[0];
}
PcapNetworkInterface nif = new NifSelector().selectNetworkInterface();
if (nif == null) {
System.exit(1);
}
final PcapHandle handle = nif.openLive(65536, PromiscuousMode.PROMISCUOUS, 10);
if (filter != null && filter.length() != 0) {
handle.setFilter(filter, BpfCompileMode.OPTIMIZE);
}
PacketListener listener = new PacketListener() {
@Override
public void gotPacket(Packet packet) {
printPacket(packet, handle);
}
};
handle.loop(5, listener);
}
private static void printPacket(Packet packet, PcapHandle ph) {
StringBuilder sb = new StringBuilder();
sb.append("A packet captured at ")
.append(ph.getTimestamp())
.append(":");
System.out.println(sb);
System.out.println(packet);
}
}
view raw Pcap4jLoop.java hosted with ❤ by GitHub

これを実行すると、パケットキャプチャするネットワークインターフェースを選択し、5つのパケットをキャプチャしてタイムスタンプと共にコンソールに表示する。

パケットキャプチャ with Pcap4J in Kotlin

上記処理をKotlinで書くと以下の様になる。

import org.pcap4j.core.BpfProgram.BpfCompileMode
import org.pcap4j.core.NotOpenException
import org.pcap4j.core.PacketListener
import org.pcap4j.core.PcapHandle
import org.pcap4j.core.PcapNativeException
import org.pcap4j.core.PcapNetworkInterface
import org.pcap4j.core.PcapNetworkInterface.PromiscuousMode
import org.pcap4j.core.PcapStat
import org.pcap4j.packet.Packet
import org.pcap4j.util.NifSelector
fun PcapHandle.printLoop(count: Int) {
loop(count, {packet: Packet ->
println("A packet captured at ${getTimestamp()}:")
println(packet)
})
}
fun main(args: Array<String>) {
val filter = if (args.size != 0) args[0] else null
val nif = NifSelector().selectNetworkInterface()
nif ?: System.exit(1)
val handle = nif.openLive(65536, PromiscuousMode.PROMISCUOUS, 10)
filter?.let {
if (filter.length != 0) {
handle.setFilter(filter, BpfCompileMode.OPTIMIZE)
}
}
handle.printLoop(count = 5)
}
view raw Pcap4jLoop.kt hosted with ❤ by GitHub

メインクラスはGroovy同様書かなくていいが、main関数は必要。

型推論があってとても楽。 ラムダ式、補間文字列(String interpolation)、名前付き引数といったモダンめな機能は普通に使える。 (名前付き引数はJavaで書いたメソッドをKotlinから呼ぶときは使えない。)

オープンクラスを実現する機能であるExtensionsPcapHandleに使ってみた。 なんだか便利そう。

Nullable/Non-Null型がすごい。言語仕様でNullPointerExceptionが発生しないように守ってくれる。 例えばfilterは宣言の時点では初期化文でnullが入る可能性があるのでNullableStringという型に推論されるが、filter?.letというNullチェックをするメソッドに渡したブロック内では自動でNon-NullStringにキャストされ、filter.lengthを安全に評価できるようになっている。 Nullチェックをしないでfilter.lengthと書くとコンパイルエラーになる。すごい。

けどJavaのコードから返ってくるオブジェクトは普通、プラットフォーム型というものになり、このNullセーフな仕組みが働かない。 これに対してはNull可能性アノテーションを使えば幸せになれるらしい。

さらに、上記コードには表れていないが、キャストも安全になっている模様。(cf. スマートキャストセーフキャスト)

Kotlinは基本コンパイラ言語なので、上記コードを実行するには以下ようなコマンドで一旦コンパイルする必要がある。

kotlinc -cp pcap4j-core.jar Pcap4jLoop.kt -include-runtime -d Pcap4jLoop.jar

このコマンドだとKotlinのランタイム入りjarファイルができる。 このjarを、Pcap4J 1.6.2、Slf4J 1.7.12、JNA 4.2.1を使って、以下のコマンドで実行できることを確認した。

java -cp pcap4j-core.jar;pcap4j-packetfactory-static.jar;jna.jar;slf4j-api.jar;Pcap4jLoop.jar Pcap4jLoopKt tcp

このコマンドで指定しているメインクラスPcap4jLoopKtは、上記コードでクラスの記述を省いた為にKotlinがソースファイル名(Pcap4jLoop.kt)を基に自動生成したもの。

コンパイル/実行方法は他にもある

スクリプトなKotlin

Kotlinプログラムはスクリプトとしても書けて、コンパイル無しで実行することができる。 この場合、main関数は消してその中身をトップレベルに書き、ファイルの拡張子を.ktsにする。

import org.pcap4j.core.BpfProgram.BpfCompileMode
import org.pcap4j.core.NotOpenException
import org.pcap4j.core.PacketListener
import org.pcap4j.core.PcapHandle
import org.pcap4j.core.PcapNativeException
import org.pcap4j.core.PcapNetworkInterface
import org.pcap4j.core.PcapNetworkInterface.PromiscuousMode
import org.pcap4j.core.PcapStat
import org.pcap4j.packet.Packet
import org.pcap4j.util.NifSelector
fun PcapHandle.printLoop(count: Int) {
loop(count, {packet: Packet ->
println("A packet captured at ${getTimestamp()}:")
println(packet)
})
}
val filter = if (args.size != 0) args[0] else null
val nif = NifSelector().selectNetworkInterface()
nif ?: System.exit(1)
val handle = nif.openLive(65536, PromiscuousMode.PROMISCUOUS, 10)
filter?.let {
if (filter.length != 0) {
handle.setFilter(filter, BpfCompileMode.OPTIMIZE)
}
}
handle.printLoop(count = 5)
view raw Pcap4jLoop.kts hosted with ❤ by GitHub

上記スクリプトは以下のコマンドで実行できた。

kotlinc -cp pcap4j-core.jar;jna.jar;pcap4j-packetfactory-static.jar;slf4j-api.jar -script Pcap4jLoop.kts tcp

EclipseのKotlinプラグインはこのスクリプト形式をまだサポートしていないようで残念。