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 {  get set ; }
        [DataMember]
        
public   string  Message {  get set ; }
    }

    第二步,实现契约,等等,这里是客户端怎么冒出来个实现契约了?这里实现契约,实际上是指做一个代理类,只不过平时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( this new  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 {  get private   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( this new  DynamicAsyncCompletedEventArgs(result, ex1,  false null )),  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 {  get private   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等,众多改良尚可去做,不过这些暂时省略了

你可能感兴趣的:(REST)