🔺

ローカルで立てたモックサーバに同一ネットワークの別端末上Flutterアプリからアクセスする[MVP検証向け]

2023/12/15に公開

TL;DR

端末Aにローカルで立てているサーバに端末BのFlutterアプリからアクセスしたい場合、以下の手順で実現できました。

  1. 端末Aと端末Bを同一ネットワークに繋ぐ
  2. モックサーバを0.0.0.0にbindする
  3. 端末AのプライベートIPアドレスを取得してFlutterのHTTP通信パッケージの通信先に指定する

以下、背景と手順についての詳細な説明をしていきます。

背景

前提

フロントエンド(iOSアプリ)をFlutterで、バックエンドをGoで実装する開発案件に取り組むにあたって、手始めにMinimum Viable Productを作ることになりました。
MVPでは、フロントエンドは実際にRESTを叩くのに用いるHTTP通信パッケージとしてRetrofitを導入し、バックエンドはとりあえずGoではなくPrismでOpenAPIからサクッと作ったモックサーバを使用することにしました。

実現したいと考えたこと

実際の検証用iOS端末にアプリをビルドした状態でクライアントにMVPを見てもらいたいと考えたので、stoplight.ioにpublic設定でデプロイする選択肢が生まれましたが、あまり全世界には見て欲しくないため[1]、見送りました。
そこで、同一ネットワークからlocalhostで立ち上げたPrismサーバを直接叩くことを考えました。

実現したいことの概要図
こういうことをやりたいが、できるかな?

起こった事象

「サーバに接続できなかったため、ページを開けません。」

「localhostにサーバを立ち上げている時、そのマシンのプライベートIPアドレスと指定ポートからでもアクセスできる」ということだけを細かい理屈を抜きに知っていました。
そのため、深く考えずにMacBookでサーバをlocalhost(127.0.0.1):4010にbindした上で、FlutterアプリからはMacBookのプライベートIPアドレスを用いて192.168.1.6:4010を叩くように設定しました。
以下、簡便のため192.168.1.6はMacBookのプライベートIPアドレスとして常に扱います。

127.0.0.1:4010にbindしたPrismサーバ
localhost Prismサーバ

Flutterアプリをビルドする前にとりあえず通信が上手くいくことを確認しようと思い、MacBookと同じWi-Fiに繋いでいるiOS端末上のSafariでhttp://192.168.1.6:4010にアクセスしたところ、「サーバに接続できなかったため、ページを開けません。」となってしまいました。

問題

ここまでで、以下の事実が分かっています。

  • MacBookでPrismサーバをlocalhost:4010として立ち上げて、同MacBook上のiOS SimulatorのFlutterアプリから192.168.1.6:4010にアクセスする→🉑
  • MacBookでPrismサーバをlocalhost:4010として立ち上げて、MacBookと同一ネットワーク上のiOS端末のFlutterアプリから192.168.1.6:4010にアクセスする→🙅‍♀️

ネットワーク初心者の自分(たち)は、ChatGPTに雑に投げつつ、一旦大人(概念)に頼ることにしました。

調査

ルータのプライバシーセパレータ機能が関係?

偉大な大人に相談したところ、一般的なルータでは端末同士での通信はできない設定がデフォルトでされていることが多く、上で自分たちがやろうとしていたような「端末から端末へのプライベートIPアドレスを指定してのlocalhostへのアクセス」はできないようになっているのが普通ということが分かりました。

そもそも、PC→ipadにpingは通る?(ipad→PCのpingはめんどいので) Wifiは(とくにゲスト系のWifi)とかだと、端末間は通信できないようになってるよ。その場合は、httpも当然とおらへんで〜

ちなみに、wifi間の通信を許可している僕の家のネットワークで、同じwifiにつないでMAC (192.168.68.100)iPad(192.168.68.110)で、MACでapache たてて、ipadから、http://192.168.68.100 で普通に動きました

この設定は「プライバシーセパレータ機能」と呼ばれるようですが、自分が使っているNURO光のONUでは見当たらず、おそらく「暗号化強化モード」が近い役割を担っているようでした。[2]

