C#客户端的异步操作
我一直认为,对于服务框架来说,最重要的事是将一个C#方法公开为一个服务方法,供远程客户端调用。 因此,我上篇博客中演示的服务框架显然已经可以简单地完成这个功能。 不过,目前如果要使用这个服务框架,客户端还不够方便: 总不能让使用者自己写代码发送HTTP请求吧?嗯,基于我的服务框架的一些约定,实现这个包装不是问题, 但前面提到的IDE能生成异步调用的代理类,这个功能就必须实现了,否则我认为太不完美了。
我是个追求完美的人,而异步又是一个很重要的功能,我自然不能不实现它。今天我就继续上篇博客的内容,来谈谈客户端的各种异步实现方法。
说明:异步调用服务却与服务端无关,属于客户端的事情。此处的客户端是相对服务端来说的,它可以是任何类型的应用程序。今天的主要话题是关于客户端的异步调用。
插个问题,为什么要实现异步,异步有什么好处?
答:简单来说,对于服务程序而言,异步处理可以提高吞吐量, 对于WinForm这类桌面客户端程序而言,将耗时任务采用异步实现可以改善用户体验,而且任务可以并行执行,提高响应速度。
回到顶部
示例项目介绍
今天我将演示如何在客户端中,以不同的异步方式调用一个服务方法。 为了让演示更有实战性,我已准备了一个完整的示例项目。如下图:
| 整个示例由四个小项目构成: 1. WebSite1 是一个用于发布服务的网站(也包含一些Asp.net异步的示例)。 2. MySimpleServiceClient是一个类库项目,包含了我封装的客户端类。 3. 服务的实现放在ServiceClassLibrary项目中。 4. WindowsFormsApplication1 是调用服务的客户端,这是一个WinForm项目。 之所以要选WinForm做为客户端演示,是因为WinForm编程模型中对操作UI方面有更多的线程要求, 如果有调用延迟也会特别明显,因此WinForm编程模型对异步的处理更为复杂。 为了能让演示更有意义,我宁可选择WinForm程序做为服务的客户端,而不是不负责的选择Console程序。 事实上,演示代码也适用于其它编程模型。 |
服务类的代码如下:
/// <summary> /// 要做为服务发布的服务类,其实就是一个普通的C#类,加了一些Attribute而已。 /// 所有幕后的工作,全由服务框架来实现,关于服务框架请参考我的博客: /// 【用Asp.net写自己的服务框架】 /// http://www.cnblogs.com/fish-li/archive/2011/09/05/2168073.html /// </summary> [MyService] public class DemoService { [MyServiceMethod] public static string ExtractNumber(string str) { // 延迟3秒,模拟一个长时间的调用操作,便于客户演示异步的效果。 System.Threading.Thread.Sleep(3000); if( string.IsNullOrEmpty(str) ) return "str IsNullOrEmpty."; return new string((from c in str where Char.IsDigit(c) orderby c select c).ToArray()); } }服务方法的功能很简单:从一个字符串中找到所有数字,然后排序输出。
客户端运行界面如下:
回到顶部
同步调用服务
为了更好的理解异步调用,也为了和后面的异步调用做个比较,这里先示例如何采用同步的方式调用服务。代码如下:
/// <summary> /// 同步调用服务,此时界面应该会【卡住】。 /// </summary> /// <param name="str"></param> private void SyncCallService(string str) { try { string result = HttpWebRequestHelper.SendHttpRequest(ServiceUrl, str); ShowResult(string.Format("{0} => {1}", str, result)); } catch( Exception ex ) { ShowResult(string.Format("{0} => Error: {1}", str, ex.Message)); } }其中,HttpWebRequestHelper.SendHttpRequest()最终调用的代码如下:
/// <summary> /// 同步调用服务 /// </summary> /// <param name="url"></param> /// <param name="input"></param> /// <returns></returns> public static TOut SendHttpRequest(string url, TIn input) { if( string.IsNullOrEmpty(url) ) throw new ArgumentNullException("url"); if( input == null ) throw new ArgumentNullException("input"); // 为了简单,这里仅使用JSON序列化方式 JavaScriptSerializer jss = new JavaScriptSerializer(); string jsonData = jss.Serialize(input); // 创建请求对象 HttpWebRequest request = CreateHttpWebRequest(url, "json"); // 发送请求数据 using( BinaryWriter bw = new BinaryWriter(request.GetRequestStream()) ) { bw.Write(DefaultEncoding.GetBytes(jsonData)); } // 获取响应对象,并读取响应内容 using( HttpWebResponse response = (HttpWebResponse)request.GetResponse() ) { string responseText = ReadResponse(response); return jss.Deserialize<TOut>(responseText); } }以上代码,也就是我前面所说的客户端的包装工具类了。有了它,就可以很容易地调用我的服务了。
代码中的CreateHttpWebRequest()以及ReadResponse()都很简单,而且与异步一点关系也没有,就不贴出了,可以在本文结尾处下载它们。
回到顶部
异步接口介绍
在开始介绍各种异步实现方法之前,有必要先明说一下: 在.net中,所有异步都是基于IAsyncResult这个最基础的接口。只是不同的API在具体实现时,创建的IAsyncResult实例不同, 以及封装方式不同而已。IAsyncResult的接口定义如下:
public interface IAsyncResult { // 获取用户定义的对象,它限定或包含关于异步操作的信息。 // 通常在调用BeginXXXX方法时传入对象,供回调方法时恢复之前的状态。 object AsyncState { get; } // 获取用于等待异步操作完成的 System.Threading.WaitHandle。 // 我们可以调用它的WaitOne()方法等待调用完成。 WaitHandle AsyncWaitHandle { get; } // 获取异步操作是否同步完成的指示。 // 如果异步操作同步完成,则为 true;否则为 false。 bool CompletedSynchronously { get; } // 获取异步操作是否已完成的指示。 // 如果操作完成则为 true,否则为 false。 bool IsCompleted { get; } }下面我们再来看一下各种异步方法的实现方式。
回到顶部
1. 委托异步调用
对于任何一个方法,.net默认是采用同步的方式去调用,即:在调用时,后面的代码一直要等待调用完成后才能继续执行。
不过,我们可以使用委托,将一个方法按异步的方式去调用。对于前面的同步调用代码,我可以使用委托来完成异步的调用:
/// <summary> /// 委托异步调用 /// </summary> /// <param name="str"></param> private void CallViaDelegate(string str) { Func<string, string, string> func = HttpWebRequestHelper.SendHttpRequest; func.BeginInvoke(ServiceUrl, str, CallViaDelegateCallback, str); } private void CallViaDelegateCallback(IAsyncResult ar) { string str = (string)ar.AsyncState; Func<string, string, string> func = (ar as AsyncResult).AsyncDelegate as Func<string, string, string>; try { // 如果有异常,会在这里被重新抛出。 string result = func.EndInvoke(ar); ShowResult(string.Format("{0} => {1}", str, result)); } catch( Exception ex ) { ShowResult(string.Format("{0} => Error: {1}", str, ex.Message)); } }说到BeginInvoke,EndInvoke就不得不停下来看一下委托的本质。为了便于理解委托,我定义一个简单的委托:
public delegate string MyFunc(int num, DateTime dt);
我们再来看一下这个委托在编译后的程序集中是个什么样的:
委托被编译成一个新的类型,拥有BeginInvoke,EndInvoke,Invoke这三个方法。前二个方法的组合使用便可实现异步调用。第三个方法将以同步的方式调用。 其中BeginInvoke方法的最后二个参数用于回调,其它参数则与委托的包装方法的输入参数是匹配的。 EndInvoke的返回值与委托的包装方法的返回值匹配。
注意:委托的BeginInvoke方法在调用后,也会返回一个IAsyncResult对象(类型为:System.Runtime.Remoting.Messaging.AsyncResult)。在IDE窗口中,我们也可以在智能提示中看到如下提示信息:
因此,我们也可以不使用回调方法,而是直接使用它的返回值,并在一个【恰当的时候】结束异步调用(其实是以同步的方式并行执行任务)。如下代码所示:
private void CallViaDelegate_X2(string str) { Func<string, string, string> func = HttpWebRequestHelper.SendHttpRequest; IAsyncResult ar = func.BeginInvoke(ServiceUrl, str, null, null); // 在此执行其它的计算操作, // 也可以在此再发起另一个异步调用。 string result = func.EndInvoke(ar); //...处理结果 ShowResult(string.Format("{0} => {1}", str, result)); }小结:使用委托的异步调用方式很简单,只要用一个方法创建一个委托对象,然后调用BeginInvoke方法就可以了。 对BeginInvoke()方法的调用是以异步方式进行,但对于调用EndInvoke()方法则是以同步方式进行的(如果任务没有执行完,将会发生阻塞)。如果您想实现无阻塞的异步, 可以在调用BeginInvoke()方法时指定回调方法,那么在异步完成时,回调方法将被调用,此时对EndInvoke()的调用将不会阻塞线程。
异常的处理:在委托的异步实现中,由于BeginInvoke的调用是无阻塞的,此时方法将立即返回,而异常则是在任务执行过程中引发的, 因此,异常只能在调用EndInvoke时重新抛出,所以,也只能在调用EndInvoke时捕获异常。如果采用委托的方式异步调用某个没有返回值的方法, 那么,当你不调用EndInvoke时,你是不知道是否有异常抛出的。
注意:委托的异步调用是将任务交给线程池的工作线程来执行的。 证明这个说法很简单,可以在任务中加以如下代码,然后设置断点观察变量的取值即可:
bool isThreadPoolThread = System.Threading.Thread.CurrentThread.IsThreadPoolThread;
回到顶部
2. 使用IAsyncResult接口实现异步调用
在.net framework中,许多I/O操作(文件I/O操作以及网络I/O)都提供异步版本的API,我们可以直接使用这些API来达到异步调用的目的。 在今天的示例中,发送HTTP请求的API中,就支持异步操作,我将演示使用这些异步API的操作过程。
在客户端,我将使用以下代码完成异步调用过程:
/// <summary> /// 使用IAsyncResult接口实现异步调用 /// </summary> /// <param name="str"></param> private void CallViaIAsyncResult(string str) { HttpWebRequestHelper.SendHttpRequestAsync(ServiceUrl, str, CallViaIAsyncResultCallback, null); } private void CallViaIAsyncResultCallback(string str, string result, Exception ex, object state) { if( ex == null ) ShowResult(string.Format("{0} => {1}", str, result)); else ShowResult(string.Format("{0} => Error: {1}", str, ex.Message)); }其中HttpWebRequestHelper.SendHttpRequestAsync()是个简单的包装方法,最终异步操作的实现代码如下:
/// <summary> /// 用于所有回调状态的数据类 /// </summary> private class MyCallbackParam { public TIn InputData; public Action<TIn, TOut, Exception, object> Callback; public object State; public HttpWebRequest Request; public JavaScriptSerializer Jss; } /// <summary> /// 异步调用服务 /// </summary> /// <param name="url"></param> /// <param name="input"></param> /// <param name="callback">服务调用完成后的回调委托,用于处理调用结果</param> /// <param name="state"></param> public static void SendHttpRequestAsync(string url, TIn input, Action<TIn, TOut, Exception, object> callback, object state) { if( string.IsNullOrEmpty(url) ) throw new ArgumentNullException("url"); if( input == null ) throw new ArgumentNullException("input"); if( callback == null ) throw new ArgumentNullException("callback"); // 创建请求对象 HttpWebRequest request = CreateHttpWebRequest(url, "json"); // 记录必要的回调参数 MyCallbackParam cp = new MyCallbackParam { Callback = callback, InputData = input, State = state, Request = request, }; // 开始异步写入请求数据 request.BeginGetRequestStream(AsyncWriteRequestStream, cp); // 虽然BeginGetRequestStream()可以返回一个IAsyncResult对象, // 但我却不想返回这个对象,因为整个过程需要二次异步。 } private static void AsyncWriteRequestStream(IAsyncResult ar) { // 取出回调前的状态参数 MyCallbackParam cp = (MyCallbackParam)ar.AsyncState; try { // 为了简单,这里仅使用JSON序列化方式 JavaScriptSerializer jss = new JavaScriptSerializer(); string jsonData = jss.Serialize(cp.InputData); cp.Jss = jss; // 结束写入数据的操作 using( BinaryWriter bw = new BinaryWriter(cp.Request.EndGetRequestStream(ar)) ) { bw.Write(DefaultEncoding.GetBytes(jsonData)); } // 开始异步向服务器发起请求 cp.Request.BeginGetResponse(GetResponseCallback, cp); } catch( Exception ex ) { cp.Callback(cp.InputData, default(TOut), ex, cp.State); } } private static void GetResponseCallback(IAsyncResult ar) { // 取出回调前的状态参数 MyCallbackParam cp = (MyCallbackParam)ar.AsyncState; try { // 读取服务器的响应 using( HttpWebResponse response = (HttpWebResponse)cp.Request.EndGetResponse(ar) ) { string responseText = ReadResponse(response); TOut result = cp.Jss.Deserialize<TOut>(responseText); // 返回结果,通过回调用户的回调方法来完成。 cp.Callback(cp.InputData, result, null, cp.State); } } catch( Exception ex ) { cp.Callback(cp.InputData, default(TOut), ex, cp.State); } }注意:在SendHttpRequestAsync方法的实现过程中,需要发起二次异步调用:BeginGetRequestStream, BeginGetResponse 。自然地, 也会引起二次回调,二次EndXXXXX()方法的调用。为了能在回调过程中,维持一些必要的状态参数,我定义了一个私有类型MyCallbackParam , 它包含了所有回调过程中所需要的中间状态。这里尤其要注意的是:如果某个异步操作过程需要多次异步调用,那么每个步骤都要求是异步的, 也就是要【一路异步到底】。如果中间任何一个步骤不是异步调用的,那么整个过程将不会是异步的,甚至某些API的设计者会抛出一个异常,这也是有可能的。 为了支持异步,我的包装方法也是通过回调的方式来设计
