在.net framework中,许多I/O操作(文件I/O操作以及网络I/O)都提供异步版本的API,我们可以直接使用这些API来达到异步调用的目的。 在今天的示例中,发送HTTP请求的API中,就支持异步操作,我将演示使用这些异步API的操作过程。
在客户端,我将使用以下代码完成异步调用过程:
////// 使用IAsyncResult接口实现异步调用 /// /// 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()是个简单的包装方法,最终异步操作的实现代码如下:
////// 用于所有回调状态的数据类 /// private class MyCallbackParam { public TIn InputData; public Action<TIn, TOut, Exception, object> Callback; public object State; public HttpWebRequest Request; public JavaScriptSerializer Jss; } ////// 异步调用服务 /// /// /// /// 服务调用完成后的回调委托,用于处理调用结果 /// 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的设计者会抛出一个异常,这也是有可能的。 为了支持异步,我的包装方法也是通过回调的方式来设计的。这些都是异步设计的关键。
当某个异步操作过程需要多次调用时,该如何知道哪些步骤必须以异步形式调用呢? 比如,前面演示的发送HTTP请求的过程,我该如何知道要调用BeginGetRequestStream,BeginGetResponse这二个异步方法呢? 对于这个问题,没有一个明确的答案,因为在这方面,并没有一个规范或者约定,要根据相应组件的具体实现过程而定。 不过,通常每个支持异步组件所提供的API接口都会以BeginXxxxxx,EndXxxxxx的形式表示支持异步操作,并提供一个Xxxxxx的同步版本。 这里我可以提供一个小经验:逐个将一些关键步骤的同步调用替换成异步调用,直到实现异步过程为止。 再补充一句:对于微软提供的组件,查阅MSDN对于该方法的说明一般是可以找到线索的。
或许有些人不想定义这些回调方法,以及用于维护回调的状态类型,而选择闭包的方式。这种方法就技术的实现而言,也是可行的。 这里我就不演示了,因为我不喜欢搞大的闭包。 如果您喜欢闭包的方式,也请不要批我,每个人有每个人的喜好。
异常的处理:与委托的异步调用一样,此时也只能在调用EndXxxxxx时捕获异常。 不过,对于一个有着多个异步调用步骤的过程来说,异常的处理将要分阶段处理。
与委托异步调用的差别: 由于委托的BeginInvoke调用也能返回IAsyncResult,因此前面演示的委托的【同步并行执行】方式也可以在BeginXxxxxx/EndXxxxxx所支持的过程中使用。 但本小节所说的异步与委托的异步还是有差别,最重要的差别在于委托的异步调用阻塞发生在线程池的工作线程, 而直接使用基于IAsyncResult的异步,阻塞发生在线程池的I/O完成线程。这二种不同的线程对于不同的编程模型来说,意义是非常重大的。
小结:如果某个组件提供BeginXxxxxx/EndXxxxxx方法,通常表示可以支持异步操作,只要我们正确地调用它们就可以实现异步。