分类 技巧 下的文章

学习 Java 的调试技术

在软件开发的过程中,可以说调试是一项基本技能。调试的英文单词为 debug ,顾名思义,就是去除 bug 的意思。俗话说的好,编程就是制造 bug 的过程,所以 debug 的重要性毋庸置疑,如果能熟练掌握调试技能,也就可以很快的定位出代码中的 bug。要知道,看的懂代码不一定写的出代码,写的出代码不一定能调试好代码,为了能写出没有 bug 的代码,我们必须得掌握一些基本的调试技巧。

工欲善其事,必先利其器。无论你的开发工具是 IntelliJ IDEA 还是 Eclipse ,调试器都是标配。在遇到有问题的程序时,合理的利用调试器的跟踪和断点技巧,可以很快的定位出问题原因。虽然说合理利用日志也可以方便定位线上问题,但是日志并不是调试工具,不要在开发环境把 System.out.println 当作调试手段,掌握调试器自带的调试技能才是正道。

一、实战 IDEA 调试技巧

如果你是做 Java 开发的,相信你不会没有听过 IntelliJ IDEA ,和大多数 Java 开发者一样,我一开始的时候也是用 Eclipse 来进行开发,但是自从换了 IDEA 之后,就再也离不开它了,彻底变成了 IDEA 的忠实粉丝(不好意思,打一波广告。。)。不得不说,JetBrains 这家来自捷克的软件公司真的是良心企业,所出产品皆是精品,除了 IDEA,还有 WebStorm,PhpStorm,PyCharm 等,风格都是很类似的,一些类似的快捷键包括调试技巧也是通用的。

打开 IDEA 的调试面板,如下图所示,可以大致的将其分成五个部分:

  • 单步跟踪
  • 断点管理
  • 求值表达式
  • 堆栈和线程
  • 变量观察

idea-debugging.png

1.1 单步跟踪

说起调试,估计很多人第一反应就是对程序进行一步一步的跟踪分析,其实 IDEA 提供了很多快捷键来帮助我们跟踪程序,大抵可以列出下面几个技巧:

  • Show Execution Point

调试时往往需要浏览代码,对代码进行分析,有时候在浏览若干个源文件之后就找不到当前执行到哪了,可能很多人会使用 Navigate Back 来返回,虽然也可以返回去,但可能需要点多次返回按钮,相对来说使用这个技巧快速定位到当前调试器正在执行的代码行要更简便。

  • Step Over

这是最基本的单步命令,每一次都是执行一行代码,如果该行代码有方法会直接跳过,可以说真的是一步一个脚印。

  • Step In / Force Step In

Step Over 会跳过方法的执行,可以观察方法的返回值,但如果需要进到方法里面,观察方法的执行细节,则需要使用 Step In 命令了。另外,Step In 命令也会跳过 jdk 自带的系统方法,如果要跟踪系统方法的执行细节,需要使用 Force Step In 命令。关于单步的时候忽略哪些系统方法,可以在 IDEA 的配置项 Settings -> Build, Execution, Deployment -> Debugger -> Stepping 中进行配置,如下图所示。

idea-step-skip.png

  • Step Out

当使用 Step In 命令跟踪到一个方法的内部时,如果发现自己不想继续调这个方法了,可以直接把这个方法执行完,并停在调用该方法的下一行位置,这就是 Step Out 命令。

  • Drop to Frame

这一招可以说是调试器的一大杀器。在单步调试的时候,如果由于粗心导致单步过了头,没有看到关键代码的执行情况,譬如想定位下某个中间变量的值,这个时候如果能回到那行关键代码再执行一遍就好了,Drop to Frame 就提供了我们这个能力,它可以回到方法调用的地方(跟 Step Out 不一样,Step Out 是回到方法调用的下一行),让我们可以再调试一次这个方法,这一次可不要再粗心了。

Drop to frame 的原理其实也非常简单,顾名思义,它将堆栈的最上面一个栈帧删除(也就是当前正在执行的方法),让程序回到上一个栈帧(父方法),可以想见,这只会恢复堆栈中的局部变量,全局变量无法恢复,如果方法中有对全局变量进行操作的地方,是没有办法再来一遍的。

  • Run to Cursor / Force Run to Cursor

这两个命令在需要临时断点时非常有用,譬如已经知道自己想分析哪一行代码了,但又不需要下很多无谓的断点,可以直接使用该命令执行到某行,Force Run to Cursor 甚至可以无视所有断点,直接到达我们想分析的地方。

1.2 断点管理

断点是调试器的基础功能之一,可以让程序暂停在需要的地方,帮助我们进行分析程序的运行过程。在 IDEA 中断点管理如下图所示,合理使用断点技巧可以快速让程序停在我们想停的地方:

idea-breakpoints.png

可以将断点分成两种类型:行断点指的是在特定的某行代码上暂停下来,而全局断点是在某个条件满足时停下来,并不限于停在固定的某一行,譬如当出现异常时暂停程序。

1.2.1 行断点

  • Suspend (All / Thread)
  • Condition

条件断点。这应该也是每个使用调试器的开发者都应该掌握的一个技巧,当遇到遍历比较大的 List 或 Map 对象时,譬如有 1000 个 Person 对象,你不可能每个对象都调一遍,你可能只想在 person.name = 'Zhangsan' 的时候让断点断下来,就可以使用条件断点,如下图所示:

idea-breakpoints-condition.png

  • Log message to console
  • Evaluate and log

当看到上面的 Suspend 这个选项的时候有的人可能会感到奇怪,我下一个断点不就是为了让程序停下来吗?还需要这个选项干什么?是不是有点多余?难道你下个断点却不想让程序停下来?在发现 Evaluate and log 这个技巧之前,我对这一点也感觉很奇怪,直到有一天我突然发现 Suspend Off + Evaluate and log 的配合真的是太有用了。前面有讲过,不要把 System.out.println 当作调试手段,因为你完全可以用这个技巧来打印所有你想打印的信息,而不需要修改你的源代码。

  • Remove once hit

一次性断点。上面介绍的 Run to Cursor 就是一次性断点的例子。

  • Instance filters
  • Class filters
  • Pass count

这几个我用的不是很多,但应该也是非常有用的技巧可以先记下来。在 IDEA 里每个对象都有一个实例ID,Instance filters 就是用于当断点处代码所处的实例和设定ID匹配则断下来。Pass count 则是在断点执行到第几次的时候暂停下来。

1.2.2 全局断点

  • Exception breakpoints
  • Method breakpoints
  • Field watchpoints

个人感觉这几个技巧都不是很常用,感兴趣的同学自己实验一把吧。

1.3 求值表达式

在一堆单步跟踪的按钮旁边,有一个不显眼的按钮,这个按钮就是 “求值表达式”。它在调试的时候很有用,可以查看某个变量的值,也可以计算某个表达式的值,甚至还可以计算自己的一段代码的值,这分别对应下面两种不同的模式:

  • 表达式模式(Expression Mode)
  • 代码片段模式(Code Fragment Mode)

这两个模式类似于 Eclipse 里面的 Expression View 和 Display View。在 Display View 里也可以编写一段代码来执行,确实非常强大,但是要注意的是,这里只能写代码片段,不能自定义方法,如下图:

idea-evaluate-code-fragment.png

1.4 堆栈和线程

这个没什么好说的,一个视图可以查看当前的所有线程,另一个视图可以查看当前的函数堆栈。在线程视图里可以进行 Thread dump,分析每个线程当前正在做什么;堆栈视图里可以切换栈帧,结合右边的变量观察区,可以方便的查看每个函数里的局部变量和参数。

  • 线程视图
  • 堆栈视图

idea-threads.png

1.5 变量观察

变量区和观察区可以合并在一起,也可以分开来显示(如下图所示),我比较喜欢分开来显示,这样局部变量、参数以及静态变量显示在变量区,要观察的表达式显示在观察区。观察区类似于求值表达式中的 Expression mode,你可以添加需要观察的表达式,在调试的时候可以实时的看到表达式的值。变量区的内容相对是固定的,随着左边的栈帧调整,值也会变得不同。在这里还可以修改变量原有的值。

idea-variables.png

二、使用 jdb 命令行调试

相信很多人都听过 gdb,这可以说是调试界的鼻祖,以前在学习 C/C++ 的时候,就是使用它来调试程序的。和 gdb 一样,jdb 也是一个命令行版的调试器,用于调试 Java 程序。而且 jdb 不需要安装下载,它是 JDK 自带的工具(在 JDK 的 bin 目录中,JRE 中没有)。

每研究一项新技术,我总是会看看有没有命令行版本的工具可以替代,在命令行下进行操作给人一种踏实的感觉,每一个指令,每一个参数,都清清楚楚的摆在那里,这相比较于图形界面的工具,可以学习更深层的知识,而不是把技术细节隐藏在图形界面之后,你可以发现命令行下的每一个参数,每一个配置,都是可以学习的点。

2.1 jdb 基本命令

在 jdb 中调试 Java 程序如下图所示,直接使用 jdb Test 命令加载程序即可。

jdb-debugging.png

运行完 jdb Test 命令之后,程序这时并没有运行起来,而是停在那里等待进一步的命令。这个时候我们可以想好在哪里下个断点,譬如在 main() 函数处下个断点,然后再使用 run 命令运行程序:

> stop in Test.main
正在延迟断点Test.main。
将在加载类后设置。
> run
运行Test
设置未捕获的 java.lang.Throwable
设置延迟的未捕获的 java.lang.Throwable
>
VM 已启动:设置延迟的断点:Test.main

可以看出在执行 run 命令之前,程序都还没有开始运行,这个时候的断点叫做“延迟断点”,当程序真正运行起来时,也就是 JVM 启动的时候,才将断点设置上。除了 stop in Class.Method 命令,还可以使用 stop at Class:LineNumber 的方式来设置断点。

main[1] stop at Test:25

在 jdb 中下断点,就没有 IDEA 中那么多名堂了,什么条件断点,什么 Instance filters 都不支持,只能乖乖的一步一步来。在断点处,可以使用 list 命令查看断点附近的代码,或者用 step 命令单步执行,print 或者 dump 打印变量或表达式的值,locals 命令查看当前方法中的所有变量,cont 命令继续执行代码。还有一些其他的命令就不多做介绍了,可以使用 help 查看所有的命令清单,或者参考 jdb 的官方文档

2.2 探索 class 文件结构

在 jdb 中调试 Java 程序时,有可能源代码文件和 class 文件不在一起,这个时候需要指定源码位置:

# jdb -sourcepath path/to/source Test

如果不指定源码位置,在使用 list 命令时会提示找不到源码文件,如果真的没有源码文件,那么在 jdb 里完全就是瞎调了。我们知道 Java 代码在执行的时候,是以字节码的形式运行在 JVM 里的,可以猜测到,class 文件中必然有着和源码相关联的一些信息,类似于 C/C++ 语言的 obj 文件一样,要不然 list 命令怎么可以显示出当前正在执行的代码是哪一行呢?我们可以使用开源的 jclasslib 软件查看 class 文件里的内容,一个标准的 class 文件包含了下面这些信息:

  • 基本信息
  • 常量池
  • 接口
  • 属性
  • 父类
  • 字段
  • 方法

    • Code 属性

      • 行号属性
      • 局部变量表

如下图所示,其中最重要的一个部分就是 Code 属性,Code 属性下面有行号属性 LineNumberTable,这个 LineNumberTable 就是调试器用来关联字节码和源代码的关键。关于 class 文件,可以参考这篇文章《深入理解 Java 虚拟机》的学习笔记

dis-class-file.png

题外话:没有源码时如何调试?

如果没有源码,虽然在 jdb 里也可以用 step 来单步,但是没有办法显示当前正在运行的代码,这简直就是盲调。这个时候只能使用字节码调试工具了,常见的字节码调试器有:Bytecode VisualizerJSwat DebuggerJava ByteCode Debugger (JBCD) 等等,参考这里

三、关于远程调试

通过对 jdb 的学习,我们已经越来越接近 Java 调试器的真相了,但是还差最后一步。让我们先看看 Java 程序在 IDEA 里是如何被调试的,如果你有很强的好奇心,那么在 IDEA 里调试程序的时候可能已经发现了下面的秘密:

idea-debug-java.png

或者在调试 tomcat 的时候,也有类似的一串仿佛魔咒一般的参数:

idea-debug-tomcat.png

这串魔咒般的参数像下面这样,一旦你理解了这串参数,你也就打破了 Java 调试器的魔咒,然后才能认识到 Java 调试器真正的面目:

"C:\Program Files\Java\jdk1.8.0_111\bin\java" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:20060,suspend=y,server=n Foo
Connected to the target VM, address: '127.0.0.1:20060', transport: 'socket'

