分类 编程语言 下的文章

Java 和 HTTP 的那些事(二) 使用代理

在上一篇博客《模拟 HTTP 请求》中,我们分别介绍了两种方法来进行 HTTP 的模拟请求:HttpURLConnectionHttpClient ,到目前为止这两种方法都工作的很好,基本上可以实现我们需要的 GET/POST 方法的模拟。对于一个爬虫来说,能发送 HTTP 请求,能获取页面数据,能解析网页内容,这相当于已经完成 80% 的工作了。只不过对于剩下的这 20% 的工作,还得花费我们另外 80% 的时间 :-)

在这篇博客里,我们将介绍剩下 20% 的工作中最为重要的一项:如何在 Java 中使用 HTTP 代理,代理也是爬虫技术中的重要一项。你如果要大规模的爬别人网页上的内容,必然会对人家的网站造成影响,如果你太拼了,就会遭人查封。要防止别人查封我们,我们要么将自己的程序分布到大量机器上去,但是对于资金和资源有限的我们来说这是很奢侈的;要么就使用代理技术,从网上捞一批代理,免费的也好收费的也好,或者购买一批廉价的 VPS 来搭建自己的代理服务器。关于如何搭建自己的代理服务器,后面有时间的话我再写一篇关于这个话题的博客。现在有了一大批代理服务器之后,就可以使用我们这篇博客所介绍的技术了。

一、简单的 HTTP 代理

我们先从最简单的开始,网上有很多免费代理,直接上百度搜索 “免费代理” 或者 “HTTP 代理” 就能找到很多(虽然网上能找到大量的免费代理,但它们的安全性已经有很多文章讨论过了,也有专门的文章对此进行调研的,譬如这篇文章,我在这里就不多作说明,如果你的爬虫爬取的信息并没有什么特别的隐私问题,可以忽略之,如果你的爬虫涉及一些例如模拟登录之类的功能,考虑到安全性,我建议你还是不要使用网上公开的免费代理,而是搭建自己的代理服务器比较靠谱)。

1.1 HttpURLConnection 使用代理

HttpURLConnection 的 openConnection() 方法可以传入一个 Proxy 参数,如下:

Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 9876));
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection(proxy);

OK 了,就这么简单!

不仅如此,我们注意到 Proxy 构造函数的第一个参数为枚举类型 Proxy.Type.HTTP ,那么很显然,如果将其修改为 Proxy.Type.SOCKS 即可以使用 SOCKS 代理。

1.2 HttpClient 使用代理

由于 HttpClient 非常灵活,使用 HttpClient 来连接代理有很多不同的方法。最简单的方法莫过于下面这样:

HttpHost proxy = new HttpHost("127.0.0.1", 9876, "HTTP");
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet request = new HttpGet(url);
CloseableHttpResponse response = httpclient.execute(proxy, request);

和上一篇中使用 HttpClient 发送请求的代码几乎一样,只是 httpclient.execute() 方法多加了一个参数,第一参数为 HttpHost 类型,我们这里设置成我们的代理即可。

这里要注意一点的是,虽然这里的 new HttpHost() 和上面的 new Proxy() 一样,也是可以指定协议类型的,但是遗憾的是 HttpClient 默认是不支持 SOCKS 协议的,如果我们使用下面的代码:

HttpHost proxy = new HttpHost("127.0.0.1", 1080, "SOCKS");

将会直接报协议不支持异常:

org.apache.http.conn.UnsupportedSchemeException: socks protocol is not supported

如果希望 HttpClient 支持 SOCKS 代理,可以参看这里:How to use Socks 5 proxy with Apache HTTP Client 4? 通过 HttpClient 提供的 ConnectionSocketFactory 类来实现。

虽然使用这种方式很简单,只需要加个参数就可以了,但是其实看 HttpClient 的代码注释,如下:

/*
* @param target    the target host for the request.
*                  Implementations may accept <code>null</code>
*                  if they can still determine a route, for example
*                  to a default target or by inspecting the request.
* @param request   the request to execute
*/

可以看到第一个参数 target 并不是代理,它的真实作用是 执行请求的目标主机,这个解释有点模糊,什么叫做 执行请求的目标主机?代理算不算执行请求的目标主机呢?因为按常理来讲,执行请求的目标主机 应该是要请求 URL 对应的站点才对。如果不算的话,为什么这里将 target 设置成代理也能正常工作?这个我也不清楚,还需要进一步研究下 HttpClient 的源码来深入了解下。

除了上面介绍的这种方式(自己写的,不推荐使用)来使用代理之外,HttpClient 官网还提供了几个示例,我将其作为推荐写法记录在此。

第一种写法是使用 RequestConfig 类,如下:

CloseableHttpClient httpclient = HttpClients.createDefault();        
HttpGet request = new HttpGet(url);

request.setConfig(
    RequestConfig.custom()
        .setProxy(new HttpHost("45.32.21.237", 8888, "HTTP"))
        .build()
);
        
CloseableHttpResponse response = httpclient.execute(request);

第二种写法是使用 RoutePlanner 类,如下:

HttpHost proxy = new HttpHost("127.0.0.1", 9876, "HTTP");
DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy); 
CloseableHttpClient httpclient = HttpClients.custom()
        .setRoutePlanner(routePlanner)
        .build();
HttpGet request = new HttpGet(url);
CloseableHttpResponse response = httpclient.execute(request);

二、使用系统代理配置

我们在调试 HTTP 爬虫程序时,常常需要切换代理来测试,有时候直接使用系统自带的代理配置将是一种简单的方法。以前在做 .Net 项目时,程序默认使用 Internet 网络设置中配的代理,遗憾的是,我这里说的系统代理配置指的 JVM 系统,而不是操作系统,我还没找到简单的方法来让 Java 程序直接使用 Windows 系统下的代理配置。

尽管如此,系统代理使用起来还是很简单的。一般有下面两种方式可以设置 JVM 的代理配置:

2.1 System.setProperty

Java 中的 System 类不仅仅是用来给我们 System.out.println() 打印信息的,它其实还有很多静态方法和属性可以用。其中 System.setProperty() 就是比较常用的一个。

可以通过下面的方式来分别设置 HTTP 代理,HTTPS 代理和 SOCKS 代理:

// HTTP 代理,只能代理 HTTP 请求
System.setProperty("http.proxyHost", "127.0.0.1");
System.setProperty("http.proxyPort", "9876");

// HTTPS 代理,只能代理 HTTPS 请求
System.setProperty("https.proxyHost", "127.0.0.1");
System.setProperty("https.proxyPort", "9876");

// SOCKS 代理,支持 HTTP 和 HTTPS 请求
// 注意:如果设置了 SOCKS 代理就不要设 HTTP/HTTPS 代理
System.setProperty("socksProxyHost", "127.0.0.1");
System.setProperty("socksProxyPort", "1080");

这里有三点要说明:

  1. 系统默认先使用 HTTP/HTTPS 代理,如果既设置了 HTTP/HTTPS 代理,又设置了 SOCKS 代理,SOCKS 代理会起不到作用
  2. 由于历史原因,注意 socksProxyHostsocksProxyPort 中间没有小数点
  3. HTTP 和 HTTPS 代理可以合起来缩写,如下:
// 同时支持代理 HTTP/HTTPS 请求
System.setProperty("proxyHost", "127.0.0.1");
System.setProperty("proxyPort", "9876");

