☆前書き☆
この記事は
Advent Calendar 2011のC#の24日目だよ。
去年のAdvent Calendarとかみんな凄いの書いてるから、
わたしなんか書いて良いのかな〜って思ったりもしたんだけど、
相談したら書いて良いんじゃない?って言われたので書くことにしたの。
他の人に比べて内容も読みやすさもレベルを下げてお届け!
読みにくいって思ったら、この日の記事はなかったことにして次に期待すると良いとおもいまーす。
あ、C#のコアな話を期待している人、そんな内容はないからね!
と、前書きはこのくらいにしてすすむね。
この1ヶ月くらいで作ったWindows Phoneアプリあたりの話をだらだら書くよ。
☆作ったWindows Phoneアプリの紹介☆
C#の勉強ついでにいろいろアプリ作ってみてるんだけど、
Windows Phoneを手に入れたので、せっかくだから何かつくろーって。
で、Androidで作った「ラティ」ってアプリのWindows Phone版を作ることにしたの。
VNCとかRDPとかの、いわゆるリモートデスクトップって言うののクライアントアプリだよ。
なんか、最初に作るならHello Worldでしょ!って言うのが普通なのかもしれないけど、
見た目さみしいし、やっぱりきれいで動くのがいいじゃん?
Android版はVNCベースじゃなかったから、今回は、VNCにちゃんとつながるようにしよう!
というわけでWindows Phone版の「ラティ」はTightVNCのServerとかにつながるようになってるよ。
有料のカテゴリ?になってるけど、トライアル版でも機能差は無いので普通に無料で使えるよ。
☆RFBプロトコル編☆
VNCって言うのはRFBプロトコルで通信するので、それを実装したライブラリとかがないと、だよね。
ってことで探したんだけど……、使えそうなのがないっ(・x・
というわけでRFBプロトコルの仕様書(日本語)を読むところから始めることに。
リモートデスクトップのプロトコルって読み解くの大変なんじゃないのかなぁ。
な〜んて最初は思って読んでみたんだけど、これが意外に凄い簡単なの。
FTPのプロトコル(日本語)よりも簡単簡単。たぶん。
最初はなんか文字列の送受信するだけで、あとは、
位置+サイズ+画像のデータをひたすら受け取るだけだよ。ほら簡単。
いろいろ手抜き感はあるけど、実装したライブラリがあるので後でリンク張るね。
ここでは簡単に使い方を紹介するよ。
基本的なプロトコルの実装は、RFBStreamって言うのがあるから、それを使えばおっけ。
でもほら、接続から一連のパスワード確認とかは、まとめてやった方が楽じゃない?
というわけでRFBClientってクラスもあるよ。
でもでも、それでももっと簡単に使いたいって人のために、EmprssiaRFBClientって言うのも用意してみた〜。
今回はこれの紹介ってことで。
えーっと、ラティから抜粋したコードをブログ用に書き換えてるので動作保証はないでーす。
あ、もちろん、書き換えて無くても動作保証はないけど。
コーディングスタイルがあり得ない!って言うのはスルーするけど、
LINQとかRxとかAsyncはぜ〜んぜん使えてないのだ。
せっかくAsync CTP入れたのにね!(謎)
this.RFBClient = new EmpressiaRFBClient();
int[] encodings = new int[] { EncodingType.tight, EncodingType.ZRLE, EncodingType.zlib, EncodingType.zlibhex, EncodingType.CoRRE, EncodingType.RRE, EncodingType.Raw, EncodingType.Hextile, EncodingType.Cursor };
// 1.セットアップでいろいろ準備。
this.RFBClient.setup(
// 1−1.使う通信方式(エンコーディング)の設定。
encodings,
// 1−2.カーソルをクライアントで描画する。
(PixelFormat, x, y, width, height, pixelBytes, maskBytes) => {
}
// 1−3.EncodingType.CopyRectを使用する場合はもう一個引数増える。
);
this.RFBClient.ConnectFailed += (sender, e) => {
if(e.IsCanceled) {
return;
}
string message = null;
if(e.SecurityResult != null) {
if(e.SecurityResult.FailedReason != null) {
message = e.SecurityResult.FailedReason;
}
}
if(message == null) {
if(e.UnexpectedException != null) {
message = e.UnexpectedException.Message;
} else {
message = "なんか接続に失敗したみたい。ごめんなさい。理由はよくわかんない。";
}
}
this.RaiseDisconnected(message);
};
this.RFBClient.SendFailed += (ex) => {
this.RaiseDisconnected(ex.Message);
};
this.RFBClient.ReceiveFailed += (ex) => {
this.RaiseDisconnected(ex.Message);
};
this.RFBClient.FrameBufferInitialized += (PixelFormat, width, height) => {
// 2.画面情報の初期化。
AutoResetEvent Lock = new AutoResetEvent(false);
this.Dispatcher.BeginInvoke((EmpressiaRFBClient.FrameBufferInitializedHandler)delegate {
// 2−1.Windows PhoneだとARGB32が楽。違う場合は要求を送信するよ。
PixelFormat ARGB32 = new PixelFormat() {
BitsPerPixel = 32,
Depth = 24,
BigEndianFlag = 0,
TrueColourFlag = 1,
RedMax = 255,
GreenMax = 255,
BlueMax = 255,
RedShift = 16,
GreenShift = 8,
BlueShift = 0,
Padding1 = 0,
Padding2 = 0,
Padding3 = 0,
};
if(PixelFormat.Equals(ARGB32) == false) {
this.RFBClient.sendSetPixelFormat(ARGB32);
}
this.Image = new WriteableBitmap(this.RFBClient.FrameBufferWidth, this.RFBClient.FrameBufferHeight);
Lock.Set();
}, PixelFormat, width, height);
Lock.WaitOne();
};
this.RFBClient.MessageReceivingStarted += () => {
// 3.初回だけリクエストのタイミングを決める。2回目以降は勝手に呼ばれる。
this.RFBClient.sendFramebufferUpdateRequest(true, 0, 0, this.RFBClient.FrameBufferWidth, this.RFBClient.FrameBufferHeight);
};
this.RFBClient.ImageReceived += (s, rects) => {
lock(this.ImageEventLock) {
this.ImageLock.Reset();
this.Dispatcher.BeginInvoke((EmpressiaRFBClient.ImageReceivedHandler)delegate {
foreach(RFBRectangle rect in rects) {
// 4.透明なのを塗りつぶすよ。
for(int i = 3; i < rect.PixelBytes.Length; i += 4) {
rect.PixelBytes[i] = 0xFF;
}
for(int lineIndex = 0; lineIndex < rect.Height; ++lineIndex) {
Buffer.BlockCopy(rect.PixelBytes, lineIndex * rect.Stride, this.Image.Pixels, (rect.Y + lineIndex) * this.Image.PixelWidth * 4 + rect.X * 4, rect.Stride);
}
}
this.Image.Invalidate();
this.ImageLock.Set();
}, s, rects);
this.ImageLock.WaitOne();
}
};
this.RFBClient.connectAsync(Host, Port, Password);
重要そうなところは1〜4の番号を振ってコメント入れておいたっ。
1−2と1−3:
特殊な画像処理が必要なところ(クライアントの描画環境によるからRFBの範囲ではどうしようもない)。
2−1:
Windows PhoneのWriteableBitmapがARGB32(BGRA32?)だから楽するための処理ね。
わたしの作ったライブラリの一部がそれ前提なところが残ってる気もするし。
3:
コメント書いてて、これライブラリとしてダメじゃないカナーとか思った!
思ったけど今はこうなってるからいいよね(゚ー゚)(。_。)(゚-゚)(。_。)ウンウン
4:
RFBプロトコルは、透明度扱わないんだけど、
WriteableBitmapのARGB32だとアルファチャンネルってことになっちゃうから、
全部透過なしに上書きしてる。
あとは、xaml側と調整すれば画面が出るはず〜。
キー送信とか、マウスの操作は、
sendKeyEventとsendPointerEventっていうメソッドがあるから画面の操作と連携させればおっけ〜。
KeyEventは結構大変だけどねっ。マウス操作くらいならすぐすぐ。
是非試してみてね(  ̄▽ ̄)ノ
☆SSHプロトコル編☆
RFBプロトコルってパスワード部分をのぞけば暗号化されないから、
SSHでポートフォワードとかやりたいよね。
というか、Empressia Tunnelなんて作ってたから、
簡単にできるでしょ〜って思って、手を出してみたの。
まぁ、つまり大変でしたって話なんだけど……。
C#でSSHと言えば、SharpSSHとかが有名……だよね、
うん、わたしが知ってるくらいだから有名なの。
その、SharpSSH使おうと思ったんだけど、
これってJavaのJSchっていうライブラリの移植版みたいなんだよね。
元が結構古くて.NEtFramework1.1ベースみたい。
ってことで、ArrayListとかHashtableとかがWindows Phoneだと動かない。
Threadとかのメソッドも減って消えてるメソッドがあるからそのままじゃ使えないし。
BigIntegerもMono?のを使ってるみたいでこれがunsafeコードを持ってるんだよね。
結局、いろんなところ書き換えちゃった。
ほんとはそのまま動くようにするだけで良いんだろうけど、
元がJavaだからか、プロパティとかイベントとかがほとんど使われてなかったから、
めんどくさくて適当に書き換えちゃったところも。
RFBと同じでライブラリは最後にまとめて張るね。
まぁ、ポートフォワード付近しかタメしてなかったりするから、いろいろなところでエラーとか出るかも(笑)
あ、おまけでEmpressiaSSHなんてライブラリも作ったよ。
このライブラリを使うと、SECSH(ssh.com)形式の秘密鍵ファイルが使えるようになるの。
あと、ポートフォワードが簡単に使えるクラスEmpressiaSSHTunnelClientも用意してあるのでお試しアレ。
RFBみたいに使い方を紹介するよ。
もちろん動かなくても知らない!さすがわたし!無責任!
EmpressiaSSHTunnelClient SSHTunnelClient = new EmpressiaSSHTunnelClient();
userInfo.FingerprintConfirmationRequired += (string host, string type, string fingerprint) => {
return true;
};
userInfo.ChangedFingerprintConfirmationRequired += (string host, string type, string fingerprint) => {
return true;
};
SSHTunnelClient.Connected += delegate {
// 接続したよ。
};
SSHTunnelClient.Bound += delegate {
// ポートフォワードが開始されたよ。
};
SSHTunnelClient.ConnectionFailed += delegate {
// 接続に失敗したよ。
};
SSHTunnelClient.Disconnected += delegate {
// 切断されたよ。
};
SSHTunnelClient.Unbound += delegate {
// ポートフォワードが終わったよ。
};
EmpressiaUserInfo userInfo = new EmpressiaUserInfo("sshpassword");
SSHTunnelClient.connectAsync(new TransferProfile() {
TunnelHost = "example.com",
TunnelPort = 22,
TunnelUser = "sshuser",
TunnelPassword = "sshpassword",
LocalHost = "127.0.0.1",
LocalPort = 5900,
RemoteHost = "192.168.0.2",
RemotePort = 5900,
}, userInfo, null);
なんか、パスワード2回指定してるけど気にしない!
でもさ、結局、Windows PhoneってSocketのAcceptできないんだよね。
というわけでローカルポートにバインドできなくて今回はあきらめたっ_(__)ノ彡☆
あ、あと、SilverlightとWindows Phoneだと、SshShellのコンソールリダイレクトが使えないはず。
Console.OpenStandardInput()とConsole.OpenStandardOutput()をどうすればいいのかがわからなくて……。
☆基本ライブラリ編☆
SSHとかRFBをWindows Phoneで作る時に思ったんだけど、
Windows Phoneって結構標準のクラスライブラリが削られてて面倒なんだよね。
暗号系はBouncy Castleあたりがあれば良いんだけど、ネットワーク関連がどうしようもなくて……。
ってことで、NetworkStreamとか作ってみてるよ。
パフォーマンスは微妙だと思うけど、だいたい同じ感じで使えるはず……。
あと、リングバッファなByteQueueとか、
それをストリームで扱うEndlessStreamなんかもあるので良ければドーゾ。
EmpressiaLibrary.dllにまとめて入れてあるので興味があれば〜。
ライブラリはまたまた最後にまとめて張っておくね。
使い方は、別に特殊なのはないので省略ね。
……みんなはこういうのどうしてるんだろう……?(・_・?)ハテ
☆その他ライブラリ編☆
RFBやってると、ZlibなStreamを延々読み続けないといけないんだよね。
ただ1個のストリーム読むだけなら良いんだけど、
飛び飛びにあるピクセルデータ部分だけを連続したZlibなStreamとして扱わないとダメって感じ。
標準クラスライブラリのDeflateStreamを使ってる分には問題ないんだけど、
DotNetZipのDeflateStreamだと、これがうまく動かないっ。
というわけでこれも書き換えてみたよ。
まぁ、使う機会はほとんどないと思うけど、後で張っておくね!
Bouncy CastleもWindows Phoneで動くように書き換えたのでこれも張っておくよ。
☆ライブラリまとめ☆
ず〜っとライブラリの話書いてきたけど、下に関連してる図を載せておくね。
であとは、ライブラリ達でーす。
えっと、Silverlight版は、プロジェクトファイル用意するだけなんだけど、
わたしが使う予定がないので用意してなかったり!
動かないじゃんっていうのがあれば、再現できるサンプルとかあればがんばるよ!たぶん(・x・
☆余談☆
ところで、Vita用のSDKが来ないんだけど……。
って書こうと思ったら、昨日(23日)の15時頃に連絡があったよ。
やっと、開発環境周りの日本語訳ができたのかな〜。
というわけでPS Suite SDKダウンロードしてみた。
まだ何も作ってないけど、なんか作りにくそうだった……(・x・