如何优雅的在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<HttpBinResponse> 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<HttpBinService>();
                }
                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<HttpBinResponse> Get(string arg1, string arg2)
        {
            return SendRequest<HttpBinResponse>(arg1,arg2) as IObservable<HttpBinResponse>;
        }
    }

在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<<List<User>> GetList(
    [Path("id")] int groupId);

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

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

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

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

带BODY的请求

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

[POST("/users/new")]
IObservable<User> 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<User> UpdateUser(
    [Field("first_name")] string first,
    [Field("last_name")] string last);

HEADER 的组装

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

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

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

[GET("/user")]
IObservable<User> 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<T> onNext, Action<Exception> onError)的调用后,网络请求才会发送出去,默认情况下,如果没有调用SubscribeOn()ObserveOn(),请求线程跟回调线程都是在主线程完成。

Scheduler+Linq多线程请求与切换

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

在上面的例子种可以看到IObserable在被订阅前使用了2个方法:SubscribeOn(Scheduler.ThreadPool)ObserveOnMainThread(),实际上他们是使用了IObserable<T>的扩展方法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();
        }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,448评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • 穿着带着彼岸花图案的藏青色裙子,遇见了曼陀罗花,也遇见了你我那么无言的结局…
    奈何桥边道奈何阅读 231评论 0 0
  • 记得高三的时候,班上转来了一个补习生。班主任将他领进教师时,他低着头,有些腼腆。班主任向我们介绍完他后,他终于...
    冷昕阅读 314评论 0 1
  • 站在2016年的尾巴上,曾经花了一个下午的时间,在咖啡厅里写下自己的愿望清单,几乎是想到了什么就写上。然后又花了一...
    欣欣向荣的小天地阅读 394评论 2 3