FlutterのAndroid Virtual Device (Emulator) × 開発PCのPythonでのTCP通信でハマりにハマった話

# 背景
TCP通信を使ってスマホ側のデータを開発PCに送りたいなぁ　と思い，簡単なTCP通信のプログラムを作ってやり取りさせてみようと思ったのですが，動作確認のタイミングでハマりにハマりました...。
Windows用のアプリでは通信ができているのに，AndoroidのVirtual Deviceを使った動作確認では通信が通らず，悩みに悩んで約2日...。泣けます．

今後このようなことがなくなるように願い，メモに残します！


なおこちらの記事は，Flutterに関する基礎的な知識を有した方向けです！
環境構築やFlutterの使い方に関しては，[公式の記事](https://docs.flutter.dev/get-started/install)や[チュートリアル](https://codelabs.developers.google.com/codelabs/flutter-codelab-first?hl=ja#0)をご確認ください！

# 環境
- OS: Windows 11
- [Flutter](https://flutter.dev/): 
    ```
    $ flutter --version
    Flutter 3.10.5 • channel stable • https://github.com/flutter/flutter.git
    Framework • revision 796c8ef792 (11 days ago) • 2023-06-13 15:51:02 -0700
    Engine • revision 45f6e00911
    Tools • Dart 3.0.5 • DevTools 2.23.1
    ```
- [Andoroid Studio](https://developer.android.com/studio): Flamingo | 2022.2.1
- Java: [java17 for Windows](https://www.oracle.com/jp/java/technologies/downloads/#jdk17-windows)
- [Visual Studio Code](https://code.visualstudio.com/)　いわずと知れたエディタ Flutterの拡張機能がかなり充実いていてとても使いやすいです！
- Application: for Android  今回はVirtual Deviceを使ったアプリ開発の話です！もしかしたら実機を使った開発ではこんなつらさはないのかもしれません...


# 内部プログラム
こちらが今回使うプログラムです　まずはWindowsビルドで試してみてください！

Flutter側　サーバーを起動してまち，クライアントが接続・切断されるとポップアップが出ます
```main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:convert';
import 'package:flutter/cupertino.dart';

void main() {
  runApp(MyApp());
}


class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String ip_port_msg = "";

  ServerSocket? serverSocket;
  @override
  void initState() {
    super.initState();
    startServer();
  }

  Future<void> startServer() async {
    try{
      serverSocket = await ServerSocket.bind("127.0.0.1", 5000);
      print('Server started on ${serverSocket?.address}:${serverSocket?.port}');
      listenForClients();
    } catch (e) {
      print('Error starting server: $e');
    }
  }

  void listenForClients(){
    serverSocket?.listen((client) {
      ip_port_msg = '${client.remoteAddress}:${client.remotePort}';
      print('New client connected: ' + ip_port_msg);
      showConnectionStateDialog(ip_port_msg, 'Connected');
      client.listen(
        (Uint8List data) async {
          String request = String.fromCharCodes(data).replaceAll('\r\n', '');
          client.write('ret:' + request + '\r\n');
        },
        // handle errors
        onError: (error) {
          client.close();
        },
        // handle the closing the connection
        onDone: () {
          client.close();
          debugPrint('socket closed');
          print("disconnected");
          showConnectionStateDialog(ip_port_msg, 'Disconnected');
        },  
      );
    });
  }

  void showConnectionStateDialog(msg, state){
    showDialog(
      context: context,
      builder: (_) => CupertinoAlertDialog(
        title: Text(state),
        content: Column(
          children: [
            Text(msg),
            Text(state)
          ],
        ),
        actions: [
          CupertinoDialogAction(
            child: Text('OK'),
            onPressed: () {
              Navigator.of(context).pop();
            },
          ),
        ],
      ),
    );
  }
  
  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Stack(
        children: [
          Align(
            alignment: Alignment.center,
            child: Text(
                'Hello world'
              ),
          ),
        ],
      ),
    );
  }
}
```
```pubspec.yaml
name: tcp_flutter_python
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 1.0.0+1

environment:
  sdk: '>=3.0.5 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.0.0
  cupertino_icons: ^1.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0

flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true
```

続いてPythonのクライアント側　ただ接続して`Ctrl`+`C`で接続を切るだけです．
```client.py
import socket

def start_client():
    server_address = ('127.0.0.1', 5000)
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect(server_address)
    print("Connected to the server.")

    while True:
        try:
            print("Now Connecting")

        except KeyboardInterrupt:
            break

    print("Disconnected")
    client_socket.close()

start_client()
```

動作はこんな感じ
https://drive.google.com/file/d/1HZZ9yNtclO26yGrg9a8WKs3JYeWP7XLS/view?usp=sharing

ですが，全く同じものでもAndoroidのVirtual Deviceを使った動作確認だとこれが通りません．
Python側でこんなエラーが出ます．

```
ConnectionRefusedError: [WinError 10061] 対象のコンピューターによって拒否されたため，接続できませんでした
```

そもそも通信自体が確立されていないようです．
今回はこれを克服していきます！

# Andoroid EmulatorのIPとPortの話
言及が遅れましたが，`Android Studio`には，`Android Emulator`という仮想デバイスを使ってアプリの動作確認ができる機能があります．これを`Flutter`でも使用することができます．
こんなかんじ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/fc4ff6d1-2449-2275-44ab-13c080ae81ea.png)

このシステムが少々厄介で，`IP`や`Port`などが特殊な構造をしています
以下のサイトにそれらがまとめられています．

https://developer.android.com/studio/run/emulator-networking?hl=ja

このあたりをいい感じにつなげてあげることで，`Android Emulator`と開発PC間のTCP通信が可能となります．

# How to

## 大まかな流れ
もっとも参考になる場所だったのが以下です

https://developer.android.com/studio/run/emulator-networking?hl=ja#connecting

ここでは，開発PC内で`Andoroid Emulator`を2台起動して，その間でTCP通信する際の例が載っています．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/02618f0c-617a-32be-8d51-61baeee4c95a.png)
(https://developer.android.com/studio/run/emulator-networking?hl=ja#connectingから拝借)

今回はココにあるAとBでの通信を考えます．

つまり，Aを開発PC　BをAで実行されるエミュレータインスタンスとして，

1. `10.0.2.15:<serverPort>` をリッスンするように B のサーバーをセットアップ
1. B のコンソールで，A:`localhost:<localPort>` から B:`10.0.2.15:<serverPort>` へのリダイレクトをセットアップ
1. AのPCで`Python`のクライアントをA:`localhost:<localPort>`で接続

というような流れです

今回は以下のような流れで進めます
1. `main.dart`内で`10.0.2.15:6000` でBのサーバーをセットアップ
1. B のコンソールで，A:`127.0.0.1:5000` から B:`10.0.2.15:6000` へのリダイレクトをセットアップ
1. AのPCでPythonのクライアントを起動　その際，A:`127.0.0.1:5000`で接続


## ファイル修正
では実際にやってみます．
まずはファイルを修正します
アプリ側の修正
```main.dart
// line 47 `main.dart`内で`10.0.2.15:6000` でBのサーバーをセットアップ
    serverSocket = await ServerSocket.bind("10.0.2.15", 6000);
```
Python側の修正
```client.py
# line 7 Pythonのクライアント`127.0.0.1:5000`で起動
    server_address = ('127.0.0.1', 5000)
```

## リダイレクト
続いて2つ目の手順のリダイレクトを行います．
以下のサイトが参考になります．

https://developer.android.com/studio/run/emulator-networking?hl=ja#redirection

まずは`Flutter`からエミュレータを起動します．
Androidデバイスになっているのを確認して
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/adfa38ea-c3b4-c4a6-7f5d-4b634819e856.png)

起動
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/654653fd-33ae-13e9-b541-623ab3b38cde.png)

その後VSCodeでターミナルを開き，`telnet`コマンドを入力します．
```
$ telnet localhost <emulator-port-number>
```

`<emulator-port-number>`はエミュレータの左上の表示で確認することができます．図の場合は5554です　小さくてすみません！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/fa85c10b-f798-cfeb-af62-2a0e36c03e09.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/54e41347-3189-1913-2424-fcf20fb240c5.png)

ここで`telnet`コマンドで以下のようなエラーが出る方はWindowsの`Telnet`設定ができていません！
以下のサイトを参考に設定します．Windows10ですがほぼほぼ同じです

https://www.imamura.biz/blog/27493

Windowsキーから「コントロール」で検索をかけてコントロールパネルを開きます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/c06c3ac9-f840-131e-d1e1-f15ca1be0fb1.png)

