如何优雅的在Unity中发送REST API请求

作者:sp958857
在Unity中经常使用的网络请求模块,有官方的UnityWebRequest模块,BestHttp第三方插件等。他们的使用方式都大同小异,通过Url的拼接处理,提供各种方法来应对REST请求的各种模式(Get/Post/Delete/Patch)与参数,使用的代码如下

//BestHttp
HTTPRequest request = new HTTPRequest(new Uri("http://server.com/path"),HTTPMethods.Post,OnRequestFinished);
    request.AddField("FieldName", "Field Value");
    request.Send();
//official UnityWebRequest module
 WWWForm form = new WWWForm();
        form.AddField("myField", "myData");
        using (UnityWebRequest www = UnityWebRequest.Post("http://www.my-server.com/myform", form))
        {
            yield return www.SendWebRequest();

            if (www.isNetworkError || www.isHttpError)
            {
                Debug.Log(www.error);
            }
            else
            {
                Debug.Log("Form upload complete!");
            }
        }

如果项目中的REST API请求的数量变大的情况下,就会出现几钟情况:

  • Http请求的代码的代码分散在工程中
  • 如果把请求代码集中放置,这个类会非常巨大,将包含各种涉及底层请求模块的代码,如www.SendWebRequest()request.AddField("FieldName", "Field Value");
  • 如果涉及多线程请求处理,例如在子线程发送请求,在主线程更新UI,那么代码将变得更加难以集中统一维护

在Android开发中,得到大量应用和验证的网络请求库Retrofit,将REST API变成一个个的接口定义,在业务代码中可以像调用方法一样去请求API函数,代码将变得非常优美和容易维护。
那么在Unity中有没有这样的库呢,奈何目前是没有的。.Net版本的Retrofit倒是有一些,例如refitRetrofit.Net,可惜的是这些版本都是基于.Net 4.5开发的,并不适用与大多数基于.NET 3.5运行时开发的Unity工程。
在这种情况下,只能自己写一个适用于Unity .NET 3.5 工程的Retrofit模块了。

Retrofit for Unity

实现过程中,尽量保持跟Java版本一致,得益于Java跟C#语言基本类似,很多实现基本没有大碍,例如Java的注解对应C#的特性。但是有一些语言特性是C#没有的,例如Java的类动态代理,在C#是没有的,就会在定义Client的REST接口的时候产生一些差别。
下面着重介绍下该模块的使用方式。

简介

Retrofit for Unity把Http API描述变成接口直接使用。下面是一个REST API的接口化定义

 public interface IHttpBinInterface
    {
        [Get("/get")]
        IObservable Get(
            [Query("query1")]string arg1,
            [Query("query2")]string arg2
            );
     }

如何使用

3个步骤:
-[1] 定义一个Interface:该接口用来管理你的 HTTP API。
-[2] 继承 RestAdapter 并实现该接口: 在接口方法实现中只需要调用 SendRequest(args...) 即可。

public class HttpBinService:RestAdapter,IHttpBinInterface
    {
        private static HttpBinService _instance;

        public static HttpBinService Instance
        {
            get
            {
                if (_instance == null)
                {
                    var go = new GameObject("HttpBinService");
                    _instance = go.AddComponent();
                }
                return _instance;
            }
        }
  
        protected override HttpImplement SetHttpImpl()
        {
            var httpImpl = new HttpClientImpl();
            httpImpl.EnableDebug = true;
            return httpImpl;
        }

        protected override void SetRestAPI()
        {
            baseUrl = "http://httpbin.org";
            iRestInterface = typeof (IHttpBinInterface);
        }


        public IObservable Get(string arg1, string arg2)
        {
            return SendRequest(arg1,arg2) as IObservable;
        }
    }

在Java版本的Retrofit中不需要这一步的原因就是使用了类的动态代理:
GitHubService service = retrofit.create(GitHubService.class);//GitHubService为Interface
由于语言特性的差别,C#没有这个特性,且Unity中官方请求模块UnityWebRequest和WWW都是基于协程实现的,而协程只能在MonoBehavior中使用,因此为了兼容官方模块,RestAdapter作为通用基类适配器需要继承MonoBehavior,业务请求服务直接继承RestAdapter,并实现REST接口的方法。

-[3] 调用方法: HttpBinService将负责把你的方法调用变成向服务端发送的Http网络请求。

 var ob = HttpBinService.Instance.Get("abc", "123");
     ob.SubscribeOn(Scheduler.ThreadPool)//send request in sub thread
          .ObserveOn(Scheduler.MainThread)//receive response in main thread
          .Subscribe(data =>
              {
                   // onSuccess
                   Debug.LogFormat("Received on threadId:{0}", Thread.CurrentThread.ManagedThreadId);
              },
              error =>
              {
                  Debug.Log("Retrofit Error:" + error);
              });

