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 namestr/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 nameurl/param /// param nameinput/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.DeserializeTOut(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 namestr/param private void CallViaDelegate(string str) { Funcstring, string, string func HttpWebRequestHelper.SendHttpRequest; func.BeginInvoke(ServiceUrl, str, CallViaDelegateCallback, str); } private void CallViaDelegateCallback(IAsyncResult ar) { string str (string)ar.AsyncState; Funcstring, string, string func (ar as AsyncResult).AsyncDelegate as Funcstring, 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)); } }说到BeginInvokeEndInvoke就不得不停下来看一下委托的本质。为了便于理解委托我定义一个简单的委托public delegate string MyFunc(int num, DateTime dt);我们再来看一下这个委托在编译后的程序集中是个什么样的委托被编译成一个新的类型拥有BeginInvokeEndInvokeInvoke这三个方法。前二个方法的组合使用便可实现异步调用。第三个方法将以同步的方式调用。 其中BeginInvoke方法的最后二个参数用于回调其它参数则与委托的包装方法的输入参数是匹配的。 EndInvoke的返回值与委托的包装方法的返回值匹配。注意委托的BeginInvoke方法在调用后也会返回一个IAsyncResult对象类型为System.Runtime.Remoting.Messaging.AsyncResult。在IDE窗口中我们也可以在智能提示中看到如下提示信息因此我们也可以不使用回调方法而是直接使用它的返回值并在一个【恰当的时候】结束异步调用其实是以同步的方式并行执行任务。如下代码所示private void CallViaDelegate_X2(string str) { Funcstring, 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 namestr/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 ActionTIn, TOut, Exception, object Callback; public object State; public HttpWebRequest Request; public JavaScriptSerializer Jss; } /// summary /// 异步调用服务 /// /summary /// param nameurl/param /// param nameinput/param /// param namecallback服务调用完成后的回调委托用于处理调用结果/param /// param namestate/param public static void SendHttpRequestAsync(string url, TIn input, ActionTIn, 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.DeserializeTOut(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的设计者会抛出一个异常这也是有可能的。 为了支持异步我的包装方法也是通过回调的方式来设计