プログラムから
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/e88bc578-dee3-6926-48f9-af35f1731755.png)


機能の有効化または無効化
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/c55fecbc-0b06-06e4-d8f7-dc994164914e.png)

TelnetクライアントをチェックしてOKを押します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/ece718c6-12b1-78d9-647e-616413599fed.png)

再度コマンドを実行すると通るはずです．私の場合はエミュレータのポートが`5554`なので，
```
$ telnet localhost 5554
```
でした

実行するとこんな感じ
```
Android Console: Authentication required
Android Console: type 'auth <auth_token>' to authenticate
Android Console: you can find your <auth_token> in
'C:\your\user\home\.emulator_console_auth_token'
OK
```
コンソール入力できるの？って感じの画面になりますができます　最初は焦りました

ここに書いてあるように，`auth <auth_token>`コマンドを使った許可が必要になります
自分のユーザー直下に`.emulator_console_auth_token`というファイルができているはずなので，メモ帳などで開き，中身をコピーして貼り付けて実行します

```
auth your-auth-token
```

以下が出ればOKです
```
Android Console: type 'help' for a list of commands
OK
```

その後リダイレクトを追加します
リダイレクトを追加するには，次のようなコマンドを使用します
```
redir add <protocol>:<host-port>:<guest-port>
```

ここで，`<protocol>`は `tcp`または`udp`です
`<host-port>`と`<guest-port>`はPCとエミュレータの間のマッピングを設定します
今回の場合は以下です
```
redir add tcp:5000:6000
```

これで晴れてPCとエミュレータが接続可能になります！

Pythonのクライアントを実行してみてください！無事エミュレータ上にPopUpが出れば成功です！

これでもうまくいかない方は，エミュレータのInternet設定がうまくいってないかもしれません．
自分の設定を置いておくので参考にしてください！

Setting -> Netword & Internet -> Internet
LTEがつながっているかを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/b5d9122f-560c-5991-dd22-dfe187626ab7.png)

つながっていない場合はエミュレータのCelullar設定を確認して再起動　自分はこんな感じでした
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/87040d96-7e66-e8bd-817f-a91c89187ab2.png)



# 余談
Flutter便利と思って始めたはいいものの
Android Studioが沼過ぎてなかなか難しい...。

今回は以上です！　最後まで見ていただきありがとうございました！