imog

主にUnityとかの事を書いています

UnityWebRequestについてちょっと調べてみた

この記事はUnity 2 Advent Calendar 2015のエントリです。

UnityWebRequest

UnityWebRequestとはWWWに変わる新しいHTTP通信用のクラスである。

docs.unity3d.com

名前空間Experimentalが含まれている通り、まだ実験的な機能なので今後も仕様が変更されていく可能性がある事をご了承いただきたい。

それでも、WWWと比較して十分使いやすくなってはいるので普通に活用して良さそう。

使い方

例えば、以下のコードはローカルホストにアクセスして、レスポンスをログに吐く

using UnityEngine;
using UnityEngine.Experimental.Networking;

public class HTTPTest : MonoBehaviour {

    // Use this for initialization
    void Start () {
        StartCoroutine(HttpRequest());
    }

    IEnumerator HttpRequest()
    {
        var request = UnityWebRequest.Get("http://localhost:4567");
        yield return request.Send();
        Debug.Log(request.responseCode.ToString() + ":" + request.downloadHandler.text);
    }
}

コルーチンで、通信終わるまで待つという点は今までと同じ。 WWWクラスの場合コンストラクタ呼び出したタイミングで通信しに行くが、UnityWebRequestの場合はSendを呼び出したタイミングで通信する。 なので通信する前にSetRequestHeaderで諸々のヘッダ設定ができるようになった。

WWWと比較した時の違いを幾つか挙げるとこんな感じ

  • RESTに対応
  • データとHTTPのハンドリング部分が分離されている
  • DownloadHandlerScriptでロギングや加工が便利

RESTに対応

多分これが一番大きな変更点。

ついにPUT、DELETEメソッドが実装された・・・。

PUTは引数にstringとbyte[]しか渡せない。 POSTはDictionaryが使用できるのでキーバリューペアでサクッといけるがPUTは自分で整形する必要がある。ちなみに文字列をバイト配列に変換する場合下記で行ける。

System.Text.Encoding.UTF8.GetBytes("foo=get")

引数を空文字列にした場合、以下のエラーが出る。

ArgumentException: Cannot create a data handler without payload data

因みにPATCHはないので、必要があればインスタンスmethodプロパティに直打ちしよう

request.method = "PATCH";

また、現状POST以外のメソッドはContent-typeが空になっている模様。 なので、そのままパラメータ付きでPUTしてもサーバ側によってはうまく受け取ってくれない可能性が高いので以下の感じでヘッダに組み込むのがいい

request.SetRequestHeader("Content-type", "application/x-www-form-urlencoded");

ここは修正されていくと思う、多分。

データとHTTPのハンドリング部分が分離されている

UploadHandlerはリクエストを飛ばす際のパラメータデータ、DownloadHandlerはレスポンスのデータを保持する小さなクラスになっている。 その二つをUnityWebRequestが所有している。 UnityWebRequestはURLやヘッダ情報、リダイレクト回数の設定などHTTP通信に関する処理を扱う。 WWWと違い、データそのものと通信がしっかり分離しており扱いやすい。 UploadHandlerとDownloadHandlerは外部から流し込むことができるからだ。 UploadHandlerを外から設定する場合はUploadHandlerRawクラスを使う。

var request = UnityWebRequest.Get("http://localhost:4567");
var upload = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes("foo=get"));
request.uploadHandler = upload;

コンストラクタの引数がバイト配列なので、変換が必要。 上記で書いたが、PUTはbody部分を空文字列にするとエラーになるので「適当な文字列を入れて初期化」->「uploadHandlerに流し込む」 という流れになった。もっといいやり方ありそう。

DownloadHandlerクラスそのものは流し込むことはできるがあまり意味はない。 むしろ、後述するDownloadHandlerScriptの時に役に立つ。

DownloadHandlerScriptでロギングや加工が便利

DownloadHandlerはダウンロード時の幾つかのタイミングでコールバックされる

  • ReceiveData データを読み取った際に呼ばれるコールバック
  • CompleteContent 読み取り完了した時に呼ばれるコールバック
  • ReceiveContentLengthヘッダーからデータの長さを受け取った時に呼ばれるコールバック

データを読み取った各所でメソッドを呼ぶようになっており、Unityではそれをユーザ側でカスタマイズできるようにDownloadHandlerScriptというクラスを提供しています。

サンプルコードから抜粋

using UnityEngine;
using System.Collections;
using UnityEngine.Experimental.Networking;

public class MyDownloadHandler : DownloadHandlerScript  {
    // Standard scripted download handler - will allocate memory on each ReceiveData callback
    public MyDownloadHandler(): base() {
    }

    // Pre-allocated scripted download handler
    // Will reuse the supplied byte array to deliver data.
    // Eliminates memory allocation.
    public MyDownloadHandler(byte[] buffer): base(buffer) {
    }

    // Required by DownloadHandler base class. Called when you address the 'bytes' property.
    protected override byte[] GetData() { return null; }

    // Called once per frame when data has been received from the network.
    protected override bool ReceiveData(byte[] data, int dataLength) {
        if(data == null || data.Length < 1) {
            Debug.Log("LoggingDownloadHandler :: ReceiveData - received a null/empty buffer");
            return false;
        }

        Debug.Log(string.Format("LoggingDownloadHandler :: ReceiveData - received {0} bytes", dataLength));
        return true;
    }

    // Called when all data has been received from the server and delivered via ReceiveData
    protected override void CompleteContent() {
        Debug.Log("LoggingDownloadHandler :: CompleteContent - DOWNLOAD COMPLETE!");
    }

    // Called when a Content-Length header is received from the server.
    protected override void ReceiveContentLength(int contentLength) {
        Debug.Log(string.Format("LoggingDownloadHandler :: ReceiveContentLength - length {0}", contentLength));
    }
}

上記のコードは、コールバックの各所でログに吐くような処理をしています。 あとは、これをUnityWebRequestのDownloadHandlerに流し込むだけです。

var request = UnityWebRequest.Get("http://localhost:4567");
var download = new MyDownloadHandler();
request.downloadHandler = download;

これで、データ受信時にログに吐いたり、ゲーム用にレスポンスを加工したりなど痒いところに手が届くようになる。

まとめ

現状、content_typeの問題など、まだまだこちらでカバーしなければならない問題はありますが、WWWクラスと比較するとかなり改善されており、期待が持てるAPIっぽい

明日は @yaegakiさんのmruby on Unityです