Windows Phone Advent Calender 9日目です。
http://www.adventar.org/calendars/201
現状、Windows Phone 8では、なかなか電話の外には出られません。
Bluetoothでキーボードとかもまだまだ難しく、デバイスとの連携なんかも難しいらしいです。
ただ、Windows Phone 8ではSocketクライアントになれるAPIが用意されているみたいなので、ちょっと試してみました。
例えば、こんなコンソールプロジェクトを作って、Socketサーバを立てます。
このSocketサーバは投げた文字列の長さを返してくれるサーバです。
using System; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; namespace SocketServer { public class Server { private const int PORT = 2001; public static void Main() { var ipHostInfo = Dns.Resolve(Dns.GetHostName()); Console.WriteLine("IPアドレス:"); foreach (var ip in ipHostInfo.AddressList) { Console.WriteLine(" "+ip); } Console.WriteLine("ポート番号:{0}", PORT); Console.WriteLine("------------------------------------------"); //IPv4とIPv6の全てのIPアドレスをListenする var listener = new TcpListener(IPAddress.IPv6Any, PORT); //IPv6Onlyを0にする listener.Server.SetSocketOption( SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 0); //Listenを開始する listener.Start(); //接続要求があったら受け入れる var client = listener.AcceptTcpClient(); Console.WriteLine("IPアドレス:{0} ポート番号:{1})。", ((IPEndPoint)client.Client.LocalEndPoint).Address, ((IPEndPoint)client.Client.LocalEndPoint).Port); //NetworkStreamを取得 var ns = client.GetStream(); var f = true; do { var disconnected = false; //クライアントから送られたデータを受信する var enc = Encoding.UTF8; var ms = new MemoryStream(); var resBytes = new byte[256]; do { //データの一部を受信する var resSize = ns.Read(resBytes, 0, resBytes.Length); //Readが0を返した時はクライアントが切断したと判断 if (resSize == 0) { f = false; disconnected = true; Console.WriteLine("クライアントが切断しました。"); break; } //受信したデータを蓄積する ms.Write(resBytes, 0, resSize); } while (ns.DataAvailable); //受信したデータを文字列に変換 var resMsg = enc.GetString(ms.ToArray()); ms.Close(); Console.WriteLine(resMsg); if (!disconnected) { //クライアントにデータを送信する //クライアントに送信する文字列を作成 var sendMsg = resMsg.Length.ToString(); //文字列をByte型配列に変換 var sendBytes = enc.GetBytes(sendMsg); //データを送信する ns.Write(sendBytes, 0, sendBytes.Length); Console.WriteLine(sendMsg); } } while (f); //閉じる ns.Close(); client.Close(); Console.WriteLine("クライアントとの接続を閉じました。"); //リスナを閉じる listener.Stop(); Console.WriteLine("Listenerを閉じました。"); Console.ReadLine(); } } }
実行するとこんなかんじ。
次に、Windows Phoneでクライアントをつくってみましょう。
プロジェクトを作って、Socket用の新しいクラスを用意します。
using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; namespace WPSocketClient { internal class SocketClient { // Cached Socket object that will be used by each call for the lifetime of this class // Define a timeout in milliseconds for each asynchronous call. If a response is not received within this // timeout period, the call is aborted. private const int TIMEOUT_MILLISECONDS = 5000; // The maximum size of the data buffer to use with the asynchronous socket methods private const int MAX_BUFFER_SIZE = 2048; private static readonly ManualResetEvent _clientDone = new ManualResetEvent(false); private Socket _socket; /// <summary> /// Attempt a TCP socket connection to the given host over the given port /// </summary> /// <param name="hostName">The name of the host</param> /// <param name="portNumber">The port number to connect</param> /// <returns>A string representing the result of this connection attempt</returns> public string Connect(string hostName, int portNumber) { var result = string.Empty; // Create DnsEndPoint. The hostName and port are passed in to this method. var hostEntry = new DnsEndPoint(hostName, portNumber); // Create a stream-based, TCP socket using the InterNetwork Address Family. _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // Create a SocketAsyncEventArgs object to be used in the connection request var socketEventArg = new SocketAsyncEventArgs(); socketEventArg.RemoteEndPoint = hostEntry; // Inline event handler for the Completed event. // Note: This event handler was implemented inline in order to make this method self-contained. socketEventArg.Completed += delegate(object s, SocketAsyncEventArgs e) { // Retrieve the result of this request result = e.SocketError.ToString(); // Signal that the request is complete, unblocking the UI thread _clientDone.Set(); }; // Sets the state of the event to nonsignaled, causing threads to block _clientDone.Reset(); // Make an asynchronous Connect request over the socket _socket.ConnectAsync(socketEventArg); // Block the UI thread for a maximum of TIMEOUT_MILLISECONDS milliseconds. // If no response comes back within this time then proceed _clientDone.WaitOne(TIMEOUT_MILLISECONDS); return result; } /// <summary> /// Send the given data to the server using the established connection /// </summary> /// <param name="data">The data to send to the server</param> /// <returns>The result of the Send request</returns> public string Send(string data) { var response = "Operation Timeout"; // We are re-using the _socket object that was initialized in the Connect method if (_socket != null) { // Create SocketAsyncEventArgs context object var socketEventArg = new SocketAsyncEventArgs(); // Set properties on context object socketEventArg.RemoteEndPoint = _socket.RemoteEndPoint; socketEventArg.UserToken = null; // Inline event handler for the Completed event. // Note: This event handler was implemented inline in order to make this method self-contained. socketEventArg.Completed += delegate(object s, SocketAsyncEventArgs e) { response = e.SocketError.ToString(); // Unblock the UI thread _clientDone.Set(); }; // Add the data to be sent into the buffer byte[] payload = Encoding.UTF8.GetBytes(data); socketEventArg.SetBuffer(payload, 0, payload.Length); // Sets the state of the event to nonsignaled, causing threads to block _clientDone.Reset(); // Make an asynchronous Send request over the socket _socket.SendAsync(socketEventArg); // Block the UI thread for a maximum of TIMEOUT_MILLISECONDS milliseconds. // If no response comes back within this time then proceed _clientDone.WaitOne(TIMEOUT_MILLISECONDS); } else { response = "Socket is not initialized"; } return response; } /// <summary> /// Receive data from the server using the established socket connection /// </summary> /// <returns>The data received from the server</returns> public string Receive() { string response = "Operation Timeout"; // We are receiving over an established socket connection if (_socket != null) { // Create SocketAsyncEventArgs context object var socketEventArg = new SocketAsyncEventArgs(); socketEventArg.RemoteEndPoint = _socket.RemoteEndPoint; // Setup the buffer to receive the data socketEventArg.SetBuffer(new Byte[MAX_BUFFER_SIZE], 0, MAX_BUFFER_SIZE); // Inline event handler for the Completed event. // Note: This even handler was implemented inline in order to make this method self-contained. socketEventArg.Completed += delegate(object s, SocketAsyncEventArgs e) { if (e.SocketError == SocketError.Success) { // Retrieve the data from the buffer response = Encoding.UTF8.GetString(e.Buffer, e.Offset, e.BytesTransferred); response = response.Trim('\0'); } else { response = e.SocketError.ToString(); } _clientDone.Set(); }; // Sets the state of the event to nonsignaled, causing threads to block _clientDone.Reset(); // Make an asynchronous Receive request over the socket _socket.ReceiveAsync(socketEventArg); // Block the UI thread for a maximum of TIMEOUT_MILLISECONDS milliseconds. // If no response comes back within this time then proceed _clientDone.WaitOne(TIMEOUT_MILLISECONDS); } else { response = "Socket is not initialized"; } return response; } /// <summary> /// Closes the Socket connection and releases all associated resources /// </summary> public void Close() { if (_socket != null) { _socket.Close(); } } } }
おきまりな感じの内容。
画面はこんなかんじにしてみました。
シンプルですね。
<phone:PhoneApplicationPage x:Class="WPSocketClient.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" SupportedOrientations="Portrait" Orientation="Portrait" shell:SystemTray.IsVisible="True"> <!--LayoutRoot は、すべてのページ コンテンツが配置されるルート グリッドです--> <Grid x:Name="LayoutRoot" Background="Transparent"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!-- ローカライズに関する注: 表示された文字列をローカライズするには、その値を、アプリのニュートラル言語 リソース ファイル (AppResources.resx) 内の適切な名前のキーにコピーしてから、 属性の引用符間のハードコーディングされたテキスト値を、パスがその文字列名を 指しているバインド句と置き換えます。 例: Text="{Binding Path=LocalizedResources.ApplicationTitle, Source={StaticResource LocalizedStrings}}" このバインドは、テンプレートの "ApplicationTitle" という文字列リソースを指します。 [プロジェクトのプロパティ] タブでサポートされている言語を追加すると、 新しい resx ファイルが、UI 文字列の翻訳された値を含む言語ごとに作成 されます。これらの例にあるバインドにより、属性の値が、実行時に アプリの CurrentUICulture と一致する .resx ファイルから描画されます。 --> <!--TitlePanel は、アプリケーション名とページ タイトルを格納します--> <!--ContentPanel - 追加コンテンツをここに入力します--> <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.ColumnDefinitions> <ColumnDefinition Width="75*"/> <ColumnDefinition Width="77*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid Grid.ColumnSpan="2" VerticalAlignment="Top" > <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <TextBox x:Name="IPBox" TextWrapping="Wrap" Text="192.168.20.11" InputScope="Number"/> <TextBox x:Name="PortBox" TextWrapping="Wrap" Text="2001" Grid.Column="2" MinWidth="100"/> <Button Content="connect" Grid.Column="3" Tap="Button_Tap_1"/> <TextBlock TextWrapping="Wrap" Text=":" VerticalAlignment="Top" Grid.Column="1" HorizontalAlignment="Center" FontSize="36" Margin="0,5,0,0"/> </Grid> <Grid Grid.Row="1" Grid.ColumnSpan="2" Background="#FF1B1B1B"> <ScrollViewer x:Name="sv" Margin="10" Width="435" Height="603" > <TextBlock x:Name="LogText" TextWrapping="Wrap" Foreground="#FF3BB900"> <Run Text="log"/> <LineBreak/> <Run/> </TextBlock> </ScrollViewer> </Grid> <Grid Grid.ColumnSpan="2" VerticalAlignment="Top" Grid.Row="2" > <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <TextBox x:Name="SendMessage" TextWrapping="Wrap" Text="message" FontFamily="Portable User Interface"/> <Button Content="send" Grid.Column="1" Tap="Button_Tap"/> </Grid> </Grid> <!--コメントを解除してアラインメント グリッドを表示し、コントロールが共通の 境界に整列されるようにします。イメージの上余白は -32px で、システム トレイを占有します。システム トレイが非表示になっている場合は、これを 0 に設定します (または余白をすべて削除します)。 製品を出荷する前に、この XAML とイメージ自体を削除してください。--> <!--<Image Source="/Assets/AlignmentGrid.png" VerticalAlignment="Top" Height="800" Width="480" Margin="0,-32,0,0" Grid.Row="0" Grid.RowSpan="2" IsHitTestVisible="False" />--> </Grid> </phone:PhoneApplicationPage>
べたーっと貼ってしまうと、イベントハンドラはこんなかんじ。
using System; using System.Windows; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Navigation; namespace WPSocketClient { public partial class MainPage { private SocketClient _client; private string _result; public MainPage() { InitializeComponent(); } protected override void OnNavigatedFrom(NavigationEventArgs e) { if (ValidateRemoteHost() && ValidateInput()) { _client.Close(); } } private void Button_Tap(object sender, GestureEventArgs e) { if (!ValidateRemoteHost() || !ValidateInput() || _client == null) { MessageBox.Show("not connected."); return; } Log(String.Format("Sending '{0}' to server ...", SendMessage.Text), true); _result = _client.Send(SendMessage.Text); Log(_result, false); Log("Requesting Receive ...", true); _result = _client.Receive(); Log(_result, false); LogText.Inlines.Add(new LineBreak()); sv.ScrollToVerticalOffset(sv.ScrollableHeight); } private void Button_Tap_1(object sender, GestureEventArgs e) { if (!ValidateRemoteHost() || !ValidateInput()) return; _client = new SocketClient(); Log(String.Format("Connecting to server '{0}' over port {1} (echo) ...", IPBox.Text, PortBox.Text), true); var result = _client.Connect(IPBox.Text, int.Parse(PortBox.Text)); Log(result, false); } private bool ValidateInput() { if (!String.IsNullOrWhiteSpace(SendMessage.Text)) return true; MessageBox.Show("Please enter some text to echo"); return false; } private bool ValidateRemoteHost() { if (!String.IsNullOrWhiteSpace(IPBox.Text)) return true; MessageBox.Show("Please enter a host name"); return false; } private void Log(string message, bool isOutgoing) { var direction = (isOutgoing) ? ">> " : "<< "; LogText.Text += Environment.NewLine + direction + message; } private void ClearLog() { LogText.Text = String.Empty; } } }
実行してみるとこんなかんじ。
まず、サーバー起動してー
Windows Phoneからつなぎにいくと、Successがかえってきます。
とりあえず接続されると、IPもっかいだしてみました。
「message」をサーバに投げつけると文字数7がかえってきます。
サーバ側も似たような反応してますね。
もちろん日本語もおっけー。
にゃんぱすー。
コンソールアプリを作るときは、Windowsが日本語のOSじゃないと、日本語が表示できないっていうのにハマりました。
なんか???になります。
回避方法はあるのかもしれませんが、よくわかりませんでした。
でも、こんなふうにSocketを使えば、ArduinoとかLeapとかともくっつけられそうですねー。