在Java版本的Retrofit中借助RxJava来实现异步的请求。好在Unity版本中也有Reactive Extension的扩展 UniRx,借助UniRx,可以快速实现Retrofit for Unity的异步请求。

API 声明

在接口的方法中使用C# 特性来标识一个请求将被怎么发送,特性可以被标识在方法和方法参数上。

请求方式

每一个方法必须要有一个Http方法类型的特性来提供请求方式以及相对地址,该组件提供6种内置的特性来标识GET, POST, PUT, DELETE, PATCH and HEAD请求,相对地址在特性中标识即可。

 [Get("/users")]

当然,也可以直接在URL地址中使用查询URL。

[GET("/users/list?sort=desc")]

URL动态组装

URL可以通过中括号+变量名来动态组装,如{any string},同时,用来动态替换URL的参数必须使用 [Path]特性来标识,并且需要使用相同的string值。

[Get("/group/{id}/users")]
IObservable<> GetList(
    [Path("id")] int groupId);

Query查询语句也可以使用同样的方式来组装动态查询URL。

[Get("/group/{id}/users")]
IObservable<> GetList(
    [Path("id")] int groupId,
    [Query("sort")] string sort);

对于更复杂的查询,建议使用QueryMap来实现。并且通过这种方式可以实现数量可变的查询URL。

[Get("/group/{id}/users")]
IObservable<>GroupList(
    [Path("id")] int groupId,
    [QueryMap] Dictionary options);

带BODY的请求

一个Body对象可以通过 [Body] 特性来完成带Body的请求。

[POST("/users/new")]
IObservable CreateUser(
    [Body] User user);

该object对象将使用RestAdapter中指定的转换器Convert来把对象转换成string并发往服务端,默认情况下,RestAdapter将使用DefalutConverter(Implemented by Newtonsoft.json) 来完成转换任务。

FORM ENCODED请求

方法也可以声明为发送form-encoded 数据的请求。
当有参数使用[Field]特性标识时,[Field]中的字符将作为健,参数值将作为值来发送form-encoded数据。

[POST("/user/edit")]
IObservable UpdateUser(
    [Field("first_name")] string first,
    [Field("last_name")] string last);

HEADER 的组装

可以使用 [Headers] 特性来标识静态的Header数据

[Headers("Cache-Control: max-age=640000")]
[GET("/widget/list")]
IObservable> WidgetList();
[Headers({
    "Accept: application/vnd.github.v3.full+json",
    "User-Agent: Retrofit-Sample-App"
})]
[GET("/users/{username}")]
IObservable GetUser(
    [Path("username")] string username);

注意:Headers没有同健覆盖功能,即使Headers的健相同也会一起发送出去。
如果需要动态组装Header,可以在参数中用[Header]来标识这是一个动态参数化的Header组装。

[GET("/user")]
IObservable GetUser(
    [Header("Authorization")] string authorization)

Retrofit 配置

RestApater 类负责将API接口转换成可以调用方法的对象。默认情况下Retrofit会自动配置好供你调用,当然所有的配置都是可以自定义的,这是为了方便扩展以及功能细分。

序列化和反序列化配置

Retrofit默认使用 Newtonsoft.json 库来完成序列化和反序列化。
如果API接口返回的结构不能被默认配置的解析库解析,或者你想要用别的库来完成转换工作,那么这个时候,你只需要创建一个类并实现 Converter 接口,并在Adapter的Setup()中指定这个实例来完成转换的工作即可。

结合Rx的异步网络请求实践

 var observable = RxRestBestHttpClient.Instance.RxGetList("square", "retrofit");//声明可观察对象
     observable
            .SubscribeOn(Scheduler.ThreadPool) //将网络请求切换到预定义的线程池中的某个线程执行
            .ObserveOnMainThread()  //请求完成时,在主线程呈现数据
            .Subscribe(
                data =>
                {
                    Debug.LogFormat("Receive on threadId:{0}", Thread.CurrentThread.ManagedThreadId);
                    Debug.Log("Retrofit Success:" + data[0].testField);
                },
                error =>
                {
                    Debug.LogError("Retrofit Error:" + error);
                });

定义可观察对象后observable之后,通过observable.Subcribe(Action onNext, Action onError)的调用后,网络请求才会发送出去,默认情况下,如果没有调用SubscribeOn()ObserveOn(),请求线程跟回调线程都是在主线程完成。