这里面有两个关键点:

  1. Java 程序在运行的时候带着 -agentlib 参数,这个参数用于指示 JVM 在启动时额外加载的动态库文件,-agentlib 参数和 -javaagent 不一样,这个库文件应该是由 C/C++ 编写的原生程序(JNI),类似于这里的 jdwp,在 Windows 上对应一个 jdwp.dll 库文件,在 Linux 上对应 jdwp.so 文件。那么这个 jdwp 库文件到底是做什么的呢?它后面的一串参数又是什么意思?
  2. jdwp 的参数里貌似提到了 socket,并有 address=127.0.0.1:20060 这样的 IP 地址和端口号,而且下面的 Connected to the target VM 也似乎表示调试器连接到了这么一个网络地址,那么这个地址到底是什么呢?由于这里是本地调试,所以 IP 地址是 127.0.0.1 ,那么如果是远程调试的话,是不是这里也是支持的呢?

在 IDEA 的 Run/Debug Configuration 配置页面,你也可以添加一个远程调试,界面如下图,可以发现上面那串魔咒参数又出现了:

idea-remote-debugging.png

在真正开始远程调试之前,我们不妨带着这些疑问,来学习 Java 调试器的基本原理。

四、Java 调试原理及 JPDA 简介

在武侠世界里,天下武功可以分为两种:一种讲究招式新奇,出招时能出其不意,善于利用兵器的特性和自身的高妙手法攻敌不备;另一种讲究内功心法,就算是最最普通的招式,结合自身的深厚内力,出招时也能有雷霆之势。其实在技术的世界里,武功也可以分为两种:技巧和原理。上面讲的那么多,无论你是掌握了 IDEA 的所有调试技巧也好,还是记熟了 jdb 的所有命令也好,都只是招式上的变化,万变不离其宗,我们接下来就看看调试器的内功心法。

4.1 JPDA

我们知道,Java 程序都是运行在 JVM 上的,我们要调试 Java 程序,事实上就需要向 JVM 请求当前运行态的状态,并对 JVM 发出一定的指令,或者接受 JVM 的回调。为了实现 Java 程序的调试,JVM 提供了一整套用于调试的工具和接口,这套接口合在一起我们就把它叫做 JPDA(Java Platform Debugger Architecture,Java 平台调试体系)。这个体系为开发人员提供了一整套用于调试 Java 程序的 API,是一套用于开发 Java 调试工具的接口和协议。本质上说,它是我们通向虚拟机,考察虚拟机运行态的一个通道,一套工具。

JPDA 由三个相对独立的层次共同组成,而且规定了它们三者之间的交互方式。这三个层次由低到高分别是 Java 虚拟机工具接口(JVMTI),Java 调试线协议(JDWP)以及 Java 调试接口(JDI),如下图所示(图片来自 IBM developerWorks):

jpda.jpg

这三个模块把调试过程分解成几个很自然的概念:调试者(debugger)和被调试者(debuggee),以及他们中间的通信器。被调试者运行于我们想调试的 Java 虚拟机之上,它可以通过 JVMTI 这个标准接口,监控当前虚拟机的信息;调试者定义了用户可使用的调试接口,通过这些接口,用户可以对被调试虚拟机发送调试命令,同时调试者接受并显示调试结果。在调试者和被调试者之间,调试命令和调试结果,都是通过 JDWP 的通讯协议传输的。所有的命令被封装成 JDWP 命令包,通过传输层发送给被调试者,被调试者接收到 JDWP 命令包后,解析这个命令并转化为 JVMTI 的调用,在被调试者上运行。类似的,JVMTI 的运行结果,被格式化成 JDWP 数据包,发送给调试者并返回给 JDI 调用。而调试器开发人员就是通过 JDI 得到数据,发出指令。详细的内容,可以参考 IBM developerWorks 上的一篇系列文章 《深入 Java 调试体系》

4.2 Connectors & Transport

到这里,我们已经知道了 jdwp 是调试器和被调试程序之间的一种通信协议。不过命令行参数 -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:20060,suspend=y,server=n 中的 jdwp 貌似还不止于此。事实上,这个地方上 jdwp.dll 库文件把 JDI,JDWP,JVMTI 三部分串联成了一个整体,它不仅能调用本地 JVMTI 提供的调试能力,还实现了 JDWP 通信协议来满足 JVMTI 与 JDI 之间的通信。想要完全理解这串参数的意思,我们还需要学习两个概念:Connectors(连接器)和 Transport(传输)。

常见的连接器有下面 5 种,它指的是 JDWP 建立连接的方式:

  • Socket-attaching connector
  • Shared-memory attaching connector
  • Socket-listening connector
  • Shared-memory listening connector
  • Command-line launching connector

其中 attaching connector 和 listening connector 的区别在于到底是调试器作为服务端,还是被调试程序作为服务端。
传输指的是 JDWP 的通信方式,一旦调试器和被调试程序之间建立起了连接,他们之间就需要开始通信,目前有两种通信方式:Socket(套接字) 和 Shared-memory(共享内存,只用在 Windows 平台)。

4.3 实战远程调试

通过上面的学习我们了解到,Java 调试器和被调试程序是以 C/S 架构的形式运行的,首先必须有一端以服务器的形式启动起来,然后另一段以客户端连接上去。如果被调试程序以服务端运行,必须加上下面的命令行参数(关于 jdwp 参数,可以参考 JavaSE 的文档):

# java -agentlib:jdwp=transport=dt_socket,server=y,address=5005 Test
# java -agentlib:jdwp=transport=dt_shmem,server=y,address=javadebug Test

第一句是以 socket 通信方式 来启动程序,第二句是以 共享内存 的方式来启动程序,socket 方式需要指定一个端口号,调试器通过该端口号来连接它,共享内存方式需要指定一个连接名,而不是端口号。在程序运行起来之后,可以使用 jdb 的 -attach 参数将调试器和被调试程序连接起来:

# jdb -attach 5005
# jdb -attach javadebug

在 Windows 平台上,第一条命令会报这样的错:java.io.IOException: shmemBase_attach failed: The system cannot find the file specified,这是由于 jdb -attach 使用系统默认的传输来建立连接,而在 Windows 上默认的传输是 共享内存 。要想在 Windows 上使用 socket 方式来连接,要使用 jdb -connect 命令,只是命令的参数在写法上不太好记忆:

# jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=5005
# jdb -connect com.sun.jdi.SharedMemoryAttach:name=javadebug

如果反过来,想让调试器以服务端运行,执行下面的命令:

# jdb -listen javadebug

然后 Java 程序通过下面的参数来连接调试器:

# java -agentlib:jdwp=transport=dt_shmem,address=javadebug, suspend=y Test
# java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:5005,suspend=y,server=n

最后我们再回过头来看一看 IDEA 打印出来的这串魔咒参数,可以大胆的猜测,IDEA 在调试的时候,首先以服务器形式启动调试器,并在 20060 端口监听,然后 Java 程序以 socket 通信方式连接该端口,并将 JVM 暂停等待调试。

"C:\Program Files\Java\jdk1.8.0_111\bin\java" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:20060,suspend=y,server=n Foo
Connected to the target VM, address: '127.0.0.1:20060', transport: 'socket'

如果在 IDEA 下进行远程调试,可以参考 IBM developerWorks 上的另一篇与调试相关的主题:使用 Eclipse 远程调试 Java 应用程序

总结

这篇文章首先介绍了 IDEA 的一些常用调试技巧,然后通过使用 jdb 进行 Java 程序的调试,学习了 jdb 的常用命令,最后通过远程调试引出调试器原理的话题,对 JPDA、JVMTI、JDWP、JDI 等概念有了一个初步的认识。从招式到心法,由技巧到原理,逐步揭开了 Java 调试器的神秘面纱。对于开发人员来说,如果只懂招式,只会一些奇淫技巧,那么他只是把工具用得更得心应手而已,很难在技术上得到质的突破;而如果只懂心法,只沉浸于基本原理和理论,那么他只能做一个眼高手低的学院派,空有满腹大道理却无用武之地。我们更应该内外兼修,把招式和心法结合起来,融会贯通,方能成正果。

最后的最后,关于调试的话题不得不补充一句:调试程序是一个费时费力的过程,一旦需要调试来定位问题,说明代码的逻辑性和清晰性有问题,最好的代码是不需要调试的。所以,少一点调试,多一点单元测试,多一点重构,将代码写的更清晰才是最好的编程方式。

番外篇:关于调试器的测不准效应

在量子物理学中,有一个名词叫 测不准原理,又叫 不确定性原理,讲的是粒子的位置与动量不可同时被确定,位置的不确定性越小,则动量的不确定性越大,反之亦然。说白点就是,你如果要很准确的测量粒子的位置,那么就不能准确的测量粒子的动量;如果要很准确的测量粒子的动量,那么粒子的位置就测不准;正是由于测量本身,会导致系统受影响。

把这个现象套在调试器领域里,也有着类似的效果。由于调试器本身的干扰,程序已经不是以前的程序了。所以问题来了,在调试器下运行出来的结果,真的可信吗?下面是我想出来的一个有趣的例子,假设我们在第 4 行下一个断点,程序最后输出结果会是什么呢?

debugger-reflactor.png

参考

  1. IntelliJ IDEA 13 debug调试细节
  2. 你所不知道的Android Studio调试技巧
  3. Eclipse 的 Debug 介绍与技巧
  4. 使用Eclipse调试Java程序的10个技巧
  5. JDB 的简单使用
  6. 《深入理解Java虚拟机》读书笔记4:类文件结构
  7. 使用 Eclipse 平台进行调试
  8. Java .class bytecode debugger
  9. Java调试——回归基础
  10. JVM源码分析之javaagent原理完全解读
  11. 使用 Eclipse 远程调试 Java 应用程序
  12. 深入 Java 调试体系,第 1 部分,JPDA 体系概览
  13. 深入 Java 调试体系,第 2 部分,JVMTI 和 Agent 实现
  14. 深入 Java 调试体系,第 3 部分,JDWP 协议及实现
  15. 深入 Java 调试体系,第 4 部分,Java 调试接口(JDI)
  16. Java Tool Tutorials - Herong's Tutorial Notes
  17. Java调试那点事
  18. 如何编写属于自己的Java / Scala的调试器
  19. jdb fails to connect to running java application over sockets
  20. Connection and Invocation Details
  21. Attach Intellij-IDEA debugger to a running java process
扫描二维码,在手机上阅读!

USB 无线网卡使用小记

鉴于笔记本网卡老化严重,信号太差,于是最近在京东上买了一个睿因(Wavlink)的 USB 无线网卡,WL-WN688A2 1200M 双频5g千兆 USB3.0 无线网卡,看官网上对这款产品的介绍,感觉还蛮高级的玩意,所以顺便学习了点无线网卡的知识,然后在自己的 Ubuntu 下成功安装其驱动并开始使用起来了。在此记录下。

wavlink.jpg

一、无线网卡基础知识

1.1 什么是 5G WiFi?

WiFi 技术从诞生到现在已经经历了五代的升级:从 a -> b -> g -> n 到 ac,第五代 WiFi 技术 IEEE 802.11ac 由于工作频率是 5GHz,所以叫做 5G WiFi 。如下图所示:

wifi-tech.png

相应的,目前最流行的是第四代,工作频率在 2.4GHz,所以叫做 2.4G WiFi。不过要注意的是,并不是工作频率在 5GHz 就是这里说的 5G WiFi,从上图中可以看到,第一代和第四代 WiFi 技术也有用到 5GHz 的频率,严格来说,这两种都不算 5G WiFi,只有第五代 802.11ac 才算是 5G WiFi。有些厂商经常吹嘘自己的网卡是 5G 网卡,实际上只是第四代的 802.11n,和第五代的区别还是很大的。百度上可以搜到 2.4G 和 5G WiFi 的区别

1.2 什么是双频?

这款无线网卡号称 AC1200 双频无线网卡,AC 好理解,上面解释了第五代 WiFi 技术就是 IEEE 802.11ac,1200 自然是指 1200Mbps 的传输速度。那双频是什么意思呢?

从上面的内容中我们可以看出 2.4G 和 5G 的很多区别,其实,对于用户感知来说,最重要区别只有两个:传输速率和距离。2.4G 传输速度比 5G 慢,但是传输距离远,穿墙能力强。而 5G 的最大优势则是近距离传输速度很快,适合看电影玩游戏。如下图(图片来自 Wavlin 广告)。

dual-hz.png

所谓双频,就是集 2.4G 和 5G 于一身,一张网卡,两个频段,既可以满足长距离传输的要求(2.4G),也可以满足传输速度的要求(5G)。其实这里厂商耍了一个滑头,它所宣称的 1200Mbps 传输速度,是双频合在一起的速度,2.4G 的速度是 300Mbps,5G 的速度是 867Mbps,加起来 1167Mbps,比 1200 还少了 33 呢。

另外,广告里宣称网卡还使用了 MIMO 架构CCA 空闲信道检测技术 来充分提升网卡的无线性能和稳定性,听起来还挺牛逼的,这些东西有时间再研究研究。

