Proximity APIを使ってWi-Fi Directでデータ通信

さて、データの送受信です。

ConnectAsync()メソッドの戻り値、または、ConnectionRequestedイベントの引数のPeerInformationから作成したストリームソケット、さらにそのストリームソケットから作成したDataReader、DataWriterを使ってデータの送受信を行うコードを紹介します。送受信するデータの形式は、

とします。”フォーマット”、”テキストやイメージなど”のブロックは、可変長データを送りあうために

とします。

先ずテキストデータですが、

        public async void SendMessage(string message)
        {
            string msgType = "text/plain";
            uint len = peerWriter.MeasureString(msgType);
            peerWriter.WriteUInt32(len);
            peerWriter.WriteString(msgType);

            uint msgLen = peerWriter.MeasureString(message);
            peerWriter.WriteUInt32(msgLen);
            peerWriter.WriteString(message);
            var writeBytes = await peerWriter.StoreAsync();
        }

で送信可能です。peerWriterはストリームソケットから作成したDataWriterです。MeasureString()メソッドでフォーマットの文字列や送信するメッセージ文字列の長さを取得し、WriteUInt32()メソッドで書き込みます。string型のデータは、WriteString()メソッドを使って書き込みます。
データをすべて書き込んだら、StoreAsync()メソッドをコールして、Connect&AcceptしたPeerにデータを送信します。この例では、フォーマット指定は、MIMEタイプを借用しています。独自に定義するよりは、ありものでよく使われているものを使った方が良いでしょう。

次に画像ファイルです。

        public async void SendJpeg(StorageFile jpegFile)
        {
            string msgType = "image/jpeg";
            uint len = peerWriter.MeasureString(msgType);
            peerWriter.WriteUInt32(len);
            peerWriter.WriteString(msgType);
           
            len = peerWriter.MeasureString(jpegFile.Name);
            peerWriter.WriteUInt32(len);
            peerWriter.WriteString(jpegFile.Name);

            var content = await FileIO.ReadBufferAsync(jpegFile);
            peerWriter.WriteUInt32(content.Length);
            peerWriter.WriteBuffer(content);
            var writeBytes = await peerWriter.StoreAsync();
        }

フォーマット名を画像用に変えています。フォーマットの部分はテキストの時と同じです。上の例では、”フォーマット”+”ファイル名”+”画像データ”の順で送信データを組み立てています。どのブロックも、データ長+データ本体で構成しています。画像ファイル(JPEG)の中身は、FileIOクラスのReadBufferAsync()メソッドで取り出しています。取り出したデータ列は、DataWriterのWriteBuffer()メソッドで書き込めます。

他のデータ形式を送りたい場合には上のコードを参考にして色々と工夫してみてください。Wi-Fi Directによる通信の場合、基本的にはアドホックディスカバリーのP2Pなので複雑な手順を含むようなプロトコルよりも送りたいときに送る形式の方が良いでしょう。