Scheduler+Linq多线程请求与切换

使用Scheduler完成多线程请求与回调切换

在上面的例子种可以看到IObserable在被订阅前使用了2个方法:SubscribeOn(Scheduler.ThreadPool)ObserveOnMainThread(),实际上他们是使用了IObserable的扩展方法SubscribeOnand ObserveOn,

1/SubscribeOn 告诉Rx可观察对象的产生运行在哪个线程。
2/ObserveOn告诉Rx数据流的观察处理运行在哪个线程。

所以如果需要做一些耗时操作,例如网络请求和文件读写都可以通过SubscribeOn来将他们放到异步线程去处理。然后通过ObserveOn来在主线程处理回调的数据。中间不需要关心线程安全的问题,Rx已经在底层帮我们处理好了。

注意:如果SubcribeOn调用之后,不调用ObserveOn,那么观察也会在运行在之前切换到的子线程。并不会自动切换回主线程处理观察数据。

使用Linq扩展操作符简化开发

Rx除了支持LINQ的原生操作符,并且拓展了许多很实用的操作符,并且可以支持自定义操作符,操作符的数量在源源不断的添加中。具体请参考文档,这里列举几个业务模块中经常使用到场景。

线程串联

在业务开发中,遇到需要先请求X,通过X的响应数据,再请求Y,通过Y的响应数据,最后请求Z,时序模型如下图。
可以通过Linq语法这样写,Rx已经内部实现对Linq的支持。


Alt text
var quert = from x in ObserableWWW.Get("http://github.com");
            from y in ObserableWWW.Get(x)
            from z in ObserableWWW.Get(y)
            select new {x,y,z}
query.Subcribe(x=>Debug.Log(x),ex=>Debug.LogException(ex));
线程合并

在业务开发中,遇到需要先请求A和B,需要A和B的请求都返回后进行下一步,任意一个请求出错下一步都无法执行,并且只有2个网络请求都返回结果之后才进行下一步的处理,而且A和B两个网络请求没有关联,运行在不同线程,时序模型如下图。


Alt text
var query = Observable.Zip(
            ObserableWWW.Get("http://github.com")),
            ObserableWWW.Get("http://google.com")),
            (github,google) => new {goole,bing});
            
query.Subcribe(x=>Debug.Log(x),ex=>Debug.LogException(ex));

这里只需要通过一个扩展操作符Zip来实现即可,完成了线程等待和线程合并的功能。

更多操作符

Rx提供了许多操作符来简化程序员的工作,上面介绍的只是冰山一角,更多的操作符在官网有介绍,并且大部分UniRx都可以支持,如果在遇到一个业务场景的时候,不知道选择哪个操作符可以实现我们的功能,那么可以参考A Decision Tree of Observable Operators,来辅助选择操作符

与不同Http模块的结合

Retrofit只是一种代码风格的设计,可以美化代码及方便管理API。它并不会真正的发送任何一个Http网络请求。这些工作都交给底层的网络请求库来实现,这也就意味着Retrofit for Unity 可以跟各种各样的网络请求插件或者是Unity官方的 UnityWebRequest模块来结合使用。默认情况下,该组件提供3种组合方式,即HttpClientUnityWebRequestImpl and BestHttpImpl,默认情况下使用HttpClient作为底层网络请求模块,因为它是一个基于System.Net的非常简洁的RESTful API的网络请求模块,并且完全开源。如果你需要跟其他网络插件结合使用,那么只需要实现HttpImplement接口,并在Adapter的Setup()中指定一个实例即可。

与 BESTHTTP的结合使用

  1. 导入BestHttp 插件.
  2. 导入Retrofit for Unity.
  3. 导入在Integrations目录下的BestHttpImpl.unitypackage .
    Alt text
  4. 用BestHttpImpl实例作为RestAdapter中的SetHttpImpl()方法的返回值.
 protected override HttpImplement SetHttpImpl()
        {
            return new BestHttpImpl();
        }

与 UNITYWEBREQUEST的结合

要想使用 UnityWebRequest, unity 版本必须是5.4或更高.

  1. 导入Retrofit for Unity.
  2. 导入在Integrations目录下的UnityWebRequestImpl.unitypackage.
    Alt text
  3. 用UnityWebRequestImpl实例作为RestAdapter中的SetHttpImpl()方法的返回值.
 protected override HttpImplement SetHttpImpl()
        {
            return new UnityWebRequestImpl();
        }

你可能感兴趣的:(如何优雅的在Unity中发送REST API请求)