分类 工具 下的文章

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

我的第一个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 & 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的差异
扫描二维码,在手机上阅读!

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内核
扫描二维码,在手机上阅读!

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

通过FiddlerScript实现根据条件重发请求

Fiddler是个强大的Web调试工具,具体的功能不在此多述,可以参考后面的链接以及Fiddler官网的手册。本文主要介绍Fiddler的重发请求功能,并通过自定义脚本实现根据条件来重发请求。 在进行Web调试时,经常会遇到浏览器请求正常但是程序请求异常的情况,这时我们常常需要使用Fiddler对比这两个请求的异同,然后将一个请求改变参数或HTTP头进行重发来查看返回结果的差异,这样可以确定哪个参数或哪个HTTP头导致的问题。如下图重发可以有多种不同的选择,常用的有三个:

  1. Reissue Requests: 直接重发选定请求
  2. Reissue and Edit: 重发选定请求,并在请求之前断点,可以对请求进行修改
  3. Reissue from Composer: 将选定请求送到Composer窗口,和将请求拖拽到Composer效果是一样的,在Composer窗口中可以对请求有更精确的控制

replay

只简单的重发指定请求,或在指定请求上进行编辑往往是不够的,在项目中我们偶尔会遇到这样的情形:先发送请求A,然后根据请求A结果中的某个值来发送请求B,譬如有这样的两个接口:get_random_server.php接口通过接收的数据随机返回一个服务器ID,get_data.php接口则根据刚刚的服务器ID来获取数据。下面是一个示例:

  1. localhost/get_random_server.php?data=Hello -> 返回JSON结果:{ success: true, sid: 2 }
  2. localhost/get_data.php?sid=2

这个时候Fiddler的可扩展性就能大显神威了,可以通过两种方式实现Fiddler的扩展:FiddlerScript和插件机制,这里使用FiddlerScript就足够应付了。在Fiddler的菜单项Rules中选择Customize Rules...就可以打开Fiddler的自定义脚本文件CustomRules.js,该脚本一般保存在\Documents\Fiddler2\Scripts目录下。我推荐使用Fidder ScriptEditor进行脚本的编辑,Fidder ScriptEditor具有语法高亮和语法检查的功能,并在右侧面板提供了Fiddler内置的函数列表。 通过展开浏览右侧的函数列表,就基本上可以大概的了解到几个可能会用到的函数了:

  1. FiddlerApplication.oProxy.SendRequest
  2. FiddlerApplication.oProxy.SendRequestAndWait
  3. FiddlerObject.utilIssueRequest

我们先通过下面的代码来练练手,将下面的代码拷贝到CustomRules.js中并保存,Fidder ScriptEditor会自动检查语法错误,并重新加载脚本,无需重启Fiddler脚本即可生效。CustomRules.js使用的是JScript.Net语法,对于Javascipt或.C#程序员应该可以很快上手。这时在Fiddler中随便选择一条请求,点击右键,会发现最上面多了一个选择项Test Send Request,选择该项可以达到和Reissue Requests同样的功能,重发指定请求。

    public static ContextAction("Test Send Request")
    function SendRequest(oSessions: Session[]) {
        
        if (oSessions.Length == 0) return;
        FiddlerApplication.Log.LogString("Sending...");
        
        var selected: Session = oSessions[0];
        
        var oSD = new System.Collections.Specialized.StringDictionary();
        var res = FiddlerApplication.oProxy.SendRequestAndWait(selected.oRequest.headers, selected.RequestBody, oSD, null);
        FiddlerApplication.Log.LogString("Request has been Send.");
        FiddlerApplication.Log.LogString(res.GetResponseBodyAsString());
    }

SendRequest/SendRequestAndWait函数有一个不方便之处,他的两个参数oHeadersarrRequestBodyBytes分别是HTTPRequestHeadersByte[]类型,为了调用这个方法必须将HTTP的header和body转换为这两个类型,不如字符串来的简便。这个时候utilIssueRequest函数正好满足我们的定制需要,可以精确的控制一个请求的细节,类似于Composer中的Raw。下面的代码是一个使用utilIssueRequest函数的实例,具体的HTTP请求以字符串的形式拼接起来。

    public static ContextAction("Probe this!")
    function ProbeSession(oSessions: Session[]) {
        
        if (oSessions.Length == 0) return;
        FiddlerApplication.Log.LogString("Probing...");
        
        var selected: Session = oSessions[0];
        var raw = "";
        
        // methods
        var method:String = selected.RequestMethod;
        var url:String = selected.fullUrl;
        var protocol = "HTTP/1.1";
        FiddlerApplication.Log.LogString(method + " " + url + " " + protocol);
        raw += method + " " + url + " " + protocol + "\r\n";
        
        // headers
        for (var i:int = 0; i < selected.oRequest.headers.Count(); i++) {
            var header = selected.oRequest.headers[i];
            FiddlerApplication.Log.LogString(header);
            raw += header + "\r\n";
        }
        
        // body
        FiddlerApplication.Log.LogString("---");
        var body = selected.GetRequestBodyAsString();
        FiddlerApplication.Log.LogString(body);
        raw += "\r\n" + body;
        
        FiddlerObject.utilIssueRequest(raw);
        FiddlerApplication.Log.LogString("Request has been Send.");
    }

