《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 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 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  ******************** */
同步请求方式 * GetResponse()
 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  ******************** */
异步请求方式 * BeginGetResponse() & EndGetResponse()
 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  ******************** */
异步请求方式 * GetResponseAsync()

 

为了能让代码尽量简短一些,撒了一些糖,有不明所以的同学,可以去找找Lambda表达式相关的东东,不是本文的重点。

本人也是经过多轮测试,发现第二种方式略优一些。

前面为什么说这个耗时的对比,意义不大?这里从大的方面稍微解释一下,有几个因素在里面:

  1. 服务器性能导致响应时间不固定。目标服务器也有忙有闲,处理每一个请求的时间是不固定的,所以,即使使用相同的方式,网络零消耗的情况下,每请求的时间也是不固定的,甚至会有很大差异;
  2. 缓存导致响应时间不固定。当目标服务器和/或客户端使用缓存技术,缓存有增加的那一刻,也有减少的那一刻,所以,即使使用相同的方式,网络零消耗的情况下,每请求的时间也是不固定的,甚至会有很大差异;
  3. 网络环境导致响应时间不固定。网络资源有限,时忙时闲,所以,即使使用相同的方式,每请求的时间也是不固定的,甚至会有很大差异;(路由学习导致改道、交换机产生回环、网络丢包等因素,都有存在的可能。)
  4. 客户端机器性能导致响应时间不固定。刨除以上几点差异,在不同的设备上,机器对I/O的处理能力也是有差别的,所以,即使使用相同的方式,每请求的时间也是不固定的,甚至会有很大差异;

那么,有没有最好的方式?没有!条件不固定,一切尚未可知!那么,有没有推荐的方式,有!第二种,异步方式。

看以上的问题列表,目标服务器的性能问题、缓存问题、网络问题,都是基本不可控的,(有同学说他的都可控,服务器就在自己家,网络什么的随便改造,那么建议你用[复制]+[粘贴]:P,就不要捣乱了)。唯一可控的就是对客户端(也就是运行爬虫的机器)的把控,这里的把控也不是说完全能够随意造,毕竟也都是银子,是说,我们能够很清楚的了解客户端的性能、瓶颈、优势劣势,我们不能改变,那么就可以想办法去适应,让他的性能最优化。客户端上是不是还跑着其他重要的任务?内存压力?单U还是多U?CPU的处理能力和网卡处理能力是不是均衡?网卡的吞吐能力?等等,都将影响我们的很多决策。这里说爬虫,也影射很多其他系统的开发。

我们不能说哪种方式最好,只能是选一个比较折中的方式。做人留一线,曰后好相奸~

这节就到这里吧,主要是了解一下几种请求的方式。

下一节,打算趁热打铁,聊一聊数据流那些事儿~敬请期待~

喜欢本系列丛书的朋友,可以点击链接加入QQ交流群(994761602)【C# 破境之道】
方便各位在有疑问的时候可以及时给我个反馈。同时,也算是给各位志同道合的朋友提供一个交流的平台。

你可能感兴趣的:(《C# 爬虫 破境之道》:第一境 爬虫原理 — 第四节:同步与异步请求方式)