https://www.buffalo.jp/support/faq/detail/16054.html

ただ、今回はMVPの共有がレンタルオフィスの会議室で行われる予定でありWi-Fiの設定はまずいじれないであろうことが想定されていたのと、初心者なりに、そもそもルータをいじらないといけない時点で何か筋が悪いような気がしていたため、サーバを立ち上げる際の設定で何か方法はないかという点にシフトして調査を続けました。

0.0.0.0との出会い

ChatGPTとの地道な壁打ちの末、ようやくヒントが得られました。

Prism Mock ServerをLAN内の他の端末で利用するためには、以下の手順を試してみてください。

  1. Prism Mock Serverを起動する際に、localhostではなく、0.0.0.0を指定します。これにより、Prism Mock Serverはすべてのネットワークインターフェースでリクエストを受け付けるようになります。コマンドは以下のようになります。

prism mock --host 0.0.0.0 your-api-specification.yaml

「すべてのネットワークインターフェース」?

ネットワークインターフェースは、一意なIPアドレスが付与される単位です。端的にはMacOSで言うところのifconfigでザッと表示される各項目がこれにあたります。
このうち、en0がWi-Fi通信で使われるネットワークインターフェースであり、MacOSで以下の画面から確認できるいわゆるプライベートIPアドレスは、これに紐付いています。

MacOSでのプライベートIPアドレスの確認方法
MacOSでのプライベートIPアドレスの確認

また以下の記事から、mDNSによりアクセス先を{ホスト名}.localにすることでも同じことができそうだということが分かります。
https://zenn.dev/gmomedia/articles/2adadb54bb5c4c

prism mock --host 0.0.0.0 your-api-specification.yml

Prismサーバを立ち上げる際、--host(-h)というオプションでアドレスを指定することができます。何も指定しないとループバックアドレスである127.0.0.1が指定されますが、これを 0.0.0.0にすることで、MacBookのプライベートIPアドレス経由でアクセスできるようになるとのことでした。

ということで、元々やりたかったことが実現可能であることが分かり、一安心しました。
実際に試してみたところ、ルータのプライバシーセパレータについての設定に関わらず上記の対応でアクセス可能になりました。

実現した方法
雑に追記するとこういうこと

0.0.0.0について参考資料

https://stackoverflow.com/questions/20778771/what-is-the-difference-between-0-0-0-0-127-0-0-1-and-localhost
https://qiita.com/1ain2/items/194a9372798eaef6c5ab
https://keens.github.io/blog/2016/02/24/bind_addressnoimigayouyakuwakatta/

対応の詳細

さて、モックサーバを全世界に公開することなく別端末からアクセスすることが実現可能であることが分かったところで、最初に示した手順に沿って説明していきます。

  1. 端末Aと端末Bを同一ネットワークに繋ぐ
  2. モックサーバを0.0.0.0にbindする
  3. 端末AのプライベートIPアドレスを取得してFlutterのHTTP通信パッケージの通信先に指定する

1 モックサーバを0.0.0.0にbindする

  • SSIA
  • バックエンド側で設定することはこれだけ

prism mock --host 0.0.0.0 -p 4010 your-api-specification.yml

2-1 APIサーバのアドレスをアプリに渡す

アプリの環境によってAPIサーバの向き先を変えるため、--dart-define-from-fileオプションにより、Flutterアプリのビルド時にAPI_SERVER_BASE_URLという定数を渡しています。

.vscode/launch.json
{
  ..
  "configurations": [
    {
      "name": "DebugMock",
      "request": "launch",
      "type": "dart",
      "program": "client/app/lib/main.dart",
      "args": [
        "--dart-define-from-file=dart_defines/debug_mock.json",
        // "-v"
      ]
    },
    ..
  ]
}
dart_defines/debug_mock.json
{
  "FLAVOR": "debug",
  "APP_ID_SUFFIX": ".debug",
  "APP_NAME": "hogefuga",
  "API_SERVER_BASE_URL": "http://192.168.1.6:4010/"
}

このAPI_SERVER_BASE_URLにホスト端末のプライベートIPアドレスを明示的に載せることで、アプリを載せた別端末からサーバにアクセスできるようになります。
手動で設定するのはイマイチなので、シェルスクリプトにより自動で置き換えるようにしました。現状では、環境構築時やFlutterアプデ時に走らせるスクリプト内に組み込んでいます。

# 前提1:JSON_FILEにdebug_mock.jsonが入っている
# 前提2:jqがインストールされている
# https://formulae.brew.sh/formula/jq
PRIVATE_ADDRESS=$(ipconfig getifaddr en0)
PORT=4010
API_SERVER_BASE_URL="http://${PRIVATE_ADDRESS}:${PORT}/"
jq --arg api_url "${API_SERVER_BASE_URL}" '.API_SERVER_BASE_URL = $api_url' "${JSON_FILE}" > tmp.json && mv tmp.json "${JSON_FILE}" && rm -f tmp.json
echo "Set API_SERVER_BASE_URL to ${API_SERVER_BASE_URL}"

ipconfig getifaddr hogeでhogeというネットワークインターフェースが持つIPアドレスを直接取得することができます。

2-2 アプリ内でアドレスを参照する

--dart-define-from-fileによって渡した定数は、FlutterアプリからはString.fromEnvironmentによって取得することができます。
今回は、DartDefinesというクラスを用意しています。

class DartDefines {
  static const flavor = String.fromEnvironment('FLAVOR');
  static const appIdSuffix = String.fromEnvironment('APP_ID_SUFFIX');
  static const appName = String.fromEnvironment('APP_NAME');
  static const apiServerBaseUrl = String.fromEnvironment('API_SERVER_BASE_URL');
}

本案件では、FlutterのAPI通信ライブラリとしてRetrofitを採用しています。自動生成により実装量が圧倒的に減ったDioです。Retrofitでは、RESTクライアントのクラスに@RestApi()をアノテートすることで自動生成を行います。このクラスのファクトリはDioインスタンスとbaseUrlを引数として取るように定義します。
詳細は割愛しますが、今回はRetrofitによるRESTクライアントをProviderで提供しています。Provider内でインスタンスを初期化する際に、DartDefines.apiServerBaseUrlの値をbaseUrlに渡しています。

import 'package:app/common/util/app_dio.dart';
import 'package:app/dart_defines.dart';
import 'package:dio/dio.dart' hide Headers;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:retrofit/retrofit.dart';

part 'auth_data_source.g.dart';

final authDataSourceProvider = Provider<AuthDataSource>((ref) => AuthDataSource(
    ref.watch(appDioProvider),
    baseUrl: DartDefines.apiServerBaseUrl));

()
abstract class AuthDataSource {
  factory AuthDataSource(Dio dio, {String baseUrl}) = _AuthDataSource;

  /// Signup
  ("/signup")
  (<String, dynamic>{
    'Content-Type': 'application/json',
  })
  Future<void> signup();
}

最後に

今回は、デプロイせずにローカルに立ち上げたバックエンドAPIサーバに、同一ネットワークの別端末からアクセスする方法についてまとめました。

データの取得と表示をベースにしたアプリのMVPを端末で触れる形で最速で提供することを考えた時、バックエンドを一切実装せずにモックデータをアプリ内にベタ書きすることも可能ですが、今回のようにフロントエンドの通信の実装までを検証したいという方に本記事が役立てば幸いです。

ChatGPTとの対話を繰り返して0.0.0.0の指定という方法に辿り着いた瞬間は、「うおおなんてhackyでcreativeなんだ。僕こそがLLMを使いこなせる次世代エンジニアだぜ」と息巻いたものですが、少し調べると実はネットワークの人には常識であることが分かりました。思えば、シチュエーション自体は割と普遍的ですね。

なんかほんと、こんなことばっかです、エンジニアリング。

脚注
  1. データの内容が全世界に見られても対して困らない場合などは、stoplightでデプロイしてしまうのも手だと思います。大人によるアドバイス
    大人によるアドバイス ↩︎

  2. https://mandola.blog.jp/archives/87167291.html
    https://hchch.net/nsd-g1000t-1/ ↩︎

Discussion