TCPでファイル(画像)を送受信する ～ファイル転送アプリを作った話～

#ファイル転送アプリを作った話
スマートフォンから画像を簡単に(無線で)PCに転送できるようなアプリを開発した時の記録です。

元の記事は[こちら](https://younaship.com/2019/03/19/tcp%e3%81%a7%e3%83%95%e3%82%a1%e3%82%a4%e3%83%ab%e3%82%92%e9%80%81%e5%8f%97%e4%bf%a1%e3%81%99%e3%82%8b-%ef%bd%9e%e3%83%95%e3%82%a1%e3%82%a4%e3%83%ab%e8%bb%a2%e9%80%81%e3%82%a2%e3%83%97%e3%83%aa/)になります。
![File_Rec_Send.gif](https://qiita-image-store.s3.amazonaws.com/0/371710/fa4d87e6-40bd-c8e7-d018-3f8c69da1a4b.gif)

##作成・動作環境
受信側はWindowsでの使用になるので.NET Frameworkでの開発。
送信側はAndroidでの使用になるのでXamarin.Formでの開発です。

※動作確認はWindows10＋Android(自分のP20Lite等、祖父のらくらくスマートフォン)で行いました👼

##パケット構造
送受信に使うパケットの構造はこんな感じに今回は考えました
![image.png](https://qiita-image-store.s3.amazonaws.com/0/371710/eab586de-802e-4e10-6cd7-df8aad5d769a.png)
ファイル名はUTF-8でバイト列に変換し、メインのファイルデーターのバイト列を1 Byte(0xFF)で仕切った構造となってます。このパケットを送信すれば受信したバイト配列から元のファイル(画像)の復元ができそうです。

##送受信の流れ
**1・受信側：ファイルの受信を受け付けるためにポート(TCP:23230)監視を開始する
2・送信側：ファイルをユーザーに選択してもらう
3・送信側：選択したファイルを読み取り、独自パケットに変換する
4・送信側：変換したパケットをTCPで送信
5・受信側：受信した独自フォーマットのパケットを変換・ファイルに書き出し**

といった流れです。
#送信側の処理
まずは送信側の処理についてです。送信側が主にすることは

**・ファイルの読み込み
・独自パケットへの変換
・パケットの送信**

です。では順を追って説明していきます。
##ファイルの読み込み
まずユーザーにスマホで送りたいファイルを選んでもらいます。

そのために画像ピッカーを開き、選択されたファイルのURIを取得し、URIを使いStreamを取得します。

```C#:そーすこーど💛
public class ImagePicker
{
    static int REQ_GALLERY = 10;
    public void GetStreamByPicker(Action&lt;string[], Stream[]> result)
    {
        ActivityHost.OnResult = (x) =>
        {
            string[] fileNames;
            Stream[] stms = GetStreamByURI(x, out fileNames);
            result(fileNames, stms);
        };
        var intent = new Intent();
        intent.SetType("image/*");
        intent.PutExtra(Intent.ExtraAllowMultiple, true);
        intent.SetAction(Android.Content.Intent.ActionGetContent);
        ActivityHost.activity.StartActivityForResult(intent, REQ_GALLERY);
    }
 
    public Stream[] GetStreamByURI(Android.Net.Uri[] uris, out string[] fileNames)
    {
        Android.Content.Context context = Android.App.Application.Context;
        Stream[] streams = new Stream[uris.Length];
        fileNames = new string[uris.Length];
 
        for (int i = 0; i &lt; uris.Length; i++)
        {
            streams[i] = context.ContentResolver.OpenInputStream(uris[i]);
 
            ICursor returnCursor = context.ContentResolver.Query(uris[i], null, null, null, null);
            int nameIndex = returnCursor.GetColumnIndex(OpenableColumns.DisplayName);
            returnCursor.MoveToFirst();
            fileNames[i] = returnCursor.GetString(nameIndex);
        }
        return streams;
    }
}
/* 以下はAndroidプロジェクト内に追加 */
public class MainActivity 
{
    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);
        ActivityHost.activity = this; //追加
    }
 
    protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
    {
        base.OnActivityResult(requestCode, resultCode, data);
 
        if (requestCode == REQ_GALLERY &amp;&amp; resultCode == Result.Ok)
        {
            int cnt = 0;
            bool isSingle = false;
 
            if (data.ClipData != null) cnt = data.ClipData.ItemCount;
            else isSingle = true;
 
            Android.Net.Uri[] uris = new Android.Net.Uri[cnt];
            for (int i = 0; i &lt; cnt; i++)
            {
                uris[i] = data.ClipData.GetItemAt(i).Uri;
            }
 
            if (isSingle)
            {
                uris = new Android.Net.Uri[1];
                uris[0] = data.Data;
            }
 
            ActivityHost.OnResult?.Invoke(uris);
        }
    }
}
 
public class ActivityHost{
    public static Activity activity;
    public static Action&lt;Android.Net.Uri[]> OnResult;
}
```
まずGetStreamByPicker()で画像ピッカーを起動します。起動方法はintentのTypeに**”image/*“**を指定し、複数選択を許可するために**ExtraAllowMultipleをtrue**にしました。

選択後、**ActivityHost.OnResult(uris)**が呼び出され、**GetStreamByURI()**で取得したUriからファイルのStreamに変換しています。

##独自パケットへ変換
先ほどのStreamからデーターのバイト配列を取得し、それを引数(baseByte)にし独自パケットのバイト配列に変換します。

```C#:パケットへの変換
    public static byte[] ToByte(string name, byte[] baseByte)
    {
        byte[] nameByte = Encoding.UTF8.GetBytes(name);
        byte[] bytes = new byte[nameByte.Length + baseByte.Length + 1];
        Console.WriteLine("Encode : " + nameByte.Length + " + " + baseByte.Length + " byte + 1byte");
 
        Array.Copy(nameByte, 0, bytes, 0, nameByte.Length);
        Array.Copy(baseByte, 0, bytes, nameByte.Length + 1, baseByte.Length);
        bytes[nameByte.Length] = 255; // 0xFF
        return bytes;
    }
```

##パケットの送信
ただただTCPでストリームに流して送信するだけです。

```C#:送信
    public async void Send(byte[] sendBytes)
    {
        try
        {
            tcpClient = new TcpClient(ipaddr, USE_PORT);
        }
        catch
        {
            Console.WriteLine("Error");
            return;
        }

        NetworkStream nStream = tcpClient.GetStream();
        nStream.ReadTimeout = 15000;
        nStream.WriteTimeout = 15000;

        //データを送信する
        await nStream.WriteAsync(sendBytes, 0, sendBytes.Length);
        Console.WriteLine("Send : " + sendBytes.Length + "Byte");

        nStream.Close();
        tcpClient.Close();
    }
```

#受信側の処理
受信側はそんなに難しくありません。こちらの流れは

**・TCPポートを監視する。
・パケットを受信する。
・パケットを変換・保存する。**

といった感じです。

```C#:受信部ソース(一部)
        private async Task Receive()
        {
            bool isError = false;
            if (tcpListener == null)
            {
                tcpListener = new TcpListener(ipaddr, USE_PORT);
                tcpListener.Start();
            }
            Console.WriteLine("Start Server. "+ipaddr);
            tcpClient = await tcpListener.AcceptTcpClientAsync();
            Console.WriteLine("Connected. " + tcpClient.Client.LocalEndPoint);

            NetworkStream nStream = tcpClient.GetStream();
            MemoryStream mStream = new MemoryStream();
            byte[] gdata = new byte[256];    
            do
            {
                int dataSize = await nStream.ReadAsync(gdata,0,gdata.Length);
                if (dataSize == 0) isError = true;
                await mStream.WriteAsync(gdata, 0, dataSize);
            }
            while (nStream.DataAvailable);
            byte[] receiveBytes = mStream.GetBuffer();

            byte[] data = new byte[mStream.Length];
            for(int i = 0; i&lt;data.Length; i++)
            {
                data[i] = receiveBytes[i];
            }
            if (isError) return;
            Console.WriteLine("Recived : " + data.Length + " bytes ");

            receiveCallBack?.Invoke(data);
            mStream.Close();
        }
```
**await tcpListener.AcceptTcpClientAsync()**で接続を待機しています。
接続後NetworkStreamでデーターを受信し、MemoryStreamに一度書き出します。
なお、**mStream.GetBuffer()**のByte配列のデーターをそのまま使うと最後が0x00の余分なデーターがついてしまうので**注意**です。

こんな感じでパケットを受け取った後は

```C#:
        public static byte[] GetByBytes(byte[] bytes,out string name) //受け取ったバイト列から名前とバイナリに分けます。
        {
            byte[] nameByte,fileByte;

            if(!CutByte(bytes,out nameByte,out fileByte)) throw new Exception("Error");
            name = Encoding.UTF8.GetString(nameByte);
            return fileByte;
        }

        private static bool CutByte(byte[] bytes,out byte[] head,out byte[] footer)
        {
            head = null; footer = null;
            for (int i = 0; i &lt; bytes.Length; i++)
            {
                if (bytes[i] == 255) // 0xFF で区切ってある
                {
                    head = new byte[i];
                    Array.Copy(bytes, 0, head, 0, i);
                    footer = new byte[bytes.Length - i - 1];
                    Array.Copy(bytes, i + 1, footer, 0, bytes.Length - i - 1);
                    return true;
                }
            }
            return false;
        }
```
こんな感じでファイル名とデーターに分けます。
そして、データーの部分をFileStreamで保存します。
保存は

```C#:保存例
string path; // 保存先パス
string name; // パケットから取得したファイル名
FileStream fs;
try{fs = new FileStream(path+name, FileMode.Create);}
catch{/*OnError*/}
await fs.WriteAsync(bytes, 0, bytes.Length);
fs.Close;
```
こんな感じです。
#実行の様子
![image.png](https://qiita-image-store.s3.amazonaws.com/0/371710/ae5a2fc6-1d6b-1d6f-8d57-7eb28e8543fc.png)
↑ 受信側(Windows10)：Windows.Form
![image.png](https://qiita-image-store.s3.amazonaws.com/0/371710/44888cb7-aacf-69fb-635a-ce7da8e205c6.png)
↑ 送信側(Android) : Xamarin.Form
※なお、使用には同じネットワーク上に存在する必要があります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/371710/a47b285f-8310-58c7-ddc5-63acdda1c478.png)
成功すると選択したファイルがパソコンに転送されます。


#まとめ
即席で作ったわりに実用性があるものができた気がしました。

※ちなみにユーザー認証などセキュリティー面、エラー処理などは全く配慮されてませんので紹介するソースコードをそのまま使って公開するアプリを作ったら終わります。