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

おきまりな感じの内容。
画面はこんなかんじにしてみました。
シンプルですね。
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とかともくっつけられそうですねー。