このアーカイブは同期化されません。 mixi の日記が更新されても、このアーカイブには反映されません。
設計だけがずっと続くとつまらないので、
軽いテストを含めながらやろう。
RFB の通信はメッセージの交換で行われる。
メッセージは、基本型で構成された可変長の構造体である。
RFB の通信で最初に行われるのは、ハンドシェイクである。
ハンドシェイクとは、クライアントとサーバの間で、
RFB のバージョンや認証、解像度などの交渉のことを表す。
ハンドシェイクの最初は、プロトコルバージョンの交渉だ。
RFB は何度か改定されており、いくつものバージョンがある。
そのため、最初に通信に使うバージョンを決めるのである。
この時に送受信されるメッセージは、
ProtocolVersion というメッセージである。
これは 12 バイト固定長の ASCII 文字列となっており、
プロトコルのバージョンを示す以下のような内容だ。
HEX: 52 46 42 20 30 30 33 2e 30 30 38 0a
ASCII: "RFB 003.008\n"
先頭の 4 バイトが "RFB " というマジックナンバー、
続く 3 バイトが 0 先導 10 進表記のメジャーバージョン、
続く 1 バイトはバージョンの区切り文字 '.'、
続く 3 バイトが 0 先導 10 進表記のマイナーバージョン、
そして最後に行送り文字が追加される。
ProtocolVersion は特殊なメッセージであり、
RFB の基本型でなく可読の文字列で表現している。
そのため、telnet 等で誤って接続しても検出できる。
バージョンの交渉は、最初にサーバがクライアント対して、
サーバの対応している最大のバージョン値を送信する。
RFB では、サーバがあるバージョンを返したとすると、
それ以下のバージョン全てに対応できることを意味している。
クライアントは、受信したサーバのバージョン以下で、
利用可能なバージョンを任意に選択してサーバに送り返す。
この往復によって、通信に使う RFB バージョンが決定する。
では、試してみよう。
まずは、RFB の主処理を行うクラスを作る。
========== RFBSession.java ==========
package jp.loafer.rfbdemo;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jp.loafer.rfb.io.RFBInputStream;
import jp.loafer.rfb.io.RFBOutputStream;
/**
* RFB プロトコルの処理。
* @author kes
*/
public class RFBSession {
/**
* {@link RFBSession} のインスタンスを作成。
*/
public RFBSession() {
// 処理不要
}
/**
* RFB プロトコルの開始点。
* @param in 汎用入力ストリーム。
* @param out 汎用出力ストリーム。
* @throws IOException 入出力エラー。
*/
public void execute(InputStream in, OutputStream out)
throws IOException {
execute(new RFBInputStream(in), new RFBOutputStream(out));
}
/**
* RFB プロトコルの開始点。
* @param in RFB 入力ストリーム。
* @param out RFB 出力ストリーム。
* @throws IOException 入出力エラー。
*/
public void execute(RFBInputStream in, RFBOutputStream out)
throws IOException {
// ProtocolVersion ハンドシェイク
// サーバのバージョンを送信
out.write("RFB 004.099\n".getBytes("us-ascii"));
out.flush();
// クライアントのバージョンを確認
byte buffer[] = new byte[12];
if (in.read(buffer) != 12) throw new IOException("RFB Protocol violation.");
String message = new String(buffer, "us-ascii");
Pattern p = Pattern.compile("^RFB (\\d{3})\\.(\\d{3})\n$");
Matcher m = p.matcher(message);
if (!m.matches()) throw new IOException("RFB Protocol violation.");
System.err.printf("Client version: %d.%d\n",
Integer.valueOf(m.group(1)), Integer.valueOf(m.group(2)));
}
}
========== end of RFBSession.java ==========
まずは RFB の入出力ストリームを作成している。
まあ、この時点では役に立たないのだが今後のためだ。
まずは、サーバのバージョンを適当に返してみる。
現在の RFB 仕様は、3.3, 3.7, 3.8 しか定義していないが、
ここでは 4.99 という未来のバージョンにしてみた。
RFB に準拠したクライアントならば、
これを無効とせずに、低いバージョンを返すはずだ。
データを送信後は、OutputStream#flush を忘れないこと。
下位層がバッファしている可能性もあるため、
ここでデータを確実に送り出しておく。
サーバがバージョンを送信後はクライアントが返事を返す。
12 バイト読み込んで us-ascii 文字列に戻した後、
正規表現でマッチングして結果を出力しておく。
処理中に失敗した場合はとりあえず IOException とする。
次に、RFBSession を VNCSession から呼び出し、
サーバが動作するように書き換える。
========== VNCSession#run ==========
/**
* @see java.lang.Runnable#run()
*/
public void run() {
try {
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
new RFBSession().execute(in, out);
} catch (Exception e) {
e.printStackTrace(System.err);
} finally {
try {
socket.close();
} catch (IOException e) {
// 例外無視
}
}
}
========== end of VNCSession#run ==========
さて、サーバをデバッグ実行し、
RealVNC を使って localhost に接続してみよう。
例によってクライアント側では予期せぬ切断であるため、
「The connection closed unexpectedly」のエラーとなるが、
デバッグ中の Eclipse のコンソールには、
「Client version: 3.8」等と表示されるはずだ。
これはバージョンの交渉が成功した事を意味する。
次に、サーバのバージョンを、2.99 等の低い値にしてみる。
out.write("RFB 002.099\n".getBytes("us-ascii"));
ではデバッグ実行して接続してみよう。
今度は、クライアントが理解できないバージョンなので、
切断されてサーバ側が IOException を受取った。
また、RealVNC 側は以下のようなメッセージを表示した。
「Server gave unsupported RFB protocol version 2.99」
よし、ちゃんと動いているようである。