Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

ファイル転送アプリを作った話

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

元の記事はこちらになります。
File_Rec_Send.gif

作成・動作環境

受信側はWindowsでの使用になるので.NET Frameworkでの開発。
送信側はAndroidでの使用になるのでXamarin.Formでの開発です。

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

パケット構造

送受信に使うパケットの構造はこんな感じに今回は考えました
image.png
ファイル名はUTF-8でバイト列に変換し、メインのファイルデーターのバイト列を1 Byte(0xFF)で仕切った構造となってます。このパケットを送信すれば受信したバイト配列から元のファイル(画像)の復元ができそうです。

送受信の流れ

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

といった流れです。

送信側の処理

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

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

です。では順を追って説明していきます。

ファイルの読み込み

まずユーザーにスマホで送りたいファイルを選んでもらいます。

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

そーすこーど💛
public class ImagePicker
{
    static int REQ_GALLERY = 10;
    public void GetStreamByPicker(Action<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 < 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 && 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 < 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<Android.Net.Uri[]> OnResult;
}

まずGetStreamByPicker()で画像ピッカーを起動します。起動方法はintentのTypeに”image/*“を指定し、複数選択を許可するためにExtraAllowMultipleをtrueにしました。

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

独自パケットへ変換

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

パケットへの変換
    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でストリームに流して送信するだけです。

送信
    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ポートを監視する。
・パケットを受信する。
・パケットを変換・保存する。

といった感じです。

受信部ソース(一部)
        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<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の余分なデーターがついてしまうので注意です。

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

        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 < 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で保存します。
保存は

保存例
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
↑ 受信側(Windows10):Windows.Form
image.png
↑ 送信側(Android) : Xamarin.Form
※なお、使用には同じネットワーク上に存在する必要があります。

image.png
成功すると選択したファイルがパソコンに転送されます。

まとめ

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

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

younaship
プログラマーを目指す大学生。Qiitaはじめました(-q-) こちらに掲載している記事は主に自分のホームページから転載したものになります。プログラミングに興味がある人、学習中の人、だれでもツイッターなどフォローしてください~👼
https://younaship.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした