ローカルで立てたモックサーバに同一ネットワークの別端末上Flutterアプリからアクセスする[MVP検証向け]
TL;DR
端末Aにローカルで立てているサーバに端末BのFlutterアプリからアクセスしたい場合、以下の手順で実現できました。
- 端末Aと端末Bを同一ネットワークに繋ぐ
- モックサーバを
0.0.0.0
にbindする - 端末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アドレスとして常に扱います。
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へのアクセス」はできないようになっているのが普通ということが分かりました。
この設定は「プライバシーセパレータ機能」と呼ばれるようですが、自分が使っているNURO光のONUでは見当たらず、おそらく「暗号化強化モード」が近い役割を担っているようでした。[2]
ただ、今回はMVPの共有がレンタルオフィスの会議室で行われる予定でありWi-Fiの設定はまずいじれないであろうことが想定されていたのと、初心者なりに、そもそもルータをいじらないといけない時点で何か筋が悪いような気がしていたため、サーバを立ち上げる際の設定で何か方法はないかという点にシフトして調査を続けました。
0.0.0.0
との出会い
ChatGPTとの地道な壁打ちの末、ようやくヒントが得られました。
Prism Mock ServerをLAN内の他の端末で利用するためには、以下の手順を試してみてください。
- 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アドレスの確認
また以下の記事から、mDNSによりアクセス先を{ホスト名}.local
にすることでも同じことができそうだということが分かります。
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
について参考資料
対応の詳細
さて、モックサーバを全世界に公開することなく別端末からアクセスすることが実現可能であることが分かったところで、最初に示した手順に沿って説明していきます。
- 端末Aと端末Bを同一ネットワークに繋ぐ
- モックサーバを
0.0.0.0
にbindする- 端末AのプライベートIPアドレスを取得してFlutterのHTTP通信パッケージの通信先に指定する
0.0.0.0
にbindする
1 モックサーバを- 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を使いこなせる次世代エンジニアだぜ」と息巻いたものですが、少し調べると実はネットワークの人には常識であることが分かりました。思えば、シチュエーション自体は割と普遍的ですね。
なんかほんと、こんなことばっかです、エンジニアリング。
-
データの内容が全世界に見られても対して困らない場合などは、stoplightでデプロイしてしまうのも手だと思います。
大人によるアドバイス ↩︎ -
https://mandola.blog.jp/archives/87167291.html
https://hchch.net/nsd-g1000t-1/ ↩︎
Discussion