1.3 什么是 USB3.0 ?

和 WiFi 技术一样,USB 技术也经过二十年的发展,经历了三代,从 1996 年提出 USB1.0 规范开始,当时的传输速度只有 1.5Mbps,然后升级到 USB1.1,速度 12Mbps,到后来演变到 USB2.0,速度也升至 480Mbps,再到最新的 USB3.0 理论速度直接达到了 5Gbps,最新一代的 USB3.1 甚至达到了 10Gbps。

延伸:如何区分 USB2.0 和 USB3.0 插口

二、安装网卡驱动

做完功课之后,就开始无线网卡之旅了。无论是有线网卡还是无线网卡,大多数情况下,在使用之前都必须得安装好网卡驱动,随着 Wavlink 包装的有一个光盘,附带有网卡驱动,但是却是 Windows 系统的。所以在使用这个无线网卡之前,还得学习下如何在 Linux 系统下安装网卡驱动。

其实,无线网卡驱动的安装相当简单,可以分成下面两个步骤:

2.1 确定无线网卡的芯片类型

在安装驱动之前,我们首先需要确定无线网卡的芯片类型,这里有两个概念很容易混淆,品牌名和芯片名。有些人可能会说,我的无线网卡是 TP-LINK 的,要在哪里下载 TP-LINK 的网卡驱动呢?这个问题其实很难回答,在确定你的芯片类型之前,具体要安装什么样的驱动也是不能确定的。一般来说,买无线网卡的时候,产品说明书上会告诉你网卡的芯片信息,是 Atheros,还是 Realtek,还是 Broadcom 或者其他的什么芯片。

譬如,我买的这款无线网卡,产品文档上明确的写了:瑞昱 RTL8812AU 芯片,兼容主流操作系统。很显然芯片类型是 Realtek 的。

如果没有其他手段来得到网卡的芯片类型信息,可以使用 lsusb 命令来查看:

$ lsusb
Bus 002 Device 004: ID 04f2:b044 Chicony Electronics Co., Ltd Acer CrystalEye Webcam
Bus 002 Device 003: ID 0bda:8812 Realtek Semiconductor Corp. RTL8812AU 802.11a/b/g/n/ac WLAN Adapter
Bus 002 Device 002: ID 8087:0020 Intel Corp. Integrated Rate Matching Hub
Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 8087:0020 Intel Corp. Integrated Rate Matching Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

从第二行网卡的名称:RTL8812AU 802.11a/b/g/n/ac WLAN Adapter 中也能看出网卡是 RTL8812AU 芯片。

有时候很郁闷的是 lsusb 也看不到自己的网卡,这个时候也不知道是 usb 接口有问题,还是自己的网卡有问题,反正系统没有一点反应。如果出现这种情况,有一个小技巧可以使用,就是查看 /var/log/syslog 这个日志文件。先将 usb 无线网卡拔下,然后在终端中输入下面的命令:

$ sudo tail -f /var/log/syslog

然后再将网卡插入 usb 接口,这时如果不出意外,会出现大量的日志,像下面这样:

Jun 19 22:05:52 l-s kernel: [ 2836.763872] usb 2-1.2: new high-speed USB device number 5 using ehci-pci
Jun 19 22:05:53 l-s kernel: [ 2836.856752] usb 2-1.2: New USB device found, idVendor=0bda, idProduct=8812
Jun 19 22:05:53 l-s kernel: [ 2836.856758] usb 2-1.2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
Jun 19 22:05:53 l-s kernel: [ 2836.856762] usb 2-1.2: Product: 802.11n NIC
Jun 19 22:05:53 l-s kernel: [ 2836.856765] usb 2-1.2: Manufacturer: Realtek
Jun 19 22:05:53 l-s kernel: [ 2836.856768] usb 2-1.2: SerialNumber: 123456

如果出现日志,则说明网卡和 usb 接口都没有问题;如果没有出现任何日志,则说明网卡或 usb 接口坏了,可以换一个 usb 接口试试;如果出现一些报错信息,则 Google 之,在安装驱动之前,先将这些错误解决掉。

2.2 下载并安装驱动程序

无线网卡的官网,基本上都会提供驱动程序的下载,学会使用 Google 搜索。譬如我买的这款网卡产品名称是:睿因 WL-WN688A2,通过官网可以搜到该产品的很多文档:Download Drives And Manual ,其中就包括了 RTL8812AU 的驱动程序。

另外,我们已经知道了网卡的芯片类型是 Realtek ,也可以去 Realtek 的官网搜搜看。

但是不幸的是,官网提供的驱动程序,只支持到 Linux 内核版本 3.10,而我的 Ubuntu 16.04 内核版本是 4.4 ,自然编译都通不过。好在开源的世界里,从不缺少勇士,早就已经有人对官网的驱动程序进行改写了,使其支持最新的 Linux 系统。最后我选中了 github 上 gnab 的代码:Realtek 802.11n WLAN Adapter Linux driver

$ git clone https://github.com/gnab/rtl8812au.git
$ ./install.sh

代码中自带了安装脚本,使用 ./install.sh 即可安装。

2.3 附安装脚本

驱动程序的安装脚本挺简单的,贴上来学习一下。

#!/bin/bash

if [ "$(id -u)" != "0" ]; then
   echo "This script must be run as root" 1>&2
   exit 1
fi

make &&
cp 8812au.ko /lib/modules/$(uname -r)/kernel/drivers/net/wireless &&
depmod &&
echo "
                       ***Success***
***Module will be activated automatically at next reboot***
" &&

while true; do
    read -p "Do you wish to activate the module now? (y/n)" yn
    case $yn in
        [Yy]* ) insmod 8812au.ko && echo "***Module activated***" && break;;
        [Nn]* ) exit;;
        * ) echo "Please answer yes or no.";;
    esac
done

首先使用 make 命令编译源码,生成驱动文件 8812au.ko 。
然后将其复制到 /lib/modules/$(uname -r)/kernel/drivers/net/wireless 目录下,并运行 depmod 命令,这一步可以使驱动在开机时自动加载。depmod 命令的用处是创建模块依赖关系的列表,生成一个 modules.dep 文件,系统启动时,modprobe 会根据该文件自动加载驱动以及驱动的依赖关系。

最后脚本提示用户是否立即启用,如果输入 yes 则执行 insmod 8812au.ko 命令,insmodmodprobe 命令一样,用来立即安装驱动。如果不出意外,无线网卡的指示灯就开始闪烁起来了。

可以通过 lsmod 命令确定驱动是否已经成功安装:

$ lsmod | grep 88
8812au                991232  0

要卸载驱动,使用 rmmod 命令。

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

在 Windows 命令行下显示目录的大小

我们知道在 Linux 系统下使用 du 命令可以很方便的查看某个目录的大小,甚至也可以列出某个目录下的所有子目录的大小。这在查找大文件时非常方便,因为有时候我们会遇到这种情况,譬如,磁盘空间快满了,我们知道 /home/apps 目录非常大,而这个目录下面又有着几十个不同的子目录,我们希望能知道每个子目录的大小以方便我们找到是哪个目录最占空间,那么怎么能快速找到最占空间的子目录呢?

在 Linux 系统下,我们使用下面的 du 命令显示当前目录的总大小:

du -sh .

也可以像下面这样,显示当前目录下的所有一级子目录的大小:

du -h --max-depth=0 .

可以看到在 Linux 下是非常方便的,而在 Windows 下就没有原生的工具可以很方便的实现这一点了。Windows Sysinternals Suite 提供了一个类似于 Linux 下的 du 命令的小工具 Disk Usage,命令的语法稍微有些不同,你可以查看下这个工具的使用文档。

借助外界的工具肯定是可以实现这个功能的,但是也可以直接在 Windows 命令行下不依赖于第三方工具来实现,譬如,使用下面的 PowerShell 命令:

Get-ChildItem -Recurse | Measure-Object -Sum Length

Get-ChildItem 命令用于遍历目录下的所有子目录和文件,类似于 dir 命令,使用 -Recurse 参数可以实现递归遍历。
Measure-Object 命令常作用于管道,对管道的结果进行统计操作,譬如:计数、求和、平均数、最大数、最小数等等。

PowerShell 的命令总给人一种怪怪的感觉,不过它也提供了简写的语法:

ls -r | measure -s Length

看起来比上面的要舒服多了。或者直接在命令行 cmd 下执行:

powershell -noprofile -command "ls -r | measure -s Length"

如果不习惯 PowerShell 这种重量级的命令,也可以直接在命令行 cmd 下使用 for 命令实现,不过要借助一个中间变量,譬如将下面的代码复制到一个批处理文件中:

@echo off
set size=0
for /r %%x in (folder\*) do set /a size+=%%~zx
echo %size% Bytes

在 Windows 命令行下,for绝对是最复杂的命令,没有之一。让我们来解析下上面的那句命令:

for /r 表示递归的遍历一个目录下的所有文件。它的语法是这样:FOR /R [[drive:]path] %%parameter IN (set) DO command,所以其中的 %%x 是我们定义的一个参数,表示目录下的某个文件。注意,在批处理文件中必须要使用两个%%,如果是在命令行下尝试该命令的话,则只需要一个%就可以了。
do 之后的部分是我们针对每个参数(在这里也就是对每个文件)执行的操作。set 命令可以用于显示、设置或删除某个变量的值,set /a 用于对变量进行数学表达式运算(arithmetic expressions),在这里我们使用 += 来对文件大小进行累加。
最后一个是 %%~zx ,这里的 %%x 是就是上面的 x 参数,但是中间添加了 ~z 这样的特殊符号,这被称为 参数扩展(Parameter Extensions),表示对应的文件大小,另外还有很多其他有用的扩展,如 ~n 表示不带扩展的文件名,~x 表示文件的扩展名,~t 表示文件的时间 等等。和参数扩展类似的,还有两个与字符串变量相关的操作:字符串替换(Variable Replace) 和 字符串截取(Variable Substring),在 Windows 批处理中经常会遇到,也可以一起了解下。

不过要特别注意的是,在 Windows 的 cmd 下面,数字类型为 32 位的符号整型,所以最多支持到 2GB 大小的目录,超出 2GB 的结果可能会变成负数。所以最好的做法还是使用上面的 PowerShell 命令。

参考

  1. du 命令
  2. Windows command line get folder size
  3. CMD命令行高级教程精选合编
  4. For - Looping commands | Windows CMD
  5. For /R - Loop through sub-folders | Windows CMD
  6. Set - Environment Variable | Windows CMD
  7. Parameters / Arguments | Windows CMD
  8. Variable substring | Windows CMD
  9. CMD Variable edit replace | Windows CMD
扫描二维码,在手机上阅读!

Nodepad++小技巧:中英双语字幕转换为英文字幕

最近在看美剧《罗马》,在网上找了很久都没有找到中英双语字幕的片源,网上流传的版本大多是人人影视(YYeTs)的中文字幕,下下来看了两集发现没有英文字幕感觉非常不爽,于是直接去下载字幕文件,又发现没有纯英文的字幕,只有纯中文和中英双语的字幕文件。

双语的字幕文件下下来之后,本来打算找个小工具来转换成纯英文字幕,后来一想没必要,直接自己手工编辑也能搞定,于是拿起Notepad++折腾了半个小时。

一、Notepad++的宏功能

字幕文件是个有一定格式的文本文件,一般情况下格式都具有某种固定的模式,譬如下面的字幕文件:

srt.png

格式都是这样的模式:序号,时间,中文,英文,再加上一行空行。我们发现中文的位置都是一致的,如果我们手工来做的话,我们会这样操作:删除第3行,然后光标下移5行到第8行,删除第8行,然后光标下移到13行,以此类推,一直操作到文件末尾。

这样有着固定模式的重复操作正是Notepad++宏的用武之地,使用宏删除中文的具体步骤如下:

  1. 首先光标定位到第3行;
  2. 点击菜单项“宏” -> “开始录制”;
  3. Ctrl+L快捷键删除第3行;
  4. 按4下向下的箭头移动光标到第7行(因为删掉了一行,所以本来第8行变成了第7行);
  5. 点击菜单项“宏” -> “停止录制”,这样我们的宏就做好了;
  6. 保持光标所在位置不要动,最后点击菜单项“宏” -> “重复运行宏...”,然后选择“运行到文件尾”,点击“运行”,等待宏运行结束,如果一切顺利,就轻松完成任务了

run-macro.png

二、Notepad++的正则匹配功能

其实使用上面介绍的方法就足够应付大多数的字幕文件了,只要字幕文件的格式标准统一,只需要一次运行宏就能搞定。但有时字幕文件并不一定是格式一致的,譬如下面的字幕:

format.png

遇到这种情况时,宏会不问青红皂白接着往下处理,把英文字幕都删了,甚至破坏了字幕格式。如果这种情况不多,手工处理一下还能接受,但是一旦多起来还是很头疼的。于是想找一种通用的方法来处理。

