《C# 爬虫 破境之道》:第一境 爬虫原理 — 第四节:同步与异步请求方式
前两节,我们对WebRequest和WebResponse这两个类做了介绍,但两者还相对独立。本节,我们来说说如何将两者结合起来,方式有哪些,有什么不同。
1.4.1 说结合,无非就是我们如何发送一个Request以及如何得到一个Response。
WebRequest提供了三组方法(Framework 4.6.1+,其它版本没去细看)
[Code 1.4.1]
1 public virtual WebResponse GetResponse(); 2 ---------------------------------------------------------------------------------- 3 public virtual IAsyncResult BeginGetResponse(AsyncCallback callback, object state); 4 public virtual WebResponse EndGetResponse(IAsyncResult asyncResult); 5 ---------------------------------------------------------------------------------- 6 public virtual Task<WebResponse> GetResponseAsync();
这三组方法,都是用来获取Response的,而且,WebRequest也是在调用它们的时候,发送出去的。
这就是关于如何结合的全部内容,没有很复杂,复杂的内容在发送前的准备工作和接收后的处理工作:)
1.4.2 说方式,无非就是同步和异步。
还是看[Code 1.4.1],第1行,是同步方式,第3、4、6行是异步方式;不过除了这两组外,WebRequest还提供了对RequestStream获取的同步与异步操作方式:
[Code 1.4.2]
1 public virtual Stream GetRequestStream(); 2 ---------------------------------------------------------------------------------- 3 public virtual IAsyncResult BeginGetRequestStream(AsyncCallback callback, object state); 4 public virtual Stream EndGetRequestStream(IAsyncResult asyncResult); 5 ---------------------------------------------------------------------------------- 6 public virtual Task<Stream> GetRequestStreamAsync();
RequestStream是做什么用的?
举个栗子,我是个俊才,要向我心仪的姑娘提亲,于是呢,就要发起一个WebRequest了,Uri就相当于姑娘家的详细地址,甚至姑娘家有多位姑娘,我要向哪位姑娘提亲呢,也可以在Uri中点明。但是,既然是提亲,总得准备点儿礼物吧,可是礼物要么数量多(Uri里装不下)、要么珍贵,不想让外人看到,怎么办,于是乎,就有了RequestStream的用武之地,它就像是提亲的随行车队,我把礼物藏到车队里,这样我就可以给礼物编个码、压个缩、打个包、加个密、装个箱、上个锁(好像成本比礼物本身都高了,谁让咱穷呢)。
那为什么需要异步的方式呢,还是拿上面的栗子来解释吧,车队虽好,不过我可没有那么多车,要么跟亲戚朋友借,要么找婚庆公司预约,同步的方式就是我亲自去借车,其它什么都不干了,就等着车队到齐(先不考虑别人不借给我的尴尬场面)。异步方式呢,就是我打电话,告诉各位,我要借车,赶紧把车开过来,撂下电话,我也不闲着,赶紧给礼物编个码、压个缩、打个包、加个密、装个箱、上个锁……等车来齐了,就可以装车出发了~
好,理呢,就是这么个理,开始动手干。
先拿同步方式开刀。
1 using System; 2 using System.IO; 3 using System.Net; 4 using System.Text; 5 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 var request = WebRequest.Create(@"https://www.cnblogs.com/mikecheers/p/12090487.html"); 11 request.Method = WebRequestMethods.Http.Get; 12 using (var response = request.GetResponse()) // 结合点,同步方式获取Response 13 { 14 using (var stream = response.GetResponseStream()) 15 { 16 using (var reader = new StreamReader(stream, new UTF8Encoding(false))) 17 { 18 var content = reader.ReadToEnd(); 19 Console.WriteLine(content); 20 } 21 } 22 response.Close(); 23 } 24 request.Abort(); 25 Console.ReadLine(); 26 } 27 }
细心的同学,会发现,这不就是第二节开篇的示例麽。没错,就是它,你能拿我怎么滴……
重点是异步怎么实现:P
为了能够同时做一些对比,对上边的同步方式也做了一点改造,对比的步骤是每种方式,执行一百次请求发送,统计一下平均每请求消耗时间;虽然意义不大,只能看个大概,当玩儿了吧:)
1 { 2 Stopwatch watch = new Stopwatch(); 3 Console.WriteLine("/* ********************** 同步请求方式 * GetResponse() **********************/"); 4 watch.Restart(); 5 for (int i = 0; i < 100; i++) 6 { 7 var request = WebRequest.Create(@"https://www.cnblogs.com/mikecheers/p/12090487.html"); 8 request.Method = WebRequestMethods.Http.Get; 9 using (var response = request.GetResponse()) 10 { 11 using (var stream = response.GetResponseStream()) 12 { 13 using (var reader = new StreamReader(stream, new UTF8Encoding(false))) 14 { 15 var content = reader.ReadToEnd(); 16 Console.WriteLine(content.Length > 100 ? content.Substring(0, 90) + " ..." : content); 17 } 18 } 19 response.Close(); 20 } 21 request.Abort(); 22 } 23 watch.Stop(); 24 Console.WriteLine("/* ********************** using {0}ms / request ******************** */" 25 + Environment.NewLine + Environment.NewLine, (watch.Elapsed.TotalMilliseconds / 100).ToString("000.00")); 26 } /* ********************** using 034.10ms / request ******************** */
1 { 2 int sum = 100; 3 Stopwatch watch = new Stopwatch(); 4 Console.WriteLine("/* ********** 异步请求方式 * BeginGetResponse() & EndGetResponse() **********/"); 5 watch.Start(); 6 for (int i = 0; i < 100; i++) 7 { 8 var request = WebRequest.Create(@"https://www.cnblogs.com/mikecheers/p/12090487.html"); 9 request.Method = WebRequestMethods.Http.Get; 10 request.BeginGetResponse(new AsyncCallback(ar => 11 { 12 using (var response = (ar.AsyncState as WebRequest).EndGetResponse(ar)) 13 { 14 using (var stream = response.GetResponseStream()) 15 { 16 using (var reader = new StreamReader(stream, new UTF8Encoding(false))) 17 { 18 var content = reader.ReadToEnd(); 19 Console.WriteLine(content.Length > 100 ? content.Substring(0, 90) + " ..." : content); 20 } 21 } 22 response.Close(); 23 } 24 if (0 == System.Threading.Interlocked.Decrement(ref sum)) 25 { 26 watch.Stop(); 27 Console.WriteLine("/* ********************** using {0}ms / request ******************** */" 28 + Environment.NewLine + Environment.NewLine, (watch.Elapsed.TotalMilliseconds / 100).ToString("000.00")); 29 } 30 }), request); 31 } 32 } /* ********************** using 008.45ms / request ******************** */
1 { 2 Stopwatch watch = new Stopwatch(); 3 Console.WriteLine("/* ******************* 异步请求方式 * GetResponseAsync() ****************** */"); 4 watch.Start(); 5 System.Threading.Tasks.Task[] tasks = new System.Threading.Tasks.Task[100]; 6 for (int i = 0; i < tasks.Length; i++) 7 { 8 var request = WebRequest.Create(@"https://www.cnblogs.com/mikecheers/p/12090487.html"); 9 request.Method = WebRequestMethods.Http.Get; 10 tasks[i] = request.GetResponseAsync().ContinueWith(t => 11 { 12 var response = t.Result; 13 using (var stream = response.GetResponseStream()) 14 { 15 using (var reader = new StreamReader(stream, new UTF8Encoding(false))) 16 { 17 var content = reader.ReadToEnd(); 18 Console.WriteLine(content.Length > 100 ? content.Substring(0, 90) + " ..." : content); 19 } 20 } 21 response.Close(); 22 }); 23 } 24 25 System.Threading.Tasks.Task.WaitAll(tasks); 26 27 watch.Stop(); 28 Console.WriteLine("/* ********************** using {0}ms / request ******************** */" 29 + Environment.NewLine + Environment.NewLine, (watch.Elapsed.TotalMilliseconds / 100).ToString("000.00")); 30 } /* ********************** using 010.06ms / request ******************** */
为了能让代码尽量简短一些,撒了一些糖,有不明所以的同学,可以去找找Lambda表达式相关的东东,不是本文的重点。
本人也是经过多轮测试,发现第二种方式略优一些。
前面为什么说这个耗时的对比,意义不大?这里从大的方面稍微解释一下,有几个因素在里面:
- 服务器性能导致响应时间不固定。目标服务器也有忙有闲,处理每一个请求的时间是不固定的,所以,即使使用相同的方式,网络零消耗的情况下,每请求的时间也是不固定的,甚至会有很大差异;
- 缓存导致响应时间不固定。当目标服务器和/或客户端使用缓存技术,缓存有增加的那一刻,也有减少的那一刻,所以,即使使用相同的方式,网络零消耗的情况下,每请求的时间也是不固定的,甚至会有很大差异;
- 网络环境导致响应时间不固定。网络资源有限,时忙时闲,所以,即使使用相同的方式,每请求的时间也是不固定的,甚至会有很大差异;(路由学习导致改道、交换机产生回环、网络丢包等因素,都有存在的可能。)
- 客户端机器性能导致响应时间不固定。刨除以上几点差异,在不同的设备上,机器对I/O的处理能力也是有差别的,所以,即使使用相同的方式,每请求的时间也是不固定的,甚至会有很大差异;
那么,有没有最好的方式?没有!条件不固定,一切尚未可知!那么,有没有推荐的方式,有!第二种,异步方式。
看以上的问题列表,目标服务器的性能问题、缓存问题、网络问题,都是基本不可控的,(有同学说他的都可控,服务器就在自己家,网络什么的随便改造,那么建议你用[复制]+[粘贴]:P,就不要捣乱了)。唯一可控的就是对客户端(也就是运行爬虫的机器)的把控,这里的把控也不是说完全能够随意造,毕竟也都是银子,是说,我们能够很清楚的了解客户端的性能、瓶颈、优势劣势,我们不能改变,那么就可以想办法去适应,让他的性能最优化。客户端上是不是还跑着其他重要的任务?内存压力?单U还是多U?CPU的处理能力和网卡处理能力是不是均衡?网卡的吞吐能力?等等,都将影响我们的很多决策。这里说爬虫,也影射很多其他系统的开发。
我们不能说哪种方式最好,只能是选一个比较折中的方式。做人留一线,曰后好相奸~
这节就到这里吧,主要是了解一下几种请求的方式。
下一节,打算趁热打铁,聊一聊数据流那些事儿~敬请期待~
喜欢本系列丛书的朋友,可以点击链接加入QQ交流群(994761602)【C# 破境之道】
方便各位在有疑问的时候可以及时给我个反馈。同时,也算是给各位志同道合的朋友提供一个交流的平台。
需要源码的童鞋,也可以在群文件中获取最新源代码。
感谢您的阅读。
《ASP.NET MVC 5 破境之道》
《C# 爬虫 破境之道》
《C# GDI+ 破境之道》
持续添加中……
喜欢本系列丛书的朋友,可以点击链接加入QQ交流群(994761602)【C# 破境之道】