最近在写一个SL的小工具,用于图形化编辑一些东西。刚好调用的服务是WCF的Rest方式,于是就碰到了在SL里面直接调用Rest服务的问题,本来Rest服务就是只要有url和内容就可以直接调用的,事实上如果搜索该主题,也可以得到漫山遍野的WebClient方案。不过看看Framework下的WebChannelFactory<TChannel>这个类(这个类型在SL下面不支持...),又感觉用WebClient方式太寒酸了点。。。
这里讨论的前提是:
- 已经有Rest服务的契约
- 不想自己去拼请求
期望的结果应该是类似与调用WebService的方式。
然后,就慢慢开始达成我们的目标吧。
第一步,Copy契约。。。废话,而且只要会按Ctrl+C和Ctrl+V的人都会做,问题是Copy过来的契约不能直接用,SL没提供这样的类,为了方便示例,就准备一个Sample契约:
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方式提供的,问题是形式,如果全部人工翻译,这个代价有点大,更重要的是:我很懒,能只写一次的代码,绝不写两次,有任何共性的代码结构+无限可能的类型组合,都倾向于使用代码生成来实现。
代理类代码准备
首先,准备好基本的代码:
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; } }
那为什么要给一个BaseUri哪?别忘了,契约接口里面的(例如:ISample)只提供了相对地址,没有基地址的话,根本找不到终结点。
然后,开始添加类型生成代码:
{
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();
现在,问题变成了如何生成这么一个类型来实现这一个或多个接口,不过,这个问题先放一下,考虑一下,如果已经提取到了地址和Method,以及可能有的Post内容,如何获得返回值哪?问题的解决方案晚上到处都有——用WebClient,先把这个实现了吧(这里仅仅实现Json的):
{
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 - 依然是麻烦
在成功的“减赋”之后,终于真的感觉事情少了很多,现在再去实现那堆接口:
{
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 result = client.Channel.Echo("Zhenway", "Hello world!");
是不是有点调用WebService的感觉?
不过还有点欠缺,多了个Channel,而且随便调用那个方法,都要出现这个Channel,感觉不爽
干脆再学一次WebService,来个外壳(当然,也可以直接拿着Channel去干活):
: 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 result = client.Echo("Zhenway", "Hello world!");
这样明显更舒服一些。
暗藏的危机
看起来万事俱备,但是实际上一使用才发现神马都是浮云。
如果在用户界面上调用client.Echo,那么就算等上一万年,也拿不到结果,而且整个浏览器也会因为Sliverlight插件而出现假死。
为什么会这样哪?分析一下原因:可以发现在实现PostJson和GetJson方法中的
永远等不到被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线程的上下文切换到了另一个上下文。
来看看修改后的代码:
{
var client = new SampleClient("http://127.0.0.1:12345/");
var result = client.Echo("Zhenway", "Hello world!");
// do something ...
});
看起来不错,不过要修改界面的话(90%的情况下都要修改界面的吧),还要回归到UI线程,干脆根据Silverlight的基于事件的异步方式,重写我们的Client
{
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线程中执行):
client.EchoCompleted += (sender, e) =>
{
// update UI elements, here.
};
var result = client.EchoAsync("Zhenway", "Hello world!");
是不是看起来还不错,不过,想想如果一个服务有10来个方法,每个这么搞一下,这个量依然很高。。。
再次使用动态代理
遇到这类重复性工作,第一反应,就是再动态一把,把这些重复工作消除成一次性的静态行为,这里就可以通过再次动态代理,来消除原来动态代理的种种不爽之处。
只不过,这次的动态代理不是用动态生成类型,而是换dynamic,也给大家换换口味
这次动态代理的功能自然是完成“重写后的Client”的功能,这里分两大块,一个是异步方法,一个是事件
: 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中前面添加的两个异步方法后,来看看如何跑起来:
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等,众多改良尚可去做,不过这些暂时省略了