其实问题很简单:找到文件中的所有中文,并删除所在行。

我们知道Notepad++中有匹配正则表达式的功能,而且我们知道匹配中文字符的正则是[\u4e00-\u9fa5],于是我们使用Notepad++的Mark功能把所有中文所在行标记出来。按Ctrl+F快捷键弹出查找窗口,切换到Mark选项卡,“查找模式”选择“正则表达式”,并勾选上“标记所在行”,输入正则表达式:

mark.png

点击“查找全部”,按理说应该会把所有中文行标记出来的。结果却是中文一个没标记上,英文行全标记上了。Google之才知道原来是正则表达式的问题,匹配中文的正则是[\u4e00-\u9fa5]没错,但是Notepad++使用的是PCRE引擎,正则的语法应该是[\x{4e00}-\x{9fa5}]

修改正则的语法后就可以标记出所有的中文行了。最后,我们拿出杀手锏:

delete-mark.png

选择 “搜索” -> "书签" -> "删除书签行",所有中文行都删除掉了,这个小技巧估计很多人都不知道,但是这个小技巧在移除某些特定行时非常有用。这整个过程总结起来就两步:

  1. 使用正则表达式[\x{4e00}-\x{9fa5}]标记出所有中文行;
  2. 删除书签行;

比起上面宏的做法这种方法要更简洁,而且更不容易出错。至此,我们的英文字幕就做好了,使用视频播放器加载英文字幕,调整下字幕的位置,虽然视频自带了中文字幕,但是看起来就跟双语字幕一样了!哈哈,搞定,继续看电视去了。

movie.png

后记

在使用Notepad++利用正则表达式匹配中文时,要特别注意一点的是:文件的格式一定要是UTF-8格式,而不是ANSI格式,否则匹配不到中文。
另外一点除了使用正则表达式[\x{4e00}-\x{9fa5}]匹配中文之外,还有其他的几种写法:

  1. 直接用中文字符来写正则也可以匹配:[一-龥!-~]
  2. Notepad++内置的匹配Unicode的写法:[[:unicode:]]

参考

  1. Anyone know how to use Regex in notepad++ to find Arabic characters?
  2. 正则表达式如何匹配中文字符?如何在一段中英混合的文本中找出中文字符?
  3. 怎么使用正则表达式表示汉字,目的是要在notepad++筛选出所有汉字?
扫描二维码,在手机上阅读!

关于 .Net 逆向的那些工具:反编译篇

在项目开发过程中,估计也有人和我遇到过同样的经历:生产环境出现了重大Bug亟需解决,而偏偏就在这时仓库中的代码却不是最新的。在这种情况下,我们不能直接在当前的代码中修改这个Bug然后发布,这会导致更严重的问题,因为相当于版本回退了。即使我们眼睁睁的看着这个Bug两行代码就能搞定,在我们的代码没更新到最新版本之前,都不敢轻举妄动。但是客户的呼声让人抵挡不住,客户声称的分分钟多少多少的经济损失我们也承受不起。这时如果你是做PHP开发的,你会庆幸,因为你可以直接去生产环境修复掉这个Bug让客户先闭嘴然后再慢慢折腾你那出问题的代码管理工具;而如果你做是.Net抑或C/C++开发的,就没这么轻松了。面对服务器上拷下来的有着重大Bug的dll或exe,你很难直接去修改它里面的代码逻辑,只能利用一些逆向的技巧和工具了。

由于我这里是.Net的环境,所以我决定在这篇博客里介绍下如何利用逆向工具来修改生产上的.Net程序集。但是就在我决定写这篇博客的时候我突然发现,其实,如果你只是单纯的修改一个.Net程序集中的某个方法或功能,而且这个程序集还是出自于你自己或你所在团队之手,这实在是一件非常容易的事情,这和破解别人的程序完全不同,你不会遇到无法破解的加密算法,也不会遇到让人恶心的加壳混淆。利用搜索引擎可以搜到大量这样的教学文章,所以我改变了下主意,决定不在这篇博客中重复造轮子,而是把已有的轮子一个个的列出来总结一下。

这篇博客主要汇总一些.Net反编译相关的工具。

一、ilasm & ildasm

ilasmildasm 都是微软官方提供的.Net编译与反编译工具,可谓是.Net逆向中的瑞士军刀。这两个工具的位置分别位于.Net Framework目录和Microsoft SDK目录中:

C:\Windows\Microsoft.NET\Framework\v2.0.50727\ilasm.exe
C:\Program Files\Microsoft SDKs\Windows\v7.0A\bin\ildasm.exe

这里有一篇文章详细介绍了如何通过ilasm 和 ildasm 修改.Net程序,归结起来就下面几个步骤:

  1. 使用工具ildasm打开要逆向的.Net程序,并另存为IL文件;
    ildasm程序有命令行和图形界面两种运行模式,一般情况下双击ildasm即可启动图形界面的主程序,然后通过菜单项 File -> Dump ,选择UTF-8编码,即可导出到IL文件中。如果是以命令行模式运行的话,可以打开Visual Studio自带的开发环境命令行工具(Developer Command Prompt for VS2012),这样可以不用关心ildasm所处的目录,直接运行下面的命令:ildasm test.exe /out:test.il

如果程序含资源文件的话,除了生成一个IL文件,可能还会有其他的*.res文件等。

  1. 打开IL文件阅读IL源码并定位到需要修改的代码处,对IL代码进行修改;
    使用ilasm 和 ildasm 反编译.Net程序需要了解一点MSIL的语法,这样无论阅读还是修改IL文件都要方便的多,好在MSIL的语法并不是很复杂,花一天的时间研究下还是值得的。这里有一篇不错的MSIL教程

如果真的对MSIL不熟悉不会编写MSIL的话,其实也没有大碍,只要你会大概的看懂MSIL源码和会编写C#程序也可以。可以参考这篇文章,具体的方法是:用C#编写你需要修改的方法,然后编译成exe/dll文件,再通过ildasm反编译成IL文件,从这个IL文件中复制出需要的IL源码覆盖掉之前那个需要修改的IL文件中的相关代码,这样你就算不会MSIL,也能修改IL文件了,确实有点偷梁换柱的味道。

  1. 使用ilasm将IL文件重新编译成.Net程序。
    最后,使用ilasm程序重新编译IL文件:ilasm test.il /output:test-mod.exe,再使用PEVerify执行校验确保文件无误:PEVerify test-mod.exe。如果一切顺利的话,将test-mod.exe替换掉老的test.exe即可。

默认情况下,ilasm将生成exe文件,如果需要生成dll文件,可以使用下面的命令:ilasm test.il /dll /output:test-mod.dll,如果需要集成资源,则需要指定/resource参数:ilasm test.il /resource:test.res /output:test-mod.exe

二、.Net Reflector & Reflexil

虽说ilasm 和 ildasm是.Net逆向中的瑞士军刀,但是一提起.Net逆向,其实很多人第一反应都是Reflector这款神器,而对微软提供的这两个官方工具知之甚少。这一方面是由于Reflector良好的用户体验和强大的插件功能,另一方面要归功于Reflector堪称完美的智能反编译能力,使用它不仅能看到反编译后的IL源码甚至能直接反编译出C#源码,而且和编写时的代码几无二致,如果需要还可以直接另存为工程文件用Visual Studio打开。
Reflector是RedGate开发的.Net逆向工具,单纯的Reflector程序只有反编译功能,可以查看IL或C#源码,以及导出源码。并不能修改.Net程序。幸好我们有sailro编写的Reflexil插件,Reflexil基于Mono.Cecil,是一个强大的程序集编辑器。Reflector + Reflexil 可谓是强强联合,和 ilasm + ildasm 这个组合比起来简直是大巫见小巫。

如果是第一次加载Reflexil插件,打开Reflector,在 Tools -> Add-Ins -> Add -> 选择Reflexil.dll,以后就可以直接在Reflector的Tools菜单中打开了。用Reflector打开test.exe,选择某个函数可以发现IL代码显示在下方的Reflexil窗口中,可以点击右键Edit,Delete,Create等操作。还可以修改类或方法的访问权限等,比如将private改成public。
另外,在编辑IL的操作中还有一个Replace all with code选项,通过这个可以直接用C#代码来对程序进行修改,无需你熟悉MSIL语法,类似于上一节介绍的“偷梁换柱”的方法,只不过集成在Reflexil中让我们的操作更方便了。如下图所示:

reflexil.png

Reflexil的作者在codeproject上写了一长篇Reflexil的各种实用技巧,可以去这里看看。Reflexil唯一的缺憾是并没有和Reflector无缝结合,使用Reflexil修改完IL源码或类的属性后,上面Reflector中显示的IL或C#源码并没有立即更新,必须保存修改后使用Reflector重新打开才能看到所做的修改。这多少有点让人感觉不爽,但比起 ilasm + ildasm 这种纯手工逆向工具来讲已经好太多了。

还有一点要说明的是:.Net Reflector很早就转向收费软件了,而且价格不菲,普通版95刀每用户,专业版甚至要199刀。对于我们大多数开发人员来说逆向并不是我们的日常工作,可能只是偶尔好奇而为之,这样为了偶尔的好奇而需要支付这么多的money实在是让人有点舍不得。于是一部分人走上了破解和盗版的路,而另一部分人走上了开源的路。这也是下一节将要介绍的ILSpy工具的由来。

三、ILSpy

ILSpy 是为了完全替代收费的Reflector而生,它是由 iCSharpCode 团队出品,这个团队开发了著名的 SharpDevelop 。ILSpy 完全开源,目前还处于开发阶段,很多功能还不够完善,但是具有强大的反编译功能对于我们来说已经足够了。对于ILSpy的使用和上面的Reflector完全类似,此处不再赘述。

在我写这篇博客的时候,ILSpy最新的发布版本为3/9/2015 Version 2.3,还没有和Reflexil具有类似功能的代码修改插件。但是在ILSpy的Further Down the Road可以看到,这样的功能也已经在计划之中了。

ilspy-down-road.png

虽然官方的发布版本还没有提供Reflexil的功能,但是Reflexil的作者sailro很早就在项目描述中介绍了对ILSpy的支持:

Reflexil is an assembly editor and runs as a plug-in for Red Gate's Reflector, ILSpy and Telerik's JustDecompile. Reflexil is using Mono.Cecil, written by Jb Evain and is able to manipulate IL code and save the modified assemblies to disk. Reflexil also supports C#/VB.NET code injection.

得益于ILSpyReflexil都是开源的,我们从GitHub上把最新的代码Clone下来并进行编译,Reflexil\Plugins\Reflexil.ILSpy这个目录下是Reflexil插件的源码,我们将编译后的所有dll文件拷贝到ILSpy的bin目录,运行ILSpy就能在View选项中看到Reflexil了,如下图:

ilspy-view.png

关于ILSpy和Reflexil的操作和上面介绍的Reflector几乎一样,不过有一点很让人振奋,ILSpy提供了更新对象模型的功能,这样Reflexil插件就可以在修改完代码后直接更新ILSpy的代码了,而不用像Reflector那样需要重新加载才能看到所做的修改。如下图所示,点击“Update ILSpy object model”,上面ILSpy的代码会立即更新:

ilspy-reflexil.png

四、Just Decompile

相信不少人都听过 Telerik 公司,该公司非常关注于.Net平台下的控件研发,并且发布了很多著名的开发工具,例如:Fiddler 和 JustDecompile。JustDecompile 正是 Telerik 的一款.Net反编译工具,和ILSpy不同的是,它并不是完全开源的,但是它有着商业化的技术支持,这一点非常难得。不仅如此,Telerik 也开源了JustDecompile的引擎部分:JustDecompile Engine,这也是非常不错的。
JustDecompile在使用上和其他的反编译工具差不多,而且它也具有插件系统,官方目前提供了三个插件,如下:

jd-plugins.png

其中Assembly Editor正是Reflexil。从菜单Plugins中调出Reflexil,enjoy it!

jd-reflexil.png

五、dotPeek

JetBrains是捷克的一家软件开发公司,出品了大量著名的开发工具,包括:IntelliJ IDEA、PHPStorm、ReSharper、TeamCity、YouTrack等等,每一款产品都如雷贯耳。dotPeek 是 JetBrains 开发的一款.Net反编译工具,是.Net工具套件中的一个,其他的还有dotTrace、dotCover 和 dotMemory。相比于前面几款工具来说,dotPeek算比较小众的一款。个人感觉它最大的特色就是Visual Studio风格,这对于那些长期在Visual Studio下进行开发的人来说应该更亲切一点。

dotPeek.png

不过dotPeek目前好像还没有类似于Reflexil这样的编辑插件,本身也并没有编辑功能。如果我们需要修改程序集的话,可以另存为工程文件,使用Visual Studio打开直接修改源码重新编译。