HTTP请求的格式如下:

POST http://localhost/get_random_server.php HTTP/1.1
Host: localhost
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4
Content-Type: application/x-www-form-urlencoded
Content-Length: 23

data=%E4%BD%A0%E5%A5%BD

后面的工作就水到渠成了,通过SendRequestAndWait获取请求A的结果,解析请求A结果获取sid参数,然后拼接HTTP请求调用utilIssueRequest函数,此处从略。

参考

  1. FiddlerScript Editor
  2. Search Sequential Pages for Target String
  3. HTTP协议详解
  4. Fiddler 教程
扫描二维码,在手机上阅读!

Unable to start debugging on the web server 错误

今天在用Visual Studio调试Web程序时,突然出现下面这个错误:

Unable to start debugging on the web server. The Microsoft Visual Studio Remote Debugging Moniter (MSVSMON.EXE) does not appear to be running on the remote computer...

如下图所示:

msvsmon_err

起先以为是VS的问题,重启了好几次,但是点击调试按钮的时候仍然是这个提示信息。百思不得其解,因为这个项目下午的时候还可以调试,一直没有人碰过电脑,没改过代码也没装过软件,到晚上的时候就报错了。
不得己Google之,发现下面的解决方案:修改注册表中的DisableLoopbackCheck项,修改方法见参考链接。 重启电脑后问题依旧,最后努力思索从下午到晚上这段时间都做过哪些操作可能会导致影响。看错误信息,感觉是服务端口被占用之类的问题,但是这段时间确实没有安装或启动过什么服务。然后想到可能是配置文件的错误导致,继而突然意识到晚上在写代码之前,用SVN Update了下代码,查看SVN日志,发现有人提交了*.csproj.user文件,查看这个文件的变更。
老的配置文件如下:

<UseIIS>True</UseIIS>
<UseCustomServer>False</UseCustomServer>
<CustomServerUrl></CustomServerUrl>

修改后的代码如下:

<UseIIS>False</UseIIS> 
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://www.test.com/</CustomServerUrl>

原来是因为团队内有人不喜欢VS自带的http://localhost:4738/这个URL,便使用修改hosts文件的方式,将www.test.com指向127.0.0.1,并在VS的Web配置中选择了"Use Custom Web Server",这样URL确实好看了不少。 但是问题是:这个文件的提交直接导致了我浪费了一晚上的时间。
最后在VS的Web配置中改回默认的"Use IIS Express",问题解决(其实参考链接中也有人回答了该解决方法,只是我一开始没有注意到...),如下图所示:

web_server_config

教训总结:

  1. 任何一点小小的操作都可能导致意想不到的问题,要留心自己的每一个操作,并注意检查每个操作可能带来的影响;
  2. 绝对不要提交私人的配置文件到SVN服务器,譬如这里的*.csproj.user文件,应该加到ignore列表中。

参考

VS2010 error: Unable to start debugging on the web server

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

使用SVN的8个技巧

1. 使用SVN钩子强制提交注释

一个好的SVN实践是文件提交时要求必须填写注释,并注明相关修改信息,如bug号、任务描述等,内容按照约定编写。这样在后期的代码审核和回溯过程中会非常方便,可以更快的定位到具体代码的修改记录。

所谓SVN钩子就是一些与版本库事件发生时触发的程序,例如新修订版本的创建,或者是未版本化属性的修改。目前subversion提供了如下几种钩子:start-commitpre-commitpost-commitpre-unlockpost-unlockpre-lockpost-lockpre-revprop-changepost-revprop-change,其中我们修改post-commit脚本即可实现强制提交注释的功能。

如下是一个实现强制提交注释的post-commit脚本例子:

REPOS="$1"
TXN="$2"

# Make sure that the log message contains some text.
SVNLOOK=/usr/bin/svnlook
LOGMSG=`$SVNLOOK log -t "$TXN" "$REPOS" | wc -m`

