私たちは、自分自身をスマートフォン用のリモートデスクトップクライアントにします。 パート1:サーバー側

いつも携帯電話にポータブルリモートデスクトップが必要だったので、たとえば、誰かがICQをノックしてバルコニーでタバコを吸ったときに、バルコニーを離れずに電話に誰がいるかを確認することができました。 さて、または、例えば、入浴して、トラックを切り替えます。 はい、私はあらゆる種類のVNCクライアントがすでに作成されていることを知っていますが、そのようなプログラムを自分で作成することにしました。



記事の最初の部分では、サーバーとクライアントの両方が通常のデスクトップコンピューターで動作する単純なリモートデスクトップアプリケーションの作成に限定します。 2番目と3番目の部分では、画像圧縮と電話自体のプログラミングを検討します。



このような究極の機能を想像してみてください:dektop(サーバー)では、プログラムの常駐がハングし、外部から来たUDPパケットによって、イメージフラグメントを返信アドレスに送信し始めます。 電話(クライアント)に、送​​信されたフラグメントが表示されます。 ユーザーは、表示ウィンドウを移動したり、その内部をクリックしたりできます。 シフトとクリックに関する情報は、UDPを介して同じ方法でサーバーに送信されます。



Javaスクリプトであるかのように、C#で記述したことを事前に謝罪します。まず、短いリストでやりたい記事で、次に、プログラムが本当にシンプルで、複雑なデータ構造を作成する理由がまったくないなぜ、そして第三に、C#はもはや純粋なOOP言語ではありません。



プログラムの複雑さが小さいため、私が知っている最も単純な「膝までの深さ」の開発プロセスを選択しました。



デスクトップの断片を単純に表示する最も単純なプログラムから始めましょう。

using System; using System.Collections.Generic; using System.Windows.Forms; using System.Drawing; namespace rd2 { class Program { static void Main(string[] args) { Form f = new Form(); var timer = new System.Windows.Forms.Timer() { Interval = 40 }; timer.Tick += (s, e) => { Graphics g = f.CreateGraphics(); g.CopyFromScreen(0, 0, 0, 0, f.Size); g.Dispose(); }; timer.Start(); Application.Run(f); } } }
      
      





ここではすべてが単純です。画像をキャプチャしてウィンドウにコピーするイベントに応じて、ウィンドウを作成し、タイマーを作成して実行します。 すべてが機能することを確認します。



すべてが機能します



次に、ウィンドウ内にデスクトップのドラッグアンドドロップを追加します。 フォームを作成した直後に、次を挿入します。

 Point window_topleft = new Point(); Size mouse_prev_loc = new Size(); bool mouse_lbdown = false; f.MouseDown += (s,e) => { mouse_lbdown = true; }; f.MouseUp += (s, e) => { mouse_lbdown = false; }; f.MouseMove += (s, e) => { if (mouse_lbdown) window_topleft += mouse_prev_loc - (Size)(e.Location); mouse_prev_loc = (Size)e.Location; };
      
      





変数window_topleftは、ウィンドウに表示される領域の左上隅の座標です。 CopyFromScreenの修正:

 g.CopyFromScreen(window_topleft.X, window_topleft.Y, 0, 0, f.Size);
      
      





いいね! ドラッグされています。



次に、マウスの左ボタンのクリック処理を追加して、ウィンドウ内のクリックがこのウィンドウに表示されるものをクリックするように変換するようにします。 ドラッグとクリックを区別するために、マウスボタンがクリックされた座標を記憶し、押してもマウスがあまり離れていない場合は、ドラッグの代わりにマウスクリックを生成します。 このように:

 Point mouse_down_loc = new Point(); f.MouseDown += (s, e) => { mouse_lbdown = true; mouse_down_loc = e.Location; }; f.MouseUp += (s, e) => { mouse_lbdown = false; if( Math.Abs(e.Location.X - mouse_down_loc.X) <1 && Math.Abs(e.Location.Y - mouse_down_loc.Y) <1) { int click_to_x = (window_topleft.X + mouse_down_loc.X) * 65536 / Screen.PrimaryScreen.Bounds.Width; int click_to_y = (window_topleft.Y + mouse_down_loc.Y) * 65536 / Screen.PrimaryScreen.Bounds.Height; mouse_event((uint)(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE), (uint)click_to_x, (uint)click_to_y, 0, 0); mouse_event((uint)(MOUSEEVENTF_LEFTUP | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE), (uint)click_to_x, (uint)click_to_y, 0, 0); } };
      
      





