• Sliverlight调用Rest服务的一点思考和实践


        最近在写一个SL的小工具,用于图形化编辑一些东西。刚好调用的服务是WCF的Rest方式,于是就碰到了在SL里面直接调用Rest服务的问题,本来Rest服务就是只要有url和内容就可以直接调用的,事实上如果搜索该主题,也可以得到漫山遍野的WebClient方案。不过看看Framework下的WebChannelFactory<TChannel>这个类(这个类型在SL下面不支持...),又感觉用WebClient方式太寒酸了点。。。

        这里讨论的前提是:

    • 已经有Rest服务的契约
    • 不想自己去拼请求

        期望的结果应该是类似与调用WebService的方式。

        然后,就慢慢开始达成我们的目标吧。

        第一步,Copy契约。。。废话,而且只要会按Ctrl+C和Ctrl+V的人都会做,问题是Copy过来的契约不能直接用,SL没提供这样的类,为了方便示例,就准备一个Sample契约:

        [ServiceContract]
        
    public interface ISample
        {
            [WebInvoke(UriTemplate 
    = "/echo/?name={name}",
                BodyStyle 
    = WebMessageBodyStyle.Bare, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
            [OperationContract]
            EchoResponse Echo(
    string name, string message);
        }
        [DataContract]
        
    public class EchoResponse
        {
            [DataMember]
            
    public string Name { getset; }
            [DataMember]
            
    public string Message { getset; }
        }

        第二步,实现契约,等等,这里是客户端怎么冒出来个实现契约了?这里实现契约,实际上是指做一个代理类,只不过平时WebService的时候是自动生成代理类的,而Rest服务没有生成代理类的手段,只能人工做了。。。

        当然,立即想到的是,代理类底层一定是调用WebClient,毕竟服务是Rest方式提供的,问题是形式,如果全部人工翻译,这个代价有点大,更重要的是:我很懒,能只写一次的代码,绝不写两次,有任何共性的代码结构+无限可能的类型组合,都倾向于使用代码生成来实现。

    代理类代码准备

        首先,准备好基本的代码:

    基础工作
        public class RestClient<T>
            
    where T : class
        {

            
    private static readonly Type s_proxyType = CreateProxyType();
            
    private readonly T m_proxy;

            
    public RestClient(string baseUrl)
            {
                m_proxy 
    = Activator.CreateInstance(s_proxyType, baseUrl) as T;
            }

            
    private static Type CreateProxyType()
            {
                
    // todo : 生成代理类型
                return null;
            }

            
    public T Channel { get { return m_proxy; } }

        这里的T需要是契约类型,也就是示例中的ISample,泛型约束不给力,不能约束到T必须是接口,只能退而求其次,约束必须是引用类型(接口类型一定是引用类型,如果想到了值类型可以实现接口,那是因为值类型的装箱形式实现了接口,值类型的装箱形式也是引用类型)

        那为什么要给一个BaseUri哪?别忘了,契约接口里面的(例如:ISample)只提供了相对地址,没有基地址的话,根本找不到终结点。

        然后,开始添加类型生成代码:

    创建类型-part-1
            private static Type CreateProxyType()
            {
                
    if (!typeof(T).IsInterface)
                    
    throw new NotSupportedException();
                
    if (!Attribute.IsDefined(typeof(T), typeof(ServiceContractAttribute)))
                    
    throw new NotSupportedException();
                var interfaces 
    = Enumerable.Repeat(typeof(T), 1).Concat(typeof(T).GetInterfaces()).ToArray();
                
    return CreateProxyType(interfaces);
            }

            
    private static Type CreateProxyType(Type[] interfaces)
            {
                var assembly 
    = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("^_^." + typeof(T).FullName), AssemblyBuilderAccess.Run);
                var module 
    = assembly.DefineDynamicModule("^_^");
                var tb 
    = module.DefineType("$_$." + typeof(T).FullName, TypeAttributes.Public | TypeAttributes.Class, typeof(object), interfaces);
                var field 
    = tb.DefineField("f"typeof(string), FieldAttributes.Private | FieldAttributes.InitOnly);
                CreateProxyCtor(tb, field);
                
    int methodCount = 0;
                
    foreach (var i in interfaces)
                    
    foreach (var m in i.GetMethods(BindingFlags.Public | BindingFlags.Instance))
                        CreateProxyMethod(tb, m, field, 
    ref methodCount);
                
    return tb.CreateType();
        这里正好可以把泛型约束的遗憾给不上,还可以变本加厉的要求接口上必须标注了ServiceContract,不满足要求的也泛型过来的话,直接给个不支持(对了别忘了接口是可以多继承的,所以实现的时候要实现全部接口,别只实现其中的一两个哦)

        现在,问题变成了如何生成这么一个类型来实现这一个或多个接口,不过,这个问题先放一下,考虑一下,如果已经提取到了地址和Method,以及可能有的Post内容,如何获得返回值哪?问题的解决方案晚上到处都有——用WebClient,先把这个实现了吧(这里仅仅实现Json的):

    对WebClient的封装
            public static TResp PostJson<TReq, TResp>(string baseUri, string uri, string method, TReq data)
            {
                
    string req;
                {
                    DataContractJsonSerializer jsonSerializer 
    = new DataContractJsonSerializer(typeof(TReq));
                    var ms 
    = new MemoryStream();
                    jsonSerializer.WriteObject(ms, data);
                    ms.Seek(
    0L, SeekOrigin.Begin);
                    req 
    = new StreamReader(ms).ReadToEnd();
                }
                ManualResetEvent mre 
    = new ManualResetEvent(false);
                UploadStringCompletedEventArgs acArgs 
    = null;
                WebClient client 
    = new WebClient();
                client.UploadStringCompleted 
    += (sender, e) =>
                {
                    acArgs 
    = e;
                    mre.Set();
                };
                client.Headers[HttpRequestHeader.ContentType] 
    = "application/json; charset=UTF-8";
                client.UploadStringAsync(
    new Uri(baseUri + uri), method, req);
                mre.WaitOne();
                
    if (acArgs.Cancelled)
                    
    throw new TimeoutException();
                
    if (acArgs.Error != null)
                    
    throw new WebException("Rest Error:" + acArgs.Error.Message, acArgs.Error);
                var str 
    = acArgs.Result;
                {
                    DataContractJsonSerializer respSerializer 
    = new DataContractJsonSerializer(typeof(TResp));
                    var ms 
    = new MemoryStream();
                    var sw 
    = new StreamWriter(ms);
                    sw.Write(acArgs.Result);
                    sw.Flush();
                    ms.Seek(
    0L, SeekOrigin.Begin);
                    
    return (TResp)respSerializer.ReadObject(ms);
                }
            }

            
    public static TResp GetJson<TResp>(string baseUri, string uri, string method)
            {
                ManualResetEvent mre 
    = new ManualResetEvent(false);
                DownloadStringCompletedEventArgs acArgs 
    = null;
                WebClient client 
    = new WebClient();
                client.DownloadStringCompleted 
    += (sender, e) =>
                {
                    acArgs 
    = e;
                    mre.Set();
                };
                client.DownloadStringAsync(
    new Uri(baseUri + uri), method);
                mre.WaitOne();
                
    if (acArgs.Cancelled)
                    
    throw new TimeoutException();
                
    if (acArgs.Error != null)
                    
    throw new WebException("Rest Error:" + acArgs.Error.Message, acArgs.Error);
                var str 
    = acArgs.Result;
                {
                    DataContractJsonSerializer respSerializer 
    = new DataContractJsonSerializer(typeof(TResp));
                    var ms 
    = new MemoryStream();
                    var sw 
    = new StreamWriter(ms);
                    sw.Write(acArgs.Result);
                    sw.Flush();
                    ms.Seek(
    0L, SeekOrigin.Begin);
                    
    return (TResp)respSerializer.ReadObject(ms);
                }

        现在可以继续思考前面的问题,我们有了契约的信息,有了如何请求Rest服务的原始代码,剩下的工作就是:

    • 所有参数的名称+他们的值+UriTemplate=实际的Uri+post内容的参数

        看起来就是个数组的查找和字符串替换的事情,问题已经被化解的差不多了,突然想起来一个蛋疼的东西。。。BodyStyle=WebMessageBodyStyle.Wrapped | WrappedRequest | WrapperResponse

        这东西还要给他们生成一个类型才能玩,算了不陪WCF玩了,其它的几种一律不支持

        既然决定不完全支持,那干脆在加几条:

    • 不支持自定义类型转换器 - 不是不能支持,而是嫌其麻烦(统一用ToString代替转换器)
    • 不支持Fault契约 - 还是麻烦
    • 不支持KnownType - 依然是麻烦

        在成功的“减赋”之后,终于真的感觉事情少了很多,现在再去实现那堆接口:

    创建类型-part-2
            private static void CreateProxyCtor(TypeBuilder tb, FieldBuilder field)
            {
                var ctor 
    = tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof(string) });
                var il 
    = ctor.GetILGenerator();
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Call, 
    typeof(object).GetConstructor(Type.EmptyTypes));
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Stfld, field);
                il.Emit(OpCodes.Ret);
            }

            
    private static void CreateProxyMethod(TypeBuilder tb, MethodInfo mi, FieldBuilder field, ref int methodCount)
            {
                var m 
    = tb.DefineMethod(
                    
    "M" + (++methodCount).ToString(),
                    MethodAttributes.Private 
    | MethodAttributes.Virtual | MethodAttributes.Final,
                    mi.ReturnType,
                    (from pi 
    in mi.GetParameters() select pi.ParameterType).ToArray());
                
    if (!Attribute.IsDefined(mi, typeof(OperationContractAttribute)))
                {
                    CreateEmptyMethod(m);
                }
                
    else
                {
                    
    string template = null;
                    
    string method = null;
                    {
                        var wga 
    = (WebGetAttribute)Attribute.GetCustomAttribute(mi, typeof(WebGetAttribute));
                        
    if (wga != null)
                        {
                            template 
    = wga.UriTemplate;
                            method 
    = "GET";
                        }
                    }
                    {
                        var wia 
    = (WebInvokeAttribute)Attribute.GetCustomAttribute(mi, typeof(WebInvokeAttribute));
                        
    if (wia != null)
                        {
                            template 
    = wia.UriTemplate;
                            method 
    = wia.Method ?? "POST";
                        }
                    }
                    
    if (template == null)
                    {
                        CreateEmptyMethod(m);
                    }
                    
    else
                    {
                        CreateCoreMethod(m, mi, template, method, field);
                    }
                }
                tb.DefineMethodOverride(m, mi);
            }

            
    private static void CreateCoreMethod(MethodBuilder m, MethodInfo mi, string template, string method, FieldBuilder field)
            {
                var il 
    = m.GetILGenerator();
                il.DeclareLocal(
    typeof(string));
                il.Emit(OpCodes.Ldstr, template);
                il.Emit(OpCodes.Stloc_0);
                var pis 
    = mi.GetParameters();
                
    int postParameter = -1;
                
    for (int i = 0; i < pis.Length; i++)
                {
                    
    if (template.Contains("{" + pis[i].Name + "}"))
                    {
                        
    // template = template.Replace("{?.Name}", HttpUtility.UrlEncode(?.ToString()));
                        il.Emit(OpCodes.Ldloc_0);
                        il.Emit(OpCodes.Ldstr, 
    "{" + pis[i].Name + "}");
                        il.Emit(OpCodes.Ldarg, i 
    + 1);
                        
    if (pis[i].ParameterType.IsValueType)
                            il.Emit(OpCodes.Box, pis[i].ParameterType);
                        il.Emit(OpCodes.Callvirt, 
    typeof(object).GetMethod("ToString"));
                        il.Emit(OpCodes.Call, 
    typeof(HttpUtility).GetMethod("UrlEncode"));
                        il.Emit(OpCodes.Call, 
    typeof(string).GetMethod("Replace"new Type[] { typeof(string), typeof(string) }));
                        il.Emit(OpCodes.Stloc_0);
                    }
                    
    else
                    {
                        
    if (postParameter > 0)
                            
    throw new NotSupportedException();
                        postParameter 
    = i;
                        
    if (method == "GET")
                            method 
    = "POST";
                    }
                }
                
    if (postParameter == -1)
                {
                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Ldfld, field);
                    il.Emit(OpCodes.Ldloc_0);
                    il.Emit(OpCodes.Ldstr, method);
                    il.Emit(OpCodes.Call, 
    typeof(RestClient).GetMethod("GetJson").MakeGenericMethod(mi.ReturnType));
                }
                
    else
                {
                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Ldfld, field);
                    il.Emit(OpCodes.Ldloc_0);
                    il.Emit(OpCodes.Ldstr, method);
                    il.Emit(OpCodes.Ldarg, postParameter 
    + 1);
                    il.Emit(OpCodes.Call, 
    typeof(RestClient).GetMethod("PostJson").MakeGenericMethod(pis[postParameter].ParameterType, mi.ReturnType));
                }
                il.Emit(OpCodes.Ret);
            }

            
    private static void CreateEmptyMethod(MethodBuilder m)
            {
                var il 
    = m.GetILGenerator();
                
    if (m.ReturnType != typeof(void))
                {
                    
    if (m.ReturnType.IsValueType)
                    {
                        il.DeclareLocal(m.ReturnType);
                        il.Emit(OpCodes.Ldarga_S, (
    byte)0);
                        il.Emit(OpCodes.Initobj, m.ReturnType);
                    }
                    
    else
                    {
                        il.Emit(OpCodes.Ldnull);
                    }
                }
                il.Emit(OpCodes.Ret);
        这里,做了几件额外的事情:

    • 如果接口里面有非契约的方法 - 用返回默认值来实现(当然也可以修改成throw)
    • 如果发现参数中多余1个未在UriTemplate中出现的参数,直接报错 - 因为不支持Wrapper
    • 如果方法为GET并且带Post信息,将方法更改为POST

        不过,还有几件事情没做:

    • 参数的值为null时会抛空引用 - 有空的话可以自己改,我这里反正都传空字符串的。。。而且,在后面的外壳部分也可以包装掉

     使用代理类

        类看看这个代理类怎么用:

    var client = new RestClient<ISample>("http://127.0.0.1:12345/");
    var result 
    = client.Channel.Echo("Zhenway""Hello world!");

        是不是有点调用WebService的感觉?

        不过还有点欠缺,多了个Channel,而且随便调用那个方法,都要出现这个Channel,感觉不爽

        干脆再学一次WebService,来个外壳(当然,也可以直接拿着Channel去干活):

        public class SampleClient
            : RestClient
    <ISample>, ISample
        {
            
    public SampleClient()
                : 
    base("the default uri") { }
            
    public SampleClient(string uri)
                : 
    base(uri) { }
            
    public EchoResponse Echo(string name, string message)
            {
                
    return Channel.Echo(name, message);
            }
        }

        这样用起来就会舒服一些(而且也可以做些额外的工作)

    var client = new SampleClient("http://127.0.0.1:12345/");
    var result 
    = client.Echo("Zhenway""Hello world!");

        这样明显更舒服一些。


     暗藏的危机

        看起来万事俱备,但是实际上一使用才发现神马都是浮云。

        如果在用户界面上调用client.Echo,那么就算等上一万年,也拿不到结果,而且整个浏览器也会因为Sliverlight插件而出现假死。

        为什么会这样哪?分析一下原因:可以发现在实现PostJson和GetJson方法中的

    mre.WaitOne();

        永远等不到被Set的那一刻,看看代码逻辑似乎没什么问题,不过仔细想想,就可以发现问题:

    • 首先,Sliverlight的UI线程是基于消息的
    • 其次,webclient的回调事件是会回到请求Async方法的同步上下文上的

        那么,是不是发现问题了,UI线程请求了client.Echo,client.Echo请求了channel.Echo,channel.Echo请求了WebClient的DownloadStringAsync,然后等待mre信号。

        WebClient在开始DownloadStringAsync时,抓取了当时的同步上下文,也就是UI的同步上下文,再开始异步下载,下载完成时,告诉同步上下文,可以执行回调事件了。

        而此时,同步上下文-也就是UI线程的消息处理机制却无法工作,之前的一个消息尚未处理完成(被迫在等待mre的信号中),于是出现了死锁。

        发现了问题所在,要排除问题,也就很简单了,只要破坏这个死锁中的一个环节,自然就能让UI活起来。

        最简单的方式是从入口下手:UI线程不直接请求client.Echo,而是修改成UI线程新开个线程(直接用线程池就可以了),请求client.Echo,这样就把同步上下文从UI线程的上下文切换到了另一个上下文。

        来看看修改后的代码:

    ThreadPool.QueueUserWorkItem(_ =>
    {
        var client 
    = new SampleClient("http://127.0.0.1:12345/");
        var result 
    = client.Echo("Zhenway""Hello world!");
        
    // do something ...
    });

        看起来不错,不过要修改界面的话(90%的情况下都要修改界面的吧),还要回归到UI线程,干脆根据Silverlight的基于事件的异步方式,重写我们的Client

    重写后的Client
            public void EchoAsync(string name, string message)
            {
                EchoAsync(name, message, 
    null);
            }

            
    public void EchoAsync(string name, string message, object userState)
            {
                var sc 
    = SynchronizationContext.Current;
                ThreadPool.QueueUserWorkItem(_ 
    =>
                {
                    EchoResponse result 
    = null;
                    Exception ex1 
    = null;
                    
    try
                    {
                        result 
    = Channel.Echo(name, message);
                    }
                    
    catch (Exception ex)
                    {
                        ex1 
    = ex;
                    }
                    
    try
                    {
                        var handler 
    = EchoCompleted;
                        
    if (handler != null)
                            sc.Post(__ 
    => handler(thisnew EchoAsyncCompletedEventArgs(result, ex1, false, userState)), null);
                    }
                    
    catch (Exception) { }
                });
            }

            
    public event EventHandler<EchoAsyncCompletedEventArgs> EchoCompleted;

            
    public class EchoAsyncCompletedEventArgs
                : AsyncCompletedEventArgs
            {
                
    public EchoAsyncCompletedEventArgs(EchoResponse response,
                    Exception error, 
    bool cancelled, object userState)
                    : 
    base(error, cancelled, userState)
                {
                    Response 
    = response;
                }
                
    public EchoResponse Response { getprivate set; } 

        这样,Echo就可以很好的工作了(在UI线程中调用EchoAsync后,EchoCompleted事件也会在UI线程中执行):

    var client = new SampleClient("http://127.0.0.1:12345/");
    client.EchoCompleted 
    += (sender, e) =>
    {
      
    // update UI elements, here.
    };
    var result 
    = client.EchoAsync("Zhenway""Hello world!");

        是不是看起来还不错,不过,想想如果一个服务有10来个方法,每个这么搞一下,这个量依然很高。。。


    再次使用动态代理

        遇到这类重复性工作,第一反应,就是再动态一把,把这些重复工作消除成一次性的静态行为,这里就可以通过再次动态代理,来消除原来动态代理的种种不爽之处。

        只不过,这次的动态代理不是用动态生成类型,而是换dynamic,也给大家换换口味

        这次动态代理的功能自然是完成“重写后的Client”的功能,这里分两大块,一个是异步方法,一个是事件

    动态异步代理
        public class DynamicAsyncClient
            : DynamicObject
        {

            
    private readonly object m_channel;
            
    private readonly HashSet<string> m_methods;
            
    private readonly Dictionary<string, DynamicAsyncCompletedEventHandler> m_events;

            
    public DynamicAsyncClient(object channel)
            {
                
    if (channel == null)
                    
    throw new ArgumentNullException("channel");
                m_channel 
    = channel;
                m_methods 
    = new HashSet<string>(from m in channel.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
                                                select m.Name);
                m_events 
    = new Dictionary<string, DynamicAsyncCompletedEventHandler>();
            }

            
    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
            {
                
    if (m_methods.Contains(binder.Name))
                {
                    result 
    = m_channel.GetType().InvokeMember(binder.Name, BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance, null, m_channel, args);
                    
    return true;
                }
                
    if (binder.Name.EndsWith("Completed"))
                {
                    
    lock (m_events)
                        m_events[binder.Name] 
    = (DynamicAsyncCompletedEventHandler)args[0];
                    result 
    = null;
                    
    return true;
                }
                
    if (binder.Name.EndsWith("Async"))
                {
                    
    try
                    {
                        AsyncInvoke(binder.Name.Remove(binder.Name.Length 
    - "Async".Length), args, null);
                        result 
    = null;
                        
    return true;
                    }
                    
    catch (MissingMethodException) { }
                }
                
    return base.TryInvokeMember(binder, args, out result);
            }

            
    private void AsyncInvoke(string name, object[] args)
            {
                var sc 
    = SynchronizationContext.Current;
                ThreadPool.QueueUserWorkItem(_ 
    =>
                {
                    dynamic result 
    = null;
                    Exception ex1 
    = null;
                    
    try
                    {
                        result 
    = m_channel.GetType().InvokeMember(name, BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance, null, m_channel, args);
                    }
                    
    catch (Exception ex)
                    {
                        ex1 
    = ex;
                    }
                    
    try
                    {
                        DynamicAsyncCompletedEventHandler handler;
                        
    lock (m_events)
                            
    if (m_events.TryGetValue(name + "Completed"out handler))
                                sc.Post(__ 
    => handler(thisnew DynamicAsyncCompletedEventArgs(result, ex1, falsenull)), null);
                    }
                    
    catch (Exception) { }
                });
            }

        }

        
    public delegate void DynamicAsyncCompletedEventHandler(object sender, DynamicAsyncCompletedEventArgs e);

        
    public class DynamicAsyncCompletedEventArgs
            : AsyncCompletedEventArgs
        {

            
    public DynamicAsyncCompletedEventArgs(dynamic response, Exception error, bool cancelled, object userState)
                : 
    base(error, cancelled, userState)
            {
                Response 
    = response;
            }

            
    public dynamic Response { getprivate set; }

        再删除SampleClient中前面添加的两个异步方法后,来看看如何跑起来:

    var client = new DynamicAsyncClient(new SampleClient("http://127.0.0.1:12345/"));
    client.EchoCompleted 
    += (sender, e) =>
    {
      
    // update UI elements, here.
      textbox1.Text = e.Response.Name;
      textbox2.Text 
    = e.Response.Message;
    };
    var result 
    = client.EchoAsync("Zhenway""Hello world!");

    小节

        到这里,整个主题也告一段落,当然这里面可以改良的东西还有很多,例如,对WCF的Rest服务更多的支持,动态异步代理支持userState等,众多改良尚可去做,不过这些暂时省略了

  • 相关阅读:
    导包Scanner类---Java
    标准类组成---Java
    Educational Codeforces Round 54 (Rated for Div. 2) A. Minimizing the String
    [专题总结] 二分搜索专题
    [最大权森林] [POJ] 3723 Conscription
    [模板] 快读快写
    [模板] 常用数论 -> gcd、exgcd、乘法逆元、快速幂、快速乘、筛素数、快速求逆元、组合数
    [模板] 堆 heap
    牛客小白月赛8 F-数列操作 (有序表)
    [模板] 字符串处理函数
  • 原文地址:https://www.cnblogs.com/vwxyzh/p/2116481.html
Copyright © 2020-2023  润新知