📲

gRPCとFlutterでファイル転送アプリを作ってみた…。

2023/03/19に公開1

はじめに

前回、Dartを使用したgRPCの記事を書きました。そのときにサンプルを作ってみたのですが、実際にgRPCを使用したソフトを作ってみたいと思っていました。
 私はFlutter大学に参加しているのですが、個人開発発表会でソケット通信を使用したファイル転送のソフトを作成している人の発表を聞く機会がありました。その時、gRPCを活用したのアプリ作成にいいんじゃない?と思い、アプリ作成に取り組むことにしました。
 Fultterなのでマルチプラットフォームで作成できるのですが、実験できる機器がAndroidしかなく、iOSについてはiPhoneを持っていないので確認していません...。ごめんなさい。また、Windowsなどのデスクトップも確認していません。次の課題にしています。

全体構成

今回製作したアプリの構成は下の図のようになります。
アプリの構成
アプリの構成
gRPCはサーバとクライアントが必要となります。また、ファイル転送をするためにはファイルの受け取りと転送がいるので、受信部分はサーバプログラムとして、送信部分をクライアントプログラム側で行うように設計しました。そのため、今回のアプリは、サーバとクライアントが両方が起動している状態になっており、gRPCのClientStremingを使用してファイル転送処理をしています。

今回作成したアプリの画面

下の図がアプリの画面です。アイコン(My)がサーバの状態、アイコン(To)が転送先の情報、選択したファイル名の表示やメッセージの表示及びファイル選択、転送のボタンがあります。右上には受信ポートの選択、転送先の変更ページに移動するポップメニューがあります。
アプリ画面
アプリの画面

プログラムについて

転送処理についてはgRPCのClient Stremingを利用しています。処理の基本的なことについては、前回の記事で紹介した「Dartを使用したgRPCプログラムについて」を見ていただけたらと思います。

受信処理

受信側では、初めにファイル名が送られてくるので、それを受信し、書き込みファイルをオープンします。その後、送られたデータを順次ファイルに書き込んでいき、最終的にはAndroidのダウンロードフォルダに保存し、終了メッセージを表示します。
受信関係の処理のプログラムは次のようになります。

server.dart
 // ファイルサーバの設定
class FileSaveSrv {

  // プロパティ
  bool _srvState = false;
  final server = Server([UpLoader()]);
  int port;

  // インターフェス情報
  Future<List<NetworkInterface>> srvInfo = NetworkInterface.list();

  // コンストラクタ
  FileSaveSrv({
    required this.port,
  });

  // Getter
  Future<String> get ipAddress => _getIPaddress("wlan0","IPv4");
  bool get srvState => _srvState;
  StreamController<String> get srvmsg => _srvmsg;

  // Get Network Interface information
  Future<String> _getIPaddress(String nwinface,String protocol) async {
    for (var interface in await srvInfo) {
      if (interface.name == nwinface) {
        for (var addr in interface.addresses) {
          if (addr.type.name == protocol) {
            return addr.address;
          }
        }
      }
    }
    return "";
  }

  Future<void> setServer() async {
    try {
      await server.serve(port: port);
      print('Server listening on port ${server.port}...');
      _srvState = true;
    } catch (e) {
      print('Server Execute Error...');
      _srvState = false;
    }
  }

  Future<void> stopServer() async {
    await server.shutdown();
    print('Server Shout down ...');
    _srvState = false;
  }
}

// gPRC CLass
class UpLoader extends FileTransferServiceBase {
  late String path;

  
  Future<UploadResponse> uploadFile(
      ServiceCall call, Stream<UploadFileStruct> request) async {
    List<int> fileData = [];

    late String filename;
    try{
      // Receive data from Client
      await for (var req in request) {
        // get Filename
        if (req.meta.filename.isEmpty) {
          for (var d in req.data.binarydata) {
            fileData.add(d);
          }
        } else {
          filename = req.meta.filename;
        }
      }
      String p = await ExternalPath.getExternalStoragePublicDirectory(
          ExternalPath.DIRECTORY_DOWNLOADS);
      final file = File("$p/$filename");
      await file.writeAsBytes(fileData);

      // Stream
      _srvmsg.sink.add("ファイルを受信しました。");

      // Response send to Client
      return UploadResponse()..result = true;

    } catch(e){
      return UploadResponse()..result = false;
    }
  }
}