六、更多选择

实际上,利用上面介绍的这些工具已经完全能够满足你的需求了。但是我们总是有更多选择(等有时间的时候再玩这些吧):

另外,本文中的一些工具可以在此下载

TODO:Mono.Cecil

这篇博客的主要目的本来只是在无源码的情况下修改.Net程序集,但到这里已经完全演变成了.Net反编译工具的罗列清单。Never mind,关于.Net程序集的修改,上面最耀眼的非Reflexil莫属。而Reflexil依赖于Jb Evain所写的Mono.Cecil,Cecil 是一个对于Mono项目具有战略意义的函数库。它为很多项目(包括:Mono Debugger、Gendarme 和 MoMA 等)提供了内部处理的能力,而且 Cecil 也能操作编译好的CIL,并把修改后的程序集保存到磁盘里。

作为一个.Net程序员,修改.Net程序集最终极的方法莫过于自己动手写出修改程序集的代码,利用Mono.Cecil可以轻松实现这个目的。暂且给自己挖个坑,以后填上吧。

参考

  1. 操作步骤:用ildasm/ilasm修改IL代码 - dudu - 博客园
  2. Ilasm.exe(IL 汇编程序)
  3. Ildasm.exe(IL 反汇编程序)
  4. MSIL Tutorial
  5. 通过学习反编译和修改IL,阅读高人的代码,提高自身的水平。 - 辰 - 博客园
  6. 初识Ildasm.exe——IL反编译的实用工具 - Youngman - 博客园
  7. Reflector 已经out了,试试ILSpy - James Li - 博客园
  8. c#:Reflector+Reflexil 修改编译后的dll/exe文件 - 菩提树下的杨过 - 博客园
  9. 几款 .Net Reflector 的替代品 - 张志敏 - 博客园
  10. Assembly Manipulation and C# / VB.NET Code Injection - CodeProject
扫描二维码,在手机上阅读!

博客正式从WordPress转为Typecho

最近深深的迷上了Markdown,无论是编写代码说明文档还是记日记写博客,都能用Markdown快速轻松的书写。使用过GitHub的人应该都知道Markdown,Markdown让人沉浸在写作之中,而不用关心排版问题,只需几个简单的标记就能完成文档的自动排版。而且由于它是纯文本,具有极强的兼容性,可以方便的转换为各种格式。

于是我开始寻找WordPress上支持Markdown的编辑器,虽然有一些但都不是很好。便又开始寻找支持Markdown的博客系统,在这个开源遍地开花的年代,这样的博客系统还真是多如牛毛,让人眼花缭乱。有系统庞大功能性非常复杂的,也有小巧轻盈扩展性非常完美的。在这么多的博客系统中,最终我选择了Typecho,不仅是因为Typecho的轻量高效稳定简洁的特性,而且这款博客系统完全是由国人团队开发,选择使用Typecho也算是对国内的开源事业做出的一点点支持吧。

Typecho仅仅使用了7张数据表和不足400KB的代码,就实现了完整的插件与模板机制,打造了一个和WordPress几无二致的博客系统,而且Typecho和WordPress兼容,很容易从WordPress转到Typecho。不过,虽说如此,但是我在安装Typecho以及转换的过程中还是遇到了不少的问题和挫折,在鼓捣了一周左右的时间后,终于大功告成,仅以此文纪念之。

一、安装Typecho

Typecho的安装非常方便,按照官方的安装步骤,首先从这里下载最新的稳定版本(写这篇博客时,最新版本为1.0),然后解压并上传到服务器Web目录,然后使用浏览器打开你的博客地址,根据Typecho安装程序的提示一步步的安装即可。

当然这是理想情况下的安装步骤,实际上我的安装过程并没有这么顺利,在填完我的配置信息提交之后,出现了下面的错误:

安装程序捕捉到以下错误:"". 程序被终止, 请检查您的配置信息.

install_fail.png

对的你没看错,以下错误是什么错误?是个空字符串!不得不说这真的是一个非常坑爹的错误提示。幸亏Typecho是个开源的博客程序,在install.php文件中搜索字符串“安装程序捕捉到以下错误”,于是便找到了这个异常代码的所在之处(无关代码已省略):

try {
    $installDb = new Typecho_Db($config['adapter'], $config['prefix']);
    $installDb->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);

    /** 初始化数据库结构 */
    // ...

    $scripts = explode(';', $scripts);
    foreach ($scripts as $script) {
        $script = trim($script);
        if ($script) {
            $installDb->query($script, Typecho_Db::WRITE);  // 这里抛出异常
        }
    }
} catch (Typecho_Db_Exception $e) {
    $success = false;
    $code = $e->getCode();

    if (...) {
        // ...
    } else {
        echo '<p class="message error">' 
         . _t('安装程序捕捉到以下错误: "%s". 程序被终止, 请检查您的配置信息.',$e->getMessage()) . '</p>';
    }
}

抛出异常的正是上面注释的那句$installDb->query($script, Typecho_Db::WRITE),而$installDb是这样定义的:

$installDb = new Typecho_Db($config['adapter'], $config['prefix']);

Typecho_Db类根据用户选择的数据库适配器进行数据库操作,安装时配置页有两个可供选择:Mysql原生的和Pdo驱动的,如下图的下拉列表所示:

db_config.png

而在这里我选择了默认的Mysql原生函数适配器,对应的数据库处理代码在/var/Typecho/Db/Adapter/Mysql.php文件中,看到下面的代码我瞬间明白了为什么会有这么奇怪的错误提示了:

    /**
     * 数据库连接函数
     *
     * @param Typecho_Config $config 数据库配置
     * @throws Typecho_Db_Exception
     * @return resource
     */
    public function connect(Typecho_Config $config)
    {
        if ($this->_dbLink = @mysql_connect($config->host . (empty($config->port) ? '' : ':' . $config->port),
        $config->user, $config->password, true)) {
            if (@mysql_select_db($config->database, $this->_dbLink)) {
                if ($config->charset) {
                    mysql_query("SET NAMES '{$config->charset}'", $this->_dbLink);
                }
                return $this->_dbLink;
            }
        }

        /** 数据库异常 */
        throw new Typecho_Db_Adapter_Exception(@mysql_error($this->_dbLink));
    }

原因就在于上面代码中的@mysql_connect@mysql_select_db@符号是php中特有的一种错误控制符(error-control operator),意味着如果这个函数抛出异常将被完全忽略掉,没有errorCode,没有errorMessage。所以上面的错误提示信息使用$e->getMessage()方法获取的是个空字符串。

安装失败的原因现在已经定位到了是数据库导致的,接下来就看看为什么数据库配置会失败。这时候我注意到上面图片中红色高亮的提示信息:

系统将为您自动匹配 SAE 环境的安装选项

其实在安装的过程中我就很纳闷,为什么都没有提示我输入数据库的用户名密码这些配置信息,原来是Typecho检测到了我的系统为SAE环境,直接使用SAE里的数据库配置。而我的环境确实是SAE不假,只不过我使用的是SAE的本地开发环境,而且我从来没有做过SAE的数据库配置。按F12查看页面源码发现数据库的配置确实都是空。

sae_cfg.png

于是打开SAE的配置文件sae.conf

DocumentRoot path/to/website

http_port 80
https_port 443
redis_port 6379
domain sinaapp

mysql_user 
mysql_pass 
mysql_host 
mysql_port 

proxy_host  
proxy_port 80
proxy_username 
proxy_password 

open_xdebug 0
autoupgrade 1

其中mysql_user, mysql_pass, mysql_host, mysql_port分别对应数据库的用户名、密码、主机地址和端口号。于是马上填好这些配置参数,重启SAE,重新安装Typecho,这时这些参数都OK了,但还是差了一个参数dbDatabase,这个值一直是app_:

<input type="hidden" name="dbDatabase" value="app_">

这个app_其实是SAE_MYSQL_DB的值,于是我们查看SAE的代码,在文件sae/emulation/loadsae.php中找到了它的定义:

define('SAE_MYSQL_DB', 'app_'.$_SERVER['HTTP_APPNAME']);

原来app_是数据库的前缀,后面还有一个叫做HTTP_APPNAME$_SERVER变量。从变量名可以推断出这应该是SAE应用的名称,所以可能还需要在SAE中创建应用,但是具体操作我就没深究了,而是采用了一个取巧的方法,直接修改下面的这两行HTML源码:

<input type="hidden" name="config" value="array (
    'host'      =>  SAE_MYSQL_HOST_M,
    'user'      =>  SAE_MYSQL_USER,
    'password'  =>  SAE_MYSQL_PASS,
    'charset'   =>  'utf8',
    'port'      =>  SAE_MYSQL_PORT,
    'database'  =>  'typecho_db'
)">
<input type="hidden" name="dbDatabase" value="typecho_db">

然后点击“确认”按钮提交,终于看到了安装成功的页面!

二、将WordPress数据导出到Typecho

接下来,我便马不停蹄的开始了我的Typecho体验之旅。首先第一件事便是将之前WordPress上的博文导到Typecho上来,好消息是Typecho官方已经提供这样的插件了,我们去这里下载wordpresstotypecho然后遵循步骤解压上传到插件目录并在后台的插件管理里启动该插件。(虽然官网上有提示信息说:仅适用于wordpress2.7,但在我的4.2版本下也转换成功了)

就在我兴高采烈的去后台的插件管理里点击“启动插件”链接满心期待着我的WordPress华丽变身Typecho时,一个白底的页面并伴随着一个Fatal error吓到了我:

Fatal error: Call to undefined function mb_regex_encoding() in typechovarTypechoCommon.php on line 810

WTF!一百只草泥马从眼前飞过有木有!哎,没办法,继续看代码看看哪里出错了吧,谁叫我们是程序猿呢。搜索函数mb_regex_encoding,还好,只有Common.php中用到了一次,代码如下:

/**
 * 生成缩略名
 */
public static function slugName($str, $default = NULL, $maxLength = 128)
{
    // ...
    
    if (__TYPECHO_MB_SUPPORTED__) {
        mb_regex_encoding(self::$charset);
        mb_ereg_search_init($str, "[\w" . preg_quote('_-') . "]+");
        $result = mb_ereg_search();
        // ...
    } else if ('UTF-8' == strtoupper(self::$charset)) {
        // ...
    } else {
        // ...
    }

    $str = trim($str, '-_');
    $str = !strlen($str) ? $default : $str;
    return substr($str, 0, $maxLength);
}

这段代码的用途是根据字符串自动生成缩略名(slugName),其中使用到的mb_打头的函数都是php提供的针对多字节字符串处理的函数。但是mb_函数并不是所有的php版本都支持,所以在使用时需要先确定系统是否支持,Typecho通过下面的方法来判断系统是否支持mb_函数的:

define('__TYPECHO_MB_SUPPORTED__', function_exists('mb_get_info'));

可见,Typecho认为只要系统支持mb_get_info函数,应该也支持mb_regex_encoding这些mb_函数。而事实却并不是这样,我尝试了几个不同版本的Windows上的PHP,发现都是支持mb_get_info而不支持mb_regex_encoding,我不知道是不是Windows版本的PHP都会存在这个问题,Google之并没有得到我需要的答案,有人说可能是权限问题,有人说是配置问题,还有人说需要使用--enable-mbstring参数重新编译下PHP。哦,在Windows下重新编译PHP?实在是太酷了!但是,等一下,我好像只是想装个博客系统而已。

让我们回到现实,不要走的太远。slugName这个函数也不是什么核心功能,而且我们看代码可以知道,如果不支持mb_函数后面照样能走通,既然如此,我们暂且就认为我们的系统不支持mb_函数好了,这又能怎么样呢?于是稍微改了下代码:

define('__TYPECHO_MB_SUPPORTED__', function_exists('mb_regex_encoding'));

再点“启用插件”链接,OK,顺利进来了。按照页面提示,需要填写WordPress的数据库配置信息,然后进入“从Wordpress导入数据”页面,点击“开始数据转换”按钮。轻松几秒,转换完成,去“管理文章”页面看下,是不是非常熟悉的文章列表页面又回来了?

三、折腾Markdown

好了,现在博客已经搭建完毕,可以在Typecho下尽情的使用我最喜爱的Markdown语法进行写作了,这就结束了吗?哦,当然不,我们的Typecho之旅才刚刚开始!谁让我们程序猿都是爱折腾的人呢!

我们随便编辑一篇文章,Typecho会弹出下面的提示文字:

这篇文章不是由Markdown语法创建的, 继续使用Markdown编辑它吗?

选择“是”即可使用Markdown语法书写,这样文章的格式就是Html和Markdown混合格式。这多多少少有些让人感觉不爽,特别是看到类似下面的列表代码时:

<ul>
    <li>blah ...</li>
    <li>blah blah ...</li>
    <li>blah blah blah ...</li>
</ul>