マウスクリックは、WinAPI mouse_event関数への2つの連続した呼び出しから取得されます。 最初の呼び出しはボタンを押す(MOUSEEVENTF_LEFTDOWN)、2番目の呼び出しはリリース(MOUSEEVENTF_LEFTUP)です。 ボタンのクリックとともに、マウスの動き(MOUSEEVENTF_MOVE)を、絶対値(MOUSEEVENTF_ABSOLUTE)で示される目的の座標に移動します。 ゼロの絶対マウス座標は、プライマリ画面(PrimaryScreen)の左上隅にあります。 ポイント(65535、65535)は、同じ画面の右下隅にあります。 他のすべての画面は、システム内にある場合、この正方形に隣接しています。



もちろん、mouse_eventを自分でエクスポートする必要があります。 この宣言は、クラス宣言にあります。

 [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, uint dwExtraInfo); private const int MOUSEEVENTF_MOVE = 0x01; private const int MOUSEEVENTF_LEFTDOWN = 0x02; private const int MOUSEEVENTF_LEFTUP = 0x04; private const int MOUSEEVENTF_ABSOLUTE = 0x8000;
      
      





先に進む前に考えること。



UDPはパケットを失い、遅延させ、並べ替えます。 解決中の問題では、パッケージを再送信しても意味がありません。パッケージが失われたと判断するまでに、画面を再度更新する必要があります。 そのため、TCPではなくUDPを選択しました。 損失に対処することは不可能ですが、それらに適応する必要があります。プロトコルの寿命が長くならないようにしてください。また、パケット損失が致命的であったり、長期間にわたって画像を損なったりすることはありません。



周波数が10ヘルツであっても、スマートフォン画面のサイズの画面フラグメントを転送すると、3 * 800 * 480 * 10 = 11520000バイト/秒になります。 これはほぼ100メガビットです。 圧縮なしではできません。

変更されていない画面の部分を再送信する必要はありません—かなりの数があります。 ただし、変更されていない部分の再送信を完全に拒否することはできません。信頼性の低いチャネルがあり、実際、クライアントに表示される内容はわかりません。



ウィンドウのサイズは異なる場合があります。 たとえば、携帯電話がポートレートモードからランドスケープモードに変わったためです。

ただし、これらすべての発言を一度に考慮することは不可能です-作業はリフレクションから発生します。 したがって、手始めに、単純にするために、できる限りすべてを無視してください。



2つに分ける



そして、既存のプログラムをサーバーとクライアントの2つに分割し始めます。 それらを同じプロセス内に残しますが、異なるスレッドで動作させ、データに従って互いに依存しないようにします。



この時点で、すでにプログラムにデータグラムをそれ自体に送信させることができますが、それは大きすぎるステップです。 まず、これら2つのプロセス間の相互作用のチャネルとしてConcurrentQueueを選択しました。これは、Producer-Consumerスキームに従って相互作用を実装するように設計されたスレッドセーフキューです。 サーバーは直接チャネルを通じて画像の断片をクライアントに配信し、クライアントは逆チャネルを通じて表示ウィンドウのシフトとマウスクリックに関する情報を提供します。



ConcurrentQueueは多くの点でUDPに関連しているため、ConcurrentQueueを介して相互作用をデバッグする場合、キューでの作業をデータグラムの送受信で置き換えるだけでよいと考えています。 このような置換を簡単にするには、プログラムを使用して、短いバイトシーケンスがキューで確実に送信されるようにする必要があります。

しかし、最初に、型付きキューを使用します。

共有する



最初に、サーバーからクライアントへ、またはその逆にメッセージを送信するために使用されるデータ構造を定義します。

 struct ImageChunk { public Rectangle place; public Bitmap img; }; struct ControlData { public enum Action : byte { Shift, Click }; public Action action; public Point point; }
      
      





そのため、サーバーは画像をクライアントに送信し、サーバー上のどこにあるかを監視します。 リバース-シフト(Shift)およびマウスクリック(クリック); どこでもサーバー座標を使用することに同意します。



