UnityWebRequestの最大同時接続数を制御する

# 概要

- UnityWebRequestの最大同時接続数を制御したい
- 外部からはasync/awaitでいい感じに待機できるようにしたい

# 実装

- SingletonなMonobehaviourとキューを使って実装を行う
- UnityWebRequestをラップしたものをキューに入れる
  - ついでにAwaitableにしとく
- 接続数が最大接続数以下の時にキューから取り出す
- SendWebRequest()で通信を開始し、接続数をインクリメントする
- UnityWebRequestAsyncOperation.completedに接続数をデクリメントするアクションを登録する
  - completedは通信失敗・中止時にも呼ばれる
  - 既に通信が完了している状態で登録すると即座に呼び出される

# コード

```cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
using UnityEngine;
using UnityEngine.Networking;

public interface IAwaitable<T>
{
    TaskAwaiter<T> GetAwaiter();
}

public interface IDispatchableTask
{
    void Start(Action completionAction);
}

public class UnityWebRequestTask : IAwaitable<byte[]>, IDispatchableTask
{
    UnityWebRequest webRequest;
    TaskCompletionSource<byte[]> completionSource = new TaskCompletionSource<byte[]>();

    public UnityWebRequestTask(UnityWebRequest webRequest)
    {
        this.webRequest = webRequest;
    }

    public void Start(Action completionAction = null)
    {
        var asyncOperation = webRequest.SendWebRequest();
        asyncOperation.completed += operation => {
            completionSource.SetResult(webRequest.downloadHandler.data);
            completionAction?.Invoke();
        };
    }

    public TaskAwaiter<byte[]> GetAwaiter()
    {
        return completionSource.Task.GetAwaiter();
    }
}

public class WebRequestDispatcher : MonoBehaviour
{
    static WebRequestDispatcher instance;
    public static WebRequestDispatcher Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new GameObject("WebRequestDispatcher").AddComponent<WebRequestDispatcher>();
            }
            return instance;
        }
    }
    Queue<IDispatchableTask> queue = new Queue<IDispatchableTask>();

    public int maxConnection = 4;
    public int ConnectionCount { get; private set; }

    public void RegisterTask(IDispatchableTask task)
    {
        queue.Enqueue(task);
    }

    private void Update()
    {
        while (ConnectionCount < maxConnection && queue.Count > 0)
        {
            var task = queue.Dequeue();
            ConnectionCount++;
            // 動作確認用のログ
            Debug.Log("Request Start: connection count " + ConnectionCount);
            task.Start(() => {
                ConnectionCount--;
                Debug.Log("Request Complete: connection count " + ConnectionCount);
            });
        }
    }
}
```

# 動作確認

## 1. サーバーを立てる

適当に時間をかけてレスポンスするサーバーを立てる。

参考：[HTTPクライアントプログラムで読み込みタイムアウトをテストするアドホックサーバーをワンライナーで \- Qiita](https://qiita.com/kozy4324/items/cd3c20a1ce234588854e)

## 2. ボタンを押すとリクエストを飛ばすようにする

以下のようなMonobehaviourを作って、適当に配置したボタンのOnClickにStartRequestを登録する

```cs
public class WebRequestTester : MonoBehaviour
{
    WebRequestDispatcher dispatcher;

    private void Start()
    {
        dispatcher = WebRequestDispatcher.Instance;
    }

    public void StartRequest()
    {
        Debug.Log("StartRequest");
        var req = UnityWebRequest.Get("http://localhost:3333/");
        var task = new UnityWebRequestTask(req);
        dispatcher.RegisterTask(task);
    }
}
```

## 3. 動作確認

適当にボタンを押しまくると、最大同時接続数が4になるように制御されていることが分かる。

<img src="https://qiita-image-store.s3.amazonaws.com/0/97014/c523e5f9-4053-dc1a-ea2c-85bc52478b5e.png" width=300>

- StartRequest: ボタンを押したタイミング（テキストがわかりにくくてよくなかった…）
- Request Start: 実際に通信が開始されたタイミング
- Request Complete: 通信が完了したタイミング

# 使用例

こんな感じでawaitできる

```cs
var req = UnityWebRequest.Get("http://localhost:3333/");
var task = new UnityWebRequestTask(req);
dispatcher.RegisterTask(task);

var bytes = await task;
Debug.Log(req.responseCode);
Debug.Log(bytes.Length);
Debug.Log(req.downloadHandler.text);
```

もともとファイルの同時DL数を制御したかったのでbyte[]を返すTaskにしてあるが、その辺は用途に応じて変えると良い。
めんどくさいので全部ラップしてstaticメソッドにしてURLを渡したらAwaitaleなオブジェクトが返ってくるようにしても良さそう。

# :memo: TODO

インスペクタ上で通信状況やキューの状態を可視化したい