对于我们这些有着“洁癖”和“强迫症”的程序猿来说,是不是就有种强烈的冲动想把它改成下面的Markdown代码:

- blah ...
- blah blah ...
- blah blah blah ...

怎么办呢?难道我们要手动把之前的所有博客都转换成Markdown语法么?Of course, No. 我们程序猿的特点就是这样:就算是能够在一个小时内手工完成的工作,也要写上六个小时的代码,然后再调试六个小时,最后花三秒钟运行自动完成!

Google上搜索 “Html to Markdown” 得到大量结果,说明这种工作已经有大把大把的人做过了,我们要做的就是找个靠谱点的工具直接用就好了。最终我把目光锁定在了html2text这个开源的Python项目上,并且打算写段Python脚本自动将我之前的所有的博客转换为Markdown语法。代码非常简单:

def main():
    
    posts = get_all_posts()
    posts = convert_posts_to_markdown(posts)
    save_posts(posts)

    return

get_all_posts函数从MySQL数据库中获取Typecho所有文章列表(也就是typecho_contents表),convert_posts_to_markdown直接调用函数html2text.html2text()将文章转换为Markdown语法,然后再调用save_posts将转换后的文章保存到数据库。

理论上这短短三行代码就能瞬间搞定,嗯,理论上。

事实上,在我的很多文章中有很多的代码块,而我为了让我的代码块看起来好看一点,使用了WordPress上提供的一个代码高亮插件:SyntaxHighlighter Evolved。它的语法类似于下面这样:

[code lang='javascript']
function sayHello() {
    console.log('hello');
}
[/code]

在使用html2text.html2text()函数转换这样的代码时,会把它视为普通的段落文本,这样我所有的代码里的空格换行全被剔除了,变成了一团乱麻。于是稍微修改了下程序的逻辑,在转换之前先用正则将文章中所有[code...]...[/code]这种格式的代码替换成唯一的uuid,然后再转换之后再将uuid替换为类似下面的代码:

function sayHello() {

console.log('hello');

}

完整的脚本可以参考这里,你可能需要根据你的插件或其他的一些不同的语法做些调整。

四、折腾主题样式

现在所有的文章都已经转换成Markdown语法了,去主页上随便逛逛,看着自己的劳动成果即将面世还是有些成就感的。接下来便是换个靠谱的主题了。

我个人是比较推崇单列布局的宽屏主题,这样不仅显得页面很干净清爽,让人看文章时能保持专注不至于被侧边栏的内容分心,而且在宽屏下代码可以完整的显示出来,如果页面很窄的话,代码过长就会导致出现大量横向的滚动条,如果设置成不显示滚动条又会导致代码出现大量的自动换行,看起来非常不舒服,完全没有在IDE中看代码的感觉。使用WordPress时我使用的就是这样的主题:Decode,而且自己做了点调整,将内容的宽度调大了一点。于是,我还要找一个类似这样的Typecho主题。

最终,我选择了Typecho论坛上一个非常火的主题:Navy,虽然没有找到这个主题比较官方的链接,但是可以直接从论坛里下载使用。Navy主题清爽简洁,唯一不足的一点是:右侧栏展开后只能显示“最新文章”和部分的“分类”,下面剩下的“分类”部分和“归档”都未显示,并且也不能用鼠标滚轮向下滚动。于是修改了下样式文件navy/style.css,对侧边栏增加了overflow-y: auto;

#secondary {
    height: 100%;
    width: 300px;
    top: 0;
    right: -330px;
    position: fixed;
    z-index: 100;
    box-shadow: -3px 0 8px rgba(0, 0, 0, 0.1);
    font-size: 13px;
    background-color: #303538;
    color: #f8f8f8;
    overflow-y: auto;  /* 右侧栏显示垂直滚动条 */
}

这样修改之后,侧边栏右边会出现一个滚动条,这个滚动条和浏览器的滚动条并行显示,虽然看起来比较ugly,但也勉强够用了。另外对Navy主题的样式还做了些其他的定制(不感兴趣的同学请直接忽略):

.container {
    width:1150px;  /* 增大内容宽度 */
    padding:0 60px;
    margin:0 auto;
}

a, button.submit {
    color:#009BCD;  /* 链接颜色改为淡蓝色 */
    text-decoration:none;
    -webkit-transition:all .1s ease-in;
    -moz-transition:all .1s ease-in;
    -o-transition:all .1s ease-in;
    transition:all .1s ease-in;
}

pre, code {
    background: #F0F0EC;
    color: #C13;  /* 代码的背景为灰色,字体为暗红色 */
    font-family:Menlo, Monaco, Consolas, "Lucida Console", "Courier New", monospace;
    font-size:.92857em;
    /*display: block;*/  /* 代码块内联显示 */
}

.post-content img {
    max-width:100%;  /* 防止图片太宽撑出去 */
    margin-left: 0;
}

blockquote,.stressed {
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    margin: 1.8em 0 1.8em 2.2em;  /* 引用文字改为向内缩进,太突出感觉怪怪的 */
    padding: 0 3.8em 0 1.6em;
    border-left: #4a4a4a 0.4em solid;
    /* font-style: italic; */  /* 去掉引用文字的斜体 */
    color: #888;
}

.post-content ul {
    overflow: auto;
    /*padding: .5em 2.4em;*/
    border-radius: 3px;
    /*margin:1.8em 0;*/  /* 列表的内外边距都太大了 */
}

五、折腾代码高亮

到这里,博客基本上已经打造的差不多了,但是还差最后一步,Typecho默认的代码样式简直让人想起了上个世纪在记事本里写代码的年代。确实,在如今的开发环境中,代码高亮几乎已成为标配,没有高亮的代码无论是给人阅读,还是书写都让人感到非常不爽。

所以,最后一步,我来为我的Typecho加上代码高亮的功能,而这样的插件也直接就已经有现成的了,我这里还是使用著名的SyntaxHighlighter插件来实现代码高亮,它的Typecho版是:SyntaxHighlighter-For-Typecho。从GitHub上直接"Download ZIP",然后解压并上传到服务器上的Typecho插件目录(要特别注意的是:插件的目录名和插件类名必须保持一致,譬如这里的类名为SyntaxHighlighter_Plugin,所以目录名必须是SyntaxHighlighter,要不然会出现500 Server Error,这又是一个非常非常坑爹的错误提示!我认为Typecho在出错时的提示信息上面还可以做的更好)

六、Markdown标记小结

Typecho实现了最基本的一些Markdown语法,我这里总结下。

6.1 字体加粗

语法:**粗体**
结果:粗体

6.2 斜体

语法:*斜体*
结果:斜体

6.3 链接

语法:[百度](http://www.baidu.com)
结果:百度

6.4 图片

语法:![image text](https://www.baidu.com/img/bdlogo.png)
结果:

image text

6.5 标题

语法:

# This is <h1>
## This is <h2>
### This is <h3>

结果:

This is <h1>

This is <h2>

This is <h3>

6.6 列表

语法:

1. 有序列表One
2. 有序列表Two
3. 有序列表Three

- 无序列表One
- 无序列表Two
- 无序列表Three

结果:

  1. 有序列表One
  2. 有序列表Two
  3. 有序列表Three
  • 无序列表One
  • 无序列表Two
  • 无序列表Three

6.7 引用

语法:> 这是引用文字
结果:

这是引用文字

6.8 行内代码

语法: `var i = 0;`
结果:
var i = 0;

6.9 代码块

语法:
```
function sayHello() {
}
```
结果:

function sayHello() {
}

Typecho支持的Markdown语法有限,其实,还有很多其他的语法在特定的场景下也会比较有用,譬如绘制表格,删除线,表情,LaTeX格式,流程图,序列图等等。可以看下作业部落的Cmd Markdown编辑器,其华丽程度简直让人目瞪口呆。然后这里有一份比较规矩还不错的语法说明,另外GitHub也有Markdown的一份指导文档

参考

  1. 开始用 Markdown 写博客
  2. Typecho文档站点
  3. php - @mysql_connect and mysql_connect - Stack Overflow
  4. PHP手册 > 函数参考 > 国际化与字符编码支持 > 多字节字符串
  5. Cmd Markdown 编辑阅读器 - 作业部落出品
  6. Markdown 语法说明 (简体中文版)
  7. Mastering Markdown · GitHub Guides
扫描二维码,在手机上阅读!

我的第一个Chrome扩展:Search-faster

花了两周左右的时间将Chrome扩展的开发文档看了一遍,把所有官方的例子也都顺便一个个的安装玩了一遍,真心感觉Chrome浏览器的博大精深。Chrome浏览器的现有功能已经足够强大,再配合Chrome扩展几乎可以说是“只有想不到,没有做不到”。于是利用业余时间做了一个简单的扩展Search-faster,可以加快Google的搜索速度,算是对近一段时间学习的总结。

一、Chrome扩展综述

Chrome扩展有两种不同的表现形式:扩展(Extension)和应用(WebApp),我们这里不讨论WebApp,但是扩展的大多数技巧对于WebApp来说也是适用的。Chrome扩展实际上就是压缩在一起的一组文件,包括HTML,CSS,Javascript,图片,还有其它任何需要的文件。它从本质上来说就是一个Web页面,可以使用所有的浏览器提供的API,可以与Web页面交互,或者通过content script或cross-origin XMLHttpRequests与服务器交互。还可以访问浏览器提供的内部功能,例如标签或书签等。
扩展在Chrome浏览器中又有着两种不同的表现形式:browser_action和page_action,browser_action在工具栏右侧添加一个图标,page_action在URL输入栏右侧添加一个图标,如下图所示。这两个action唯一的区别在于:当你的扩展是否显示取决于单个页面时,该使用page_action,page_action默认是不显示的。

extensions

1.1 manifest.json

每一个Chrome扩展都有一个清单文件包含了这个扩展的所有重要信息,这个文件的名称固定为manifest.json,文件内容为JSON格式。下面是一个manifest.json文件的实例(来自JSONView扩展)

{
   "background": {
      "scripts": [ "background.js" ]
   },
   "content_scripts": [ {
      "all_frames": true,
      "js": [ "content.js" ],
      "matches": [ "http://*/*", "https://*/*", "ftp://*/*", "file:///*" ],
      "run_at": "document_end"
   } ],
   "description": "Validate and view JSON documents",
   "icons": {
      "128": "jsonview128.png",
      "16": "jsonview16.png",
      "48": "jsonview48.png"
   },
   "key": "...",
   "manifest_version": 2,
   "name": "JSONView",
   "options_page": "options.html",
   "permissions": [ "clipboardWrite", "http://*/", "contextMenus", "https://*/", "ftp://*/" ],
   "update_url": "https://clients2.google.com/service/update2/crx",
   "version": "0.0.32.2",
   "web_accessible_resources": [ 
      "jsonview.css", "jsonview-core.css", "content_error.css", 
      "options.png", "close_icon.gif", "error.gif" 
   ]
}

其中nameversionmanifest_version三个字段是必选的,每个字段的含义显而易见,另外在当前版本下manifest_version的值推荐为2,版本1已经被弃用。
除这三个字段之外,description为对扩展的一句描述,虽然是可选的,但是建议使用。
icons为扩展的图标,一般情况下需要提供三种不同尺寸的图标:16*16的图标用于扩展的favicon,在查看扩展的option页面时可以看到;48*48的图标在扩展的管理页面可以看到;128*128的图标用于WebApp。这三种图标分别如下所示:

icons

图标建议都使用png格式,因为png对透明的支持最好。要注意的是:icons里的图标和browser_actionpage_action里的default_icon可能是不一样的,default_icon显示在工具栏或URL输入栏右侧,建议采用19*19的图标。
key字段为扩展的唯一标识,这个字段是浏览器在安装.crx文件时自动生成的,通常不需要手工指定。
permissions为扩展所需要的权限列表,列表中的每一项要么是一个已知的权限名称,要么是一个URL匹配模式。一些常见的权限名称有background、bookmarks、contextMenus、cookies、experimental、geolocation、history、idle、management、notifications、tabs、unlimitedStorage等;URL匹配模式用于指定访问特定的主机权限,譬如:"http://*.google.com/"、"http://www.baidu.com/"。关于permissions字段可以参考这里的文档
update_url用于扩展的自动升级,默认情况下Chrome浏览器会每隔一小时检测一次是否需要升级,也可以点击扩展管理页面的“立即更新”按钮强制升级。
另外backgroundcontent_scriptsoptions_page这三字段,还有这个例子里没包含的browser_action/page_action字段是构成Chrome扩展的核心元素。下面分别进行介绍。

1.2 background

背景页通常是Javascript脚本,是一个在扩展进程中一直保持运行的页面。它在你的扩展的整个生命周期都存在,在同一时间只有一个实例处于活动状态。在manifest.json中像下面这样使用scripts字段注册背景页:

{
  "name": "My extension",
  // ...
  "background": {
    "scripts": ["background.js"]
  },
  // ...
}

也可以使用page字段注册HTML页面:

{
  "name": "My extension",
  // ...
  "background": {
    "page": ["background.html"]
  },
  // ...
}

背景页和browser_action/page_action是运行在同一个环境下的,可以通过chrome.extension.getBackgroundPage()chrome.extension.getViews()进行两者之间的互相通信。
背景页也常常需要和content_scripts之间进行通信,要特别注意的是背景页和content_scripts是运行在两个独立的上下文环境中的,只能通过messages机制来通信,这个通信可以是双向的,首先写下消息的监听方:

chrome.extension.onRequest.addListener(function(request, sender, callback) {
   console.log(JSON.stringify(request));
   // deal with the request...
   sendResponse({success: true});
});

然后写下消息的发送方:

chrome.tabs.sendRequest(tabId, cron, function(response) {
   if (response.success) {
      // deal with the response...
   }
});

1.3 content_scripts

content scripts是一个很酷的东西,它可以让我们在Web页面上运行我们自定义的Javascript脚本。content scripts可以访问或操作Web页面上的DOM元素,从而实现和Web页面的交互。但是要注意的是,它不能访问Web页面中的Javascript变量或函数,content scripts是运行在一个独立的上下文环境中的,类似于沙盒技术,这样不仅可以确保安全性,而且不会导致页面上的脚本冲突(譬如Web页面上使用了jquery 1.9版本,而content scripts中使用了jquery 2.0版本,这两个版本的jquery其实运行在两个独立的上下文环境中互不影响)。content scripts除了不能访问Web页面中Javascript变量和函数外,还有其他的一些限制:

  • 不能使用除了chrome.extension之外的chrome.* 的接口
  • 不能访问它所在扩展中定义的函数和变量
  • 不能做cross-site XMLHttpRequests

但这些限制其实并不影响content scripts实现其强大功能,因为可以使用Chrome扩展的messages机制来和其所在的扩展进行通信,从而间接的实现上面的功能;而且,content scripts甚至可以通过操作DOM来间接的和Web页面进行通信。
使用content scripts在Web页面注入自定义脚本可以通过两种方法来实现:第一种方法是在manifest.json文件中使用content_scripts字段来指定,还有一种方法是通过编程的方式调用chrome.tabs.executeScript()函数动态的注入。这里有详细的介绍。

1.4 options_page

当你的扩展拥有众多参数可供用户选择时,可以通过选项页来实现。选项页就是一个单纯的HTML文件,可以引用脚本,CSS,图片等其他资源。这在Web开发中是家常便饭,只要你会制作网页,那么制作一个选项页肯定也没问题,这并没有什么好说的。但是,如果我们仔细想一想,当用户在选项页点击保存修改后,修改后的配置信息保存在哪儿呢?如何做到选项页中的配置在重启浏览器后甚至是清除浏览器数据后仍然存在呢?这就需要我们将配置信息保存到硬盘上的某个文件中,而浏览器Web脚本中的Javascript代码很显然是不能访问物理文件的。
这就是chrome.storage.local的由来,chrome.storage.local是Chrome浏览器提供的存储API,这个接口用来将扩展中需要保存的数据写入本地磁盘。Chrome提供的存储API可以说是对localStorage的改进,它与localStorage相比有以下区别:

  • 如果储存区域指定为sync,数据可以自动同步
  • 在隐身模式下仍然可以读出之前存储的数据
  • 读写速度更快
  • 用户数据可以以对象的类型保存
  • 清除浏览器数据后仍然可以访问

1.5 browser_action vs. page_action

上面已经说过,browser_action和page_action是扩展在Chrome浏览器中的两种不同的表现形式,browser_action显示在工具栏右侧,page_action显示在URL输入栏右侧。下面的代码示例说明了如何注册一个browser_action(page_action的注册方法类似,只要将browser_action替换成page_action即可):

{
  "name": "My extension",
  // ...
  "browser_action": {
    "default_icon": "images/icon19.png", // optional 
    "default_title": "Google Mail",      // optional; shown in tooltip 
    "default_popup": "popup.html"        // optional 
  },
  // ...
}

一个browser_action可以拥有一个icon,一个tooltip,一个badge和一个popup,page_action没有badge,也可以拥有一个icon,一个tooltip和一个popupicon是action的图标,一般情况下是一个19*19的png图片,也可以是HTML5中的一个canvas元素可以实现任意的自定义图片;tooltip是提示信息,当鼠标移到action图标上时会显示出来;popup是当用户点击action图标时弹出的窗口;badge是写在图标上文字,譬如下图中显示在RSS Feed Reader这个扩展图标上的67就是一个badge,由于badge空间有限,一般不会超过4个字符,超出部分会被截断。

actions

如果在default_popup字段中指定了popup.html,当用户点击图标时就会弹出来。这也是个简单的HTML文件,可以包含自己的脚本,样式和图片文件。如果没有指定popup.html,点击图标时会触发action的onClicked事件。如果你需要处理该事件可以在背景页background.js中使用类似于下面的代码:

chrome.browserAction.onClicked.addListener(function(tab) {
   // ...
});

browser_action默认总是显示,除非你在扩展管理里选择了隐藏按钮,而page_action默认是不显示的,需要使用函数chrome.pageAction.show()chrome.pageAction.hide()来控制page_action的显示。下面是一个简单的示例,只有当URL是www.baidu.com才会显示page_action:

function update(tabId) {
  if (location.host.indexOf('www.baidu.com') == -1) {
    chrome.pageAction.hide(tabId);
  }
  else {
    chrome.pageAction.show(tabId);
  }
}

chrome.tabs.onUpdated.addListener(function(tabId, change, tab) {
  if (change.status == "complete") {
    update(tabId);
  }
});

chrome.tabs.onSelectionChanged.addListener(function(tabId, info) {
  update(tabId);
});

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  update(tabs[0].id);
});