2.2 JVM 命令行参数

可以使用 System.setProperty() 方法来设置系统代理,也可以直接将这些参数通过 JVM 的命令行参数来指定。如果你使用的是 Eclipse ,可以按下面的步骤来设置:

  1. 按顺序打开:Window -> Preferences -> Java -> Installed JREs -> Edit
  2. 在 Default VM arguments 中填写参数: -DproxyHost=127.0.0.1 -DproxyPort=9876

jvm-arguments.jpg

2.3 使用系统代理

上面两种方法都可以设置系统,下面要怎么在程序中自动使用系统代理呢?

对于 HttpURLConnection 类来说,程序不用做任何变动,它会默认使用系统代理。但是 HttpClient 默认是不使用系统代理的,如果想让它默认使用系统代理,可以通过 SystemDefaultRoutePlannerProxySelector 来设置。示例代码如下:

SystemDefaultRoutePlanner routePlanner = new SystemDefaultRoutePlanner(ProxySelector.getDefault());
CloseableHttpClient httpclient = HttpClients.custom()
        .setRoutePlanner(routePlanner)
        .build();
HttpGet request = new HttpGet(url);        
CloseableHttpResponse response = httpclient.execute(request);

参考

  1. HttpClient Tutorial
  2. 评测告诉你:那些免费代理悄悄做的龌蹉事儿
  3. How to use Socks 5 proxy with Apache HTTP Client 4?
  4. 使用ProxySelector选择代理服务器
  5. Java Networking and Proxies
扫描二维码,在手机上阅读!

Java 和 HTTP 的那些事(一) 模拟 HTTP 请求

最新在学习使用 Java 来写网络爬虫,模拟浏览器发送 HTTP 请求,并抓取返回页面中的信息。由于对 Java 刚接触,以前用 .Net 写的一些网络请求相关的工具类都派不上用场,于是对如何使用 Java 模拟 HTTP 请求潜心研究了一番,在此写下这个《Java 和 HTTP 的那些事》系列的博客,并记录一些我中途遇到了明坑和暗坑,供后来人参考。此为第一篇。

一、使用 HttpURLConnection 发送 HTTP 请求

Java 自带的 java.net 这个包中包含了很多与网络请求相关的类,但是对于我们来说,最关心的应该是 HttpURLConnection 这个类了。

1.1 创建 HTTP 连接对象

要得到一个 HttpURLConnection HTTP 连接对象,首先需要一个 URL,代码如下:

URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();

1.2 添加 HTTP 请求头

得到 HTTP 连接对象之后,我们就可以进行 HTTP 操作了,我们可以添加任意的 HTTP 请求头,然后执行我们需要的 GET 或者 POST 请求。我们像下面这样,添加两个 HTTP 头(User-Agent 和 Accept-Language):

con.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) ...");
con.setRequestProperty("Accept-Language", "en-US,en;q=0.5");

对于有些爬虫来说,这个设置是必要的,譬如有很多网站会对请求头中的 Referer 进行检查,以此来防爬或者防盗链。又譬如有些网站还会对 User-Agent 进行检查,根据这个字段来过滤一些非浏览器的请求。如果请求头设置不对的话,很可能是爬不下正确的数据的。

1.3 HTTP GET

HTTP 协议中定义了很多种 HTTP 请求方法:GET、POST、PUT、DELETE、OPTIONS 等等,其中最常用到的就是 GET 和 POST,因为在浏览器中大多都是使用这两种请求方法。

使用 HttpURLConnection 来发送 GET 请求是非常简单的,通过上面的代码创建并初始化好一个 HTTP 连接之后,就可以直接来发送 GET 请求了。

con.setRequestMethod("GET");
int responseCode = con.getResponseCode();
String responseBody = readResponseBody(con.getInputStream());

可以看到,代码非常简洁,没有任何累赘的代码,甚至没有任何和发送请求相关的代码,请求就是在 getResponseCode() 函数中默默的执行了。其中 readResponseBody() 函数用于读取流并转换为字符串,具体的实现如下:

// 读取输入流中的数据
private String readResponseBody(InputStream inputStream) throws IOException {

    BufferedReader in = new BufferedReader(
            new InputStreamReader(inputStream));
    String inputLine;
    StringBuffer response = new StringBuffer();

    while ((inputLine = in.readLine()) != null) {
        response.append(inputLine);
    }
    in.close();
    
    return response.toString();
}

1.4 HTTP POST

使用 HttpURLConnection 来模拟 POST 请求和 GET 请求基本上是一样的,但是有一点不同,由于 POST 请求一般都会向服务端发送一段数据,所以 HttpURLConnection 提供了一个方法 setDoOutput(true) 来表示有数据要输出给服务端,并可以通过 getOutputStream() 得到输出流,我们将要写的数据通过这个输出流 POST 到服务端。

con.setRequestMethod("POST");
con.setDoOutput(true);
DataOutputStream wr = new DataOutputStream(con.getOutputStream());
wr.writeBytes(parameter);
wr.flush();
wr.close();

POST 完成之后,和 GET 请求一样,我们通过 getInputStream() 函数来读取服务端返回的数据。

二、使用 HttpClient 发送 HTTP 请求

使用 Java 自带的 HttpURLConnection 类完全可以满足我们的一些日常需求,不过对于网络爬虫这种高度依赖于 HTTP 工具类的程序来说,它在有些方面还是显得略为不足(我们之后会讨论到),我们需要一种扩展性定制性更强的类。Apache 的 HttpClient 就是首选。

HttpClient 是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包。它相比传统的 HttpURLConnection,增加了易用性和灵活性,它不仅让客户端发送 HTTP 请求变得更容易,而且也方便了开发人员测试接口(基于 HTTP 协议的),即提高了开发的效率,也方便提高代码的健壮性。

好了,关于 HttpClient 介绍的大话空话套话结束,让我们来看一段使用 HttpClient 来模拟 HTTP GET 请求的代码片段:

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet request = new HttpGet(url);
request.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) ...");
CloseableHttpResponse response = httpclient.execute(request);

// read response

2.1 HTTP GET 与 HTTP POST

上面的示例代码展示了如何使用 HttpClient 来模拟 HTTP GET 请求,可以看出 HttpClient 对每一种 HTTP 方法都准备了一个类,GET 请求使用 HttpGet 类,POST 请求使用 HttpPost 类。

和上文中介绍的一样,在发送 POST 请求时,需要向服务端写入一段数据,我们这里使用 setEntity() 函数来写入数据:

String parameter = "key=value";
HttpPost request = new HttpPost(url);
request.setEntity(
    new StringEntity(parameter, ContentType.create("application/x-www-form-urlencoded"))
);

Entity 是 HttpClient 中的一个特别的概念,有着各种的 Entity ,都实现自 HttpEntity 接口,输入是一个 Entity,输出也是一个 Entity 。要注意的是,在这里我采用一种取巧的方式,直接使用 StringEntity 来写入 POST 数据,然后将 Content-type 改成 application/x-www-form-urlencoded ,这样就和浏览器里的表单提交请求一致了。但是我们要知道的是,一般情况下,我们可能还会使用 UrlEncodedFormEntity 这个类,只是在写爬虫的时候比较繁琐,使用起来像下面这样:

