最近在写一个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等,众多改良尚可去做,不过这些暂时省略了

posted on 2011-08-13 15:16  Zhenway  阅读(1099)  评论(3编辑  收藏  举报