二、如何调试Chrome扩展

调试永远是软件开发中至关重要的一步,无论是桌面应用还是Web应用都是如此。Chrome浏览器的开发者工具提供给开发人员一个近乎完美的Web调试器,不仅可以查看Web页面的HTML源码,分析和修改DOM树,调试CSS样式,查看每一个网络请求进行性能分析,以及强大的Javascript脚本调试器。Chrome扩展和Web应用别无二致,自然也可以使用相同的调试方法对其进行调试,只不过要在几点不同之处特别注意一下。

2.1 popup的调试

如果你的扩展使用了popup,无论是browser_action还是page_action,都可以通过下面的方法调试popup:首先在扩展的图标上点击右键,然后选择“审查弹出内容”,如下图所示:

popup_dbg

这时会弹出开发者工具的窗口,我们选择Sources选项卡,在左侧就可以看到所有popup相关的HTML、Javascript以及CSS了。如下图:

popup_dbg2

查看Javascript代码找到我们感兴趣的地方,在该处下个断点,然后就可以进行调试了。如果断点处的代码是弹出窗口时就已经执行过了,那么可以切换到Console选项卡,输入location.reload(true)执行后popup会重新加载,并断在断点处,如下图:

popup_dbg3

2.2 background的调试

尽管背景页和popup是属于同一个执行环境下,但是点击“审查弹出内容”时并不能看到背景页的代码,也不能对其进行调试。要调试背景页,首先需要打开扩展管理页面,找到要调试的扩展,如果该扩展有背景页,会显示类似于如下图所示的“检查视图:background.html”字样,用户点击background.html弹出开发者工具既可以进行调试。

bg_dbg

和popup一样,也可以使用在Console选项卡中执行location.reload(true)这个小技巧来重新加载背景页。

2.3 content_scripts的调试

content_scripts是注入到Web页面中的Javascript代码,所以调试content_scripts和调试Web页面是完全一样的。我们直接按F12调出开发者工具,然后切换到Sources选项卡,在下面的左侧可以看到又有几个小的选项卡:Sources、Content scripts、Snippets。我们选择Content scripts就可以找到已经注入到这个页面的所有content_scripts。如下图:

content_dbg

可以看出一个页面可以被注入多个content_scripts,每一个content_scripts都有着他们自己独立的运行空间。找到感兴趣的代码下断点,然后就可以调试了。如果代码已经运行过,刷新下页面即可(当然,如果你在Console选项卡中执行location.reload(true)也是完全可以的,但这哪有F5方便呢:-))。

2.4 option.html的调试

选项页就是一个静态的HTML页面,和调试Web页面完全一样。没什么好说的了。

三、Search-faster的实现

通过上面的学习,我们基本上已经了解到了开发一个Chrome扩展所需要的基本知识了。现在我们通过实现一个最最简单的Chrome扩展来对学到的内容进行巩固。Search-faster非常简单,写它的目的也非常简单:加快我们在搜索引擎上的搜索速度。听起来很高大上,其实很简单,我们知道在我们搜索的时候,很多搜索引擎搜出来的结果并不是直接跳转到原网页,而且先跳转到搜索引擎自身,然后再跳转到原网页。如下图所示,百度搜索就是这样做的:

baidu_link

当然Google也是这样:

google_link

这样做搜索引擎可以对每个搜索结果进行统计分析然后优化,但是对于我们用户来说,多做一次跳转显然会降低我们的速度。而且在我们大天朝,通过代理访问Google本来就已经够慢的了,点击每个搜索结果再跳转一次到Google实在是有点让人受不了。
我本来打算对百度、Google和Bing做统一处理的,后来发现百度的跳转链接是经过加密后的,一时破解不了,而Bing的搜索结果并没有跳转而是直接到原网页。于是这个Search-faster其实就变成了Google-search-faster了。

首先我们确定我们的扩展类型,因为只有在访问Google搜索时才需要显示,所以我们采用page_action而不是browser_action。然后我们确定需要哪些文件,因为要访问Google搜索的结果页面,所以肯定需要一个content_scripts,content_scripts的内容是遍历Google搜索结果页面上的所有跳转链接,获取每个链接的原链接,然后在每个链接上添加一个click事件,当用户点击该链接时直接跳转到原链接。(本来是打算直接修改链接为原链接的,但是发现Google的代码中有检测功能,会自动将跳转链接替换回来,所以使用click事件的方法最为保险)。最后我们需要一个背景页,检测浏览器选项卡的变动,当用户切换选项卡或选项卡有变动时执行content_scripts。
manifest.json的代码如下:

{
  "name": "Search-faster",
  "version": "1.0",
  "description": "Replace the search engine redirect url to direct url when searching baidu, google, etc.",
  "background": { "scripts": ["background.js"] },
  "content_scripts": [{ 
    "matches": ["http://*/*", "https://*/*"], 
    "js": ["jquery.min.js", "content_script.js"] 
  }],
  "page_action": {
    "default_icon" : "icons/google.png",
    "default_title" : "It works!"
  },
  "permissions" : ["tabs"],
  "manifest_version": 2
}

content_scripts的关键代码如下:

chrome.extension.onRequest.addListener(function(req, sender, sendResponse) {
    var response = doReplace();
    sendResponse(response);
});

/**
 * replace the redirect url to direct url
 */
function doReplace() {

    // url not match
    if(location.host.indexOf('www.google.com') == -1) {
        return null;
    }

    // get the keyword
    var lstib = $('#lst-ib');
    if(lstib.length == 0) {
        return null;
    }

    // get the links &amp; add a click event
    var links = $('.srg .g h3.r a');
    links.on('click', function(e) {
        var href = $(e.target).attr('data-href');
        window.open(href);
        return false;
    });

    return {
        keyword: lstib[0].value,
        replace_cnt: links.length
    };
}

background的关键代码如下:

function updateSearch(tabId) {
  chrome.tabs.sendRequest(tabId, {}, function(search) {
    searches[tabId] = search;
    if (!search) {
      chrome.pageAction.hide(tabId);
    } else {
      chrome.pageAction.show(tabId);
      if (selectedId == tabId) {
        updateSelected(tabId);
      }
    }
  });
}

chrome.tabs.onUpdated.addListener(function(tabId, change, tab) {
  if (change.status == "complete") {
    updateSearch(tabId);
  }
});

chrome.tabs.onSelectionChanged.addListener(function(tabId, info) {
  selectedId = tabId;
  updateSelected(tabId);
});

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  updateSearch(tabs[0].id);
});

完整的代码在这里

参考

  1. 综述--扩展开发文档
  2. Chrome 扩展程序、应用开发文档(非官方中文版)
  3. Sample Extensions - Google Chrome
  4. Chrome插件(Extensions)开发攻略
  5. 手把手教你开发chrome扩展一:开发Chrome Extenstion其实很简单
  6. 手把手教你开发Chrome扩展二:为html添加行为
  7. 手把手教你开发Chrome扩展三:关于本地存储数据
  8. Chrome.storage和HTML5中localStorage的差异
扫描二维码,在手机上阅读!

Win7下VMware的NAT网络模式不能正常工作