if [ "$LOGMSG" -lt 4 ]
then
    echo "拜托,写点注释吧!" 1>&2
    exit 1
fi

exit 0

2. 创建本地仓库

有时候我们在没有网络的情况下无法连接外网上的SVN仓库,或者没有条件搭建SVN服务,这时我们同样可以使用SVN管理我们自己的代码。我们可以使用TortoiseSVN在本地创建代码仓库,并进行代码的版本管理。具体步骤如下:

  1. 新建一个空文件夹,用于存放本地的代码仓库;
  2. 在这个空文件夹上点击右键 -> TortoiseSVN -> Create Respository here,创建仓库;
  3. 在另一个目录Checkout,本地SVN路径格式类似于:file:///C:Repo

3. SVN命令行操作

在Windows系统中TortoiseSVN是进行SVN代码管理的最佳利器,操作也非常简单方便。但是在一些特殊环境下,熟悉SVN命令行操作也是必须的,譬如想在一些自动化脚本程序中使用SVN的功能。 这里总结一些常用命令如下,更多命令请访问后面的参考链接:

(1) 从版本库获取信息

  • svn help
  • svn info $url
  • svn list
  • svn log
  • svn diff

(2) 从版本库到本地

  • svn [co|checkout] $url $local
  • svn export $url $local
  • svn [up|update]

(3) 从本地到版本库

  • svn import $local $url -m "some comments..."
  • svn add $file
  • svn delete $file
  • svn mv $oldfile $newfile
  • svn rm $url
  • svn [ci|commit]
  • svn revert $file 和 svn revert -R $dir

4. 更换版本比较工具

在进行SVN提交时需要非常谨慎,每次提交之前应先做SVN更新或与资源库同步,要特别注意SVN关于冲突和错误的提示信息。对每个提交的文件进行多次检查,确认它们是不是你真正想要提交的。

在提交的文件上选择Compare with base可以将本地代码和版本库中的代码进行比较,确保修改的内容无误。TortoiseSVN默认使用自带的TortoiseMerge工具进行代码比较,也可以更换其他的代码比较工具:在TortoiseSVN -> Settings -> Diff Viewer选项中找到Configure the program used for comparing different revisions of files,选择External,然后选择你喜欢的比较工具,如:Beyond Compare。

5. SVN服务器迁移

这条其实并不算SVN技巧,但是在我们日常工作中确实经常会遇到这样的情况,想将一台SVN服务器上的仓库迁移到另一台服务器。如果SVN服务器是一台Windows服务器,可以直接将SVN仓库目录复制到新服务器上,然后在新服务器上重启SVN服务即可。如果SVN服务器是linux服务器,可以通过下面的命令操作,将一台服务器上的目录拷贝到另一台服务器。

$ scp -r root@x.x.x.x:/home/svn /home/svn
$ svnserve -d -r /home/svn

在TortoiseSVN客户端,需要更新SVN地址:右键 -> TortoiseSVN -> Relocate...

6. svn:ignore

我们在用SVN提交代码时,常常有一些文件未版本化并且也不想提交,所以在提交时根本不想看到这些文件,譬如类似于Visual Studio工程的bin obj目录。

可以使用 svn propset svn:ignore 命令来将某个文件或目录添加到忽略列表中。可以在下面的链接中找到一些常见的ignore文件:Best general SVN Ignore Pattern?

7. SVN目录结构

Subversion有一个标准的目录结构,如下所示:

svn://projects/
|
+- trunck
+- branches
+- tags

其中,trunk为主开发目录,branches为分支开发目录,tags为tag存档目录(不允许修改)。这几个目录具体怎么使用,svn并没有明确规范,一般有两种方式:trunk作为主开发目录或者trunk作为发布目录。

8. SVN使用原则

  • 代码变更及时提交,避免本地修改后无法修复;
  • 提交前确认代码可编译通过,保证新增的文件同时被提交;
  • 不要将格式修正和代码修改混合提交。修正格式包括增加缩进、减少空格等,如果把这些和代码修改一起提交,很难从日志信息中发现代码修正记录;
  • 每次提交尽量是一个最小粒度的修改,如果一次提交涉及到两个完全不同的功能,那么分两次提交,并在注释中写清楚提交内容;
  • 所有代码文件使用UTF-8格式;
  • 提交的文件必须是开发者公用的程序文件,不要提交私人测试程序、程序缓存、图片缓存、自动生成的文件等。

参考

  1. SVN之使用原则
  2. svn强制写日志和可以修改历史日志(svn钩子的应用)
  3. SVN搭建本地文件版本管理
  4. SVN 命令参考
  5. 关于SVN 目录结构
扫描二维码,在手机上阅读!