
置顶随笔
Application Developer
If you would like to...
In
this position you will be responsible for technical leadership
contributing to the successful delivery of application development
projects. Your responsibilities will also include but not be limited to:
- Create
complex, enterprise-transforming business applications as a member of
diverse, high energy teams focused on full life-cycle development
- Use the latest tools and techniques (currently J2EE, C#/.NET, XML,Agile Methodologies, Web Services, EAI tools...)
- Do
hands-on coding and proactively mentoring of developers (including pair
programming), using J2EE and/or C# Continually learn, mentor and grow
- Work closely with senior project management and clients
- Travel to work at client sites, possibly extensively, which may include opportunities for international travel
And you have...
- Strong
development experience with OO languages, specifically on complex
enterprise systems using C/C++, Java J2EE and/or within the .NET
framework (framework including C#, [ADO.NET], [ASP.NET] and [VB.NET])
- Broad understanding of the Java API, including but not limited to JMS, EJB, JSP, Servlets
- Experience with object oriented analysis/design
- Strong
knowledge of design patterns, refactoring and unit testing Agile
Methodology knowledge, such as Extreme Programming (XP) & Scrum
- Participated in full life-cycle development
- Experience with relational databases is a plus
- Exposure to EAI technologies (MSMQ, Tibco, Vitria, SeeBeyond, MQ Series) is very helpful
- Excellent written and oral communication skills in both English and Chinese
- Ideally with an academic background in Computer Science or Engineering
Location
We want to hear from you! ThoughtWorks values
aptitude, attitude, and integrity. If you thrive on challenge,
unlimited possibilities, and unparalleled learning, send your resume or
apply online now.
http://www.thoughtworks.com.cn/work-for-us/jobs/Application-Developer.html
posted @
2008-11-03 09:36 taowen 阅读(175) |
评论 (1) |
编辑

2008年11月11日
谈到敏捷,人们往往都非常高调地打探TDD与持续集成。同时很多实践,非常低调。比如,估算。学习TDD,你有很多具体例子可以学习。但是学习估算,却无从下手。在“搞砸”了几个项目之后,貌似我摸着了一些门道了。
估算的目的
在我看来,估算有两个截然不同的目的。第一个是报价,第二个是规划。为了报价,我们追求的是所有故事卡的总体的绝对大小。为了指导发布规划和在迭代开发中做调整,我们追求的是故事卡之间的相对大小的准确。但是无论,这两个目的都很难达到。
别说不靠谱
首先我想说,估算故事卡的绝对大小,然后拿去报价,TMD这就是扯——淡——。不过,谁让我们是小角色呢,领导让你做估算,你还能不做不成?想不想干了?所以,我们还得干。但是,还千万别不认真做。做得一塌糊涂的估算最后还是要我们一个一个故事卡地去码代码,最后倒霉的还不是我们这些小角色吗?所以,即使你觉得这追求估算绝对大小的事再不靠谱,你还得认认真真,仔仔细细地做好它。
你的痛苦我明了。敏捷从来不打算在项目开始的时候把故事卡写得能让我们读得懂的。如果一开始就把验收条件写得清清楚楚,再按上若干个手印,那就不叫敏捷了。所以,从一开始敏捷就是玩我们开发人员的,你得有这个心理准备。
一般,项目开始阶段的时候都有一个需求收集阶段。幸运的话,你也参与了这个阶段。然后,会有商务分析师写出一个高屋建瓴的故事卡列表来,非常非常的高屋建瓴。拿到这个故事卡列表之后,你干什么?
第一件事
第一件事情就是浏览整个列表,试图归纳一下商务分析师是按照什么思路去划分故事的。如果我们把整个项目看做一团泥:

那商务分析师写的Story,就是对它的第一次切分:

到我们写代码,把一个个故事卡变成一打一打的类的时候,是对它第二次切分:

所以,我们程序员和商务分析师做的事情,本质上是一脉相承的。商务分析师的职责就是把故事卡写到最小的程度,写到”外部可见”地最小程度。而外部不可见的部分,也就是类之间的协作等面向对象建模就由我们程序员来完成。所以,好的商务分析师写出来的故事,你非常舒服,拿来就可以写。但是,我们不在理想的世界种,不是吗?不可能给每个项目都配备最强的商务分析师。很多时候,你面对那故事列表会哭笑不得。体现出来的现象是,很复杂的一件事情,没有足够的故事来覆盖。而很简单,或者本质一样的事情,却写出了一堆故事来。所以这就需要你做两件事情。
第一件事情是查漏补缺,无论故事列表看上去是怎么完善,你自己要心里面对整个流程有一个数。然后去确认没有遗漏的故事。第二件事情是寻找重复,大概明白它们是怎么实现的,分析故事之间是不是从实现地角度看是重复的。
最糟糕的情况是商务分析师的切分角度完全错误,切分的时候没有把握住系统的变化点(真正是代码复杂度所在的地方)。对于整体不了解就开始估算是非常危险的。开发人员在这个时候一定要强硬,对于遗漏的故事一定要提出来,对于重复的故事一定要去掉,对于切分完全错误的故事列表一定要求她们返工。
不明白的时候,我们能做什么?
别指望在项目开始的时候,故事卡上会有很多字。让你估算,你算不出 。难不成你给你的项目经理说,不如你来?NO,NO WAY。这个时候你首先应该与商务分析师交谈,尽可能多的了解细节。但是我们在报价阶段,或者项目前期,商务分析师自己也不是很了解业务,而且我们也没有无限多的时间是去调查了解。所以,真实世界中,你必须在没有充足信息的情况下做出估算。这个时候,你有两个选择:
1、拒绝估算
有的时候,我们可以说不。荒谬的估算,害人害己。不要指望,一个“充分大”的估算可以把你罩住。把小的故事估算大了,只能让其他真正大的故事相对变得小。不要给10,或者8这样没有谱的数字。不要在你所有的估算值中随意地选一个非常大的值。因为你一旦选择了这个值,比如10,项目经理在做发布计划的时候就真的会当10来计划,这意味着5张2的故事卡,3张多3的故事卡。你要扪心自问,这真的有5张2的故事卡那么多吗?或者这够吗?10不是10,10代表你“不知道”。程序员可以说不。
2、做一些假设,把假设写下来,然后在假设的基础上做估算
这是迫不得已的做法,特别是你要报价的话。你必须要把你做的假设写下来。因为你会忘记,你会离开这个项目。你不希望你的同事实现这张故事卡的时候骂你对不对?那请你把你做的每个假设,比如不包括数据库,只有前台界面等等,写在你的估算的旁边。剩下的事情就是祈求你的项目经理和商务分析师会在做迭代开发的时候尊重你的假设。当假设变化的时候,让客户知道,我们做了额外的超出了估算范围的事情,你必须补偿我(。。。Ideally)
做加减法不要做乘除法
在具体做估算的时候,有两种风格。一个种是乘除法,一种是加减法
1、乘除法
选择一张所谓的一个点的故事卡。然后其他的故事卡去和这张一个点的卡做比较,得出它们的估算。如果需要两倍时间,那就是两个点。如果是三倍时间就是三个点。
2、加减法
选择一张中等尺寸的故事卡。然后其他的故事卡去和这张卡做比较,比它大的一点的,我们加一,比它大很多的,我们加二,比它小一点的,我们减一,比它小很多的,我们减二。
我个人觉得, 不要去用乘除法,只用加减法。因为人对于衡量两个物体的倍数,非常不擅长。即便把项目做完了,你也未必能准确估计两张故事卡的倍数。另外,写到好的故事,应该是大小相当的。如果故事的尺寸相差三倍是一个常态,那我觉得这是不正常的。理论上来说,最小的和最大的故事卡,应该在两倍大小左右。如果太大了,说明我们没有理解业务,或者切分方法有问题。特别是你要理解估算是要指导迭代开发的。你估算出一张卡是8个点,那要4张2个点的卡才与之相当。如果用乘除法,导致故事卡的尺寸往往相差很悬殊,在实际开发中真的会有那么悬殊的差距吗?个人推荐的估算值是2,3,4,5。一般取2或者3,大一点的4,大一点的带一些风险的5,如果更大的,那你就要考虑拆分故事了。
总结
估算应该算是敏捷的薄弱环节吧。而且也是成功敏捷项目中的关键环节。传统项目,预先设计,闭口合同等等很多东西,有一整套方法学来指导我们做估算。在这个方面,敏捷毫无疑问把担子都推卸到了具体去做的人身上。关于估算,还有很多东西值得我们去学习探讨。
posted @
2008-11-11 11:32 taowen 阅读(1780) |
评论 (9) |
编辑

2008年11月7日
以下是我个人的一些主观感受,没有任何客观数据支持。
————————
按实现方式来划分,从下面几个方面做一个比较
CPU效率:
软虚拟和硬件虚拟基本差不多,但是模拟的CPU速度最慢,而且不是慢一点半点。
内存效率:
大家都差不多,即便是QEMU这样的模拟器加上了KQEMU的加速,也能凑合。
内存占用:
硬虚拟的一大软肋。软虚拟可以轻松地在一台机器上安装十多个虚拟机,但是硬虚拟就很难做到。主要的障碍是硬虚拟上的操作系统彼此独立,而且主流的操作系统对内存消耗都是非常贪婪的(用尽最后一个比特)。
I/O效率:
硬虚拟的另一大软肋。在Guest操作系统上装特殊的驱动,然后通过虚拟的总线(Hyper-V的VMBus)把操作直接委托给Host来完成,速度有所提升。但是由于DMA的缘故,在AMD/Intel不推出硬件支持的情况下,没法有根本改观。
可管理型:
硬虚拟的弱项。控制Guest操作系统基本上都是靠在Guest操作系统上安装控制软件来完成的。在没有控制软件的帮助下,就是关闭操作系统之类的事情都无法完成,要强制关机。软虚拟在这个方面非常方便。
安全性:
软虚拟的大软肋。就是Unix的chroot,人称jail系统。但是越狱的事情时有发生。硬虚拟,特别是纯硬件的全虚拟,有很高的安全性,能够真正达到沙箱(sandbox)的效果。
通用性:
软虚拟机的大软肋。最明显的是Virtuozzo,这个windows下唯一的操作系统级别的虚拟化技术,它只支持Windows Server 2003 r2 sp2。硬虚拟在没有驱动加速的情况下,通用性一般是比较好的。但是如果需要驱动加速,仍然是要给每个不同平台写不同的驱动。
————————
按我用过的产品来分,大概列出它们的一些特色和适用的场合
Hyper-V
实现:硬件虚拟(对于Windows和Suse Linux支持驱动加速的半虚拟)
特色:对Windows支持好,将来支持会更好。管理型不错,微软有一套管理工具。
适用:负载不高的服务器,安全性敏感的场合,开发。
Virtual Server/PC
实现:硬件虚拟
特色:使用简单,和Windows集成好。性能差。
适用:开发,负载非常低的服务器
Vmware Workstation/GSX Server
实现:硬件虚拟
特色:使用简单,支持多平台。性能差。
适用:非Windows平台的开发,以及负载非常低的服务器
Virtual Box
实现:硬件虚拟
特色:另外一个Virtual PC或者Vmware Workstation
Vmware ESX Server
实现:硬件虚拟机+对主流平台的驱动加速+内存优化
特色:硬虚拟中的速度标杆,成熟
使用:负载中等的服务器
Xen
实现:同Vmware ESX Server
特色:速度也很快,有开源社区支持(不过Windows优化那部分没有开源。。。)
适用:负载中等的服务器
QEMU
实现:动态编译模拟器+KQEMU(内存加速)
特色:兼容性好,支持多CPU。
适用:玩具?
KVM
实现:硬件虚拟+Linux平台的驱动加速和内存优化+Windows平台的网卡加速
特色:后起之秀,有强大的社区和大公司支持。直接整合进Linux内核
适用:Linux平台上的开发与不重要的服务器
Virtuozzo
实现:操作系统虚拟化
特色:Windows下的唯一操作系统级别虚拟化产品(也有Linux版本OpenVZ)
适用:大规模的虚拟服务器部署,虚拟主机提供商
————————
做开发的:
Windows用Virtual PC,Linux用Vmware
做中低负载服务器:
Vmware ESX,Xen,KVM,Hyper-V
做高负载,高密度服务器:
Virtuozzo,OpenVZ,Linux系的chroot族产品,Solaris Zone
————————
状态管理
最后来关注一下虚拟机的一个非常重要的特性,状态管理。功能强弱排列:
静态磁盘状态
最弱,不支持Differencing。Xen?Virtuozzo?
动态磁盘状态
支持Differencing
静态内存状态
支持把内存状态存档和恢复,但是需要重启,所谓Snapshot。大部分虚拟机都支持到这(Virtual PC,Vmware Workstation。。。)
动态内存状态
动态记录内存状态和恢复。QEMU/KVM
全管理
包括网卡状态等都管理了。从而能够实现Live Migration(动态迁移)。Hyper-V 2.0的重要目标就是这个了。所以从角度来说,硬虚拟最成熟的还是Vmware ESX,其次是Xen。KVM也不能小视,短短时间连Live Migration都支持了。
posted @
2008-11-07 12:56 taowen 阅读(1841) |
评论 (4) |
编辑

2008年11月6日
上回我们是从硬件下手了。经历了一个从纯模拟,到半虚拟化,到全虚拟化,最终又半虚拟化的过程。但是,它们都是在同一个地方插了一脚:

现在,我们把目光往上移,从应用程序到硬件之间,还有一层,那就是操作系统。虚拟机的第二个流派就是OS-Level Virtualization(操作系统级别的虚拟化),Application Virtualization(应用程序虚拟化)。

说到硬件,作为应用程序开发人员的我们,都不怎么熟悉。实际上硬件也是提供了类似API一样的东西的,硬件流派的虚拟机实际上,就是通过模拟常见硬件的API来达到虚拟化的目的的。同样,在操作系统和应用程序之间也有一个API层。那么我们为什么不能虚拟化这个API呢?让应用程序调用我们的API,然后我们再调用真实操作系统的API。比如它访问c:\temp,我就把路径重定向到c:\virtualized\temp。
这方面,数Chroot历史最悠久。Chroot就是在某个文件夹下比如/home/wtao/root模拟出一个新的root来。在chroot之后执行的程序,它去访问root,实际上是/home/wtao/root不再是真正的root(/)了。由于Unix的Posix API是以文件路径为约定的,而且啥都是基于文件的(比如/proc就是所有的进程所在的目录)。虚拟的根目录,相当于虚拟的操作系统API。所以,从这个意义上来说,Chroot就是启动了一个虚拟机。基于Chroot的原理,有BSD的jail和Solaris的Zone等等。
Windows方面的OS Level虚拟机只有一个真正能用的实现,那就是Virtuozzo。它的原理我也不是很清楚。不过根据观察每个虚拟机都有一个自己的smss.exe。而这个是Windows的Session Manager,是第一个被启动的用户态进程。而每个虚拟机也有自己的驱动程序。在主机上也有一个特殊的文件系统(不启动就看不到里面的文件)。所以我推测Virtuozzo并不是一个纯粹的OS Level虚拟化,它应该是硬件流和软件流混血的产物。
大厂都是主要玩硬件流的,比如Vmware,微软。Linux社区由于内核开放,所以操作系统虚拟化比较成熟。一些小厂没法直接加入到那么高层次的竞争,最容易进入的就是Application Virtualization(应用程序虚拟化)这个领域了,相比前两者门槛最低。我个人理解应用程序虚拟化就是不完整版的操作系统虚拟化。很多Windows下的应用程序虚拟化的产品只虚拟化文件系统和注册表。实现起来相对容易,只需要应用我之前提到过的API Hooking技术修改几个ntdll导出的API就行了。

我得承认,在操作系统级别的实现上,我没有深入的研究,可能是错误的。不过对于应用程序虚拟化(Application Virtualization)我非常确信它是如何实现的。今天的软件流派就讲到这里。明天从速度,可管理性,安全等方面给这些实现方案给一个我个人的主观评价(没有数据支持的),并推荐一些产品,以及它们适用的场合。
posted @
2008-11-06 10:54 taowen 阅读(1896) |
评论 (6) |
编辑

2008年11月5日
80x86机器的虚拟化最近太火了,不但微软加入了战团,还有一堆Startup摇旗呐喊。那到底哪种虚拟机好呢?本文就尝试接着笔者的一些经验,做一些个人总结。
好是一个相对的概念。市面上的虚拟机不但表面上产自不同的公司,其核心实现技术也五花八门,适用的场合也千差万别。一方面你要知道虚拟化技术能给你带来什么,一方面你要知道你自己需要什么, 一方面还要知道要达到你的目标选用什么虚拟机,用什么虚拟化战略最适合你。这个过程就是选型的过程。希望读完本文之后,你对80x86的虚拟化技术和其发展现状能够有一个大概的了解。略去虚谈虚拟化技术的好处的文字,让我们直接来看虚拟机是怎么实现的,从而给市面上的虚拟机做一个分类。

如果我们重新实现一个虚拟机,有好几种选择。最直接的选择是用软件来模拟硬件。写一个简单的while循环就可以模拟一个cpu的工作了:
while (true) {
var instr = GetNextInstruction();
switch (instr) {
case xxx:
// do xxx
case HALT:
return;
}
}
这也是普通的游戏机模拟器的实现原理。再加上软件模拟的外设,比如主板,硬盘什么的。这类虚拟机叫Enumlator。

Emulator有一个最大的优势是独立于底层硬件。你可以在586上模拟出6502CPU来。这对于开发特殊硬件的软件的工程师特别有用。而由于模拟器的效率低下,最终也只能沦落为调试工具。为了改进性能,人们引入了编译来代替解释执行。著名的JIT(JVM也算是虚拟机啊)就是其中的一种,QEMU则是80x86领域的字节码编译的虚拟机实现。但是这更多地是一种运行时的优化技术,并没有根本改变其实现,也没有根本地改变执行效率。
根本地解决方案是,不用软件的循环去模拟CPU,而是直接使用宿主机的CPU来执行。而且内存也不用去模拟,也可以直接用宿主机的CPU来执行。这就是Virtualization技术。

内存是比较容易实现的,一般都是有一个映射表来做虚拟内存和实际内存地址的映射。这种内存虚拟化已经是广泛使用并且非常成熟的技术。AMD后来提出的虚拟化技术进一步把这种表映射在硬件层面上提供了加速。
问题的难点在于80x86的ring结构上。ring0到ring3,权限依次递减。实际使用中,Windows只利用了ring 0和ring 3两个级别。Guest OS需要运行在80x86的Ring 0级别上。但是Host OS自己已经是在80x86的Ring 0级别上了。虚拟机作为一个用户态的应用程序,只有ring3的特权,这个状态的CPU是不能满足Guest OS的需要的。这也是为什么80x86虚拟化技术难以实现的原因。所以,在最开始的时候,为了达到虚拟化的目的,Guest操作系统必须经过修改。最开始的时候Xen就是这么实现的,需要Guest OS是打过补丁之后的特定版本的Linux。显而易见,Windows的内核是无法被打补丁的。所以,Para-Virtualization只适合开放源代码或者厂商支持的操作系统。
但是随着硬件的进步。Intel提出了Vt-d技术,Amd提出了Pacifica技术。虽然名称不同,但是结果都是让ring0之上再多出了一层。从而使得虚拟机可以欺骗过Guest操作系统,让它认为自己就是运行在普通的CPU之上。所以Full-Virtualization就出现了。为了利用这些CPU的新技术,它们都有一个直接贴近硬件的驱动程序,并不是完全基于Host OS之上的。

人类的追求是无止尽的。在把全虚拟化的虚拟机用在实战中,特别是高负载的服务器场合之后,很快性能问题就暴露出来。CPU速度是没有问题的,非常快。问题集中在内存消耗和I/O操作速度与随之而来的CPU占用率的问题。
当一台机器能够安装多个虚拟机之后,人们就想装尽可能多的虚拟机,从而提高整体的资源利用率。但是由于内存的限制,使得这个单机负载的虚拟机数量很难提高,同时内存的利用率却并不高。假设每个虚拟机都分配了1G内存,内存利用率是60%,那就每台有400M的内存空闲。一台4G的机器装了4个虚拟机,就有1.6G的空闲内存。所以装5台虚拟机是一点问题都没有的。但是由于每个虚拟机都自己捏了一把富余的内存在手上以防万一,导致整体的利用率上不来。为了解决这个问题,大厂的虚拟机都有很多优化,比如Deflate,Ballooning之类的。而这些优化往往是在Guest操作系统上装一些特殊的驱动程序去控制Guest操作系统的内存分配(或者是Linux内核的直接支持)。
I/O速度和CPU占用率的问题是由软件模拟的外设,特别是硬盘和网卡照成的。解决方案自然也是尽可能的利用真实的硬件。通过给Guest OS上装优化的驱动程序就可以实现。不过要进一步提高性能,就卡在了DMA(Direct Memory Access,直接内存访问)技术上。DMA是一个I/O优化技术,它可以让CPU不参与I/O,而外设直接访问内存。由于内存地址是虚拟化的,而DMA是硬件控制的,又无法被修改从而知道虚拟化之后的地址。这个问题的解决,仍然依赖于Intel、AMD。相信将来会有支持硬件支持的虚拟I/O的CPU上市。不过从计划发布,到现在我们已经等了很久了。

这个时候,由于效率的原因Full-Virtualization又变成Para-Virtualization了。因为Driver也是对Guest OS的一种修改,不再是透明地虚拟化了。如果对应操作系统没有驱动,你就没法用(或者说没法利用优化之后的外设和内存)。而这个对于Windows来说,又是一个问题了。KVM至今都没有发布Windows的硬盘Virtio驱动,导致效率低下。
今天讲的是硬件流派,明天继续讲软件流派。未完待续……
posted @
2008-11-05 13:55 taowen 阅读(2259) |
评论 (9) |
编辑

2008年11月3日
上一次提到了如何跨线程访问GUI。而这个需求往往是异步操作导致的。今天我们就来看看Jeffrey Richter写的AsyncEnumerator如何帮助我们处理异步问题。
先来看看最简单的一段异步下载网页的代码:
public class Program
{
private static WebRequest request;
public static void Main(string[] args)
{
request = WebRequest.Create("http://www.thoughtworks.com.cn");
request.BeginGetResponse(HandleResponse, null);
Console.ReadLine();
}
private static void HandleResponse(IAsyncResult ar)
{
request.EndGetResponse(ar);
Console.WriteLine("Get response from web");
}
}
很简单不是吗?如果我们下载之后还要异步存储到本地的磁盘,这个时候就不是那么容易了:

Code
public class Program
{
private static WebRequest webRequest;
private static FileStream fileStream;
private static Stream responseStream;
private static byte[] buffer;
private static int count;
public static void Main(string[] args)
{
webRequest = WebRequest.Create("http://www.thoughtworks.com.cn");
webRequest.BeginGetResponse(HandleGetResponse, null);
Console.ReadLine();
}
private static void HandleGetResponse(IAsyncResult ar)
{
var response = webRequest.EndGetResponse(ar);
Console.WriteLine("Get response from web");
responseStream = response.GetResponseStream();
buffer = new byte[4096];
BeginRead();
fileStream = new FileStream(@"c:\downloaded.html", FileMode.Create);
}
private static void BeginRead()
{
responseStream.BeginRead(buffer, 0, buffer.Length, HandleReadResponseStream, null);
}
private static void HandleReadResponseStream(IAsyncResult ar)
{
Console.WriteLine("Read a chunk");
count = responseStream.EndRead(ar);
if (count == 0)
{
Console.WriteLine("Finished downloading from response");
responseStream.Dispose();
fileStream.Dispose();
return;
}
fileStream.BeginWrite(buffer, 0, count, HandleWriteFileStream, null);
}
private static void HandleWriteFileStream(IAsyncResult ar)
{
Console.WriteLine("Write a chunk");
fileStream.EndWrite(ar);
BeginRead();
}
}
代码太长了,以至于我不得不折叠起来。这段代码还是有问题的,因为它没有处理异常情况,中途出个错,文件就不会被关闭。
从逻辑上来说获取Response,读取Response流,写入本地文件流从执行顺序上来说是一个完成之后,然后过一会儿下个接上执行。理论上来讲它就是
HandleGetResponse(xxx);
while(NotFinished(xxx)) {
HandleReadResponseStream(xxx);
HandleWriteFileStream(xxx);
}
CleanUp();
但是我们不能这么写。因为在每个操作之间都是一个异步等待的过程。实际上,是因为异步操作把一个完成的流程打散到了多个回调函数中去完成。那么有什么办法可以让一个方法执行一段,然后等一会,再执行一段呢?有,这就是yield。yield代表我暂时放弃执行的权利,等IO完成之后你再来执行我,我接着干下面的操作。
private static void Demo()
{
int i = 1;
foreach (var fib in Fib())
{
Console.WriteLine(i + ": " + fib);
if (i++ > 10)
{
break;
}
}
}
private static IEnumerable<int> Fib()
{
int i = 1;
int j = 1;
yield return i;
yield return j;
while (true)
{
var k = i + j;
yield return k;
i = j;
j = k;
}
}
这个例子中,Fib(斐波那契额数列)是一个死循环。如果它是一个普通的函数,你是不能执行它的,因为它永远不会放弃执行权,它会一只拽着CPU去算终极的fib。但是我们这个例子中的Fib不会。它在每次yield return的时候,都会跳出函数,返回到调用的地方。然后每次调用,都会从上次执行的地方继续下去,继续执行的时候所有的局部状态(局部变量的值)都保留着上次的值。在foreach的背后是这么一个过程:
var enumerable = Fib();
var enumerator = enumerable.GetEnumerator();
enumerator.MoveNext(); //Fib被执行 return i;
Console.WriteLine(enumerator.Current);
enumerator.MoveNext(); //Fib被继续执行 return j;
Console.WriteLine(enumerator.Current);
enumerator.MoveNext(); //Fib被继续执行 return i+j;
Console.WriteLine(enumerator.Current);
所以我们只要把上面的IO操作序列,稍微改写就可以让它们不四处散落了:
BeginGetResponse(xxx);
yield return 1;
EndGetReponse(xxx);
while(NotFinished(xxx)) {
BeginReadResponseStream(xxx);
yield return 1;
EndGetResponseStream(xxx);
BeginWriteFileStream(xxx);
yield return 1;
EndGetResponseStream(xxx);
}
CleanUp();
因为每次yield return都会放弃执行权,所以我们可以在这个函数外的某处等待BeginXXX操作的回调,等IO操作完成了再来继续执行这个函数。基于这个想法(最开始是这位仁兄想出来的http://msmvps.com/blogs/mihailik/archive/2005/12/26/79813.aspx),Jeffrey Richter写了Power Threading库(wintellect上的下载链接坏了,用这个http://www.wintellect.com/Downloads/PowerThreadingAttachments/Wintellect_Power_Threading_Library_(May_15,_2008).zip)。最后的代码是这个样子的:
private static IEnumerator<int> Download(AsyncEnumerator ae)
{
var webRequest = WebRequest.Create("http://www.thoughtworks.com.cn");
webRequest.BeginGetResponse(ae.End(), null);
yield return 1;
var response = webRequest.EndGetResponse(ae.DequeueAsyncResult());
Console.WriteLine("Get response from web");
var buffer = new byte[4096];
var count = buffer.Length;
using (var responseStream = response.GetResponseStream())
{
using (var fileStream = new FileStream(@"c:\downloaded.html", FileMode.Create))
{
while (count > 0)
{
Console.WriteLine("Read a chunk");
responseStream.BeginRead(buffer, 0, buffer.Length, ae.End(), null);
yield return 1;
count = responseStream.EndRead(ae.DequeueAsyncResult());
Console.WriteLine("Write a chunk");
fileStream.BeginWrite(buffer, 0, count, ae.End(), null);
yield return 1;
fileStream.EndWrite(ae.DequeueAsyncResult());
}
}
}
Console.WriteLine("Finished downloading from response");
}
是不是很简单呢?不过还有一个问题,那就是yield return我明白,是为了暂时退出这个函数,等待异步操作完成之后继续执行。但是我不明白的是,为什么是yield return 1呢?
其实这个yield return 1是给另外一个高级功能使用的。它的意思是“等待1个异步操作结束,然后执行我这行之后的代码“。如果yield return 2,就是等待两个异步操作。所以你必须先begin两个异步操作,然后yield return 2去等待。AsyncEnumerator还有返回值等高级功能,并且AsycnEnumerator内部使用了上文提到的AsyncOperationManager,所以在你的代码中可以安全地操作GUI不用害怕跨线程的问题。
参考资料:
Asynchronous iterators:
http://msmvps.com/blogs/mihailik/archive/2005/12/26/79813.aspx
Simplified APM With The AsyncEnumerator:
http://msdn.microsoft.com/en-us/magazine/cc546608.aspx
Simplified APM with C#:
http://msdn.microsoft.com/en-us/magazine/cc163323.aspx
More AsyncEnumerator Features:
http://msdn.microsoft.com/en-us/magazine/cc721613.aspx
Using C# 2.0 iterators to simplify writing asynchronous code:
http://blogs.msdn.com/michen/archive/2006/03/30/using-c-2-0-iterators-to-simplify-writing-asynchronous-code.aspx
http://blogs.msdn.com/michen/archive/2006/04/01/using-c-2-0-iterators-to-simplify-writing-asynchronous-code-part-2.aspx
附记:
喂,博主啊?为什么不直接创建一个新线程,然后在那里用同步操作完成上述动作?这个问题在我这里等价为什么要使用.NET的APM(Asynchronous Programming Model,异步编程模型)。正确的答案,参见http://msdn.microsoft.com/en-us/magazine/cc301191.aspx,Jeffrey Richter肯定写过这个问题的答案。不那么正确的答案:
1、提供如何实现异步操作的灵活性,新线程只是很多实现中的一种
这样我们可以利用Windows的Overlapped I/O,而这个就是一个内核级别的回调,不牵涉线程的问题了。性能直追epoll。
2、提供了何时使用新线程的灵活性,在一开始创建一个新线程然后把所有代码放到那里同步执行只是其中一种。
一个很流行的idea,叫SEDA(Staged Event Driven Architecture),究其核心就是把长操作分解成为异步的短操作,然后用不同大小的Thread Pool来回调不同类型的异步操作,通过调优达到线程在stage之间的最佳配比。这样避免了一有请求就起新线程的开销,线程多了系统就响应不过来了。又避免了单线程异步回调的低资源利用率,特别是CPU已经多核了的情况下。利用APM和AsyncEnumerator,再加上自己实现的ThreadPool,做一个.NET版本的SEDA架构也是可能的。
posted @
2008-11-03 22:11 taowen 阅读(1465) |
评论 (3) |
编辑
Application Developer
If you would like to...
In
this position you will be responsible for technical leadership
contributing to the successful delivery of application development
projects. Your responsibilities will also include but not be limited to:
- Create
complex, enterprise-transforming business applications as a member of
diverse, high energy teams focused on full life-cycle development
- Use the latest tools and techniques (currently J2EE, C#/.NET, XML,Agile Methodologies, Web Services, EAI tools...)
- Do
hands-on coding and proactively mentoring of developers (including pair
programming), using J2EE and/or C# Continually learn, mentor and grow
- Work closely with senior project management and clients
- Travel to work at client sites, possibly extensively, which may include opportunities for international travel
And you have...
- Strong
development experience with OO languages, specifically on complex
enterprise systems using C/C++, Java J2EE and/or within the .NET
framework (framework including C#, [ADO.NET], [ASP.NET] and [VB.NET])
- Broad understanding of the Java API, including but not limited to JMS, EJB, JSP, Servlets
- Experience with object oriented analysis/design
- Strong
knowledge of design patterns, refactoring and unit testing Agile
Methodology knowledge, such as Extreme Programming (XP) & Scrum
- Participated in full life-cycle development
- Experience with relational databases is a plus
- Exposure to EAI technologies (MSMQ, Tibco, Vitria, SeeBeyond, MQ Series) is very helpful
- Excellent written and oral communication skills in both English and Chinese
- Ideally with an academic background in Computer Science or Engineering
Location
We want to hear from you! ThoughtWorks values
aptitude, attitude, and integrity. If you thrive on challenge,
unlimited possibilities, and unparalleled learning, send your resume or
apply online now.
http://www.thoughtworks.com.cn/work-for-us/jobs/Application-Developer.html
posted @
2008-11-03 09:36 taowen 阅读(175) |
评论 (1) |
编辑
无论是WIN32还是Windows Form还是WPF还是Swing,都不支持GUI线程之外的线程直接访问其API。今天我们来回顾一下这个发展过程。一个普通的操作是怎么被封装封装再封装的。
Win32
在Windows SDK时代,我们都知道,界面就是一个大的WndProc控制的。
switch (message)
{
case WM_PAINT:

case WM_DESTROY:

default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
如果我们需要另外一个线程去做一些耗时的IO操作,同时要回调回来更新的界面,这个时候要么自己定义一个回调队列,然后在WM_IDLE的时候处理,要么就利用窗口本身的消息队列,自己创建一个消息类型:
// at startup
int OUR_APP_IO_COMPLETED = RegisterWindowMessage("OUR_APP_IO_COMPLETED");
----
// after io completed
PostMessage(hwnd, OUR_APP_IO_COMPLETED, xxx, xxx);
这样,我们就可以让界面更新的操作在GUI线程完成了。
.NET 1.0 Windows Forms
Windows Forms的出现使得很多东西简单多了。跨线程的GUI操作也简化了许多。因为我们有了Delegate和Control.Invoke。前者就是一个函数指针的升级版,Control.Invoke就是PostMessage的升级版:
public delegate void IOCompletedHandler();
public void OnIOCompleted() {
control.Invoke(new IOCompletedHandler(HandleIOCompleted));
}
public void HandleIOCompleted() {

}
在内部Control维护了一个threadCallbackList。调用Invoke相当于
1、把Delegate和参数封装到一起,压入到threadCallbackList中
2、确保ThreadCallbackMessage被注册了(RegisterWindowMessage)
3、PostMessage,触发Callback
4、WaitForHandle,等待GUI线程完成处理(如果是BeginInvoke则可以省略这个步骤)
而Windows Forms的WndProc的消息循环,就需要加一个case语句,来响应PostMessage。在得到消息的同时,去threadCallbackList中取出需要回调的Delegate,然后一一调用。
Control.Invoke确实是让我们的生活方便很多。但是它还是不能避免所谓的BeginInvoke之舞(BeginInvoke Dance):
//http://ikriv.com:8765/en/prog/info/dotnet/MysteriousHang.html
delegate void MyHandlerDelegate();
void MyHandler()
{
// "The BeginInvoke dance"
if (this.InvokeRequired) // assuming this descends from Control
{
BeginInvoke( new MyHandlerDelegate(MyHandler) );
return;
}
// assume we are on the main GUI thread
do GUI stuff 
}
InvokeRequired内部实现就是线程安全地比较一下当前线程和GUI线程的线程ID是不是相等的。
另外Control.Invoke还需要我们确保对应的Control的句柄已经创建。为了避免这个问题,WinForm有一个MarshingControl可以给我们使用。
Application.ThreadContext.FromCurrent().MarshingControl;
.NET 2.0 Windows Forms
如果我提供了一个回调接口。我一般会直接调用回调方法,如果我知道回调方法必须在GUI线程中执行的话,用必须用Control.Invoke来调用回调方法。所以,在缺乏信息的情况下,回调接口的提供者是没有办法保证回调方法的执行线程的环境的。所以,按照Strategy模式,我们可以定义一个CallbackStrategy
public interface CallbackExecutor {
void Execute(Delegate callback, object[] args);
}
public class DefaultCallbackExecutor : CallbackExecutor {
public void Execute(Delegate callback, object[] args) {
callback.Invoke(args);
}
}
public class WindowsFormsCallbackExecutor : CallbackExecutor {
private Control marshalingControl;
public WindowsFormsCallbackExecutor() {
Application.ThreadContext context = Application.ThreadContext.FromCurrent();
if (context != null)
{
this.marshalingControl= context.MarshalingControl;
}
}
public void Execute(Delegate callback, object[] args) {
marshalingControl.Invoke(callback, args);
}
}
然后回调方法的提供者就不用为难了,它只要调用CallbackExecutor就可以了,同时回调方法也可以安心地直接去干自己的事情。从而,谁都不需要做BeginInvoke Dance了。
这就是.NET引入的SynchronizationContext的基本原理。由于微软不是接口爱好者,所以它省去了共同的那个接口,直接让WindowsFormSynchronizationContext继承自SynchronizationContext类了。SynchronizationContext是线程唯一的,取得当前线程的Sync Context可以使用:
SynchronizationContext.Current.Post(delegate{//your code}, null);
与SynchronizationContext类同时引入的还有AsyncOperationManager,AsyncOperation。它又是SynchronizationContext的封装。一般来说,推荐使用AsyncOperationManager。AsyncOperationManager在创建AsyncOperation的时候会取得当前线程的Sync Context,并存储在SyncOperation之中。 所以后来利用AsyncOperation来回调的时候,就会用一开始创建时候所在线程的context来回调。
// initialize in GUI thread
asyncOperation = AsyncOperationManager.Create(null);

// when you want to call back
asyncOperation.Post(delegate{//your code}, null);
像BackgroundWorker这样的类,之所以其Callback可以直接访问GUI,其秘诀就在于回调不是直接调用的,而是有
AsyncOperation来间接代劳的。只要AsyncOperation是在GUI线程创建的,所有由它代劳的回调都会经过
MarshalingControl来做Control.Invoke。
.NET 3.0 WPF
Control.Invoke变成了Dispatcher.Invoke。内部实现还是一样的,有一个Callback队列,然后PostMessage。同时引入的还有DispatcherSynchronizationContext。
Conculsion
从直接PostMessage到Control.Invoke到SynchronizationContext到AsyncOperationManager到BackgroundWorker,职责最终推卸到了回调的提供方。但是由于引入的时间不一致,很多回调提供方是没有使用AsyncOperation的。这个时候,我们自己需要自己去包装,比如:

Code
using System;
using System.ComponentModel;
using System.Net;
using log4net;
namespace xxx
{
public class WebClientFileUploader : FileUploaderListeners, FileUploader
{
private static readonly ILog LOGGER = LogManager.GetLogger(typeof (WebClientFileUploader));
private readonly WebClient webClient;
private readonly Uri address;
private readonly AsyncOperation asyncOperation;
public WebClientFileUploader(Uri address)
{
asyncOperation = AsyncOperationManager.CreateOperation(null);
webClient = new WebClient();
webClient.UploadFileCompleted += HandleUploadFileCompleted;
this.address = address;
}
private void HandleUploadFileCompleted(object sender, UploadFileCompletedEventArgs eventArgs)
{
try
{
var result = eventArgs.Result;
LOGGER.Info("Request completed.");
asyncOperation.Post(state => HandleCompleted(result), null);
}
catch (Exception exception)
{
LOGGER.Error("Request failed.", exception);
asyncOperation.Post(state => HandleError(exception), null);
}
}
public void Cancel()
{
LOGGER.Info("Cancelling the request.");
webClient.CancelAsync();
}
public void Upload(string fileName)
{
LOGGER.Info("Starting to upload to " + address + " with file " + fileName + " using real web client.");
webClient.UploadFileAsync(address, WebRequestMethods.Http.Post, fileName);
}
}
}
这样,我们就不需要在手工地去用Control.Invoke了,也不应该再直接用Control.Invoke了。
posted @
2008-11-03 07:58 taowen 阅读(1824) |
评论 (4) |
编辑

2008年11月1日
不是我变态,我也是被逼的。客户喜欢Word 2007里的Custom Task Pane,希望在侧边栏上放一些界面。但是他们刚刚从Word 97升级到2003,完全没有可能升级到更高的版本。我之前给他们做过一个DEMO,是用ActionsPane技术实现的。他们觉得挺好,就要这个。但是我不喜欢ActionsPane,所以只能自己想办法喽。为什么不喜欢ActionsPane?理由有很多:
1、绑定到Template,部署困难
2、排版有问题
3、稳定性不好,.NET控件是以ActiveX形式放置在上面的
很快,我就注意到Word 2003不是没有TaskPane的:

那么我可不可以把我的东西放上去呢?那我们来查查MSDN吧。确实还有这么一个Interface,就叫TaskPane:
http://msdn.microsoft.com/de-de/library/microsoft.office.interop.word.taskpane_members(VS.80).aspx
但是很遗憾,我们在上面找不到任何线索,可以把我们控件加上去的。但是我们可以用Application.TaskPanes[xxx].Visible来控制特定TaskPane的开关。也算有点收获啦。
然后我们又注意到,这个TaskPane怎么看起来这么像Command Bar?它还真的就是一个CommandBar。用Application.CommandBars["Task Pane"].Visible,我们也可以开关TaskPane(不过必须要在TaskPane已经打开过一次的前提下)。而且MsoControlType里有也有一条msoControlWorkPane。嗯,看起来有戏?。。。门都没有。MSDN上明说了,Work pane. Cannot be created through the object model。
显然,用微软官方API的办法是死路一条。但是也不是完全没有收获。
1、通过Application.TaskPanes[xxx]我们可以可靠的开关TaskPane上的某一页
2、我们知道Application.CommandBars["Task Pane"]对应的就是TaskPane所在的Command Bar。
曾经有一个让我恨之入骨的Office开发库,名字叫Addin-Express(大家不要用啊,谁用谁后悔)。当年我们用它就是因为它可以在Outlook里做类似的事情,在Email Composer上凭空多一块区域出来(类似于Outlook 2007的Form Region)。它的实现原理其实就是用一个独立的WinForm窗口,然后用为win32 api SetParent把窗口“融合”到Outlook上去。同时通过SetWindowLong的办法拦截目标窗口的WndProc(窗口事件处理函数,所有的GUI事件的起点),达到你缩放我也缩放的目的。这次我决定不用Addin-Express来做(它也没实现这个功能),但是可以利用其原理。
在.NET中拦截WndProc的最简单的办法是用NativeWindow这个类。只要override WndProc那个方法就可以了 。
public class Window : NativeWindow, IDisposable {
protected override void WndProc(ref Message message) {
// Your code
base.WndProc(ref message);
}
}
虽然知道大概的原理,不过还有一个难点一个疑问需要解决。那就是,到底以哪个Window来Parent?而且,这个Parent在隐藏之后再重现出来会不会被重新创建。如果父窗口每次都会被销毁,那我们也没法用SetParent大法了。这个时候就是Spy++出场的时候了。

高亮的那个窗口就是我们的目标了。而且经过试验证明,窗口SetParent到它身上之后,一直都在。 注意,如果设在NUIPane上是不行的,必须是在NetUIHWND上。这个窗口的ClassName是固定的,但是XML Structure那个TaskPane的窗口ClassName不一样,不过也是一样可以SetParent的。
不过还有一个问题。我如何得到这个目标窗口的句柄呢?我甚至都没法得到当前Word窗口的句柄。这个时候就需要依赖CommandBar的一个特性了,Accessible。这是Windows用来服务残障人士的API。对于我们来说,就是CommandBar对象都实现了IAccessible这个Interface,然后我们就可以:
OleAcc.WindowFromAccessibleObject((IAccessible) commandBar);
WindowFromAccessibleObject是OleAcc这个DLL上的一个API。用来获得Accssible对象的窗口句柄。利用Accessibility API可以做很多有意思的事情,比如http://blogs.officezealot.com/whitechapel/archive/2005/04/10/4514.aspx。这里就用了相反的一个操作,AccessibleObjectFromWindow来获得Excel的对象,当年在写Excel的UDF的时候帮了我的大忙。
还有一个难点就是,如果TaskPane没有打开我怎么SetParent。这个时候当然是利用前面提到的Application.TaskPanes[xxx]的方法来得到接口,并设置Visible了。不过还是有一个时间差。你设置下Visible之后,是一个异步地打开TaskPane的过程。所以当你接下来做SetParent的时候,那边可能还没有完成窗口的初始化。我现在只能用一个新线程Sleep(300)来完成这个任务。并不是完全不可靠,也不是完全可靠。最完美的解决方案还是升级到Word 2007。
知道了上述的机要。实现Word 2003下的Custom Task Pane就是体力活了。用GetWindowRect来缩放,用GetPixel来融合背景色。最终的效果是这个样子的。

而且能缩能放,能关闭能打开,能Dock能Undock。遗憾的是必须要牺牲掉Word内建的TaskPane中的某一个。不过好在Word 2003中有那么多人们用不着的功能,牺牲掉一两个,没谁会注意到的,对吧?:)
posted @
2008-11-01 19:38 taowen 阅读(1315) |
评论 (3) |
编辑

2008年10月31日
昨天提到了如何用Hook Ole32 提供的两个API来实现Hook Drag & Drop的过程。算是给EasyHook库做一个广告吧。今天给大家讲讲EasyHook的实现原理。
API Hooking有两种,内核态级别的和用户态级别的。内核态级别的API Hooking也是很多病毒的实现原理,而且在微软不断强化其PatchGuard技术之后,已经越来越困难。做为建设四化的四有青年,我们还是把精力集中在用户态级别的API Hooking上好了。用户态的API Hooking其实就是为了在应用程序和Windows之间插一脚,从而达到用户调用的不再直接是Windows提供的函数,而是调用我们提供的函数。我们可以选择把调用转交给真正的Windows提供的函数,也可以选择自己处理掉,如果我们知道怎么处理的话。API Hooking非常强大可以应用在很多地方,比如监测(非安全领域,因为很容易绕过),虚拟化,软件破解等。要实现API Hooking有很多选择,基本上可以分为:
1、木马DLL
2、修改导出表
3、修改导入表
4、代码重写
实现的难易程度也基本上是上述的顺序。木马DLL是最好实现的,前年我因为需要解码nellymoser编码的音频,但是只有Flash播放器才有它的解码器。所以我就把winmm.dll这个系统提供多媒体功能的dll做了一个木马,实现了拦截wav输出的目的。木马DLL只需要你写一个同名的dll,导出相同名字和签名的函数即可。不过它的限制也最大,最大的原因是windows核心的DLL是不会从当前目录加载的(比如user32.dll),同时它还要求木马DLL必须预先放置在exe的同级目录下。
修改导出和导入表限制也很大。真正的王道是代码重写,不过难度陡增。在这个方面比较出名的有Detours(微软研究院的,不过已经没更新了),和madshi(DELPHI领域的,用户众多)。不过最近新出了一种非常强大的库,叫EasyHook。它具有非常独特的特性,支持.NET。也就是说,你可以用C#来写API钩子。这在以前是无法想象的。它的项目地址是:http://www.codeplex.com/easyhook。EasyHook其实不光是一个API Hook的库,它还提供了Remote Injection(远程注入)的功能。也就是说,你不但能够在Hook当前进程的API,还能去Hook其他进程的API。
所谓代码重写,是指修改DLL映射到当前进程的内存中的代码(二进制的机器指令)。每个API都会在运行时解析出一个地址,然后程序会用call指令去调用这个地址所包含的机器指令。代码重写就是把这个地址所在位置的机器指令重写了,从而达到从中插一脚的目的。简化后的流程为:
1、调用API地址所在处的指令
2、被改写的代码被执行(一般就是一个jmp指令,跳转到一个更开阔的地方去做更多的事情)
3、执行Hook
4、执行被覆盖的指令(一般是5个字节)
5、跳转回(API地址所在处+5个字节)的位置继续执行原代码
要完成这样一个流程已经很不容易。它需要你懂得80x86的汇编语言,而且要有很强的debug能力。但是这只是一个简化的流程,真正复杂的地方在于:
1、不可覆盖的代码的监测(比如jne就无法被覆盖)
2、Hook的可靠卸载
3、被覆盖指令长度的确定(一般是5个字节,但是那只是一般)
4、不同CPU指令集的支持(64位?)
5、Hook中调用了被Hook的API怎么办?
对于上述问题考虑的周到与否是衡量一个API Hook库的标准。很高兴的告诉你,EasyHook把上述问题都考虑进去了。而且依靠CLR Hosting API,EasyHook还实现了.NET的API钩子。原理就是EasyHook自己先把API调用拦截下来,然后用CLR Hosting API启动一个CLR的runtime,然后用runtime去加载一个GAC中的Assembly,然后用反射去调用你的代码,并且给你提供了足够的RuntimeInfo,使得你可以知道当前的环境,以及回调原始的API。不过仍然在Hook系统的API的时候,仍然要非常小心死锁的问题。
EasyHook也不是完美的。据我个人测试,它远程Hook .NET产生出来的exe有问题。不过这是它的Remote Injection部分的问题(很可能是CreateRemoteThread与CLR冲突导致的)。而且EasyHook确定指令长度那块内嵌的反汇编器来路不明(作者自述是从一个德文论坛上下载的程序反汇编来的),其实可以考虑用distorm代替。
其内部实现有一段关键的汇编代码叫trampoline(蹦床)。这段代码就是那被覆盖的5个字节要jmp到的一段代码,它负责调用你的Hook和善后。所以说如果你用的是.NET写的钩子,这是一个三级跳的过程:
1、应用程序调用API
2、API被调用(其实是我们的jmp指令被调用,因为头5个字节被覆盖了)
3、那个5个字节的jmp,跳转到了trampoline,这蹦床被jmp了,哈哈,所以弹了回去
4、trampoline跳转到了EasyHook的Thread Deadlock Barrier
5、EasyHook再用CLR Hosting API跳转到了我们的.NET代码
非常Hard Core。有机会给大家注释这段代码(我没告诉你EasyHook是开源的吗?),不过今天就写到这里了。
posted @
2008-10-31 23:08 taowen 阅读(1576) |
评论 (7) |
编辑

2008年10月30日
最近在做的一个项目是一个Word 2003的插件。项目的一个需求是控制用户在Word中的拖拽行为。具体来说有三种:
1、用户完全不能把某些文字Drag起来
2、对于某些文字,可以Drag起来,但是不能Drop到除当前文档之外的任何地方
3、对于某些文字,任意东西都不能Drop于其上
说实话,我了解到这个是需求的时候, 第一反应就是,这可能吗?借用阿迪的广告语,Impossible is Nothing。的确,Windows之所以千疮百孔,在我看来很大程度上就是它提供了太多的可Hack的手段了。在实现这个可控的拖拽行为之前,已经通过SetWindowsHook控制了用户的鼠标和键盘(当然这种技术已经用烂了,我就不再炒冷饭了)。这次,我们使用Windows API Hook来达到这个目的。
如果不了解拖拽到底是怎么实现的,我们是不可能控制它的行为的。 我们要做的,其实就是找出标准的拖拽实现方式。然后在其中插一脚,把我们感兴趣的东西拦截下来,并篡改掉本来的输出结果。说实话,Hook的善恶就在一念之间。那么我们先来简单了解一下拖拽的流程:
Drag:
1、应用程序在用户用鼠标拖拽了一个物体之后,调用 DoDragDrop(dataObject, dropSource, okEffect, effects) 开始拖拽
2、Windows 回调 dropSource 的 QueryContinueDrag 来决定是不是继续Drag
Drop:
1、应用程序在初始化的时候,调用 RegisterDragDrop(hwnd, dropTarget)
2、当有物体拖拽进了 hwnd 所在的区域时,Windows 回调 dropTarget 的 DragEnter
3、当物体在 hwnd 所在区域内滑动时,Windows 回调 dropTarget 的 DragOver
4、当物体拖拽出 hwnd 所在区域时,Windows 回调 dropTarget 的 DragLeave
5、当拖拽的物体放下是,Windows 回调 dropTarget 的 Drop
所以对于我们来说,重点关注的就是两个API,两个Com Interface。分别是 Ole32 的 DoDragDrop 和 RegisterDragDrop。以及 Ole32 中的 IDropTarget, IDropSource。要实现开头所述的三种行为,我们只需要:
1、 用户完全不能把某些文字Drag起来:
拦截DoDragDrop的调用,如果dataObject或者用户当前选中的区域是受到保护的,就不调用Ole32的真实实现,直接返回0。
2、 对于某些文字,可以Drag起来,但是不能Drop到除当前文档之外的任何地方:
拦截DoDragDrop的调用,真实地去调用Ole32的真实实现,但是不直接使用原装的DropSource(包装之后再用)。因为我们要监听Windows对DropSource的回调,再“恰当”的时候篡QueryContinueDrag的返回值。从而使得用户无法Drop到当前文档之外的区域。
3、 对于某些文字,任意东西都不能Drop于其上:
拦截RegisterDragDrop的调用,确实使用Ole32的真实实现,但是也不直接使用原装的DropTarget(包装之后再用)。因为我们要监听Windows对DropTarget的回调。同样,在“恰当” 的时候篡改DragOver和Drop的返回值。
那么问题就集中在了如何拦截Ole32的两个API上。这个问题很好解决,直接使EasyHook(http://www.codeplex.com/easyhook)库就行了。我阅读了这个库的所有源代码,有机会可以给大家讲讲Windows API Hook的原理,也挺有意思的。而且使用EasyHook,我们还可以做到远程注入(底层实现是CreateRemoteThread,老套但是可靠,关键是支持.NET)。这样,就不用受限于当前进程了。不过由于RegisterDragDrop一般在初始化的时候调用,所以最好使用EasyHook的CreateAndInject来做,要不然就时机太晚了。在本例中是一个Word 2003的插件,而且恰巧Word 2003是先加载AddIn再RegisterDragDrop,要不然也就Hook无门了。
下面是一些关键细节的源代码:
安装DoDragDrop的钩子

Code
using System;
using System.Runtime.InteropServices.ComTypes;
using EasyHook;
namespace xxx
{
public class RangeDragEvents : IDisposable
{
private readonly WordApplication application;
private readonly RangeListener listener;
private readonly Ole32._DoDragDrop doDragDropHandler;
private readonly LocalHook doDragDropHook;
public RangeDragEvents(WordApplication application, RangeListener listener)
{
this.application = application;
this.listener = listener;
doDragDropHandler = HandleDoDragDrop;
doDragDropHook = LocalHook.Create(LocalHook.GetProcAddress("ole32.dll", "DoDragDrop"),
doDragDropHandler, new object());
doDragDropHook.ThreadACL.SetExclusiveACL(new int[0]);
}
private int HandleDoDragDrop(IDataObject rawDataObject, Ole32.DropSource originalDropSource, int allowedEffect,
int[] finalEffect)
{
Ole32.DropSource dropSource = originalDropSource;
switch (GetDragAction())
{
case DragAction.JAILED:
dropSource = new JailDropSource(originalDropSource);
break;
case DragAction.CANCELLED:
return 0;
}
return Ole32.DoDragDrop(rawDataObject, dropSource, allowedEffect, finalEffect);
}
private DragAction GetDragAction()
{
WordSelection selection = application.Selection;
if (selection == null)
{
return DragAction.PROCEEDED;
}
using (var detached = selection.Detach())
{
var range = detached.Range;
if (range.Start == range.End)
{
return DragAction.PROCEEDED;
}
return listener.HandleDrag(range);
}
}
public void Dispose()
{
doDragDropHook.Dispose();
}
}
}
安装RegisterDragDrop的钩子

Code
using System;
using EasyHook;
namespace xxx
{
public class RangeDropEvents : IDisposable
{
private readonly Ole32._RegisterDragDrop registerDragDropHandler;
private readonly LocalHook registerDragDropHook;
private readonly RangeListener listener;
private readonly WordApplication application;
public RangeDropEvents(WordApplication application, RangeListener listener)
{
this.listener = listener;
this.application = application;
registerDragDropHandler = HandleRegisterDragDrop;
registerDragDropHook = LocalHook.Create(LocalHook.GetProcAddress("ole32.dll", "RegisterDragDrop"),
registerDragDropHandler, new object());
registerDragDropHook.ThreadACL.SetExclusiveACL(new int[0]);
}
private int HandleRegisterDragDrop(IntPtr hwnd, Ole32.DropTarget target)
{
return Ole32.RegisterDragDrop(hwnd, new RangeAwareDropTarget(application, listener, target));
}
public void Dispose()
{
registerDragDropHook.Dispose();
}
}
}
把Drag的物体“Jail”在当前文档中

Code
using System;
using System.Drawing;
using System.Windows.Forms;
using log4net;
namespace xxx
{
public class JailDropSource : Ole32.DropSource
{
private static readonly ILog LOGGER = LogManager.GetLogger(typeof (JailDropSource));
private const int DRAGDROP_S_DROP = 0x00040100;
private const int DRAGDROP_S_CANCEL = 0x00040101;
private readonly Ole32.DropSource dropSource;
private readonly IntPtr jailedIn;
public JailDropSource(Ole32.DropSource dropSource)
{
jailedIn = User32.GetForegroundWindow();
this.dropSource = dropSource;
}
public int QueryContinueDrag(int escapePressed, int keyState)
{
int result = dropSource.QueryContinueDrag(escapePressed, keyState);
if (result == DRAGDROP_S_DROP)
{
if (!IsDropable())
{
return DRAGDROP_S_CANCEL;
}
}
return result;
}
public int GiveFeedback(int effect)
{
if (!IsDropable())
{
Cursor.Current = Cursors.No;
return 0;
}
return dropSource.GiveFeedback(effect);
}
private bool IsDropable()
{
try
{
if (jailedIn != User32.GetForegroundWindow())
{
return false;
}
User32.RECT rect = User32.GetWindowRect(jailedIn);
var rectangle = new Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top);
return rectangle.Contains(Cursor.Position);
}
catch (Exception e)
{
LOGGER.Error("can not decide it is droppable or not, default to false", e);
return false;
}
}
}
}
监听DropTarget的回调,拒绝某些场合下的Drop下来东西

Code
using System.Windows.Forms;
namespace xxx
{
public class RangeAwareDropTarget : ProxyDropTarget
{
private readonly RangeListener listener;
private readonly WordApplication application;
public RangeAwareDropTarget(WordApplication application, RangeListener listener, Ole32.DropTarget dropTarget)
: base(dropTarget)
{
this.listener = listener;
this.application = application;
}
public override int DragOver(int keyState, long point, ref int effect)
{
var result = base.DragOver(keyState, point, ref effect);
if (ShouldHandle())
{
effect = (int) DragDropEffects.None;
}
return result;
}
public override int Drop(object dataObject, int keyState, long point, ref int effect)
{
if (ShouldHandle())
{
DragLeave();
return 0;
}
return base.Drop(dataObject, keyState, point, ref effect);
}
private bool ShouldHandle()
{
var window = application.ActiveWindow;
if (window == null)
{
return false;
}
using (var detached = window.Detach())
{
var range = detached.RangeFromPoint(Cursor.Position.X, Cursor.Position.Y);
if (range == null)
{
return false;
}
return listener.HandleChange(range, ChangeSource.Drop);
}
}
}
}
posted @
2008-10-30 22:27 taowen 阅读(1610) |
评论 (3) |
编辑