PC でネットワーク通信をするときは IIS を使って HTTP プロトコルを使うのが定番なのですが、途中に IIS みたいな Web サーバーが入ってしまうのが難点です。いや、ASP.NET MVC や Web API レベルで C/S 作っている分にはいいけど、昔からの TCP/IP を使わないと実現できない(実現しやすい)というパターンを想定して、HttpListener と TcpListener を改めて使ってみます。
HttpListener を使う
HTTPプロトコルを 8188 ポートで受けて、Win キーを押すというサンプルコードです。Win キーを押すのはおまけで、まあ、こんな感じで独自の HTTP サーバーが作れるという意味ですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | private void button1_Click( object sender, EventArgs e) { string url = "http://+:8188/" ; sv = new HttpListener(); sv.Prefixes.Add(url); this .listBox1.Items.Add( "start: " + url); task = new Task( () => { this .sv.Start(); while (loop) { var cont = this .sv.GetContext(); Debug.WriteLine( "url:" + cont.Request.Url); this .listBox1.Invoke( new AddListItem( () => { this .listBox1.Items.Add(cont.Request.Url); })); var cmd = cont.Request.Url.Query; cmd = cmd.Substring(1); var dic = System.Web.HttpUtility.ParseQueryString(cmd); var val = dic[ "key" ]; switch (val) { case "HttpKey" : this .Invoke( new Click(() => { SendKeys.Send( "^{ESC}" ); })); break ; } using ( var sw = new StreamWriter(cont.Response.OutputStream)) { sw.WriteLine( "OK" ); } cont.Response.Close(); } this .sv.Stop(); }); task.Start(); } |
動かすときは、管理者モード&ローカル環境で動作確認するのが手っ取り早いです。というのも、別のPCから送信する場合は、Firewall と netsh の設定が必要になって結構ややこしいのです。
が、結構ややこしいところは避けて通れないので、後で解説します。
クライアントは、ブラウザに http://localhost:8188/sendkey?key=HttpKey な感じで呼び出せば ok です。
SendKeys.Send を送るときは一旦 Invoke しないと呼び出せないようです。new Click はデリゲート型です。
TcpListener を使う
同じものを TCP/IP のリスナー TcpListener を使って実装するとこんな感じです。今度は 8189 ポートで待ちます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | private void button2_Click( object sender, EventArgs e) { this .tcp = new TcpListener(IPAddress.Any, 8189); this .listBox1.Items.Add( "start: " + "tcp" ); tcp.Start(); task = new Task(() => { while (loop) { var sock = tcp.AcceptSocket(); byte [] data = new byte [1024]; sock.Receive(data); var url = System.Text.Encoding.ASCII.GetString(data); this .listBox1.Invoke( new AddListItem( () => { this .listBox1.Items.Add(url); })); // 実際はもうちょっとパースしないと駄目 var cmd = url.Substring(url.LastIndexOf( "/" ) + 1); cmd = cmd.Substring(1); var dic = System.Web.HttpUtility.ParseQueryString(cmd); var val = dic[ "key" ]; switch (val) { case "WinKey" : //SendKeys.Send("^{ESC}"); break ; } var res = "OK" ; data = System.Text.Encoding.ASCII.GetBytes(res); sock.Send(data); sock.Close(); } }); task.Start(); } |
同じことができるならば、HttpListener でも TcpListener でもどちらでも良いような気がするのですが、実は違います。もちろん、TcpListener のほうでバイナリデータを渡せるという利点も捨てがたいのですが、いまとなっては HTTP プロトコルで XML 形式で渡したほうが手軽でしょう。まあ、ストリーミングなんかは TCP/IP にしないと駄目なんですが、Win キーを押させるぐらいの単発動作ならば、どちらでやっても構いません。
が、いざ、別の PC から送信しようとすると、Firewall という壁があってそれぞれの設定が異なります。
Surface RT から PC にキー送信する
こんな風に、Surface RT のストアアプリから、別の PC のストアアプリを操作しよう、というのを考えました。PC のほうのストアアプリにリスナーを追加することはできないので、デスクトップにリスナー用のアプリをかませます。PC 内でのデスクトップアプリとストアアプリとの連携をどうするのか?は懸念事項ですが(たぶん、ショートカットキーの送信で済ませる予定)、タブレットから扱えると便利かなと。
タブレット自体は、iPhone でも Android でも良いわけですが、ひとまず Surface RT のストアアプリを想定します。配布が楽ですからね。
ここで、問題になるのが真ん中にはさまっている Firewall です。無効にすれば簡単(笑)なのですが、まあ配布するとなるとそうはいかないし、この際だからきっちりと Firewall の設定も済ませよう、という魂胆です。
HttpListener の場合は、Firewall と netsh の両方の設定が必要
HttpListener の場合は、HTTP プロトコルが通るので netsh http を使って HTTP プロトコルのアドレスを開けると同時に、Firewall を開けないといけません。なんとなく両方とも開けないと駄目なことが分かるのですが、実は HttpListener は Http.sys を通しているので設定が曲者です。
HTTP.SYS は、HTTP プロトコルを扱う特別なカーネルプロセスで、これが HTTP プロトコルを特別扱いしているための、Firewall の設定が一筋縄ではいきません。
まずは、netsh http を使って http://+:8188/ の呼び出しが有効になるようにします。呼び出し元を絞ることができるのですが、ここでは everyone を指定してすべてのユーザーを有効にしておきます。
1 | netsh http add urlacl url=http: //+:8188/ user=everyone |
コントロールパネルでファイアウォールを開いて「詳細設定」をクリックします。
受信の規則で目的のルールをクリックするか新規に作成して、次のプログラムを「system」に書き換えます。
これを通常の *.exe ファイルにしておくと HttpListener を使ったときに Firewall が通りません。
C# HttpListener and Windows Firewall – Stack Overflow
http://stackoverflow.com/questions/17863294/c-sharp-httplistener-and-windows-firewall
理由は簡単で、HttpListener を使っているときは HTTP.SYS が一括して HTTP プロトコルを管理するので、HTTP.SYS のほうの Firewall を設定しないと駄目なんですよね。
で、せっかくプログラム単位でポートを絞っていたのが、HTTP.SYS 単位になってしまうので穴が大きくなってしまいます。仕方がないので「プログラムおよびポート」のほうで、許可するポートを絞っておきましょう。
ここでは、TCP の 8188 のポートだけを通すようにします。
netsh http のほうですが、本来は、http://+:8188/sendkey/ のように特定のアドレスを通すようにします。こうするほうがセキュリティが高いのですが、HttpListener を使うと 8188 ポートを占有してしまうので、他のアプリでこのポートで待つことは当然できなくなります。なので、あまりアドレスを書いても意味がないので、http://+:8188/ な感じでポートだけ指定しています。この firewall + netsh の書き方では、どのプログラムも通るようになるので、別のプログラムを使って先に同じポートを使うことができます。排他的ですが共有できるということですね。逆に言えば、他のプログラムに使われてしまうリスクがあるので、セキュリティが低いとも言えます。
TcpListener の場合は Firewall だけを設定する
TcpListener でリスナーを作るときは Firewall だけを設定します。
指定プログラムだけ通すので、安全といえば安全ですよね。HTTP.SYS の場合も、こんな風にできればいいのですが、ちょっと無理みたいです。
ただし、動かしてみたいときは、TcpListener の呼び出しにちょっと時間がかかります。たぶん、プログラムの起動時とファイアウォールの設定を再読み込みすることになるので、スタートだけ遅いって感じなんでしょう。
HttpListener と同じようにポートで絞ることもできるのですが、プログラム単位で絞ってあるので必要はないでしょう。このプログラム自体をすり替えれば別の動きもできるのですが、まあ、それはバージョンアップ時も再設定がいらないということです。
スクリプトを使って netsh http を設定する
windows – C# HttpListener without using netsh to register a URI – Stack Overflow
http://stackoverflow.com/questions/2583347/c-sharp-httplistener-without-using-netsh-to-register-a-uri/2782880
1 | netsh http add urlacl url=http: //+:8188/ user=everyone |
削除するときは
1 | netsh http delete urlacl url=http: //+:8188/ |
スクリプトを使って firewall を設定する
HTTP.SYS を使う時は program=system を設定する。
1 2 3 4 5 | netsh advfirewall firewall add rule name= "SendKeySv HTTP" dir= in action=allow netsh advfirewall firewall set rule name= "SendKeySv HTTP" new ^ program=system ^ profile= private ^ protocol=tcp localport=8188 |
TcpListener を使う場合は、program=<プログラム名>にしておく。
1 2 3 4 | netsh advfirewall firewall add rule name= "SendKeySv TCP" dir= in action=allow netsh advfirewall firewall set rule name= "SendKeySv TCP" new ^ program=<プログラム名> ^ profile= private |
コマンド自体は、netsh http add urlacl などと打つとヘルプが出るので調べられる。
設定自体を見るときは、show コマンドを使えばよい。ファイルに落とす dump コマンドがあるけども、内部実装はされていません。これはヘルプにも記述がある。
1 2 3 4 5 6 | > netsh http show urlacl 予約済み URL : http: //+:8089/ ユーザー: Everyone リッスン: Yes 委任: No SDDL: D:(A;;GX;;;WD) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | D:temp>netsh advfirewall firewall show rule name= "SendKeySv HTTP" 規則名: SendKeySv HTTP ---------------------------------------------------------------------- 有効: はい 方向: 入力 プロファイル: プライベート グループ: ローカル IP: 任意 リモート IP: 任意 プロトコル: TCP ローカル ポート: 8188 リモート ポート: 任意 エッジ トラバーサル: いいえ 操作: 許可 OK |
プログラムから runas を使って管理モードで動かす
インストーラを使うときは、管理者モードのプロンプトが必要になるので runas を使う。
1 2 3 4 5 6 7 8 9 10 11 12 | private void button4_Click( object sender, EventArgs e) { string args = "http add urlacl url=http://+:8188/ user=everyone" ; ProcessStartInfo psi = new ProcessStartInfo( "netsh" , args) { Verb = "runas" , CreateNoWindow = true , WindowStyle = ProcessWindowStyle.Normal, UseShellExecute = true }; Process.Start(psi).WaitForExit(); } |
あえて、ProcessWindowStyle.Normal を使っているけど Hidden にするとウィンドウは表示されない。UAC(ユーザアカウント制御)を有効にしておくと(大抵のユーザーは「有効」のまま)、管理者モードで動かしますか?のダイアログが表示されるので「はい」を押してもらう。
ひとまず、これで外部の PC から HttpListener に送信するプログラムが作れる&動作します。
アプリにちょっとしたWebサーバやTcp通信機能付けるのにHttpListnerやTcpListnerは重宝するので、Firewallの設定方法まで公開してくださりほんと助かりました