List<NameValuePair> nvps = new ArrayList <NameValuePair>();
nvps.add(new BasicNameValuePair("key", "value"));
request.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));

2.2 读取响应

正如上文所说,HttpClient 的输入是一个 Entity,输出也是一个 Entity 。这和 HttpURLConnection 的流有些不同,但是基本理念是相通的。对于 Entity ,HttpClient 提供给我们一个工具类 EntityUtils,使用它可以很方便的将其转换为字符串。

CloseableHttpResponse response = httpclient.execute(request);
String responseBody = EntityUtils.toString(response.getEntity());

2.3 HttpEntiy 接口

上面说到了 HttpClient 中的 HttpEntity 这个接口,这个接口在使用 HttpClient 的时候相当重要,这里对其略做补充。

大多数的 HTTP 请求和响应都会包含两个部分:头和体,譬如请求头请求体,响应头响应体, Entity 也就是这里的 “体” 部分,这里暂且称之为 “实体” 。一般情况下,请求包含实体的有 POST 和 PUT 方法,而绝大多数的响应都是包含实体的,除了 HEAD 请求的响应,还有 204 No Content、304 Not Modified 和 205 Reset Content 这些不包含实体。

HttpClient 将实体分为三种类型:

  1. 流类型(streamed):实体内容从流中读取的,通常只能读一次
  2. 自包含类型(self-contained):手工创建的,通常可重复读取
  3. 包装类型(wrapping):使用一种实体包装另一种实体

上面的例子中我们直接使用工具方法 EntityUtils.toString() 将一个 HttpEntity 转换为字符串,虽然使用起来非常方便,但是要特别注意的是这其实是不安全的做法,要确保返回内容的长度不能太长,如果太长的话,还是建议使用流的方式来读取:

CloseableHttpResponse response = httpclient.execute(request);
HttpEntity entity = response.getEntity();
if (entity != null) {
    long length = entity.getContentLength();
    if (length != -1 && length < 2048) {
        String responseBody = EntityUtils.toString(entity);
    }
    else {
        InputStream in = entity.getContent();
        // read from the input stream ...
    }
}

三、关于 HTTPS 请求

到这里为止,我们一直忽略了 HTTP 请求和 HTTPS 请求之间的差异,因为大多数情况下,我们确实不需要关心 URL 是 HTTP 的还是 HTTPS 的,上面给出的代码也都能很好的自动处理这两种不同类型的请求。

但是,我们还是应该注意下这两种请求的差异,后面我们在介绍 HTTP 代理时将会特别看到这两者之间的差异。另外还有一点,在调用 URLopenConnection() 方法时,如果 URL 是 HTTP 协议的,返回的是一个 HttpURLConnection 对象,而如果 URL 是 HTTPS 协议的,返回的将是一个 HttpsURLConnection 对象。

参考

  1. How to send HTTP request GET/POST in Java
  2. HttpClient Tutorial
  3. HttpClient Examples
扫描二维码,在手机上阅读!

Windows下搭建PHP本地开发环境

一直以来,我都是使用新浪的SAE作为我本地的PHP开发环境。SAE使用起来非常方便,是个纯绿色的软件包,可以从这里下载,使用之前首先配置sae.conf文件中的DocumentRoot参数为网站根目录(也就是你PHP开发的目录),然后运行init.cmd脚本即可(Win7以上系统可能需要以管理员身份运行)。 我将SAE作为我的本地开发环境,官方的SAE只包含了PHP、Apache和Redis,而我的生产环境是PHP + Nginx + MySQL。这样不仅需要安装MySQL,而且不能和生产环境的Nginx保持一致,导致很多生产上的Nginx重写规则,在本地要再写一份Apache版的,虽然差异不大,但是依然感觉很麻烦。所以就想打造一个自己的PHP开发环境,像SAE一样绿色版,并且支持Apache和Nginx两个服务器。

一、搭建步骤

我们按PHP、Apache和Nginx的顺序来搭建PHP的本地开发环境,MySQL没有包含其中,以后有机会再做。这次的重点是学习Apache和Nginx的配置以及PHP是怎么和这两大Web服务器交互的。

1.1 PHP

首先,我们在这里下载PHP的Windows版本,我们直接下载编译好的Zip包即可。这里要特别注意的是,PHP for Windows有多种不同的版本,VC9和VC11,TS和NTS,x86和x64等。

  • VC9 vs VC11:代表使用哪个VS版本编译的,VC9表示Visual Studio 2008,VC11表示Visual Studio 2012
  • TS vs NTS:TS代表线程安全,也就是说支持多线程,PHP以模块形式加载到Web服务器中需要使用TS;NTS代表只支持单线程,在CLI/CGI/FastCGI等模式下建议使用NTS
  • x86 vs x64:使用32位还是64位,x64目前是实验版本
    综上所述,这里我们选择PHP 5.6 (5.6.9) VC11 x86 Thread Safe

1.2 Apache

Apache官网上并没有提供Windows上的可执行文件供下载,而是只提供了源码和编译安装步骤。我们这里省去编译可能会带来的问题和麻烦,直接使用第三方编译好的可执行文件。我们有很多不同的选择,下面是Apache官方提供的选择列表:

这里我们使用Apache Lounge提供的httpd-2.4.12-win32-VC11.zip,这是因为上面提到的PHP中一些SAPI就是使用Apache Lounge提供的二进制文件来编译的。注意选择和上面PHP一样的VC11和32位版本。

1.3 Nginx

比较流行的Nginx for Windows有两种不同的版本,一种是官方提供的使用native Win32 API的原生版本,另一种是利用Cygwin模拟编译出来的版本。这里我们选择原生版本

二、PHP的两种配置方式

将可执行文件都下下来之后,我们就需要对其进行配置了。首先我们以下图所示结构对刚下载的软件包进行组织:

structure

webserver目录用于存放Web服务器的二进制文件,譬如Apache和Nginx,之后可以添加lighttpd,IIS等;database目录用于存放数据库相关文件,譬如MySQL、Redis和Memcached等;interpreter目录用于存放脚本解析文件,如PHP、Perl、Python等。 PHP一般有两种配置方式:一种方式是以Apache的模块mod_php运行在Apache服务器中,这种方式最传统最著名,历史也最为悠久,在Nginx服务器没流行起来的时代,Apache统治了Web服务器的绝大部分,当时几乎都是以mod_php方式来配置的。这种配置方式虽然简单而且运行效率很高,但是这种配置不够灵活,导致Web服务器臃肿,也导致Apache服务器会占用更多的内存。于是出现了第二种配置方式CGI/FastCGI,CGI由于其效率低下已经基本上被FastCGI替代了。这种配置方式将Web服务器和PHP解析器解耦分离开来,PHP和Web服务器使用两个进程,甚至可以运行在两个不同服务器上,两者之间通过CGI/FastCGI协议进行通信。关于CGI/FastCGI/PHP-FPM的资料网上有很多,可以参考这里这里这里了解下。关于PHP的两种配置方式可以参考这里这里这里。 下面我们就以这两种方式分别来配置我们刚下载的Apache和Nginx。

2.1 mod_php