送信処理

送信側では、転送するファイルをオープンします。転送処理では初めにファイル名を送り、その後、転送するファイルのバイナリデータを順次送信し、終了後、終了メッセージを表示させます。
送信関係の処理のプログラムは次のようになります。

client.dart
class FileTransfer {
  String address;
  int port;

  FileTransfer({
    required this.address,
    required this.port,
  });

  StreamController<String> get climsg => _climsg;

  // バッファサイズ -> 2 * 1024*1024 = 2Mib
  static const int buffer = 2 * 1024 * 1024;

// gRPCクライアント
  Future<bool> fileTrans(String filename, Uint8List data) async {

    // メッセージ初期化
    _climsg.sink.add("");

    UploadResponse res = UploadResponse();
    final channel = ClientChannel(
      address,
      port: port,
      options: const ChannelOptions(
        credentials: ChannelCredentials.insecure(),
      ),
    );

    // gRPCの設定 タイムアウト設定:10秒
    final stub = FileTransferClient(channel,options: CallOptions(timeout: const Duration(seconds: 40)));
    try {
      // データの送信 -> gRPCのメソッドをコール
      res = await stub.uploadFile(getStream(filename, data));
    } catch (e) {
      // エラー -> falseを返す
      res.result = false;
      _climsg.sink.add("エラーが発生しました。");
    } finally {
      await channel.shutdown();
    }

    // 処理結果
    return Future.value(res.result);
  }


  // Upload処理
  Stream<UploadFileStruct> getStream(String filename, Uint8List data) async* {
    int remainSize = data.length;
    int sendSize;
    int start = 0;
    int end = 0;
    yield UploadFileStruct(
        meta: UploadFileStruct_FileMeta(filename: filename)
    );

    // 転送処理
    while (true) {
      // 転送サイズの計算
      if (remainSize < buffer) {
        // バッファ以下
        end += remainSize;
        sendSize = remainSize;
        remainSize = 0;
      } else {
        // バッファ以上 -> 最大バッファに設定
        end += buffer;
        sendSize = buffer;
        remainSize -= buffer;
      }
      // データ送信
      yield UploadFileStruct(
          data: UploadFileStruct_FileData(
              size: sendSize,
              binarydata: data.sublist(start, end)
          )
      );

      // スタート位置の更新および終了処理
      if (remainSize > 0) {
        start = end;
      } else {
        // データサイズが0なら終了
        // Stream
        _climsg.sink.add("ファイルを送信しました。");
        break;
      }
    }
  }
}

最後に

今回、gRPCを使用したFlutterの例として、ファイル転送アプリを紹介しました。プログラム全体に興味があれば私のgithubで公開していますので、ご覧下さい。
https://github.com/Keisuke-Hongyo/Flutter_FileTransfer
 Flutterを初めて、日がたっていないので、ソースコードは見にくいと思います。ごめんなさい。また、プログラムは趣味でやっているので、業務でアプリを製作している方には「これ、おかしくない?」ということもあると思います。コメントで意見をいただけたらと思います。

Discussion

Keisuke HongyoKeisuke Hongyo

コメント、ありがとうございます。
一度送信したIPアドレスとポートの記憶する機能ですね。
すぐには解答できないですが、やるとしたら、sqliteを使ってデータベースで管理し、起動したときに履歴を読み込み、ListViewで表示するのが良いのかなぁと思います。
すぐにコードを見せるというわけにはいきませんが、時間があるときに取り組んでみます。