次に、既存のMain関数を再度コピーし、両方のコピーの名前をサーバーとクライアントに変更して、新しいMainを作成します。

 static void Main(string[] args) { var img_channel = new BlockingCollection<ImageChunk>( new ConcurrentQueue<ImageChunk>() ); var control_channel = new BlockingCollection<ControlData>( new ConcurrentQueue<ControlData>()); Server(control_channel, img_channel); Client(img_channel, control_channel); }
      
      





簡単なフローチャートのテキストアウトラインのようですね。



画面キャプチャに適用されないサーバーからすべてを削除し、キューの処理を追加します。 また、クライアントとサーバーでウィンドウサイズを400x300のサイズに修正して、リストがさらに数段落増加しないようにすることにしました。

 static void Server(BlockingCollection<ControlData> input, BlockingCollection<ImageChunk> output) { Point window_topleft = new Point(); Size window_size = new Size(400, 300); var timer = new System.Windows.Forms.Timer() { Interval = 40 }; timer.Tick += (s, e) => { //       ControlData incoming; while (input.TryTake(out incoming)) { switch (incoming.action) { case ControlData.Action.Click: mouse_event((uint)(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE), (uint)incoming.point.X, (uint)incoming.point.Y, 0, 0); mouse_event((uint)(MOUSEEVENTF_LEFTUP | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE), (uint)incoming.point.X, (uint)incoming.point.Y, 0, 0); break; case ControlData.Action.Shift: window_topleft = incoming.point; break; } } //        var b = new Bitmap(window_size.Width,window_size.Height); var g = Graphics.FromImage(b); g.CopyFromScreen(window_topleft.X, window_topleft.Y, 0, 0, window_size); g.Dispose(); output.Add(new ImageChunk() { img = b, place = new Rectangle(window_topleft, window_size) } ); }; timer.Start(); }
      
      





クライアントから、サーバーが現在実行しているすべてを削除します。

 static void Client(BlockingCollection<ImageChunk> input, BlockingCollection<ControlData> output) { Form f = new Form(){ ClientSize = new Size(400, 300) }; Point window_topleft = new Point(); Size mouse_prev_loc = new Size(); bool mouse_lbdown = false; Point mouse_down_loc = new Point(); //    f.MouseDown += (s, e) => { mouse_lbdown = true; mouse_down_loc = e.Location; }; f.MouseUp += (s, e) => { mouse_lbdown = false; if (Math.Abs(e.Location.X - mouse_down_loc.X) < 1 && Math.Abs(e.Location.Y - mouse_down_loc.Y) < 1) { int click_to_x = (window_topleft.X + mouse_down_loc.X) * 65536 / Screen.PrimaryScreen.Bounds.Width; int click_to_y = (window_topleft.Y + mouse_down_loc.Y) * 65536 / Screen.PrimaryScreen.Bounds.Height; output.Add(new ControlData() { action=ControlData.Action.Click, point=new Point(click_to_x,click_to_y) }); } }; f.MouseMove += (s, e) => { if (mouse_lbdown) { window_topleft += mouse_prev_loc - (Size)(e.Location); output.Add(new ControlData() { action = ControlData.Action.Shift, point = window_topleft } ); } mouse_prev_loc = (Size)e.Location; }; //    var timer = new System.Windows.Forms.Timer() { Interval = 40 }; timer.Tick += (s, e) => { ImageChunk incoming; //    -  if( ! input.TryTake(out incoming,5) ) return; Graphics g = f.CreateGraphics(); g.DrawImageUnscaled(incoming.img, incoming.place.X - window_topleft.X, incoming.place.Y - window_topleft.Y); g.Dispose(); incoming.img.Dispose(); }; timer.Start(); Application.Run(f); }
      
      





アプリケーションの外観は変更されていないため、スクリーンショットは表示されません。

注意:ところで、ここでフォークできます..



...そして、パイプ、http、RS232などを介してリモートデスクトップを作成します。キューにあるオブジェクトのシリアル化、圧縮、およびトランスポートを記述するだけです。



記事の次の部分では、UDPを介した後続の送信のためにシャープ化された圧縮について説明します。 UDPの機能は、アトミックに送信されるデータ(パケット)のサイズが小さいことと、パケットの損失と並べ替えです。




All Articles