Apache支持以模块的方式来配置PHP,这个模块就是mod_php5.so,要注意的是mod_php是Linux下的名称,在Windows下的名称是php5apache2_4.dllphp5apache2_4.dll这个文件只包含在TS版的PHP打包中,如果你下载的是NTS版的可能找不到这个文件,因为Apache以模块形式载入PHP需要保证其是线程安全的。Nginx暂时还不支持这种配置方式。 参考PHP官方文档,我们在webserver\Apache-2.4\conf\httpd.conf文件中加入下面的代码来完成配置:

#
# mod_php
#
LoadModule php5_module "full-path-of-php5apache2_4.dll"
PHPIniDir "full-path-of-php.ini-dir"
AddType application/x-httpd-php .php

确保配置文件中的ServerRootDocumentRoot路径正确,然后运行webserver\Apache-2.4\bin\httpd.exe即可。可以在htdocs目录下新建一个PHP文件,使用phpinfo()方法来确保PHP和Apache已经正确运行。

2.2 CGI / FastCGI / PHP-FPM

在Windows环境下通过CGI方式解析PHP脚本一般使用的是PHP自带的php-cgi.exe程序。该程序运行方式如下:

php-cgi.exe -b 127.0.0.1:9000 -c path/to/php.ini

一旦按上面的方式运行php-cgi.exe程序之后就可以通过9000端口使用CGI协议来和它进行通信解析PHP脚本了。然后我们参考这里,在Nginx的配置文件中配上下面的代码(Nginx的配置文件位于webservernginx-1.9.0confnginx.conf):

#
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
location ~ \.php$ {
    root           html;
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    include        fastcgi_params;
}

配置好了以后,运行webserver\nginx-1.9.0\nginx.exe即可。在html目录下新建一个PHP文件,使用phpinfo()方法确保PHP和Nginx正确运行。可以看到mod_php方式配置简单,而且只要运行Web服务器即可,FastCGI方式不仅要运行Web服务器,还要运行FastCGI解析程序。 当然不仅仅是Nginx支持FastCGI这种配置方式,几乎所有其他的Web服务器都支持这种配置方式,譬如:Apache、IIS和lighttpd等。如果你想在Apache上使用这种配置,可以使用mod_fcgid扩展模块,在httpd.conf配置文件中添加如下代码:

#
# fast_cgi
#
LoadModule fcgid_module modules/mod_fcgid.so
FcgidInitialEnv PHPRC        "c:/php" 
FcgidWrapper "c:/php/php-cgi.exe" .php  
AddHandler fcgid-script .php

另外要提出的一点是,这里不管是mod_php方式还是FastCGI方式,我们都使用的是PHP的线程安全(TS)版本。而一般推荐做法是mod_php使用多线程(TS)版本,FastCGI使用单线程(NTS)版本,使用NTS版本不仅可以提高性能,也是为了和PHP的扩展(有很多PHP扩展是非线程安全的)保持兼容。

2.3 总结

为了方便,我写了两个批处理程序来启动Apache和Nginx。完整的版本可以看这里start-apache.bat文件:

@echo off
set APACHEROOT="path/to/webserver/Apache-2.4"
set PHPROOT="path/to/interpreter/php-5.6.9"
echo Starting Apache...
copy "%APACHEROOT:~1,-1%\conf\httpd.conf.bak" "%APACHEROOT:~1,-1%\conf\httpd.conf" /Y
utils\fnr.exe --cl --dir "%APACHEROOT:~1,-1%\conf" --fileMask "httpd.conf" --find "

start-nginx.bat文件:

@echo off
set NGINXROOT="path/to/webserver/nginx-1.9.0"
set PHPROOT="path/to/interpreter/php-5.6.9"
echo Starting PHP FastCGI...
utils\RunHiddenConsole "%PHPROOT:~1,-1%\php-cgi.exe" -b 9000 -c "%PHPROOT:~1,-1%\php.ini-development"
echo Starting Nginx...
utils\RunHiddenConsole "%NGINXROOT:~1,-1%\nginx.exe" -p "%NGINXROOT:~1,-1%"
pause

三、PHP SAPI

上面介绍了PHP的两种不同的配置方式,这两种配置都可以使PHP工作,实现这一点其实要归功于PHP的SAPI。SAPI(Server Application Programming Interface)是服务器应用编程接口的缩写,PHP通过SAPI提供了一组接口供应用和PHP内核之间进行数据交互。PHP提供很多种形式的接口,包括apache、apache2filter、apache2handler、cgi 、cgi-fcgi、cli、cli-server、embed、isapi、litespeed等等。但是常用的只有5种形式,CLI/CGI(单进程)、Multiprocess(多进程,PHP可以编译成Apache下的prefork MPM模式和APXS模块)、Multithreaded(多线程,Apache2的Worker MPM模块)、FastCGIEmbedded(内嵌)。可以使用PHP的php_sapi_name()函数获取当前的SAPI接口类型。参考这里这里了解更多。另外在简明现代魔法上有一个系列的文章对SAPI进行了详细的介绍:

参考

  1. PHP For Windows
  2. Using Apache HTTP Server on Microsoft Windows
  3. Apache Lounge
  4. nginx for Windows
  5. 什么是CGI、FastCGI、PHP-CGI、PHP-FPM、Spawn-FCGI?
  6. 概念了解:CGI,FastCGI,PHP-CGI与PHP-FPM
  7. 搞不清FastCgi与PHP-fpm之间是个什么样的关系
  8. mod_php vs FastCGI vs PHP-FPM
  9. [[好文]mod_php和mod_fastcgi和php-fpm的介绍,对比,和性能数据](http://wenku.baidu.com/view/887de969561252d380eb6e92.html)
  10. mod_php和mod_fastcgi(待整理)
  11. Microsoft Windows 下的 Apache 2.x
  12. php-fcgi on Windows
  13. windows下配置nginx+php环境
  14. Thread Safe PHP with FastCGI on IIS 7 or not?
  15. 【问底】王帅:深入PHP内核(二)——SAPI探究
  16. 第二章 » 第二节 SAPI概述 | TIPI: 深入理解PHP内核
扫描二维码,在手机上阅读!

LINQ中的Distinct

一、从去重说起

去重一直是数据处理中的一个重要操作,保证处理的数据中没有冗余,而在编写代码的时候更是经常需要剔除重复的数据避免重复计算。LINQ中的Distinct方法可以很好的处理基本数据类型的去重操作,如下所示:

// int类型
List<int> ints = new List<int> { 
    4, 5, 2, 1, 4, 6, 3, 2, 1, 3 
};
ints.Distinct().ToList().ForEach(x => Console.WriteLine(x));

// string类型
List<string> strings = new List<string> { 
    "Tom", "John", "Lily", "Tom", "Jess", "John" 
};
strings.Distinct().ToList().ForEach(x => Console.WriteLine(x));

但是在使用Distinct方法处理对象类型的数据时却没有这么好的体验了,假设我们有下面这个Person类,和类似的去重代码:

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }

    public override string ToString()
    {
        return string.Format("Id: {0}, Name: {1}, Age: {2}", 
            this.Id, this.Name, this.Age);
    }
}

List<Person> personList = new List<Person> 
{
    new Person { Id = 1, Name = "Zhangsan", Age = 23 },
    new Person { Id = 2, Name = "Lisi", Age = 22 },
    new Person { Id = 3, Name = "Wangwu", Age = 25 },
    new Person { Id = 2, Name = "Lisi", Age = 22 },
    new Person { Id = 1, Name = "Zhangsan", Age = 23 }
};
var dist = personList.Distinct().ToList();
dist.ForEach(x => Console.WriteLine(x));