以上説明した方法でデータが送信されることを前提に、受信方法を説明します。このポストを読んでトライした読者が色々嵌ってしまわないように、筆者が実際に動作することを確認した主要コードを以下に示しますね。ちょっと長いですがじっくりと読んでください。

        private async void StartReader( CoreDispatcher dispatcher)
        {
            Exception currentEx = null;
            bool hasClosed = false;
            try
            {
                uint bytesRead = await peerReader.LoadAsync(sizeof(uint));
                if (bytesRead > 0)
                {
                    var typeLen = peerReader.ReadUInt32();
                    bytesRead = await peerReader.LoadAsync(typeLen);
                    if (bytesRead > 0)
                    {
                        var msgType = peerReader.ReadString(typeLen);
                        switch (msgType)
                        {
                            case "text/plain":
                                bytesRead = await peerReader.LoadAsync(sizeof(uint));
                                if (bytesRead > 0)
                                {
                                    var msgLen = peerReader.ReadUInt32();
                                    bytesRead = await peerReader.LoadAsync(msgLen);
                                    if (bytesRead > 0)
                                    {
                                        var msg = peerReader.ReadString(msgLen);
                                        WriteStatusAsync(dispatcher, msg);
                                    }
                                    else
                                    {
                                        hasClosed = true;
                                    }
                                }
                                else
                                {
                                    hasClosed = true;
                                }
                                break;
                            case "image/jpeg":
                                bytesRead = await peerReader.LoadAsync(sizeof(uint));
                                if (bytesRead > 0)
                                {
                                    var nameLen = peerReader.ReadUInt32();
                                    bytesRead = await peerReader.LoadAsync(nameLen);
                                    if (bytesRead > 0)
                                    {
                                        var fileName = peerReader.ReadString(nameLen);

                                        bytesRead = await peerReader.LoadAsync(sizeof(uint));
                                        if (bytesRead > 0)
                                        {
                                            var imgLen = peerReader.ReadUInt32();
                                            bytesRead = await peerReader.LoadAsync(imgLen);
                                            if (bytesRead > 0)
                                            {
                                                var content = peerReader.ReadBuffer(bytesRead);
                                                StoreJpegFile(fileName, content);
                                                WriteStatusAsync(dispatcher, "Received JPEG File : " + fileName + " at " + DateTime.Now);
                                            }
                                            else
                                            {
                                                hasClosed = true;
                                            }
                                        }
                                        else
                                        {
                                            hasClosed = true;
                                        }
                                    }
                                    else
                                    {
                                        hasClosed = true;
                                    }
                                }
                                else
                                {
                                    hasClosed = true;
                                }
                                break;
                        }
                    }
                    else
                    {
                        hasClosed = true;
                    }
                }
                else
                {
                    hasClosed = true;
                }
            }
            catch (Exception ex)
            {
                currentEx = ex;
                hasClosed = true;
            }
            if (hasClosed)
            {
                Disconnect();
                if (currentEx != null)
                {
                    var dialog = new MessageDialog("Read Error - " + currentEx.Message);
                    await dialog.ShowAsync();
                    WriteStatusAsync(dispatcher, "Disconnected at " + DateTime.Now);
                }
            }
            else
            {
                StartReader(dispatcher);
            }
        }

 データ受信の基本は、DataReaderのLoadAsync()メソッドコールでデータ長を指定してロード、ReadXXX()※XXXは型名メソッドコールで実際に読み込みの順に行います。データ長、文字列、バイト列はぞれぞれ

  • ReadUInt32
  • ReadString
  • ReadBuffer

メソッドが対応します。受信中に相手とのコネクションが切れた場合、LoadAsyncの戻り値は0になります。また、DataReader、DataWriterのメソッドコールでは、通信の状況でExceptionが発生する場合があり、この状況が発生すると通信は継続できないので、受信状態をやめます。問題が発生していない場合には、最後の方でStartReader()をコールして更に相手からのデータの待ちに入ります。WinRT APIは非同期プログラミングに対応しているので、この一連の流れで、相手からのデータを順次受信していくことが可能です。

他PCとの接続から、StartReader()メソッドをコールする流れは、

            peerWriter = new DataWriter(streamSocket.OutputStream);
            peerReader = new DataReader(streamSocket.InputStream);
            StartReader(Window.Current.CoreWindow.Dispatcher);

です。※peerWriter、peerReaderは、それぞれクラスのメンバー変数として定義されているものとする
StartReader()メソッドコールにawaitをつけていない点に注意してください。awaitをつけないことにより、StartReader()メソッドの実行は、非同期に行われることになり、データ受信によるブロックを避けることができます。

Disconnectメソッドでは、peerWriterとpeerReaderに対し、Dispose()メソッドをコールして後始末してください。

WriteStatusAsync()は、UIの表示情報の更新などを行うことを想定しています。以下のようにUIを管理するディスパッチャーに処理を委譲します。

        private async void WriteStatusAsync(CoreDispatcher dispatcher, string msg)
        {
            await dispatcher.RunAsync(CoreDispatcherPriority.Normal,
                () =>
                {
                    // XAML上のTextBoxなどへのアクセス
                });
        }

StartReader()メソッドの中で、WriteStatusAsync()メソッドをコールする際にもawaitをつけずに非同期実行しているところも実は重要なので、ご注意ください。