今天在Windows 7下实验VMware的NAT网络模式时遇到了障碍,Guest-OS不仅无法访问Internet,而且连Host-OS也ping不通。有时候仿佛能ping通Host-OS,但是却返回大量的(DUP!)包,如下图所示:

ping-dup

根据这个提示,在Google上搜索“ping DUP!”得到大量的结果,有的说禁用VMnet8网卡然后重启即可解决,有的说可能是局域网上有IP冲突,有的说可能ping的是广播地址,还有的说局域网有回路等等,除了第一个禁用/启用VMnet8具有可操作性之外,其他都是些莫名其妙的不知道具体如何解决的答案。但是,遗憾的是禁用/启用VMnet8后问题依旧。 在Guest-OS上执行iptables -L并未发现有设置防火墙规则,并且在Host-OS上已经禁用防火墙了,所以应该可以排除防火墙的原因。然后在vmware社区上看到了这篇帖子,有个人使用tcpdump来定位问题,实验后仍然没有发现什么异常,无果而终。
在ping DUP!这个问题上花费了大约整整一天的时间,最终意识到ping DUP!可能并不是虚拟机无法访问网络的症结所在,通过网络上给出的答案可以发现ping DUP!问题更多的原因可能是局域网内网络配置错误或者网卡异常导致的,而且出现这种现象的原因太多,很难入手。于是重新审视这个问题,把问题的所有现象一一列出来:

  • Host-OS IP: 192.168.0.107
  • Host-OS Gateway: 192.168.0.1
  • Guest-OS IP: 192.168.220.128
  • Guest-OS Gateway: 192.168.220.2
  • Guest-OS ping自身192.168.220.128正常,ping网关192.168.220.2正常
  • Guest-OS ping Host-OS 192.168.0.107返回大量DUP包
  • Guest-OS ping Host-OS的网关 192.168.0.1返回大量DUP包
  • Host-OS ping Guest-OS 192.168.220.128正常
  • 打开网络和共享中心,VMnet1和VMnet8属于未识别的网络,而且公共网络不能修改

最后两点现象很快就让人感到异常,特别是未识别网络这条,如下图所示:

unidentified-networks

于是Google关键字“VMnet8 未识别的网络”,根据第一条记录的结果修改注册表中的*NdisDeviceType=1然后重启VMnet8网卡,问题竟然就解决了!修改注册表参见下图:

ndis-device-type

原来是Windows 7网络访问的限制,也可以说是VMware的一个Bug,VMware在新建网卡时设备类型*NdisDeviceType使用了默认值0,0意味着网卡是标准的真实网卡,VMware应该将其设置为NDIS_DEVICE_TYPE_ENDPOINT(1)才对,这样Windows会知道这个网卡是个虚拟网卡并没有和真实的外部网络连接,在“网络和共享中心”中忽略这些网卡,不会受网络共享设置的限制。

通过解决这个问题,再次意识到真的“不能在一颗树上吊死”。解决问题往往有两种方式:

  • 第一种方式是:首先找出问题的一个异常现象,然后通过各种手段解决这个异常,最终会有两种结果:一是异常解决并且问题解决,二是异常解决问题并未解决。如果是情况一,则问题解决,结束。如果是情况二,则排除了这个异常和问题的联系,这时需要寻找另一个异常现象,重复上述步骤继续解决
  • 第二种方式是:首先列出所有发现的异常现象,然后针对每个异常现象快速确定和问题有联系的可能性(譬如利用搜索引擎),然后针对每个可能性再继续对问题进行分析

可以发现两种解决问题的方式正如图的两种遍历算法:深度优先算法和广度优先算法,方式一为深度优先,方式二为广度优先。深度优先的缺点是可能会导致无止境的往下延伸,越往下延伸解决方案离最初的问题可能就越远,直到排除所有可能性后才能解决最初的问题,在遇到奇葩的问题时,效率可能会非常低下。我们将每一次延伸称之为问题的解决路径,如果你的问题的解决路径很短,譬如我们这里的这个问题,根据现象“VMnet8 未识别网络”就可以直接解决问题,而根本不需要在ping DUP!问题上耗费无数的时间来研究。整个问题的解决示意图如下:

solve

可见,如果根本不能确定问题原因时,通过广度遍历的方式,加上问题之间的联系,可能会更好更快的解决问题。所以,在面临问题时不要沉陷在自己的某一个想法中,可以从多个角度去看待问题。

参考

  1. VMware Workstation环境中Linux ping返回出现大量“DUP!”的解决方法
  2. ping DUP! 错误
  3. Duplicate packets with ping on guest OS(Linux)
  4. VMware Network Adapter VMnet1和VMnet8未识别的网络修复
  5. Hide VMWare Virtual Network Interfaces from Windows Firewall and Network and Sharing Center [需翻墙]
扫描二维码,在手机上阅读!

实战VMware的三种网络模式

一、实验目的

最近在使用VMware搭建虚拟网络环境时遇到了很多问题,经过对VMware网络模式的一番学习和实战,总算是对其有了一定的认识。所以决定完成一次比较完整的针对VMware网络配置的实验,并写下这篇博客记录整个实验的过程。在进行虚拟机实验时,我们往往关注下面这三个问题:

  • 虚拟机和主机能不能互相访问
  • 虚拟机能不能访问Internet
  • 外部网络如何访问虚拟机中的服务

我们会通过后面的实验分别利用VMware的不同网络模式来解决这三个问题。VMware提供了三种不同的网络模式,分别是Bridged(桥接模式)、NAT(网络地址转换模式)、Host-only(主机模式)。不同的网络模式适用于不同的网络环境,实现不同的需求,你需要根据实际情况选择合适的模式。 就网络环境来说,我们常见的家庭网络拓扑结构有下面两种:

  • 主机通过拨号直接连接Internet
  • 主机处于局域网环境中,通过路由器拨号连接Internet

如果你是属于第一种网络环境,由于是ISP分配你的公网IP(假设只有一个地址),则不能使用桥接模式,因为桥接模式需要你拥有属于你机器相同网段内的另一个IP地址。这种情况下可以使用NATHost-only。而如果是属于第二种网络环境,则三种模式可以任意选用。 就需求来说,你可能只是想简单配置一个虚拟机来玩玩,也可能是为局域网内的其他机器提供服务,或者是进行一些特殊目的的网络实验,这个就要具体情况具体对待了。

二、实验环境

本次实验软硬件环境如下:

  • 操作系统:Windows 7 旗舰版(32位)
  • CPU:Intel(R) Core(TM) i3
  • 内存:4G
  • 虚拟机:VMware® Workstation 8.0.0 build-471780

网络环境为家庭局域网,由路由器通过PPPOE拨号上网,如下图所示:

network-basic

三、实验步骤

了解了我们的需求和网络环境之后,就开始我们的实验之旅吧。首先是下载并安装VMware Workstation,然后下载虚拟机上需要的操作系统镜像,并安装到虚拟机中。我这里创建了三个虚拟机,分别是Windows XP、Ubuntu和Backtrack操作系统。

3.1 桥接模式

桥接模式是三种模式中最简单的一种,VMware安装的时候默认就使用这种配置方式。在这种模式下,虚拟机相当于局域网中的一台独立机器,和主机处于同一个网段,公用同一个网关。桥接模式使用的是虚拟机的VMnet0网卡,一般情况下,在虚拟机中将网络设置成自动获取IP就能直接联网。示意图如下:

bridge

在桥接模式下,虚拟机和主机可以互相ping通,虚拟机可以访问Internet,虚拟机上的服务也可以通过虚拟机IP地址在本机直接访问,如果有任何问题,可以按下面的步骤检查各个配置:

  1. 检查本地连接的属性里,是否勾选了VMware Bridge Protocol,如果没勾选上,则虚拟机和本机不能互相ping通,如下图:

bridge-protocol

  1. 检查虚拟机的IP地址,看是否和本机处于同一个网段内,如果不是,可以手工设置下IP地址,网关和DNS服务器配置可以参考本机,和本机一样即可
  2. 检查本机防火墙和虚拟机防火墙

3.2 NAT模式

上面也说了,如果你不在局域网内,只有一个IP,那么NAT模式正适合你。当然如果你在局域网内,NAT模式也未尝不可,不过使用NAT模式后,主机就变成了双网卡:本身的网卡连接Internet或连接拨号的路由器,另一个虚拟网卡VMnet8连接由虚拟机组成的一个虚拟网络。从外部网络来看,无法直接访问这个虚拟网络。虚拟网络则通过本机上的NAT虚拟服务器进行转发访问Internet。示意图如下:

nat

NAT模式是让虚拟机实现访问Internet最快的方式,几乎不用任何配置,只要主机能上网,那么虚拟机也就肯定能上网。如果有任何问题,可以通过下面的步骤进行排查:

  1. 检查主机上VMware的NAT服务和DHCP服务是否开启,如下图:

nat-service

  1. 检查虚拟机的IP地址,是否和虚拟机NAT配置的Subnet Address在同一个网段内,选择Edit -> Virtual Network Editor可以看到NAT的配置信息
  2. 检查主机和虚拟机的防火墙设置 默认情况下,NAT配置好之后,主机和虚拟机之间应该可以互相访问,虚拟机也可以借助主机上的NAT访问Internet,但是外部网络无法访问虚拟机上的服务。如果需要让同一个局域网内的其他机器(譬如:192.168.0.100)访问虚拟机上的WEB服务,可以通过NAT的端口转发(Port Forwarding)功能来实现,如下图。具体的配置细节可以看参考链接

nat-port-forwarding

3.3 Host-only模式

Host-only模式和NAT一样,也相当于主机双网卡,网络拓扑和NAT也是一样,只是主机不提供NAT功能了,所以虚拟网络只能和主机访问,不能访问Internet。如果需要一个完全隔离的网络环境,则Host-only最合适不过。Host-only相当于使用双绞线直接连接虚拟机和主机,这是最原始的网络结构,当然也是最灵活的。这种情况下虚拟机就不能访问Internet了吗?局域网下的其他机器就不能访问虚拟机上的服务了吗?当然不是。如果我们自己在主机上搭建起我们自己的NAT服务和DHCP服务,那么Host-only其实和NAT是一样的。从下面的示意图也可以看出,Host-only和NAT的唯一区别就在于,主机上少了NAT这个部分。

host-only

类似于NAT,具体的配置这里略过。下面通过Windows上的ICS服务(Internet Connection Sharing,Internet连接共享)来实现Host-only模式的虚拟机访问Internet。ICS是Windows上的一种共享网络访问的服务,类似于mini版NAT,提供了NAT地址转换和DHCP的功能,但不支持端口转发(Port Forwarding)。 首先在网络连接里找到当前正在使用的连接,选择属性 -> 共享,选中“允许其他网络用户通过此计算机的Internet连接来连接”,然后在网络连接下拉框中选择Host-only对应的虚拟网卡(这里是VMnet1),如下图:

ics

在确定的时候,可能会弹出对话框提示错误:“Internet连接共享访问被启用时,出现了一个错误(null)”,这时去服务中找到Windows Firewall,启动即可。 ICS配置好之后,Host-only就和NAT一样了,在虚拟机中设置自动获取IP或手工设置IP,保证和VMnet1处于同一个网段内,如果一切顺利,就可以在虚拟机中访问Internet了。

四、总结

通过这次的实验,重新学习并巩固了计算机网络的相关知识。特别是NATPort Forwarding,是网络管理中很重要的手段。虽然模式各不相同,但是在局域网环境下,通过各种技术手段,最终都能实现相同的目的:虚拟机和主机互相访问(三种模式都可以),虚拟机访问Internet(Bridged和NAT直接可以访问,Host-only通过ICS也可以),外网访问虚拟机上的服务(Bridged直接访问,NAT通过端口转发也可以,Host-only通过架设自己的NAT服务也应该可以)。 本次实验环境比较简单,也没有考虑双网卡的情况,可以参考下面的链接进一步研究。

参考

  1. 简单区分Vmware的三种网络连接模式(bridged、NAT、host-only)
  2. Vmware虚拟机下三种网络模式配置
  3. VMWare虚拟机 网络连接模式 [汇总帖]
  4. VMware虚拟机上网络连接(network type)的三种模式--bridged、host-only、NAT [汇总帖]
  5. VMWare虚拟机下为Ubuntu 12.04.1配置静态IP(NAT方式)
  6. VMware Workstation虚拟机网络连接杂记、给Windows虚拟机配置固定IP
  7. 解决Windows 7/win8 使用VMware虚拟机的NAT 不能上网
  8. 使用 ICS(Internet 连接共享)
  9. internet连接共享访问被启用时,出现一个错误
  10. VMware Workstation虚拟机实例:让外网访问虚拟机
  11. VMware Workstation实例二:单IP的虚拟机提供外网访问
扫描二维码,在手机上阅读!

为什么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)
扫描二维码,在手机上阅读!