得到的结果如下图,可以看到直接简单的使用Distinct来对对象列表进行去重,得到的结果和没去重的结果是一样的!原因在于Distinct方法默认的实现是根据对象的引用来进行的,而上面new了5个Person对象,是5个不同的引用。

distinct

我们查看Distinct方法的定义,发现该方法还可以带一个IEqualityComparer<TSource>类型的参数:

namespace System.Linq
{
    public static class Enumerable
    {
        // ...
        public static IEnumerable<TSource> Distinct<TSource>(this IEnumerable<TSource> source);
        public static IEnumerable<TSource> Distinct<TSource>(
                this IEnumerable<TSource> source, IEqualityComparer<TSource> comparer);
        // ...
    }
}

下面我们通过实现IEqualityComparer接口来实现对象列表的去重。

二、实现IEqualityComparer

IEqualityComparer接口的定义如下:

namespace System.Collections.Generic
{
    public interface IEqualityComparer<in T>
    {
        bool Equals(T x, T y);
        int GetHashCode(T obj);
    }
}

可以看到通过实现IEqualityComparer接口封装了EqualsGetHashCode这两个函数的实现,而这两个正是用于判断对象是否相等的重要函数。因为这里我们是想通过Id字段来去重,所以我们新建一个PersonIdComparer类:

public class PersonIdComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        if (x == null)
            return y == null;
        return x.Id == y.Id;
    }

    public int GetHashCode(Person obj)
    {
        if (obj == null)
            return 0;
        return obj.Id.GetHashCode();
    }
}

使用Distinct去重的时候只需要这样就可以了:

var dist1 = personList.Distinct(new PersonIdComparer()).ToList();

看起来很简单的一个去重操作,却要每次都要新建一个类,然后实现它的两个方法。而且在需求变更的同时,这个类还不能很好的适应变化,譬如现在我们需要根据Name字段去重,那么我们又需要新建一个PersonNameComparer类,当我们需要根据Age字段去重的时候,又需要新建一个PersonAgeComparer类,如此反复,非常繁琐,为什么我们不能写一个通用的类直接根据某个字段来去重呢? LoveJenny在《Linq的Distinct太不给力了》这篇博文中使用泛型、反射和表达式树的方法实现了一个PropertyComparer类,这个类可以很好的适应上面的变化。另外这篇文章也有类似的实现。

三、简化IEqualityComparer的实现

LoveJenny的博文给出的解决方法虽然满足了适应变化的能力,但在使用上仍然感觉不是很便捷,为什么我们不能像LINQ中的其他方法如OrderBy(x => x.Id)这样使用lambda表达式来去重呢?鹤冲天在他的两篇博文《c# 扩展方法奇思妙用基础篇八:Distinct 扩展》《何止 Linq 的 Distinct 不给力》中介绍了一种更简单通用的实现IEqualityComparer接口的方法,并通过结合C#的扩展方法使得LINQ中的Distinct使用起来非常便捷。下面直接上代码:

public static class Equality<T>
{
    public static IEqualityComparer<T> CreateComparer<V>(Func<T, V> keySelector)
    {
        return new CommonEqualityComparer<V>(keySelector);
    }
    public static IEqualityComparer<T> CreateComparer<V>(
            Func<T, V> keySelector, IEqualityComparer<V> comparer)
    {
        return new CommonEqualityComparer<V>(keySelector, comparer);
    }

    class CommonEqualityComparer<V> : IEqualityComparer<T>
    {
        private Func<T, V> keySelector;
        private IEqualityComparer<V> comparer;

        public CommonEqualityComparer(
                Func<T, V> keySelector, IEqualityComparer<V> comparer)
        {
            this.keySelector = keySelector;
            this.comparer = comparer;
        }
        public CommonEqualityComparer(Func<T, V> keySelector)
            : this(keySelector, EqualityComparer<V>.Default)
        { }

        public bool Equals(T x, T y)
        {
            return comparer.Equals(keySelector(x), keySelector(y));
        }
        public int GetHashCode(T obj)
        {
            return comparer.GetHashCode(keySelector(obj));
        }
    }
}

public static class DistinctExtensions
{
    public static IEnumerable<T> DistinctBy<T, V>(
            this IEnumerable<T> source, Func<T, V> keySelector)
    {
        return source.Distinct(Equality<T>.CreateComparer(keySelector));
    }
    public static IEnumerable<T> DistinctBy<T, V>(
            this IEnumerable<T> source, Func<T, V> keySelector, IEqualityComparer<V> comparer)
    {
        return source.Distinct(Equality<T>.CreateComparer(keySelector, comparer));
    }
}

原文中的方法名保持了LINQ中的Distinct不变,我这里改成了DistinctBy,这样和OrderByGroupBy相一致,而且在使用的时候personList.DistinctBy(x => x.Id)在语义上感觉更清晰。而且这里的Equality静态类,可以为我们很方便的创建IEqualityComparer接口,直接通过lambda表达式就可以而不需要再另外定义一个类了,譬如:var idComparer = Equality<Person>.CreateComparer(x => x.Id)

四、第三方库中的解决方法

4.1 morelinq

morelinq是一个关于linq的开源项目,添加了一些很实用的扩展方法,像ExcludeMinByMaxByTakeEvery等等,当然还有我们这里讲的DistinctBy,用法和上面的一样:personList.DistinctBy(x => x.Id)就可以了。我们这里主要关心它的实现方法,因为它并没有通过实现IEqualityComparer接口来,而是巧妙的利用了C#中的HashSet类型和yield关键字,实现了去重的目的。我将无关部分剔除,关键代码如下:

static partial class MoreEnumerable
{   
    public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
            this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
    {
        return source.DistinctBy(keySelector, null);
    }

    public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
            this IEnumerable<TSource> source,
            Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
    {
        return DistinctByImpl(source, keySelector, comparer);
    }

