atelier:mitsuba

i love UI/UX, Blend, XAML, Behavior, P5, oF, Web, Tangible Bits and Physical computing. なにかあればお気軽にご連絡ください。atelier@c-mitsuba.com

Socket通信でWindows Phoneの呪縛を解く

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();
 }
 }
}

実行するとこんなかんじ。
f:id:c-mitsuba:20131209154100p:plain

次に、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>
 /// <paramname="hostName">The name of the host</param>
 /// <paramname="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>
 /// <paramname="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();
 }
 }
 }
}

おきまりな感じの内容。
画面はこんなかんじにしてみました。
シンプルですね。
f:id:c-mitsuba:20131209154457p:plain

<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;
 }
 }
}

実行してみるとこんなかんじ。
まず、サーバー起動してー
f:id:c-mitsuba:20131209154100p:plain
Windows Phoneからつなぎにいくと、Successがかえってきます。
f:id:c-mitsuba:20131209155507p:plain
とりあえず接続されると、IPもっかいだしてみました。
f:id:c-mitsuba:20131209155524p:plain
「message」をサーバに投げつけると文字数7がかえってきます。
f:id:c-mitsuba:20131209155512p:plain
サーバ側も似たような反応してますね。
f:id:c-mitsuba:20131209155516p:plain
もちろん日本語もおっけー。
f:id:c-mitsuba:20131209155529p:plain
にゃんぱすー。
f:id:c-mitsuba:20131209155533p:plain


コンソールアプリを作るときは、Windowsが日本語のOSじゃないと、日本語が表示できないっていうのにハマりました。
なんか???になります。
回避方法はあるのかもしれませんが、よくわかりませんでした。

でも、こんなふうにSocketを使えば、ArduinoとかLeapとかともくっつけられそうですねー。

引用をストックしました

引用するにはまずログインしてください

引用をストックできませんでした。再度お試しください

限定公開記事のため引用できません。

読者です 読者をやめる 読者になる 読者になる

AltStyle によって変換されたページ (->オリジナル) /