    private static IEnumerable<TSource> DistinctByImpl<TSource, TKey>(
            IEnumerable<TSource> source,
            Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
    {
        var knownKeys = new HashSet<TKey>(comparer);
        foreach (var element in source)
        {
            if (knownKeys.Add(keySelector(element)))
            {
                yield return element;
            }
        }
    }
}

具体的实现在DistinctByImpl这个方法里,首先新建一个HashSet,然后通过Add方法将元素添加到HashSet中,如果元素已存在Add方法返回false,则会继续插入下一个元素直到下一个元素插入成功,然后通过yield返回。需要注意的是HashSet接受一个IEqualityComparer类型的参数,如果HashSet的key是个对象而不是简单类型,则需要和我们上面一样定义一个类实现IEqualityComparer接口了。

4.2 Miscellaneous Utility Library

MiscUtil是一个C#的工具包大杂烩,它有各种辅助方法可以方便我们的开发工作。其中有一个ProjectionEqualityComparer类是和上面介绍的鹤冲天的Equality<T>类几乎完全一样,具体实现可以去看下它的源代码。唯一的区别在于它提供了两个ProjectionEqualityComparer类的实现,一个是ProjectionEqualityComparer,另一个是ProjectionEqualityComparer<TSource>。如下:

public static class ProjectionEqualityComparer
{
    public static ProjectionEqualityComparer<TSource, TKey> 
            Create<TSource, TKey>(Func<TSource, TKey> projection)
    {
        return new ProjectionEqualityComparer<TSource, TKey>(projection);
    }
}

public static class ProjectionEqualityComparer<TSource>
{    
    public static ProjectionEqualityComparer<TSource, TKey> 
            Create<TKey>(Func<TSource, TKey> projection)
    {
        return new ProjectionEqualityComparer<TSource, TKey>(projection);
    }
}

这样当我们需要实现IEqualityComparer接口时可以有更多的选择,如下所示。我们注意到CreateComparer<int>可以简写成CreateComparer,所以ProjectionEqualityComparer<Person>.CreateComparer(x => x.Id)这种写法可能要更实用一点。

var c1 = ProjectionEqualityComparer.CreateComparer<Person, int>(x => x.Id);
var c2 = ProjectionEqualityComparer<Person>.CreateComparer<int>(x => x.Id);
var c3 = ProjectionEqualityComparer<Person>.CreateComparer(x => x.Id);

4.3 AnonymousComparer

最后我们再来看下codeplex上的一个项目:AnonymousComparer,它可以通过lambda表达式直接创建匿名类实现IComparer<T>IEqualityComparer<T>接口。所以通过AnonymousComparer不仅简化了IEqualityComparer接口的实现,还简化了IComparer接口,IComparer接口在LINQ中的很多方法如OrderByGroupByContains等中有着广泛的应用。AnonymousComparer实现IEqualityComparer的方式大同小异,但是有一点不同的是,它提供了一种Full IEqualtyComparer<T> overload,这样我们可以在代码里完全通过lambda实现IEqualtyComparer的两个方法:EqualsGetHashCode。而上面所介绍的其他方法中都是将这两个方法封装起来的。

public static IEqualityComparer<T> Create<T, TKey>(Func<T, TKey> compareKeySelector)
{
    if (compareKeySelector == null) throw new ArgumentNullException("compareKeySelector");

    return new EqualityComparer<T>(
        (x, y) =>
        {
            if (object.ReferenceEquals(x, y)) return true;
            if (x == null || y == null) return false;
            return compareKeySelector(x).Equals(compareKeySelector(y));
        },
        obj =>
        {
            if (obj == null) return 0;
            return compareKeySelector(obj).GetHashCode();
        });
}

public static IEqualityComparer<T> Create<T>(Func<T, T, bool> equals, Func<T, int> getHashCode)
{
    if (equals == null) throw new ArgumentNullException("equals");
    if (getHashCode == null) throw new ArgumentNullException("getHashCode");

    return new EqualityComparer<T>(equals, getHashCode);
}

private class EqualityComparer<T> : IEqualityComparer<T>
{
    private readonly Func<T, T, bool> equals;
    private readonly Func<T, int> getHashCode;

    public EqualityComparer(Func<T, T, bool> equals, Func<T, int> getHashCode)
    {
        this.equals = equals;
        this.getHashCode = getHashCode;
    }

    public bool Equals(T x, T y)
    {
        return equals(x, y);
    }

    public int GetHashCode(T obj)
    {
        return getHashCode(obj);
    }
}

还是用Person列表根据Id去重的例子,这个Comparer可以写成这样(写起来可能会没有上面的方便快捷,但是可以实现更强大的定制功能,譬如要根据多列来去重或根据某种算法来去重而不是简单的根据单列去重):

var idComparer = AnonymousComparer.Create<Person>(
    (x, y) => x.Id == y.Id,         // Equals
    obj => obj.Id.GetHashCode()     // GetHashCode
);

五、最简单的解决方法

上面说了这么多,大多是通过实现IEqualityComparer接口的,也有根据HashSetyield实现的。但是其实通过LINQ自带的GroupBy方法也可以实现去重的目的,像下面这样:

var dist = personList.GroupBy(x => x.Id).Select(x => x.First());

尽管在性能上有一定的折扣,在可读性方面也不容易让人理解,但这确实应该算是最简单的做法。这让我想起了SQL中的去重,SQL语言中DISTINCT是根据所有字段来去重的,如果需要根据某一列或几列来去重也会使用GROUP BY,类似于:

SELECT * FROM Person GROUP BY Id

从这一点看上去,LINQ真的和SQL有着惊人的相似。

参考

  1. Distinct() with lambda?
  2. Can I specify my explicit type comparator inline?
  3. Distinct list of objects based on an arbitrary key in LINQ
  4. Linq的Distinct太不给力了
  5. c# 扩展方法奇思妙用基础篇八:Distinct 扩展
  6. 何止 Linq 的 Distinct 不给力
  7. morelinq - Extensions to LINQ to Objects
  8. Miscellaneous Utility Library
  9. AnonymousComparer - lambda compare selector for Linq
  10. 快速创建 IEqualityComparer 和 IComparer 的实例
  11. A Generic IEqualityComparer for Linq Distinct()
扫描二维码,在手机上阅读!

为什么Visual Studio不能在调试时使用lambda表达式

一、引子

相信很多人都有这样的经历,在Visual Studio的Watch窗口中查看一个List类型的变量,如果这个List中的元素太多,有时想使用LINQ的Where方法筛选一下,类似于下图中的代码:

code

假设我们断在图示断点所在位置,这时我们在Watch窗口中查看personList变量是没问题的,但是如果我只想查看满足Age==20personList,则会看到Visual Studio提示错误:Expression cannot contain lambda expressions

watch

这个问题一直困扰我很久,为什么在Visual Studio中不能在Watch窗口中使用lambda表达式呢?今天忍不住Google了一把,才发现这并不是我一个人的需求,网上有大把大把的人表示不理解为什么Visual Studio不提供这一功能,以至于Visual Studio官方的uservoice上积累了将近一万人的投票,建议Visual Studio引入这个功能。并且好消息是,在Visual Studio最新的2015版本中终于加入了这一特性。

二、原因初探

查看在uservoice下面的回复,我意识到为什么这一特性姗姗来迟,原来是因为这确实是一件很复杂的事。在Stackoverflow上也有很多相关的提问,其中有一篇JaredPar在评论中给出了很精彩的回复。我将他的回复原文摘抄下来放在下面:

No you cannot use lambda expressions in the watch / locals / immediate window. As Marc has pointed out this is incredibly complex. I wanted to dive a bit further into the topic though. What most people don't consider with executing an anonymous function in the debugger is that it does not occur in a vaccuum. The very act of definining and running an anonymous function changes the underlying structure of the code base. Changing the code, in general, and in particular from the immediate window, is a very difficult task. Consider the following code.

void Example() {
  var v1 = 42;
  var v2 = 56; 
  Func<int> func1 = () => v1;
  System.Diagnostics.Debugger.Break();
  var v3 = v1 + v2;
}

This particular code creates a single closure to capture the value v1. Closure capture is required whenever an anonymous function uses a variable declared outside it's scope. For all intents and purposes v1 no longer exists in this function. The last line actually looks more like the following

var v3 = closure1.v1 + v2;

If the function Example is run in the debugger it will stop at the Break line. Now imagine if the user typed the following into the watch window

(Func<int>)(() => v2);

In order to properly execute this the debugger (or more appropriatel the EE) would need to create a closure for variable v2. This is difficult but not impossible to do. What really makes this a tough job for the EE though is that last line. How should that line now be executed? For all intents and purposes the anonymous function deleted the v2 variable and replaced it with closure2.v2. So the last line of code really now needs to read

var v3 = closure1.v1 + closure2.v2;

Yet to actually get this effect in code requires the EE to change the last line of code which is actually an ENC action. While this specific example is possible, a good portion of the scenarios are not. What's even worse is executing that lambda expression shouldn't be creating a new closure. It should actually be appending data to the original closure. At this point you run straight on into the limitations ENC. My small example unfortunately only scratches the surface of the problems we run into. I keep saying I'll write a full blog post on this subject and hopefully I'll have time this weekend.

大意是讲由于lambda表达式涉及到了C#的闭包,而由于C#闭包的特性,导致在调试时如果在Watch窗口中输入lambda表达式将会修改原有代码结构,这并不是不可能,但是确实是一件非常困难且巨大的工程。

三、理解C#的lambda表达式和闭包

好奇心驱使我使用.NET Reflector查看了一下生成的exe文件,看到了下面这样的代码:

[CompilerGenerated]
private static bool <Main>b__0(Person x)
{
    return x.Age < 20;
}

[CompilerGenerated]
private static void <Main>b__1(Person x)
{
    Console.WriteLine(x.Name);
}

private static void Main(string[] args)
{
    personList.Where<Person>(Program.<Main>b__0).ToList<Person>().ForEach(Program.<Main>b__1);
    Console.Read();
}

可以看到lambda表达式被转换成了带有[CompilerGenerated]特性的静态方法,这也就意味着如果我们在调试的时候在Watch中每写一个lambda表达式,Visual Studio都需要动态的创建一个新的静态方法出来然后重新编译。这恐怕是最简单的情形,对调试器而言只是插入一个新的方法然后重新编译而已,Visual Studio已经支持运行时修改代码了,所以这种情形实现起来应该是没问题的。但是复杂就复杂在并不是每个lambda表达式都是这样简单,闭包特性的引入使得lambda表达式中的代码不再只是上下文无关的一个静态方法了,这使得问题变得越来越有意思。我们看下面的示例代码,其中用到了C#的闭包特性:

static void TestOne()
{
    int x = 1, y = 2, z = 3;
    Func<int> f1 = () => x;
    Func<int> f2 = () => y + z;
    Func<int> f3 = () => 3;

    x = f2();
    y = f1();
    z = f1() + f2();

    Console.WriteLine(x + y + z);
}

再看反编译的代码:

[CompilerGenerated]
private sealed class <>c__DisplayClass6
{
    // Fields
    public int x;
    public int y;
    public int z;

    // Methods
    public int <TestOne>b__4()
    {
        return this.x;
    }

    public int <TestOne>b__5()
    {
        return this.y + this.z;
    }
}

[CompilerGenerated]
private static int <TestOne>b__6()
{
    return 3;
}

private static void TestOne()
{
    <>c__DisplayClass6 CS$<>8__locals7 = new <>c__DisplayClass6();
    CS$<>8__locals7.x = 1;
    CS$<>8__locals7.y = 2;
    CS$<>8__locals7.z = 3;
    Func<int> f1 = new Func<int>(CS$<>8__locals7.<TestOne>b__4);
    Func<int> f2 = new Func<int>(CS$<>8__locals7.<TestOne>b__5);
    Func<int> f3 = new Func<int>(Program.<TestOne>b__6);
    CS$<>8__locals7.x = f2();
    CS$<>8__locals7.y = f1();
    CS$<>8__locals7.z = f1() + f2();
    Console.WriteLine((int) (CS$<>8__locals7.x + CS$<>8__locals7.y + CS$<>8__locals7.z));
}

从生成的代码可以看到编译器帮我们做了很多事情,lambda表达式只是语法糖而已。简单的lambda表达式被转换成带有[CompilerGenerated]特性的静态方法,使用闭包特性的lambda表达式被转换成带有[CompilerGenerated]特性的封装(sealed)类,并将所有涉及到的局部变量移到该类中作为该类的public字段,表达式本身移到该类中作为该类的public方法。而且下面所有对闭包涉及到的变量的操作都转换成了对类的字段的操作。 所以回到上文中JaredPar给出的解释,当我们在调试器中输入lambda表达式(Func)(() => v2)时可能存在两种不同的解决方法:

  1. 该方法中已经存在一个闭包,则需要将v2变量提到该闭包类中,并添加一个() => v2方法;
  2. 新建一个闭包类,将v2变量移到该类中,并添加一个() => v2方法;

但是这还远远不够,所有涉及到v2变量的操作,都需要调整为类似于CS$<>8__locals7.v2这样的代码,哦,想想就觉得好麻烦。

四、解决方法

先不谈Visual Studio 2015已经引入这个特性吧,我还没尝试过,也不知道是怎么实现的。暂时我还是在用Visual Studio 2010,所以如果想在老版本中提供这样的功能,就得另谋他策。下面是一些已知的解决方法,我仅是记下来而已,也没尝试过,等试过再写篇新博客记录下吧。

  1. 使用动态LINQ 参考链接中列出了一些动态LINQ的实现,其中用的比较多的应该是Scott提供的LINQ动态查询库,这样在调试时可以使用personList.Where("Age = @0", 20)来替代personList.Where(x => x.Age == 20)
  2. 使用一个VS的开源插件:Extended Immediate Window for Visual Studio
  3. 升级成Visual Studio 2015 ;-)

参考

  1. Allow the evaluation of lambda expressions while debugging
  2. VS debugging “quick watch” tool and lambda expressions
  3. 闭包在.NET下的实现,编译器玩的小把戏
  4. C#与闭包
  5. LINQ to SQL实战 动态LINQ的几种方法
  6. Extended Immediate Window for Visual Studio (use Linq, Lambda Expr in Debugging)
扫描二维码,在手机上阅读!

PHP的类自动加载机制

假设我们有一个php文件需要引用5个不同的类文件,可能会写出下面这样的代码:

require_once ("A.php");
require_once ("B.php");
require_once ("C.php");
require_once ("D.php");
require_once ("E.php");

$a = new A();
$b = new B();
$c = new C();
$d = new D();
$e = new E();

随着类的增多,文件之间依赖关系的复杂化,这里的require也会越来越多,程序员在编写代码的时候很容易忘记或者漏掉某个require,而且在每个php文件中都大量充斥着这种代码,看着真心累。 在PHP5中为这个问题提供了一个解决方案,这就是类的自动加载机制(autoload),这个机制可以使得程序在真正使用某个类时才开始加载这个类,所以也可以叫做类的延迟加载(lazy loading)。PHP提供了两种方法来达到这个目的:

  1. __autoload方法
  2. SPL的autoload机制

使用__autoload方法,可以将类名和所需要的类文件建立一个映射关系,一般情况是让类名和文件名保持一致,让PHP自动的去加载这些类。一个典型的__autoload方法如下:

function __autoload($classname) {
    require_once ($classname . ".php");
}
 
$a = new A();
$b = new B();
$c = new C();
$d = new D();
$e = new E();

参考

  1. php _autoload自动加载类与机制分析
  2. PHP自动加载__autoload的工作机制
  3. PHP的类自动加载机制
  4. PHP SPL,被遗落的宝石
扫描二维码,在手机上阅读!

WebRequest.Create报异常:The specified registry key does not exist

在Visual Studio中调试网络程序时经常报下面这样的异常,异常的类型是IOException,内容为The specified registry key does not exist,展开里面的详情更是一头雾水,不知道到底是什么错误,而且这个异常只会报一次,后面的请求都是正常。

io_exception

今天忍不住Google了一把,才明白原来是.Net Framework的一次安全更新(MS12-074)导致的,要避免每次都无厘头的抛这样的异常也很简单,向注册表中添加一条记录LegacyWPADSupport即可。 32位版本:

Registry location: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework  
DWORD (32-bit) Value name: LegacyWPADSupport
Value data: 0

64位版本:

Registry location: HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\.NETFramework
DWORD (32-bit) Value name: LegacyWPADSupport
Value data: 0

reg

参考:

HttpClient request throws IOException

扫描二维码,在手机上阅读!

学习PHP的错误处理

在日常的PHP开发中,我们经常会遇到各种各样的PHP错误,有常见的语法错误,数据库错误,也有各种很难调试的奇怪问题。

一、PHP错误等级

在PHP中预定义了一些PHP错误常量,在Core.php文件中可以查看定义。这里是一份完整的列表:Predefined Constants

这些变量不仅可以在PHP文件中使用,也可以用在php.ini配置文件中。譬如在PHP中打开错误提示,可以使用下面的代码:

ini_set('display_errors','on'); 
error_reporting(E_ALL & ~E_NOTICE); 

这两句PHP代码对应的php.ini配置是:

display_errors = On 
error_reporting = E_ALL & ~E_NOTICE 

一般在生产环境不应该将错误信息打开,这样容易暴露服务器上的一些敏感信息,如网站路径,php文件函数,数据库连接等等。可以参考CodeIgniter的做法,在CI的入口文件index.php中有这样的错误处理代码:

/*
 *---------------------------------------------------------------
 * ERROR REPORTING
 *---------------------------------------------------------------
 *
 * Different environments will require different levels of error reporting.
 * By default development will show errors but testing and live will hide them.
 */

if (defined('ENVIRONMENT'))
{
    switch (ENVIRONMENT)
    {
        case 'development':
            error_reporting(E_ALL);
        break;
    
        case 'testing':
        case 'production':
            error_reporting(0);
        break;

        default:
            exit('The application environment is not set correctly.');
    }
}

在开发环境,需要打开错误提示,可以在这段代码的上面 define('ENVIRONMENT', 'development'),或者测试和生产环境定义testingproduction就可以了。

二、PHP异常处理

PHP也和其他编程语言一样,支持异常的处理,基本的语法如下:

try
{
    //可能出现错误或异常的代码
} 
catch(Exception $e)
{
    //对异常进行处理
    die( 'Exception: ' .$e->getMessage() );
}

当一个异常被抛出时,其后的代码将不会继续执行,PHP 会尝试查找匹配的 catch 代码块。如果一个异常没有被捕获,而且又没用使用set_exception_handler()作相应的处理的话,那么 PHP 将会产生一个严重的错误,并且输出未能捕获异常 "Uncaught Exception ..." 的提示信息。

三、顶层异常处理器 (Top Level Exception Handler)

使用set_error_handler可以自定义PHP的错误处理函数。下面是一个简单的例子:

<?php
//error handler function
function customError($errno, $errstr, $errfile, $errline)
{
    echo "<b>Custom error:</b> [$errno] $errstr<br />";
    echo " Error on line $errline in $errfile<br />";
    die();
}

//set error handler
set_error_handler("customError");

//trigger error
trigger_error("A custom error has been triggered");
?>

要注意的是,set_error_handler例程只能处理用户级错误,如变量未定义或通过trigger_error引发的错误等。如果需要处理其他错误譬如E_ERROR, E_PARSE, E_CORE_ERROR,一种解决方法是使用register_shutdown_function函数,这个函数可以让用户自定义PHP程序执行完成后执行的函数。如果PHP程序是出现错误导致的退出,可以通过error_get_last函数获取最后一次发生的错误。在CodeIgniter.php文件中可以看到使用set_error_handler自定义了错误提示信息,当CodeIgniter出错时页面上可以看到自定义的显示,该代码位于Common.php中的_exception_handlerExceptions.php中的show_php_error。我们修改CodeIgniter的代码,让其支持register_shutdown_function
首先在CodeIgniter.php中添加如下代码:

set_error_handler('_exception_handler');
register_shutdown_function('_shutdown_handler');

然后在Common.php中添加_shutdown_handler

/**
 * Shutdown Handler
 *
 * The following error types cannot be handled with a user defined function: 
 *         E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING, 
 *         and most of E_STRICT raised in the file where set_error_handler() is called.
 * So, we use shutdown function to work around this situation.
 *
 * @access    private
 * @return    void
 */
if ( ! function_exists('_shutdown_handler'))
{
    function _shutdown_handler()
    {
        $lasterror = error_get_last();
        switch ($lasterror['type'])
        {
            case E_ERROR:
            case E_CORE_ERROR:
            case E_COMPILE_ERROR:
            case E_USER_ERROR:
            case E_RECOVERABLE_ERROR:
            case E_CORE_WARNING:
            case E_COMPILE_WARNING:
            case E_PARSE:
                $severity = $lasterror['type'];
                $message = $lasterror['message'];
                $filepath = $lasterror['file'];
                $line = $lasterror['line'];
                //header('Location: /ci/index.php/error');
        }
        
        $_error =& load_class('Exceptions', 'core');

        // Should we display the error? We'll get the current error_reporting
        // level and add its bits with the severity bits to find out.
        
        if (($severity & error_reporting()) == $severity)
        {
            $_error->show_php_error($severity, $message, $filepath, $line);
        }
        
        // Should we log the error?  No?  We're done...
        if (config_item('log_threshold') == 0)
        {
            return;
        }
        
        $_error->log_exception();
    }
}

注意:shutdown_handler可以捕获到E_PARSE,但是只能捕获到include的PHP文件里的语法错误。

四、PHP错误实例

熟悉常见的错误代码和返回信息可以快速诊断出代码中出现的问题。下面列出一些常见的PHP错误,及提示信息。

1. E_ERROR = 1

$b = undefined_func();  // 函数未定义 Fatal error: Call to undefined function func()
$a = new C(); // 类未定义 Fatal error: Class 'C' not found in
$a->undefined_func(); // 类的成员函数未定义 Fatal error: Call to undefined method C::undefined_func()
$a = 0;
for (;;) {  // 死循环 Fatal error: Maximum execution time of 300 seconds exceeded
    $a ++;
}

2. E_WARNING = 2

$a = 1;
$b = 0;
$c = $a / $b;  // 被零除 Warning: Division by zero

3. E_PARSE = 4

$b = 1  // 语法错误,少个逗号 Parse error: syntax error, unexpected $end
if (true)
{
    if (true)
    {
        if (false)
        {  // 语法错误,缺右括号
    }
}

4. E_NOTICE = 8

$b = $undefined_var;  // 变量未定义 Notice:Undefined variable: undefined_var

5. E_USER_ERROR/E_USER_WARNING/E_USER_NOTICE

trigger_error("This is an error.", E_USER_ERROR);  // User error

参考

  1. PHP异常处理详解
  2. PHP Error Handling
  3. PHP set_error_handler() 函数
  4. register_shutdown_function
  5. register_shutdown_function 函数详解
扫描二维码,在手机上阅读!