终于结束了近一个小时的枯燥会议,每周五公司级别的项目周会就像一个例行的检查,多少有点不痛不痒的味道,大部分的时候就是一个例行贯事,每个项目组按照顺序汇报一下各自项目组的情况和需要,如果不出现大的问题的话,也就是有本上奏,无本退朝的一个过程。或许有些人觉得这是一个比较浪费时间而且意义不大的会议,每周浪费大伙一个小时的时间去再次陈述这些本来在邮件中已经说明的问题和项目进度,不过存在就有价值,这个会议的最大的功能在于能尽量使各个项目组之间有部分的消息互通,至少能让大家了解到一点,其他人都在做什么,从而避免了每个项目组过于孤立的现象。
       今天的周会时间有点长,主要由于两件事情的讨论超出了会议的议程安排,第一件事情是大伟负责的项目目前进度出了问题,目前人员处在一个持续加班的紧张状态,即便如此,对于项目的最终Release时间还是比较确定的状态,用大伟的话来形容就是:“我们能够按时完成,但是可能会有点小问题。”一句比较含糊的托词,如果说套用官方的言词就是:“对于项目能够按时完成,我们还是谨慎乐观。”另外事情对于公司来说是一个好消息,公司将在下个月启动一个新的项目,这个项目是今年规模最大的一个项目,其业务方向主要针对美国的医疗领域,规模大概有150人/月。
这个项目将会被安排到我们项目组来处理,消息对于我来说就是一个挑战,对于自己来说,这个规模的项目也是超过了以往的一切项目,在会议上听到这个项目将由我来接手处理,不由小小的兴奋了一把,但是这股兴奋的感觉很快就被冲淡,自己粗略的考虑了一下项目组内目前的工作量和目前的人员情况,不由感觉到这个活不太好揽。不过有压力才有动力,这是我一贯的做事风格,类是这种明知会将苦重重的活,我越能够信心十足。我简单的在自己的记事本上写下了以下下周的关健工作内容:确认项目的具体开始时间和结束时间;调整目前项目组内的工作内容;向客户先要部分的开发相关资料;了解一下公司内部其它项目组的人员状况。
       会议后半程的内容我没有听多少,只是对于大伟的问题大家有部分的讨论,我才回过神来参与,大伟虽然说自己还是有信心能够保证项目的进度,但是为了保险起见,希望从其它的项目组借调两个人来协助他们做一些类是测试的工作,这样的话总的进度就可以进一步得到保证。以目前公司的架构模式,每个项目组相对独立的运作模式,如果说希望从其它的项目组借调人员,这种短期人员的借调有时候是比较困难,因为每个项目组目前来说都处于比较繁忙的阶段,目前闲暇的人员不多,即便其它项目组肯借人,大多放出去的人员也都是能力相对比较弱,正常情况下人不可能把项目组内部优秀的人员借调出去,这或许也是人的一种私心。
       在公司的协调安排之下,最后还是给大伟支援了两个人员,这才把讨论不休的会议打住。会议结束后我还在会议室坐了一会,我的脑袋里还在考虑下周的工作顺序,盯着自己的笔记本在对刚刚写的问题进行细化,把工作分解到下周的每一天。突然一股浓浓的烟味直接灌进我的鼻子,我不用抬头就能猜出是大伟,这个烟枪一天最少一包烟,如果遇到加班或则心情不好的话,估计两到三包都有可能。
       “少抽点烟,怎么心情不好?”看着大伟吐着烟圈,我问道。
       “没有,只是有点累。”大伟嘴里叼着烟,目无表情的应道。
       “我看到你们项目组的那些去年刚刚毕业的学生,最近是天天加班到晚上11点左右。这是怎么回事?”
       “噢,那时我故意的。”
       “故意?”我一时没有明白,疑惑的问道,“为什么要故意呢?”
       “我是按照我自己的工作能力给那些新人安排工作,如果说我是能够在上班时间内完成工作的话,那么那些新人肯定需要加班了。这样他们才能进步。”大伟的表情中开始露出得意的神色。
       “怪不得他们要加班,你工作了多少年,他们才工作多少年,再说了你拿的工资和他们那得工资能一样吗?”
       “我就告诉他们,如果你想工资那得和我一样,那么这些工作你能够准时完成,那么你就有机会。”大伟熄掉手中烟头,用双掌在脸上使劲得措动。
       “那你该不会是按照你的能力来做的项目估算吧?”对于大伟的这个逻辑我并不能苟同,但是我还是想了解项目的问题。
       “嗯…..”大伟支吾着回答到。
       “哈哈,你这是自作自受。”我对着大伟带着嘲讽的笑声说道。
       对于大伟的做法我却是无法苟同,虽然他的失误在于前期的估算过于乐观而且并不准确。但是对于给新人安排工作的方式上存在有比较大的问题,如果说前期的估算出现失误,这些错误在后期项目开始过程中还可以进行添加人员的方法进行补救的话,那么对于人员使用不当的问题,就像整个项目过程的绝症,其会影响到整个项目过程。
       项目组中开发人员从能力上有几个不同的级别,系统架构师(Systems Architect),高级工程师(Senior Engineer),中级工程师(Middle Engineer)和初级工程师(Junior Engineer)等等。对于每个级别上的人员的能力和开发技能都有一定的差异。这些就像我们手的手指一样,五指有长短,能力个不同。不同能力的人员应该在项目组中相应的工作内容和工作量。
       在正常的项目中我们讲究的是人尽其用,但是不能把一个项目在开始的时候就设想是通过发挥项目组人员的超水准的基础上来完成。很多人都说人在一定的压力之下,能够超能力的发挥出他们的水平,但是比代表说所有的人都能够在这种环境下能够有说突破,很多人在持续长时间的高压下会显得精神不振,萎靡的情况。所以作为项目管理来说,不能再项目一开始的时候就采用这个立足点来进行开发工作。
       对于大伟所说的那种歪论,我只能回应以自作自受的答案。我们给个人都是在逐步的学习和进步,如果说有一定压力灾,对于学习确实是一个比较有效的促进。但是作为项目经理来说,需要明白的是一个问题,我们的职责在于项目,而不是在于锻炼新人,新人进入项目组织后,通过项目过程的工作和学习能够提高他们的能力,这些是项目的副产物。而我们首先要保证项目的进度,质量。我在实际的项目开发过程中,对于给新人的工作计划都是按照其能力的80%-90%进行安排,很少会针对新人能力的100%进行工作任务分派,因为新人由于缺乏经验,所以在项目实际过程中如果遇到问题,他们解决这些问题的所采用的方法和思路都存在有时间的风险,有时候他们可能由于一个问题会难住他们,消耗掉很长的时间。即便我不断强调说如果有遇到问题,在一个小时内解决不了的话,就要把问题抛出来给大伙来共同处理,但是往往这个问题被最后迫不得已提出来的时候,都是在一天或则更长的时间之后。新人在开发过程中遇到这些问题的概率非常高,所以在工作安排工程中必须要考虑到这种风险。所着一旦推倒第一张加班的牌,那么后续的将是多米洛效应,加班将一发不可收拾,进入恶性循环阶段。
       对于实际的项目中我会采用减算安排工作的方式,那是因为我需要保留对项目整体的进度有更大的回旋缓冲时间。无论对新人还是对高级工程师,中级工程师和初级工程师我都会采用一样的策略,因为人脑不是机器,所以我们无法保证我们时时刻刻都能正常的满负荷运转。但是在平常的学习过程或者非实际项目的锻炼过程中,我会采用超负荷的工作安排模式,这个时候给人员的工作量安排都是100%-120%的程度,因为这个时候我的主要目的在于人员培养,所以需要一种超强度的环境,营造一种压力的气氛。其实开发过程何尝不是一个不见硝烟的战场,如果你想要在战场上不受伤,只有在平时多多受伤,这样在真实的战场上才能从容应对。
       对于项目组中我们需要比较清楚的了解每个成员的能力和个性,如果对于他们的能力了解能够像看着我们手指头那么清楚,我们就能够依据他们的长短,让他们协调发挥出水平来。我们需要避免工作不分能力,均等划分的安排方式,更不能把项目寄托在成员的超水平发挥的基础上。在工作中我们需要明白项目的最终目的,项目成功还是锻炼人员,从而采用减算还是加算得工作安排方式。
作者:Yice(小余)
出处:http://www.yice800.cn

你有没有考虑过自己能够管理多大的项目,能够带领多少人员的项目团队?5人?10人?100人?还是千军万马?但是在现实的项目中,能够带领100人员的项目经理未必能够带好10人的团队,反之亦然。因为作为软件项目来说存在有非常大的差异?无论你是大才还是小才,我们首先要清楚的认识到自己的才能是否能符合项目的实际应用,5人的项目和100人的项目团队中项目经理的工作重心必然不同,如果不区别对待,那么你的结局是大才小用,或者是小才大用。
项目的差异性
我没有机会参加类似IBM的OS/360规模的项目,我所能够参与的最大项目规模不过是100人/月上下的项目,当然也做过产品线的长期开发项目。所以对于那种巨无霸形的项目也只能是望梅止渴,对于其中的奥妙也只能捧读《人月神话》这类的经典,希望能够从中吸取精华来强壮自身。
项目的规模不一样,项目所能够配备给定的人员也不一样,对于大型的项目,除了项目经理之外,还会配置项目辅助管理人员和咨询顾问管理人员等。如果说项目超过了10000人/月这个规模,项目往往会采用纵向切割来进行管理,整个项目会像工厂中产品线生产方式:系统需求;系统设计;配置管理;代码开发;系统测试;文档编写;产品构建等过程,整个项目会根据不同的分工被切割成每个小项目团队,虽然每个团队可能的工作都只是针对于局部,在各自的内部这些工作是相对独立的,但是每个项目又都对其他部分有比较严重的依托,比如系统设计是以项目需求为基础,代码开发是以系统设计为前提,所有的工作序列彼此关联,每个工作都可以独立安排二级甚至三级的项目经理,这样整个项目的组织管理模式也就形成了金字塔的模式,从项目经理到最底下的开发人员形成一个自顶向下的体系结构。这个时候对于项目经理的主要工作也就不能要求事必躬亲,小到一个螺丝钉都要亲自过问,对于这种项目经理的要求更多在于总体协调和整体的掌控上面,他就像一个元帅一般的任务,要的是果断的决策,准确的判断,良好的协调和丰富的管理经验。
实际上大部分的项目经理很难有机会成为如此大规模项目的最高决策者,即便有机会参与的时候,更多都是处在二线或者三线的位置,所能够管理的实际人员也大部分在10人或则20人左右。更多项目经理参与的项目都是中小规模的项目,毕竟中小的项目的数量还是非常巨大,所以有很多的项目经理在从事这种的开发工作。对于项目规模在100人/月的项目对于很多公司来说也算是具备有一定规模的项目,这些项目的人员投入一般都会在10人之上,不会有公司对这种项目采用投入一个人做一百个月的方式。对于10人规模的项目管理对于大部分的项目经理来说可以是一个不小的挑战,因为虽然说项目的规模不能与上述所说的超级项目抗衡,但是项目在整体过程中所做的事情和上述相当,系统需求,系统设计,配置管理,代码开发,系统测试,文档编写,产品构建等都不会缺少,但是在人员配备里面可没有二级或则三级的项目经理,甚至很多时候会有人力资源捉襟见肘的情况,在10人的规模里面可是要包含需求做成人员,项目开发人员,测试人员等。在很多的时候一个人需要充当多个角色,可能在某一个阶段你需要做需求,当需求做完了之后,你又需要投入开发,后期的时候可能需要做项目文档和项目部署,甚至用户培训。所以对于我的直观感觉来说,这种项目对于项目经理的能力要求可能更高,如果要做好这种项目的项目经理,首先最好有比较扎实的技术开发能力和工程背景,这样你的项目估算才能做得比较准确,为后续的开发赢得更多的时间;其次好需要又比较良好的业务知识,因为你可能更多需要做一个用户的接口,所有的需要都需要通过你和客户去沟通,以及最终的定案,所以对于业务的熟悉程度也至关重要;再者需要又良好的计划性,因为这种项目的变数比较多,项目不会像又很长的规划周期,所以如果控制好这些可变因素,已经如何协调内部的有限资源,让资源发挥最大的优势可能成为项目经理每日计划中关键的一部分。
还有不少的项目规模在10人/月左右的项目,这种项目对于很多公司来说属于不痛不痒的项目类型,因为同一个时期内公司可能会有多个这种类型的项目同时在进行中,在这种情况下项目所能够获得的资源就更加有限,往往项目的安置人员也就在5人左右,当然和上面两种情况类是,项目过程的事情还是不可省缺,但是由于项目总体的规模偏小,所以项目的复杂度也比较容易控制,但是这种项目对于项目经理的要求更多在于技术上的要求,因为如果这个时候在安排一个缺乏技术经验的人员做项目经理,那么整个项目组的安排出现一个外行领导四个内行的局面,而且四个内行中可能有一半以上是新人或者初级的开发人员,如果再包括测试等一系列的工作,人员分布上就缺乏合理性。所以这个时候的项目经理往往都是技术过硬的人员担任,这样可以化解人员不足的问题,但是也有一个问题,对于技术优秀的人员都会有一个通病,首先看不上别人的工作结果和质量,另外一方面往往会以自己的标准去衡量别人和给人安排工作,这样对于新人或者初级的开发人员来说,他们的工作就会出现不合理的问题。同时在做估算的时候也比较容易出现轻易的问题,以自己的能力估算项目进度,结果造成项目估算不准确,后期严重加班的问题。所以这种类型的项目经理需要又良好的技术背景,还需要学习项目的实际管理和合理的人员安排,以及如何做好与项目内部成员的友好沟通和关系。
最后一种项目类型就是那种一个人吃饱,全家不额的类型,项目规模非常小,不到1人/月,所以在这种项目的投入人数往往只有一个人或则两个人。当然这个时候也不会要求说项目开发的那一系列的工作都做到位,很多时候都是口头相传,这种项目的结果大部分之要求软件代码和程序能够满足要求。所以这个过程中很多人可能认为只要管好自己就可以了,但是我认为,时间虽然短,但是事情还是需要做足,比如说需求的明确文档化,还有与外界的沟通等一些列过程实际上都可以和正常的模式一样,对自己的工作也需要有一个良好的计划,这个时候对于自己的要求就是一个锻炼的机会,为今后做更大的项目准备好时机。所以这种项目经理的要求实际不抵,如何管理好自己可能会是一个比较大的难题。
项目类型的差异性
项目从规模差异上来说是对一个项目经理的开发能力,管理能力等有不同的要求。但是如果说从项目类型的差异的角度来看,就会对项目经理的一些其他能力又要求,如果说你做的是国内的项目,那么你需要有与客户沟通的良好能力,能够有足够的能力应对客户的各种要求,如何应对客户千奇百怪的要求,如何合理的说“NO.”都是你需要具备的能力。如果说是对日,对欧美的外包服务开发,需要有良好的语言沟通能力,比如说日语,英语。还需要了解不同国家的文化差异等等,这个时候你可能需要充当起桥梁的作用,如何与国外的客户进行沟通和交流,包括有工作安排,技术上等一系列问题的沟通工作。
给自己的定位
项目管理本身就是一个比较复杂的过程,不像行军打战那样,有了一盯一眼的制度就可以管理好项目,因为项目的变数太多,情况迥异,也就没有放之四海而皆准的管理方法。所以对于不同类型的项目来说,我们需要了解项目的特点,在我们有良好的基础准备的前提下,根据自己的能力特点,再结合项目的实际情款来不断调整工作中的方法和内容。
虽然我们很难有一个标准化的管理手册来指导每一个希望做项目经理的人,但是我们可以从别人身上去借鉴各种成功或则失败的经验,特别是别人失败的经验,因为别人的成功可能我们很难克隆,但是我们可以避免别人错误再我们身上重演。
不想当将军的士兵不是好士兵,但是不想当项目经理的程序员未必是坏的程序员。毕竟对于技术领域来说,程序员的最终发展方向项目经理未必是一个最优秀的方向,程序员可以走的道路有很多,可以往架构师,分析师,资深技术人员,咨询师等等。路可能有很多条,而且每一条对于人员的能力要求也都不一样,都有良好的发展机会。所以对于自己能力的判断和分析,认清自己,给自己合理的定位是直观重要。让自己的才华得到发展和认可是今后职业道路上一个关键,自己要才尽其用。
作者:Yice(小余)
出处:http://www.yice800.cn

自己一个人独自回想工作了这么多年,到底给自己留下了什么?如果要给自己找一个答案,或许有两个会在今后很长一段时间一直影响自己的东西,一个是在工作了这么多年让自己明白了我们要担负什么样的责任,另外一件是工作这么多年给自己的颈椎留下了不小的病症。一好一坏或许是这么多年的最大收获。
我们该如何看待责任
时至今日蒙牛老总牛根生说过的那句话还让我记忆犹新,“有德有才,破格重用;有德无才,培养使用;有才无德,限制录用;无德无才,坚决不用。”在我认为德的基本就是这个人的责任心和他的态度。无论是软件行业还是其他的行业,在很多的时候考核一个人是否符合相应的工作岗位,职位技能往往不是最主要的考核标准,而在所有的考核标准中,人的品性和责任态度才是考核的关键。
什么人活得最轻松,不负责人的人。丈夫有责任给妻子一个良好的生活环境;父亲有责任给孩子一个良好的生长环境;孩子有责任赡养自己的父母;员工有责任做好自己的本职工作;所有的这些都是一些基本的责任。如果一个人富有责任心,生活中的这些责任会让自己感觉到沉重的压力,一旦人在这些压力的之下,要想过的轻松舒服就很困难。所以如果你希望自己过得轻松写意,你放下一切的责任态度,做个不负任何责任的人,那么或许你会过得比较轻松,但是你也将成为社会中一个不合格的人。
软件开发本身属于脑力密集型的劳动,所有的一切都依托于人,软件行业本身和其它的制造行业有一个本质性的差异,软件主要依靠人的智慧来进行工作,虽然我们现在看到很多的软件工程管理书籍,无论是大师的作品,还是坊间口声相传的经验之谈,我们都很难解决一个问题,我们很难像制造行业一样,让我们每个程序员写出来的代码都能够像机器加工出来的那样,每一个行代码都能够像一个模子印出来的,很难做到整个团队写出的代码像出至一人之手。所以软件行业的工程管理比其它的工程来说要存在有更大的困难,那么在这些问题困挠之下,整个行业对人的依赖尤为严重,对人的依赖程度也就造成对人本身品德和责任心的要求程度相对要高。
在自己工作这么多年来,感觉最轻松的时候还是刚刚毕业的时候,那个时候是初出校门,作为一个新人进入项目组中,对于自己最大的挑战主要是能不能按时准确的完成自己相应的模块开发工作。后来开始负责项目之后,虽然也在做编码开发工作,但是所担心的和让自己欢欣的事情和开始已经完全不同。如果你希望自己能够能够作为一个合格的项目经理经理,那么你首先需要明确自己的肩上的担子的分量。在我们的工作中,我们每个人必须承担工作的各种责任,如果我们缺失这种看待责任的态度,那么工作最后的结果往往将会以失败告终。
对于自己在这几年的最大一个一个收获就是让自己更明确到责任重于泰山的道理。当然也是这种态度让自己能够坚持着把一个一个项目做下去,做好这些项目。如果要对自己做一个评价,或许在技术上我不敢说自己有十足的优势,但是这些年养成的这种做事情的方法和态度还能值得一提。
在项目中你该承担什么责任
如果你要问项目组中谁的压力最大,一般说来应该是项目经理,当然项目经理所拿的工资相对来说也会比较高,责任和压力本就应该和报酬成正向比例关系。项目经理对下必须对项目团队成员负责,向上必须对公司负责,同时还需要纵向向客户负责,所以项目经理经理就像一个传动轴承,这个项目的运作应该在他驱使之下有效稳定的运转。
项目经理需要考虑项目的成本因素,所有的公司都是以盈利为主要目的,有时候公司会出于其他的因素和目的,对于项目的盈利并没有很严格的要求,但是项目经理要有成本的概念和意识,要有团队的总体成本和利润要有基本的计算方式。同时还需要控制项目的质量和进度,还有些项目还要求有保密意识和其他的相关要求,这些都是项目经理需要向公司负责的地方。
如何组建好一个团队,如何培养团队的成员,让每个团员很够在一个比较良好的环境中工作和学习,能够实现每个人的目标和各自的价值观,把团队建设成什么样类型,和项目经理所采取的方法有直接的关系,有些项目团队组建后团队人员如走马灯似的换,有些团队组建后队员除了编码之后就没有任何的学习机会,如何利用有限的资源和合理的安排,让团队的成员都能够发挥各自的特长,让每个人都能够体现自己的价值,有时候需要替项目组的成员去和公司去争取他们所应该有的福利和报酬,这就是对项目团队和团队成员所要承担的负责。
我们每一个项目最终都将面对我们的客户,有时候我们的客户的要求会让我们很难接受,甚至有时候会让我们团队感觉到很恼火,但是作为一个项目经理需要化解这些对项目团队一个不利的因素,一方面需要避免影响团队的士气,另外一方面需要和客户进行沟通,明确那些要求可行,那些要求不可行,对于不可行的要求需要给一个比较合理的解释,避免由于后期无法完成对客户造成欺骗的行为。同时需要控制项目的整体进度和质量,保证项目最终能够解决客户的为,这些就是对客户负责的一个态度。
如何树立团队的责任意识
对于项目组中项目尽力所承担的责任应该说是最大的,无论项目的成败都和项目经理由直接的关系,所以任何一个项目做项目总结的时候,如果项目成功的话,需要给项目经理记上一功,如果项目失败的话,不论任何原因,棒子首先需要打在项目经理的身上。项目经理需要对项目的得失成败负上完全的责任。
在一个项目团队中,需要有各个不同的角色相互配合才能最终完成项目,需求调研人员,系统分析人员,高级程序员,初级程序员,测试人员,配置管理人员等,虽然项目经理在项目中的责任最大,但是项目中每一个成员都会有相应承担的责任,或许说着这都是一些工作的职责,开发人员需要按照开发中的各种要求进行开发工作,测试人员需要按照测试的要求准备测试文档和数据等,所有的角色都会有各自的工作内容,在实际工作中我们是每一个角色协作来完成项目,有时候由于项目的规模偏小,有些人可能会同时充当多个角色,比如说高级程序员有时候需要同时兼顾系统分析人员,项目经理有时候还需要兼顾测试等等。但是不论规模的大小,项目经理需要非常清晰的意思到每个角色的工作职责,在项目分工中对各种工作要比较清晰,合理化安排。同时也需要让每个工作的人员清楚的知道自己的工作要求和检验的方式,避免含糊性的安排,做到责任清晰。
明确责任的首要是明确工作内容,对于团队成员中需要做到责任均衡,尽量避免能者多劳的问题,工作中进行工作安排时往往容易把工作重点都落到部分能力较强的人员上,这种安排比较容易造成工作天平的倾斜,一旦倾斜的严重,这部分开发人员就比较容易造成由于精力不足造成质量问题。所以项目经理在安排工作中需要有个权衡,如果说存在人员能力不均衡问题,那么在工作安排的时候,需要尽可能抽取重复性的工作,让能力欠缺的人员做这部分工作,同时需要协调他们学习,也需要明确学习的目标和结果。
对于团队中我们需要树立一个责任意识,同时需要有合作精神,在项目开发中整个团队需要彼此合作,有时候一个人的问题有时候会影响到整个团队的质量和进度。我们最终给客户交付的是一个完整的程序,其中任何一个部分出了问题,客户对整个产品的评价都会因此而改观,客户不会说那个模块怎么怎么样,而是说你这个程序怎么怎么样,这就代表说是一个整体的结果,所以项目经理需要在团队中树立起这种整体的责任意识,避免团队中出现个人自管门前雪,不理他人瓦上霜的现象。
你能承受多大责任
你有没有评价过你的项目经理,你有没有对你的项目经理感到无奈和气氛,如果你希望自己能够往项目经理方向发展,在我看来你首先需要考虑的不是你的技术背景,你的管理经验是不是足够,首先要考虑的是你是不是有能够承受那些责任的心态。如果你是项目经理的话,那么你负责管理的团队无论人员多少,他们需要在你的协调下工作,那么你肩上担负的就是那些人的工作结果和评定,还有来自公司的各种压力和考核。这些比单纯写代码要劳心许多。
影响项目成败的因素有很多,但是如果说项目经理缺少责任心,我可以说项目必败无疑。正是因为这样,我才会在此一再强调责任心的问题。
作者:Yice(小余)
出处:http://www.yice800.cn

如果你爱他,那么让他去当项目经理,因为那里会是他事业的天堂;如果你狠他,送他去当项目经理,因为那将是他的地狱。
软 件开发工作应该属于分工比较明确的行业,每一个项目的启动,调研,开发,测试,部署,用户培训和后期维护等一系列的过程都有不同的角色参与其中。在这一系 列的角色中项目经理是最直接的管理者,无疑显得格外的突出和重要。软件项目开发的成功率本身就不高,在众多的失败过程中,由于项目经理在管理上存在的问题 造成项目无法按时交纳,质量不高甚至失败的例子在我看来数不胜数。虽然项目经理的能力并不是项目失败的直接原因,因为影响项目成败的因素有很多,但是如果 一个合格的项目经理,对于项目的整个开发过程来说,如何利用他的经验和能力来有效合理的管理项目进度,从而避免很多无谓的失误,在项目的最终成败中还是占 有关键作用。
对 于很多从事软件开发的人来说,项目经理是他们事业上追求的目标,从初出校园的小牛犊,从最低级的学徒似的初级开发人员,再不断的努力和学习,慢慢得爬到有 经验的中级程序员,再后来到高级程序员,到后来的大牛人才,慢慢开始带领新人,开始接触项目管理上的工作。我想很多人的轨迹都是这么一步一步的过来。在整 个过程中我们彼此都在学习,关于很多的技术方面的知识可以通过网络和书籍进行学习。但是如何做一名项目经理,如何做好一名项目经理,倒缺乏一个系统的学习 框架,包括我自己在内,也是跟随前人身边学习,自己观察,在一次次错误后进行反思后才有所进步。这个话题的文章我考虑了很久后才决定要写出来,在一系列的 文章中结合我自己的项目和我自己身边的项目,希望能够将这些经验与大伙分享,通过讨论,彼此共勉。机会往往是给有所准备的人,不论你现在是否是充当项目经 理的角色,但是如果你有所准备,我想对于你来说机会只是迟早的事情。
项目从规模来说,可以划分微型项目,小型项目,中型项目和大项目,当然还有超大型的项目,对于工数在一人/月(一个中级程序员开发一个月,总计21个工作日)的项目定为微型项目;对于工数在1人/月到10人/月之间的规模称为小型项目;对于工数在10人/月到100人/月之间的规模称为中型项目;如果超过 100人/月的项目称为大型项目;对于我们所讨论的项目管理中,对于超过1000人/月的项目不做讨论,因为一般的公司来说,还是比较少能够遇到中规模的项目。
如 果从类型来说可以简单的划分为产品开发和项目开发,产品的开发一般会有后续定期的产品升级性开发,项目的开发时间跨度也会比较长,对于项目的开发来说,一 般是指为了满足某一特定客户而开发的软件,其开发周期往往会比较紧张,后续的开发主要是针对客户的新功能追加,这种项目的开发往往会划分为几个阶段分步进 行。
如果从合作方式上也可以划分为自主研发和外包开发,甚至还有部分项目使用外驻人员进行项目开发,有时候开发还受到地域性的影响,两地,三地合作开发,国内国外的合作开发,还有甚至多国之间的合作开发。
不 同的项目开发方式都会有不同的问题出现,比如说小型项目和大型项目的人员配备上就不可能一样,外包开发和自主研发的项目计划也不一样,跨地域的合作上的时 间差异和人员的沟通和本公司内部背靠背的模式也不一样。项目中实际可能发生的事情千奇百怪,这些问题绝大部分都需要项目经理来过问,分析和决策。所以说项 目经理或许对于很多人来说将会是地狱,一旦深陷其中,很难有苦尽甘来的那一天。但是如果方法得当,管理手段有效,能够合理的规避风险呢?那你将会感到项目 中的一切对你来说游刃有余,团队中每个人也都能相应发挥自己的特长,也都能从中找到各自的成就感。
生与死只在一线间,好和坏也是如此。希望能够从项目经理的角度来看看项目实际过程中我们会遇到哪些问题,该如何去处理这些问题,通过着一些列的文章能让你对项目的整体过程有更全面的了解,同时也能够让你更清楚项目经理的日常工作和行为职责。
作者:Yice(小余)
出处:http://www.yice800.cn

作者:Michael Howard
原文出处:Strsafe.h: Safer String Handling in C

  在微软公司举行的Microsoft Windows Security Push 活动期间,一批测试者、程序管理经理和普通程序员共同决定要为 C 语言量身定制一套具有较高安全性的字符串处理函数,并且希望这些函数能被 Windows 程序员和微软公司内部的程序员所采用。
简单说来,现有的 C 语言运行时函数实在难以在当今充斥着恶意攻击企图的大环境下立足。这些函数要么在返回值和参数上缺乏一致性,要么隐含着所谓的“截断误差”(truncation errors) 错误,要么无法提供足够强大的功能。坦言之,调用这些函数的代码太容易产生“内存溢出”问题了。
  我们发现,面向 C++ 程序员的类足以应付各种安全处理字符串的编程需要;他们能够选择 MFC 的Cstring 类、ATL 的CComBSTR 类 或者STL 的string 类,等等。然而,经典的 C 语言程序仍然普遍地存在,何况许多人正在把 C++ 当作 “改良的 C 语言” 来用,却把丰富的 C++ 类束之高阁。
  其实只需要添加一行代码,你就能在 C 语言代码中调用安全性良好的 strsafe 系列函数了,详细请参阅:
《Using the Strsafe.h Functions》
这些新函数包含在一个头文件和一个函数库(可选)中,而后两者能在新版的 Platform SDK 中找到。对,就这么简单:

#include "strsafe.h"

还等什么呢!
再强调一次,对 strsafe 函数库的引用是可选的。
为了实现 strsafe 系列函数的目标,你的代码必须满足下列条件:

  • 始终以 NULL 字符结束字符串。
  • 始终检测目标缓冲区的长度。
  • 始终用 HRESULT 语句产生统一的返回值。
  • 兼顾 32 位与 64 位两种运行环境。
  • 具有灵活性。

  我们觉得,缺乏统一性是导致现有许多 C 语言字符串处理函数容易产生安全漏洞的根本原因,而 strsafe 系列函数所带来的高度统一性恰恰是解决此问题的一剂良药。然而,strsafe 也不是万能药。单纯依靠 strsafe 系列函数并不能保证代码的安全性和坚固性——你还必须开动你的大脑才行——然而这样对解决问题还是大有帮助的!
下面给出一段采用经典 C 语言运行时间函数的代码:

void UnsafeFunc(LPTSTR szPath,DWORD cchPath) {

TCHAR szCWD[MAX_PATH];



GetCurrentDirectory(ARRAYSIZE(szCWD), szCWD);

strncpy(szPath, szCWD, cchPath);

strncat(szPath, TEXT("\\"), cchPath);

strncat(szPath, TEXT("desktop.ini"),cchPath);

}

  以上代码中的 bug 随处可见 —— 它没有检查任何一个返回值,而且在对 strncat 函数的调用中也没有正确地使用 cchPath (因为MAX_PATH 中保存的是目标缓冲区内剩余空间的长度,而不是目标缓冲区的总长度)。于是,“内存溢出” 问题将会快找上门来。然而,象这样的代码片段早已泛滥成灾了。如果改用 strsafe 系列函数,那么以上代码应该变成:

bool SaferFunc(LPTSTR szPath,DWORD cchPath) {

TCHAR szCWD[MAX_PATH];



if (GetCurrentDirectory(ARRAYSIZE(szCWD), szCWD) &&

        SUCCEEDED(StringCchCopy(szPath, cchPath, szCWD)) &&

        SUCCEEDED(StringCchCat(szPath, cchPath, TEXT("\\"))) &&

        SUCCEEDED(StringCchCat(szPath, cchPath, TEXT("desktop.ini")))) {



        return true;

}



return false;

}

  这段代码不但检查了每一个返回值,还保证了适时传入同一目标缓冲区的总长度。你还可以采用 Ex 版本的 strsafe 系列函数来实现更加高级的功能,比如:

  • 获取目标缓冲区的当前指针。
  • 获取目标缓冲区的剩余空间长度。
  • 以某个特定字符填充空闲缓冲区。
  • 一旦字符串处理函数失败,就把用特定值填充字符串。
  • 一旦字符串处理函数失败,就把目标缓冲区设成 NULL 。

  如此改进后的代码性能又如何呢?告诉你一个好消息:它与原先的代码在性能上几乎没有差别。我曾在自己的 1.8 GHz 电脑上测试过混用经典 C 语言中各种字符串连接函数的代码、混用 strsafe 系列中各种字符串连接函数的代码和混用 Ex 版本 strsafe 系列中各种字符串连接函数的代码。它们各自独立运行一百万次(没错,就是 10,000,000 次)所消耗的时间分别为:

  • 经典 C 语言 —— 7.3 秒
  • Strsafe 系列—— 8.3 秒
  • Strsafe 系列 (Ex 版) —— 11.1 秒

在测试中,调用 Ex 版本的 strsafe 系列函数的程序会在调用失败时把缓冲区设为 NULL ,并以 0xFE 作为填充字节,代码如下:

DWORD dwFlags = STRSAFE_NULL_ON_FAILURE | STRSAFE_FILL_BYTE(0xFE);

  其中设置填充字节的代码耗时较多。事实上,如果这里仅仅把缓冲区设置为 NULL 的话,则采用 Ex 版本的 strsafe 系列函数的代码将会与采用普通的 strsafe 系列函数的代码耗时相同。
  由此可见,以上三种方案的性能差异极小。我相信你也不会经常在一个程序中数百万次地反复执行包含大量字符串处理函数的代码吧!
还有一点值得引起注意:当你引用 strsafe 系列函数时,原有的 C 语言字符串处理函数都将被自动进行 #undef 处理。这也没问题,因为调试过程中的出错信息将会告诉你哪些函数已经被相应的 strsafe 系列函数取代了。好了,请放心地使用 strsafe.h 吧!更多相关信息请参阅 《Using the Strsafe.h Functions》

一、墨菲定律

1949年,一位名叫墨菲的空军上尉工程师,认为他的某位同事是个倒霉蛋,不经意间开了句玩笑:“如果一件事情有可能被弄糟,让他去做就一定会弄糟。” 这句话迅速流传,并扩散到世界各地。在流传扩散的过程中,这句笑话逐渐失去它原有的局限性,演变成各种各样的形式,其中一个最通行的形式是:“如果坏事情有可能发生,不管这种可能性多么小,它总会发生,并引起最大可能的损失。” 这就是著名的“墨菲定律”。下面是墨菲定律的一些变种或推论。 人生哲学 1.别试图教猫唱歌,这样不但不会有结果,还会惹猫不高兴? 2.别跟傻瓜吵架,不然旁人会搞不清楚,到底谁是傻瓜? 3.不要以为自己很重要,因为没有你,太阳明天还是一样从东方升上来? 4.笑一笑,明天未必比今天好。 5.好的开始,未必就有好结果;坏的开始,结果往往会更糟。 处世原理 6.你若帮助了一个急需用钱的朋友,他一定会记得你——在他下次急需用钱的时候。 7.有能力的——让他做;没能力的──教他做;做不来的──管理他。 8.你早到了,会议却取消;你准时到,却还要等;迟到,就是迟了。 9.你携伴出游,越不想让人看见,越会遇见熟人。 爱情意义 10.你爱上的人,总以为你爱上他是因为:他使你想起你的老情人。 11.你最后硬着头皮寄出的情书;寄达对方的时间有多长,你反悔的时间就有多长。 生活常识 12.东西越好,越不中用。 13.一种产品保证60天不会出故障,等于保证第61天一定就会坏掉。 14.东西久久都派不上用场,就可以丢掉;东西一丢掉,往往就必须要用它。 15.你丢掉了东西时,最先去找的地方,往往也是可能找到的最后一个地方。 16.你往往会找到不是你正想找的东西。 17.你出去买爆米花的时候,银幕上偏偏就出现了精彩镜头。 18.另一排总是动的比较快;你换到另一排,你原来站的那一排,就开始动的比较快了;你站的越久,越有可能是站错了排。 19.一分钟有多长? 这要看你是蹲在厕所里面,还是等在厕所外面。

二、二八法则

1897年,意大利经济学家帕列托在对19世纪英国社会各阶层的财富和收益统计分析时发现:80%的社会财富集中在20%的人手里,而80%的人只拥有社 会财富的20%,这就是“二八法则”。“二八法则”反应了一种不平衡性,但它却在社会、经济及生活中无处不在。 附:破窗理论等

在商品营销中,商家往往会认为所有顾客一样重要;所有生意、每一种产品都必须付出相同的努力,所有机会都必须抓住。而“二八法则”恰恰指出了在原因和结 果、投入和产出、努力和报酬之间存在这样一种典型的不平衡现象:80%的成绩,归功于20%的努力;市场上80%的产品可能是20%的企业生产的;20% 的顾客可能给商家带来80%的利润。遵循“二八法则”的企业在经营和管理中往往能抓住关键的少数顾客,精确定位,加强服务,达到事半功倍的效果。美国的普 尔斯马特会员店始终坚持会员制,就是基于这一经营理念。

“二八法则”同样适用于我们的生活,如一个人应该选择在几件事上追求卓越,而不必强求在每件事上都有好的表现;锁定少数能完成的人生目标,而不必追求所有的机会。

三、马太效应

《新约·马太福音》中有这样一个故事,一个国王远行前,交给三个仆人每人一锭银子,吩咐他们:“你们去做生意,等我回来时,再来见我。”国王回来时,第一 个仆人说:“主人,你交给我的一锭银子,我已赚了10锭。”于是国王奖励了他10座城邑。第二个仆人报告说:“主人,你给我的一锭银子,我已赚了5锭。” 于是国王便奖励了他5座城邑。第三个仆人报告说:“主人,你给我的一锭银子,我一直包在手巾里存着,我怕丢失,一直没有拿出来。”于是国王命令将第三个仆 人的那锭银子赏给第一个仆人,并且说:“凡是少的,就连他所有的,也要夺过来。凡是多的,还要给他,叫他多多益善。”这就是马太效应,它反映了当今社会中 存在的一个普遍现象,即赢家通吃。 对企业经营发展而言,马太效应告诉我们,要想在某个领域保持优势,就必须在此领域迅速做大。当你成为某个领域的领头羊的时候,即便投资回报率相同,你也能 更轻易地获得比弱小的同行更大的收益。而若没有实力迅速在某个领域做大,就要不停地寻找新的发展领域,才能保证获得较好的回报。

四、手表定理

手表定理是指一个人有一只表时,可以知道现在是几点钟,而当他同时拥有两只表时却无法确定。两只表并不能告诉一个人更准确的时间,反而会让看表的人失去对 准确时间的信心。你要做的就是选择其中较信赖的一只,尽力校准它,并以此作为你的标准,听从它的指引行事。记住尼采的话:“兄弟,如果你是幸运的,你只需 有一种道德而不要贪多,这样,你过桥更容易些。” 如果每个人都“选择你所爱,爱你所选择”,无论成败都可以心安理得。然而,困扰很多人的是:他们被“两只表”弄得无所,心身交瘁,不知自己该信仰哪一 个,还有人在环境、他人的压力下,违心选择了自己并不喜欢的道路,为此而郁郁终生,即使取得了受人瞩目的成就,也体会不到成功的快乐。 手表定理在企业经营管理方面给我们一种非常直观的启发,就是对同一个人或同一个组织的管理不能同时采用两种不同的方法,不能同时设置两个不同的目标。 甚至每一个人不能由两个人来同时指挥,否则将使这个企业或这个人无所适从。手表定理所指的另一层含义在于每个人都不能同时挑选两种不同的价值观,否则,你 的行为将陷于混乱。

五、“不值得”定律

不值得定律最直观的表述是:不值得做的事情,就不值得做好,这个定律似乎再简单不过了,但它的重要性却时时被人们疏忘。不值得定律反映出人们的一种心理, 一个人如果从事的是一份自认为不值得做的事情,往往会保持冷嘲热讽,敷衍了事的态度。不仅成功率小,而且即使成功,也不会觉得有多大的成就感。 哪些事值得做呢?一般而言,这取决于三个因素。 1、价值观。关于价值观我们已经谈了很多,只有符合我们价值观的事,我们才会满怀热情去做。 2、个性和气质。一个人如果做一份与他的个性气质完全背离的工作,他是很难做好的,如一个好交往的人成了档案员,或一个害羞者不得不每天和不同的人打交道。 3、现实的处境。同样一份工作,在不同的处境下去做,给我们的感受也是不同的。例如,在一家大公司,如果你最初做的是打杂跑腿的工作,你很可能认为是不值得的,可是,一旦你被提升为领班或部门经理,你就不会这样认为了。 总结一下,值得做的工作是:符合我们的价值观,适合我们的个性与气质,并能让我们看到期望。如果你的工作不具备这三个因素,你就要考虑换一个更合适的工作,并努力做好它。 因此,对个人来说,应在多种可供选择的奋斗目标及价值观中挑选一种,然后为之而奋斗。“选择你所爱的,爱你所选择的”,才可能激发我们的奋斗毅力,也 才可以心安理得。而对一个企业或组织来说,则要很好地分析员工的性格特性,合理分配工作,如让成就欲较强的职工单独或牵头来完成具有一定风险和难度的工 作,并在其完成时给予定时的肯定和赞扬;让依附欲较强的职工更多地参加到某个团体中共同工作;让权力欲较强的职工担任一个与之能力相适应的主管。同时要加 强员工对企业目标的认同感,让员工感觉到自己所做的工作是值得的,这样才能激发职工的热情。

六、彼得原理

管理学家劳伦斯·丁·彼得(Laurence.J.Peter),1917年牛于加拿大的范库弗,1957年获美国华盛顿州立大学学士学位,6年后 又获得该校教育哲学博士学位,他阅历丰富,博学多才,著述颇丰,他的名字还被收人了《美国名人榜》、《美国科学界名人录》和《国际名人传记辞典》等辞书 中。

彼得原理(The PeterPrinciPle)正是彼得根据千百个有关组织中不能胜任的失败实例的分析而归纳出来的。其具体内容是:“在一个等级制度中,每个职工趋向于 上升到他所不能胜任的地位”。彼得指出,每一个职工由于在原有职位上工作成绩表现好(胜任),就将被提升到更高一级职位;其后,如果继续胜任则将进一步被 提升,直至到达他所不能胜任的职位。由此导出的彼得推论是,“每一个职位最终都将被一个不能胜任其工作的职工所占据。层级组织的工作任务多半是由尚未达到 不胜任阶层的员工完成的。”每一个职工最终都将达到彼得高地,在该处他的提升商数(PQ)为零。至于如何加速提升到这个高地,有两种方法。其一。是上面的 “拉动”,即依靠裙带关系和熟人等从上面拉;其二是自我的“推动”,即自我训练和进步等,而前者是被普遍采用的。

彼得认为,由于彼得原理的推出,使他“无意间”创设了一门新的科学——层级组织学(hierarchiolgy)。该科学是解开所有阶层制度之谜的钥匙, 因此也是了解整个文明结构的关键所在。凡是置身于商业、工业、政治、行政、军亨、宗教、教育各界的每个人都和层级组织息息相关,亦都受彼得原理的控制。当 然,原理的假设条件是:时间足够长,五层级组织里有足够的阶层。彼得原理被认为是同帕金森定律有联系的。

帕金森(C.N.Parkinson)是著名的社会理论家,他曾仔细观察并有趣地描述层级组织中冗员累积的现象。他假设,组织中的高级主管采用分化和征服 的策略,故意使组织效率降低,借以提升自己的权势,这种现象即帕金森所说的“爬升金字塔”。彼得认为这种理论设计是有缺陷的,他给出的解释员工累增现象的 原因是层级组织的高级主管真诚追求效率(虽然徒劳无功)。正如彼得原理显示的,许多或大多数主管必已到达他们的不胜任阶层。这些人无法改进现有的状况,因 为所有的员工已经竭尽全力了,于是为了再增进效率,他们只好雇用更多的员工。员工的增加或许可以使效率暂时提升,但是这些新进的人员最后将因晋升过程而到 达不胜任阶层,于是唯一改善的方法就是再次增雇员工,再次获得暂时的高效率,然后是另一。次逐渐归于无效率。这样就使组织中的人数超过了工作的实际需要。

彼得原理首次公开发表于1960年9月美国联邦出资的一次研习会上,听众是一群负责教育研究计划、并刚获晋升的项目主管,彼得认为他们多数人“只是拼命地 想复制一些老掉牙了的统计习题”,于是引介彼得原理说明他们的困境。演说召来了敌意与嘲笑,但是彼得仍然决定以独特的讽刺手法呈现彼得原理,尽管所有案例 研究都经过精确编纂,且引用的资料也都符合事实,最后定稿于1965年春完成,然后总计有16家之多的出版社无情地拒绝了该书的手稿。1966年,作者零 星地在报纸上发表了几篇述论同一主题的文章,读者的反应异常热烈,引得各个出版社趋之若婺鸳。正如彼得在自传中提到的,人偶尔会在镜中瞥见自己的身影而不 能立即自我辩认,于是在不自知前就加以嘲笑一番,这样的片刻里正好可以使人进一步认识自己,“彼得原理”扮演的正是那样一面镜子。

七、零和游戏

一个游戏无论几个人来玩,总有输家和赢家,赢家所赢的都是输家所翰的,所以无论输赢多少,正负相抵,最后游戏的总和都为零,这就是零和游戏。 零和游戏之所以受人关注,是因为人们在社会生活中处处都能找到与零和游戏雷同或类似的现象。我们大肆开发利用煤炭石油资源,留给后人的便越来越少;我们研 究生产了大量的转基因产品,一些新的病毒也跟着冒了出来;我们修筑了葛洲坝水利工程,白鳍豚就再也不能洄游到金沙江产卵了…… 发展是硬道理。人类在经历了经济高速增长、科技迅猛发展、全球经济一体化及曰益严重的生态破坏、环境污染之后,可持续发展理论才逐渐浮出水面。零和游戏原 理正在逐渐为“双赢”观念所取代,人们逐渐认识到“利己”而不“损人”才是最美好的结局。实践证明,通过有效合作,实现皆大欢喜的结局是可能的。 领导者要善于跳出“零和”的圈子,寻找能够实现“双赢”的机遇和突破口,防止负面影响抵消正面成绩。批评下属如何才能做到使其接受而不抵触,发展经济如何 才能做到不损害环境,开展竞争如何使自己胜出而不让对方受到伤害,这些都是每一个为官者应该仔细思考的问题。 还是那句话,世上没有现成的标准答案。这些企业经营管理定律只能供我们参考和借鉴,至于什么条件下适合借鉴哪一种,回到手表定理上去,你需要自己选择一块戴着舒适而又走时准确的手表。

八、华盛顿合作规律

华盛顿合作规律说的是:一个人敷衍了事,两个人互相推诿,三个人则永无成事之日。多少有点类似于“三个和尚”的故事。

  人与人的合作不是人力的简单相加,而是复杂和微妙得多。在人与人的合作中,假定每一个人的能力都为1,那么10个人的合作结果有时比10大得多,有时甚至比1还要小。因为人不是静止的物,而更像方向不同的能量,相互推动时自然事半功倍,相互抵触时则一事无成。

九、酒与污水定律

酒与污水定律是指,如果把一匙酒倒进一桶污水中,你得到的是一桶污水;如果把一匙污水倒进一桶酒中,你得到的还是一桶污水。几乎在任何组织里,都存在几个 难弄的人物,他们存在的目的似乎就是为了把事情搞糟。他们到处搬弄是非,传播流言、破坏组织内部的和谐。最糟糕的是,他们像果箱里的烂苹果,如果你不及时 处理,它会迅速传染,把果箱里其它苹果也弄烂,“烂苹果”的可怕之处在于它那惊人的破坏力。一个正直能干的人进入一个混乱的部门可能会被吞没,而一个人无 德无才者能很快将一个高效的部门变成一盘散沙。组织系统往往是脆弱的,是建立在相互理解、妥协和容忍的基础上的,它很容易被侵害、被毒化。破坏者能力非凡 的另一个重要原因在于,破坏总比建设容易。一个能工巧匠花费时日精心制作的陶瓷器,一头驴子一秒钟就能毁坏掉。如果拥有再多的能工巧匠,也不会有多少像样 的工作成果。如果你的组织里有这样的一头驴子,你应该马上把它清除掉;如果你无力这样做,你就应该把它拴起来。

十、水桶定律

一只水桶能装多少水取决于水桶中最短的一块木板而不是最长的那块木板。 任何组织几乎都有一个共同的特点,即构成组织的各个部分往往是优劣不齐的,但劣势部分却往往决定着整个组织的水平。问题是劣势部分是组织中一个有用的部分,你不能把它当成烂苹果扔掉,否则,你会一点水也装不了,可它却让你那些长的东西白长了! 劣势决定优势,劣势决定生死,这是市场竞争的残酷法则。水桶定律告诉我们,领导者要有忧患意识,如果你个人身上某个方面是“最短的一块”,你应该考虑尽快 把它补起来;如果你所领导的集体中存在着“一块最短的木板”,你一定要迅速将它做长补齐,否则,它带给你的损失可能是毁灭性的—叫艮多时候,往往就是因为 一件事没做好而毁了所有的努力。有些人也许不知道水桶定律,但都知道“一票否决”,这是中国的“水桶”,有了它你便矢口道水桶定律是多么重要。 决策和执行发生的机率。让谋划的人尽心谋划,让执行的人全力执行,让他们都从中获得间接而不是直接的功利,这样我们才能把“粥”分得更好。当然,还应有必不可少的独立的监督。

十一、蘑菇管理原理

蘑菇长在阴暗的角落,得不到阳光,也没有肥料,自生自灭,只有长到足够高的时候才开始被人关注,可此时它自己已经能够接受阳光了。 蘑菇管理是大多数组织对待初入门者、初学者的一种管理方法。从传统的观念上讲,“蘑菇经历”是一件好事,它是人才蜕壳羽化前的一种磨炼,对人的意志和耐力 的培养有促进作用。但用发展的眼光来看,蘑菇管理有着先天的不足:一是太慢,还没等它长高长大恐怕疯长的野草就已经把它盖住了,使它没有成长的机会;二是 缺乏主动,有些本来基因较好的蘑菇,一钻出土就碰上了石头,因为得不到帮助,结果胎死腹中。 让初入门者当上一段时间的“蘑菇”,可以消除他们不切实际的幻想,从而使他们更加接近现实,更实际、更理性地思考问题和处理问题。领导者应当注意的是,这 一过程不可过长,时间太长便会使其消极退化乃至枯萎,须知不给阳光不给关爱不仅是任其自生自灭,而且更是对其成长的抑制。如何让他们成功地走过生命中的这 一段,尽快吸取经验、成熟起来,这才是领导者所应当考虑的。

十二、钱的问题

当某人告诉你:“不是钱,而是原则问题”时,十有八九就是钱的问题。 照一般的说法,金钱是价值的尺度,交换的媒介,财富的贮藏。但是这种说法忽略了它的另一面,它令人陶醉、令人疯狂、令人激动的一面,也撇开了爱钱的心理不谈。马克思说,金钱是“人情的离心力”,就是指这一方面而言。 关于金钱的本质、作用和功过,从古到今,人们已经留下了无数精辟深刻的格言和妙语。我们常会看到,人们为钱而兴奋,努力赚钱,用财富的画面挑逗自己。 金钱对世界的秩序以及我们的生活产生的影响是巨大的、广泛的,这种影响有时是潜在的,我们往往意识不到它的作用如此巨大,然而奇妙的是:它完全是人类自己 创造的。致富的驱动力并不是起源于生物学上的需要,动物生活中也找不到任何相同的现象。它不能顺应基本的目标,不能满足根本的需求-的确,“致富”的定义 就是获得超过自己需要的东西。然而这个看起来漫无目标的驱动力却是人类最强大的力量,人类为金钱而互相伤害,远超过其他原因。

附:一街东头那个乞丐去摸彩票。中了五等奖,得款50元。甚喜!这个冬天好过了,他拥有棉袄、棉裤、和棉鞋了。街西头那个乞丐也去摸彩票。中了一等奖,得款50万元。狂喜! 

 首先大宴宾客,热闹三天,煞是风光,耗款3千元,小意思。然后买西服、配手机、穿金戴银,容光焕发,一扫穷气。再次,买房子,满街转悠,要带车库 的。第四,就是买车了。“夏利”太便宜,“奔驰”太贵,还是“桑塔纳”吧!第五……第六……第七……过年的时候,街东头那个乞丐还在到处晃悠,穿着那50 元买的“家当”;而街西头那个乞丐却在劳教所里,据说是因为赌博、嫖娼、吸毒、闹事……拥有50元,街东头的乞丐不再受冻,自得其乐;拥有50万元,街西 头乞丐成了个大富翁,却也把自己送上了死路。不懂得善用钱财的人,还是没钱点才安全。

十三、奥卡姆剃刀定律

12世纪,英国奥卡姆的威廉主张唯名论,只承认确实存在的东西,认为那些空洞无物的普遍性概念都是无用的累赘,应当被无情地“剃除”。他主张“如无必要, 勿增实体”。这就是常说的“奥卡姆剃刀”。这把剃刀曾使很多人感到威胁,被认为是异端邪说,威廉本人也因此受到迫害。然而,并未损害这把刀的锋利,相反, 经过数百年的岁月,奥卡姆剃刀已被历史磨得越来越快,并早已超载原来狭窄的领域,而具有广泛、丰富、深刻的意义。

  奥卡姆剃刀定律在企业管理中可进一步演化为简单与复杂定律:把事情变复杂很简单,把事情变简单很复杂。这个定律要求,我们在处理事情时,要把握事情的主要实质,把握主流,解决最根本的问题,尤其要顺应自然,不要把事情人为地复杂化,这样才能把事情处理好。

美国政治学家威尔逊和犯罪学家凯林经过观察提出了“破窗理论”。

  如果有人打坏了一栋建筑上的一块玻璃,又没有及时修复,别人就可能受到某些暗示性的纵容,去打碎更多的玻璃。久而久之,这些窗户就给人造成一种无序的感觉,在这种麻木不仁的氛围中,犯罪就会滋生、蔓延。

  “破窗理论”更多的是从犯罪的心理去思考问题,但不管把“破窗理论”用在什么领域,角度不同,道理却相似:环境具有强烈的暗示性和诱导性,必须及时修好“第一扇被打碎玻璃的窗户”。

  推而广之,从人与环境的关系这个角度去看,我们周围生活中所发生的许多事情,不正是环境暗示和诱导作用的结果吗?

  比如,在窗明几净、环境优雅的场所,没有人会大声喧哗,或“噗”地吐出一口痰来;相反,如果环境脏乱不堪,倒是时常可以看见吐痰、便溺、打闹、互骂等不文明的举止。

  又比如,在公交车站,如果大家都井然有序地排队上车,又有多少人会不顾众人的文明举动和鄙夷眼光而贸然插队?与这相反,车辆尚未停稳,猴急的人 们你推我拥,争先恐后,后来的人如果想排队上车,恐怕也没有耐心了。因此,环境好,不文明之举也会有所收敛;环境不好,文明的举动也会受到影响。人是环境 的产物,同样,人的行为也是环境的一部分,两者之间是一种互动的关系。

  在公共场合,如果每个人都举止优雅、谈吐文明、遵守公德,往往能够营造出文明而富有教养的氛围。千万不要因为我们个人的粗鲁、野蛮和低俗行为而形成“破窗效应”,进而给公共场所带来无序和失去规范的感觉。

  从这个意义上说,我们平时一直强调的“从我做起,从身边做起”,就不仅仅是一个空洞的口号,它决定了我们自身的一言一行对环境造成什么样的影响。

帕金森定律

  英国著名历史学家诺斯古德·帕金森通过长期调查研究,写出一本名叫《帕金森定律》的书。他在书中阐述了机构人员膨胀的原因及后果:一个不称职的 官员,可能有三条出路,第一是申请退职,把位子让给能干的人;第二是让一位能干的人来协助自己工作;第三是任用两个水平比自己更低的人当助手。这第一条路 是万万走不得的,因为那样会丧失许多权利;第二条路也不能走,因为那个能干的人会成为自己的对手;看来只有第三条路最适宜。于是,两个平庸的助手分担了他 的工作,他自己则高高在上发号施令,他们不会对自己的权利构成威胁。两个助手既然无能,他们就上行下效,再为自己找两个更加无能的助手。如此类推,就形成 了一个机构臃肿,人浮于事,相互扯皮,效率低下的领导体系。

  苛希纳定律

  西方管理学中有一条著名的苛希纳定律:如果实际管理人员比最佳人数多两倍,工作时间就要多两倍,工作成本就要多4倍;如果实际管理人员比最佳人员多3倍,工作时间就要多3倍,工作成本就要多6倍。

  250定律

  美国著名推销员拉德在商战中总结出了“250定律”。他认为每一位顾客身后,大体有250名亲朋好友。如果您赢得了一位顾客的好感,就意味着赢 得了250个人的好感;反之,如果你得罪了一名顾客,也就意味着得罪了250名顾客。这一定律有力地论证了“顾客就是上帝”的真谛。由此,我们可以得到如 下启示:必须认真对待身边的每一个人,因为每一个人的身后,都有一个相对稳定的、数量不小的群体。善待一个人,就像拨亮一盏灯,照亮一大片。

  达维多定律

  达维多定律是以英特尔公司副总裁达维多的名字命名的。他认为,一个企业要想在市场上总是占据主导地位,那么就要做到第一个开发出新产品,又第一 个淘汰自己的老产品。这一定律的基点是着眼于市场开发和利益分割的成效。因为人们在市场竞争中无时无刻不在抢占先机,只有先入市场才能更容易获取较大的份 额和较高的利润。

  木桶定律

一个有许多块长短不同的木板箍成的木桶,决定其容量大小的并非其中最长的那块木板,而是其中最短的那块木板。同样,在一个企业的营销过程中,必然存在着许多相关的环节,只有找出制约企业经济效益提高的某一关键环节,把这一个矛盾解决了,其它矛盾就可以迎刃而解了。

a、领导

太长了!先看到这里去看看是谁发的这么火的帖子~~~

一、 素养

蓝斯登原则

在你往上爬的时候,一定要保持梯子的整洁,否则你下来时可能会滑倒。 提出者:美国管理学家蓝斯登 点评:进退有度,才不至进退维谷;宠辱皆忘,方可以宠辱不惊。

卢维斯定理

谦虚不是把自己想得很糟,而是完全不想自己。 提出者:美国心理学家h·卢维斯 点评:如果把自己想得太好,就很容易将别人想得很糟。

托利得定理

测验一个人的智力是否属于上乘,只看脑子里能否同时容纳两种相反的思想,而无碍于其处世行事。 提出者:法国社会心理学家h·m·托利得 点评:思可相反,得须相成。

二、统御

刺猬理论

刺猬在天冷时彼此靠拢取暖,但保持一定距离,以免互相刺伤。 点评:保持亲密的重要方法,乃是保持适当的距离。

鲦鱼效应

鲦鱼因个体弱小而常常群居,并以强健者为自然首领。将一只稍强的鲦鱼脑后控制行为的部分割除后,此鱼便失去自制力,行动也发生紊乱,但其他鲦鱼却仍像从前一样盲目追随。 提出者:德国动物学家霍斯特 点评:1、下属的悲剧总是领导一手造成的。2、下属觉得最没劲的事,是他们跟着一位最差劲的领导。

雷鲍夫法则

在你着手建立合作和信任时要牢记我们语言中: 1、最重要的八个字是:我承认我犯过错误 2、最重要的七个字是:你干了一件好事 3、最重要的六个字是:你的看法如何 4、最重要的五个字是:咱们一起干 5、最重要的三个字是:谢谢您 6、最重要的两个字是:咱们 提出者:美国管理学家雷鲍夫 点评:1、最重要的四个字是:不妨试试;2、最重要的一个字是:您

洛伯定理

对于一个经理人来说,最要紧的不是你在场时的情况,而是你不在场时发生了什么。 提出者:美国管理学家r·洛伯 点评:如果只想让下属听你的,那么当你不在身边时他们就不知道应该听谁的了。

三、沟通

斯坦纳定理

在哪里说得愈少,在哪里听到的就愈多。 提出者:美国心理学家s·t·斯坦纳 点评:只有很好听取别人的,才能更好说出自己的。

费斯诺定理

人有两只耳朵却只有一张嘴巴,这意味着人应该多听少讲。 提出者:英国联合航空公司总裁兼总经理l·费斯诺 点评:说得过多了,说的就会成为做的障碍。

牢骚效应

凡是公司中有对工作发牢骚的人,那家公司或老板一定比没有这种人或有这种人而把牢骚埋在肚子里的公司要成功得多。 提出者:美国密歇根大学社会研究院 点评:1、牢骚是改变不合理现状的催化剂。2、牢骚虽不总是正确的,但认真对待牢骚却总是正确的。

避雷针效应

在高大建筑物顶端安装一个金属棒,用金属线与埋在地下的一块金属板连接起来,利用金属棒的尖端放电,使云层所带的电和地上的电逐渐中和,从而保护建筑物等避免雷击。 点评:善疏则通,能导必安

四、协调

氨基酸组合效应

组成人体蛋白的八种氨基酸,只要有一种含量不足,其他七种就无法合成蛋白质。 点评:当缺一不可时,"一"就是一切。

米格-25效应

前苏联研制的米格-25喷气式战斗机的许多零部件与美国的相比都落后,但因设计者考虑了整体性能,故能在升降、速度、应急反应等方面成为当时世界一流。 点评:所谓最佳整体,乃是个体的最佳组合。

磨合效应

新组装的机器,通过一定时期的使用,把磨擦面上的加工痕迹磨光而变得更加密合。 点评:要想达到完整的契合,须双方都作出必要的割舍。

五、指导

波特定理

当遭受许多批评时,下级往往只记住开头的一些,其余就不听了,因为他们忙于思索论据来反驳开头的批评。 提出者:英国行为科学家l·w·波特 点评:总盯着下属的失误,是一个领导者的最大失误。

蓝斯登定律

跟一位朋友一起工作,远较在"父亲"之下工作有趣得多。 提出者:美国管理学家蓝斯登 点评:可敬不可亲,终难敬;有权没有威,常失权。

吉尔伯特法则

工作危机最确凿的信号,是没有人跟你说该怎样作。 提出者:英国人力培训专家b·吉尔伯特 点评:真正危险的事,是没人跟你谈危险。

权威暗示效应

一化学家称,他将测验一瓶臭气的传播速度,他打开瓶盖15秒后,前排学生即举手,称自己闻到臭气,而后排的人则陆续举手,纷纷称自己也已闻到,其实瓶中什么也没有。 点评:迷信则轻信,盲目必盲从。

没走过的路。2、特色不特,优势无优。

古特雷定理

每一处出口都是另一处的入口。 提出者:美国管理学家W·古特雷 点评:上一个目标是下一个目标的基础,下一个目标是上一个目标的延续。

十四、计划

列文定理

那些犹豫着迟迟不能作出计划的人,通常是因为对自己的能力没有把握。 提出者:法国管理学家P·列文 点评:如果没有能力去筹划,就只有时间去后悔了。

弗洛斯特法则

在筑墙之前应该知道把什么圈出去,把什么圈进来。 提出者:美国思想家W·P·弗洛斯特 点评:开始就明确了界限,最终就不会作出超越界限的事来。

十五、参谋

波克定理

只有在争辩中,才可能诞生最好的主意和最好的决定。 提出者:美国庄臣公司总经理詹姆士·波克 点评:无磨擦便无磨合,有争论才有高论。

韦奇定理

即使你已有了主见,但如果有十个朋友看法和你相反,你就很难不动摇。 提出者:美国洛杉矶加州大学经济学家伊渥·韦奇 点评:1、未听之时不应有成见,既听之后不可无主见。2、不怕开始众说纷纭,只怕最后莫衷一是。

十六、决策

福克兰定律

没有必要作出决定时,就有必要不作决定。 提出者:法国管理学家D·L·福克兰 点评:当不知如何行动时,最好的行动就是不采取任何行动。

王安论断

犹豫不决固然可以免去一些作错事的机会,但也失去了成功的机遇。 提出者:美籍华裔企业家王安博士 点评:寡断能使好事由好变坏,果断可将危机转危为安。

十七、执行

格瑞斯特定理

杰出的策略必须加上杰出的执行才能奏效。 提出者:美国企业家H·格瑞斯特 点评:好事干实更好,实事办好愈实。

吉德林法则

把难题清清楚楚地写出来,便已经解决了一半。 提出者:美国通用汽车公司管理顾问查尔斯·吉德林 点评:杂乱无章的思维,不可能产生有条有理的行动

d、调控

十八、信息

沃尔森法则

把信息和情报放在第一位,金钱就会滚滚而来。 提出者:美国企业家s·m·沃尔森 点评:你能得到多少,往往取决于你能知道多少。

塔马拉效应

塔马拉是捷克雷达专家弗·佩赫发明的一种雷达,它与其他雷达的最大不同是不发射信号而只接收信号,故不会被敌方反雷达装置发现。 点评:善藏者人不可知,能知者人无以藏。

十九、监督

小池定理

越是沉醉,就越是抓住眼前的东西不放。 提出者:日本管理学家小池敬 点评:自我陶醉不易清醒,自以为是不喜批评。

赫勒法则

当人们知道自己的工作成绩有人检查的时候会加倍努力。 提出者:英国管理学家h·赫勒 点评:只有在相互信任的情况下,监督才会成为动力。

二十、控制

横山法则

最有效并持续不断的控制不是强制,而是触发个人内在的自发控制。 提出者:日本社会学家横山宁夫 点评:有自觉性才有积极性,无自决权便无主动权。

蝴蝶效应

"紊乱学"研究者称,南半球某地的一只蝴蝶偶尔扇动一下翅膀所引起的微弱气流,几星期后可变成席卷北半球某地的一场龙卷风。他们将这种由一个极小起因,经过一定的时间,在其他因素的参与作用下,发展成极为巨大和复杂后果的现象称为"蝴蝶效应"。 点评:善终者慎始,谨小者慎微。

阿什法则

承认问题是解决问题的第一步。 提出者:美国企业家m·k·阿什 点评:你愈是躲着问题,问题愈会揪住你不放。

二十一、法纪

洛克忠告

规定应该少定,一旦定下之后,便得严格遵守。 提出者:英国教育家洛克 点评:简则易循,严则必行。

热炉法则

当人用手去碰烧热的火炉时,就会受到"烫"的惩罚,其有以下三个特点:即时性、预警性、平等性。 点评:罪与罚能相符,法与治可相期。

二十二、改革

柯美雅定律

世上没有十全十美的东西,所以任何东西都有改革的余地。 提出者:美国社会心理学家m·r·柯美雅 点评:不拘于常规,才能激发出创造力。

达维多夫定律

没有创新精神的人永远也只能是一个执行者。 提出者:前苏联心理学家达维多夫 点评:只有敢为人先的人,才最有资格成为真正的先驱者。

自吃幼崽效应

美国硅谷企业竞争十分激烈,以至于各公司都积极寻找自己的致命弱点,所有公司共同的生存之道是:拿出更好看产品来击败自己的原有产品。有人将这种行为戏称为"自吃幼崽"。 点评:1、自己不逼自己,别人迟早会逼你。2、敢于对过去告一个段落,才有信心掀开新的一章。

二十三、创新

舍恩定理

新思想只有落到真正相信它,对它着迷的人手里才能开花结果。 提出者:美国麻省理工学院教授舍恩 点评:只有信之不疑,才能持之以恒。

吉宁定理

真正的错误是害怕犯错误。 提出者:美国管理学家h·吉宁 点评:不怕错误的人,错误往往也离他最远。

卡贝定理

放弃是创新的钥匙。 提出者:美国电话电报公司前总经理卡贝 点评:在未学会放弃之前,你将很难懂得什么是争取。

史密斯原则

如果你不能战胜他们,你就加入到他们之中去。 提出者:美国通用汽车公司前董事长r·史密斯 点评:竞争使人进步最快,合作让人得到最多。

二十六、营销

阿尔巴德定理一个企业经营成功与否,全靠对顾客的要求了解到什么程度。 提出者:匈牙利全面质量管理国际有限公司顾问波尔加·韦雷什·阿尔巴德 点评:看到了别人的需要,你就成功了一半;满足了别人的需求,你就成功了全部。

冰淇淋哲学

卖冰淇淋必须从冬天开始,因为冬天顾客少,会逼迫你降低成本,改善服务。如果能在冬天的逆境中生存,就再也不会害怕夏天的竞争。 提出者:台湾著名企业家王永庆 点评:逆境不错,顺境更好。

二十七、广告

布里特定理

商品不作广告,就像姑娘在暗处向小伙子递送秋波,脉脉此情只有她自己知道。 提出者:英国广告学专家s·布里特 点评:要推而广之,先广而告之。

伯内特定理

只有占领头脑,才会占有市场。 提出者:美国广告专家利奥·伯内特 点评:脍炙人口才会耐人寻味,深得人心方可引人入胜。

拉图尔定律

一个好品名可能无助于劣质产品的销售,但是一个坏品名则会使好产品滞销。 提出者:法国诺门公司德国分公司负责人苏珊·拉图尔 点评:取名的艺术,亦是取得成功的艺术。

赫斯定律

广告超过12个字,读者的记忆力要降低50%。 提出者:澳大利亚广告家h·赫斯 点评:忘却即等于抛弃。

二十八、公关

玛丽法则

假如还没有破,就不要去修它,免得弄巧成拙。 提出者:美国著名企业家玛丽·凯·阿什 点评:弄巧之所以成拙,往往是因为本来就无巧可弄。

弗里施定理

没有满意的工作人员,就没有满意的顾客。 提出者:德国慕尼黑企业咨询顾问弗里施 点评:协调内外关系,从协调内部关系开始。

反哺效应

动物学家将某些动物长大后把觅到的食物给予其父母的行为称为反哺。 点评:给别人好处的人,往往也是得到好处最多的人。

史崔维兹定理

如果你为获得好处而帮助他人,就不算帮助他人。 提出者:美国社会心理学家g·史崔维兹 点评:动机不纯,行为失真。

二十九、谈判

奥狄思法则

在每一次谈判中,你都应准备向对方作出让步。 提出者:美国谈判专家j·s·奥狄思 点评:争,丈不足;让,寸有余。

居家效应

一个人在家里或自己最熟悉的环境中,言谈举止表现得最为自信和从容。 点评:没有实力垫底,自信永远是苍白的。

尼伦伯格原则

一场圆满的、成功的谈判,每一方都应是胜利者。 提出者:美国著名谈判学家尼伦伯格 点评:总想自己得势,必然势不两立。

比林定律

一生中的麻烦有一半是由于太快说"是",太慢说"不"造成的。 提出者:美国幽默作家比林 点评:1、没有否决权,发言权也很容易被剥夺。2、对不该让步的事不让步,别人反而更容易给你让步。

三十、交往

克林纳德法则

与人相处得好坏,在很大程度上取决于我们用什么方式与人打交道。 提出者:美国人际关系学家h·h·克林纳德 点评:交之有道,能打好交道;来而无往,难有常来往。

忌讳效应

因风俗习惯或个人理由等,对某些言语或举动有所顾忌,积久成为禁忌。 点评:知道别人不喜欢什么,比知道别人喜欢什么更重要。

弗里德曼定律

当一个人的需要可以满足另一个人的需要时,两人就趋于互相喜欢。 提出者:美国心理学家n·w·弗里德曼 点评:有利益与利益的相互补充,才会有需要与需要的相互满足。

三十一、成果

基利定理

容忍失败,这是人们可以学习并加以运用的极为积极的东西。 提出者:美国多布林咨询公司集团总经理拉里·基利 点评:1、成功者之所以成功,只不过是他不被失败左右而已。2、不许失败,无异于不许成功。

沸腾效应

水温升到99度,还不是开水,其价值有限;若再添一把火,在99度的基础上再升高1度,就会使水沸腾,并产生大量水蒸气来开动机器,从而获得巨大的经济效益。 点评:只差一点点,往往是导致最大差别的关键。

王永庆法则

节省一元钱等于净赚一元钱。 提出者:台湾企业界"精神领袖"台塑总裁王永庆 点评:赚钱要依赖别人,节省只取决自己。

算法的力量
算法是计算机科学领域最重要的基石之一,但却受到了国内一些程序员的冷落。许多学生看到一些公司在招聘时要求的编程语言五花八门,就产生了一种误解,认为 学计算机就是学各种编程语言,或者认为,学习最新的语言、技术、标准就是最好的铺路方法。其实,大家被这些公司误导了。编程语言虽然该学,但是学习计算机 算法和理论更重要,因为计算机语言和开发平台日新月异,但万变不离其宗的是那些算法和理论,例如数据结构、算法、编译原理、计算机体系结构、关系型数据库 原理等等。在“开复学生网”上,有位同学生动地把这些基础课程比拟为“内功”,把新的语言、技术、标准比拟为“外功”。整天赶时髦的人最后只懂得招式,没 有功力,是不可能成为高手的。
算法与我
当我在1980年转入计算机科学系时,还没有多少人的专业方向是计算机科学。有许多其他系的人嘲笑我们说:“知道为什么只有你们系要加一个‘科学’,而没 有‘物理科学系’或‘化学科学系’吗?因为人家是真的科学,不需要画蛇添足,而你们自己心虚,生怕不‘科学’,才这样欲盖弥彰。” 其实,这点他们彻底弄 错了。真正学懂计算机的人(不只是“编程匠”)都对数学有相当的造诣,既能用科学家的严谨思维来求证,也能用工程师的务实手段来解决问题——而这种思维和 手段的最佳演绎就是“算法”。
记得我读博时写的Othello对弈软件获得了世界冠军。当时,得第二名的人认为我是靠侥幸才打赢他,不服气地问我的程序平均每秒能搜索多少步棋,当他发 现我的软件在搜索效率上比他快60多倍时,才彻底服输。为什么在同样的机器上,我可以多做60倍的工作呢?这是因为我用了一个最新的算法,能够把一个指数 函数转换成四个近似的表,只要用常数时间就可得到近似的答案。在这个例子中,是否用对算法才是能否赢得世界冠军的关键。
还记得1988年贝尔实验室副总裁亲自来访问我的学校,目的就是为了想了解为什么他们的语音识别系统比我开发的慢几十倍,而且,在扩大至大词汇系统后,速 度差异更有几百倍之多。他们虽然买了几台超级计算机,勉强让系统跑了起来,但这么贵的计算资源让他们的产品部门很反感,因为“昂贵”的技术是没有应用前景 的。在与他们探讨的过程中,我惊讶地发现一个O(n*m)的动态规划(dynamic programming)居然被他们做成了O(n*n*m)。更惊 讶的是,他们还为此发表了不少文章,甚至为自己的算法起了一个很特别的名字,并将算法提名到一个科学会议里,希望能得到大奖。当时,贝尔实验室的研究员当 然绝顶聪明,但他们全都是学数学、物理或电机出身,从未学过计算机科学或算法,才犯了这么基本的错误。我想那些人以后再也不会嘲笑学计算机科学的人了吧!
网络时代的算法
有人也许会说:“今天计算机这么快,算法还重要吗?”其实永远不会有太快的计算机,因为我们总会想出新的应用。虽然在摩尔定律的作用下,计算机的计算能力 每年都在飞快增长,价格也在不断下降。可我们不要忘记,需要处理的信息量更是呈指数级的增长。现在每人每天都会创造出大量数据(照片,视频,语音,文本等 等)。日益先进的记录和存储手段使我们每个人的信息量都在爆炸式的增长。互联网的信息流量和日志容量也在飞快增长。在科学研究方面,随着研究手段的进步, 数据量更是达到了前所未有的程度。无论是三维图形、海量数据处理、机器学习、语音识别,都需要极大的计算量。在网络时代,越来越多的挑战需要靠卓越的算法 来解决。
再举另一个网络时代的例子。在互联网和手机搜索上,如果要找附近的咖啡店,那么搜索引擎该怎么处理这个请求呢?
最简单的办法就是把整个城市的咖啡馆都找出来,然后计算出它们的所在位置与你之间的距离,再进行排序,然后返回最近的结果。但该如何计算距离呢?图论里有不少算法可以解决这个问题。
这么做也许是最直观的,但绝对不是最迅速的。如果一个城市只有为数不多的咖啡馆,那这么做应该没什么问题,反正计算量不大。但如果一个城市里有很多咖啡馆,又有很多用户都需要类似的搜索,那么服务器所承受的压力就大多了。在这种情况下,我们该怎样优化算法呢?
首先,我们可以把整个城市的咖啡馆做一次“预处理”。比如,把一个城市分成若干个“格子(grid)”,然后根据用户所在的位置把他放到某一个格子里,只对格子里的咖啡馆进行距离排序。
问题又来了,如果格子大小一样,那么绝大多数结果都可能出现在市中心的一个格子里,而郊区的格子里只有极少的结果。在这种情况下,我们应该把市中心多分出 几个格子。更进一步,格子应该是一个“树结构”,最顶层是一个大格——整个城市,然后逐层下降,格子越来越小,这样有利于用户进行精确搜索——如果在最底 层的格子里搜索结果不多,用户可以逐级上升,放大搜索范围。
上述算法对咖啡馆的例子很实用,但是它具有通用性吗?答案是否定的。把咖啡馆抽象一下,它是一个“点”,如果要搜索一个“面”该怎么办呢?比如,用户想去 一个水库玩,而一个水库有好几个入口,那么哪一个离用户最近呢?这个时候,上述“树结构”就要改成“r-tree”,因为树中间的每一个节点都是一个范 围,一个有边界的范围(参考:http://www.cs.umd.edu/~hjs/rtrees/index.html)。
通过这个小例子,我们看到,应用程序的要求千变万化,很多时候需要把一个复杂的问题分解成若干简单的小问题,然后再选用合适的算法和数据结构。
并行算法:Google的核心优势
上面的例子在Google里就要算是小case了!每天Google的网站要处理十亿个以上的搜索,GMail要储存几千万用户的2G邮箱, Google Earth要让数十万用户同时在整个地球上遨游,并将合适的图片经过互联网提交给每个用户。如果没有好的算法,这些应用都无法成为现实。
在这些的应用中,哪怕是最基本的问题都会给传统的计算带来很大的挑战。例如,每天都有十亿以上的用户访问Google的网站,使用Google的服务,也 产生很多很多的日志(Log)。因为Log每分每秒都在飞速增加,我们必须有聪明的办法来进行处理。我曾经在面试中问过关于如何对log进行一些分析处理 的问题,有很多面试者的回答虽然在逻辑上正确,但在实际应用中是几乎不可行的。按照他们的算法,即便用上几万台机器,我们的处理速度都跟不上数据产生的速 度。
那么Google是如何解决这些问题的呢?
首先,在网络时代,就算有最好的算法,也要能在并行计算的环境下执行。在Google的数据中心,我们使用的是超大的并行计算机。但传统的并行算法运行 时,效率会在增加机器数量后迅速降低,也就是说,十台机器如果有五倍的效果,增加到一千台时也许就只有几十倍的效果。这种事倍功半的代价是没有哪家公司可 以负担得起的。而且,在许多并行算法中,只要一个结点犯错误,所有计算都会前功尽弃。
那么Google是如何开发出既有效率又能容错的并行计算的呢?
Google最资深的计算机科学家Jeff Dean认识到, Google 所需的绝大部分数据处理都可以归结为一个简单的并行算法: Map and Reduce(http://labs.google.com/papers/mapreduce.html)。 这个算法能够在很多种 计算中达到相当高的效率,而且是可扩展的(也就是说,一千台机器就算不能达到一千倍的效果,至少也可以达到几百倍的效果)。Map and Reduce 的另外一大特色是它可以利用大批廉价的机器组成功能强大的server farm。最后,它的容错性能异常出色,就算一个server farm里面的机 器down掉一半,整个farm依然能够运行。正是因为这个天才的认识,才有了Map and Reduce算法。借助该算法,Google几乎能无限地 增加计算量,与日新月异的互联网应用一同成长。
算法并不局限于计算机和网络
举一个计算机领域外的例子:在高能物理研究方面,很多实验每秒钟都产生几个TB的数据量。但因为处理能力和存储能力的不足,科学家不得不把绝大部分未经处 理的数据丢弃掉。可大家要知道,新元素的信息很有可能就藏在我们来不及处理的数据里面。同样的,在其他任何领域里,算法都可以改变人类的生活。例如人类基 因的研究,就可能因为算法而发明新的医疗方式。在国家安全领域,有效的算法可能避免下一个911的发生。在气象方面,算法可以更好地预测未来天灾的发生, 以拯救生命。
所以,如果你把计算机的发展放到应用和数据飞速增长的大环境下,你一定会发现,算法的重要性不是在日益减小,而是在日益加强。
给程序员的七个建议
(1)练内功。不要只花功夫学习各种流行的编程语言和工具,以及某些公司招聘广告上要求的科目。要把数据结构、算法、数据库、操作系统原理、计算机体系结 构、计算机网络,离散数学等基础课程学好。大家不妨试试高德纳所著The Art of Computer Programming里的题目,如果你能够 解决其中的大部分题目,就说明你在算法方面有一定的功力了。
(2)多实战。通过编程的实战积累经验、巩固知识。很多中国大学毕业生缺乏编程和调试经验;学习C语言,考试过关就算学会了;课题项目中,只要程序能够编 译,运行,并且输入输出满足要求就算了事。这些做法是不行的。写程序的时候,大家必须多想想如何把程序写得更加精炼、高效、高质量。建议大家争取在大学四 年中积累编写十万行代码的经验。我们必须明白的是:好程序员是写出来的,不是学出来的。
(3)求实干。不要轻视任何实际工作,比如一些看似简单的编码或测试。要不懈追求对细节一丝不苟的实干作风与敬业精神。我发现不少程序员对于知识的掌握很 肤浅,不求甚解,没有好奇心,不会刨根问底。比如,学会了C++,是否了解一个对象在编译后,在汇编代码中是如何被初始化的?这个对象的各个成员在内存中 是如何存放的?当一个成员函数被调用时,编译器在汇编代码中加入了哪些额外的动作?虚函数的调用是如何实现的? 这些东西恐怕在编程语言或编译原理中都没 有详细提到,只有通过踏实的实干才能真正掌握。
(4)重视数学学习。数学是思维的体操,数学无处不在。学计算机至少要学习离散数学、概率论、布尔代数、集合论和数理逻辑。这些知识并不难,但是对你未来 的工作帮助会很大。 尤其当你对一些“数学密集型”的领域如视频、图像处理等有兴趣时,这些知识将成为你手中的利器。
(5)培养团队精神,学会与人合作。今天的软件工程早已经不是一个人可以单独操作的,而必须靠团队合作才能成功。不懂得合作的人是不能成大器的。大家要多去寻找可以与人一起做项目的机会。
(6)激励创新意识,培养好奇心,不要死记硬背。没有掌握某种算法技术的根本原理,就不会有应变和创新的能力。想成为一位好程序员(其实从事任何一个行业 都是如此),重要的是要养成钻研,好奇,创新,动手,合作的优秀习惯,不满足于填鸭,不满足于考试交差,不满足于表象。这不是学几门课能够一蹴而就的。
(7)有策略地“打工”。在不影响学业的前提下,寻找真正有意义的暑期工作或兼职。去找一个重视技术的公司,在一个好的“老板”指导下完成真正会被用户使 用的程序。不要急于去一个要你做“头”而独挡一面的地方,因为向别人学习才是你的目的。找工作也是一样,不要只看待遇和职衔,要挑一个你能够学习的环境, 一个愿意培养员工的企业,一个重视你的专业的公司。最后,还要挑一个好老板。
希望大家都能把握机会,养成好的学习习惯,把算法学精学透;希望大家都能有一个美好的未来!

http://www.ayssss.cn/

最近老有人问我是不是出了改dota英雄技能的外挂,什么月骑无限大,剑圣无限斩,巫妖无限弹之类。
我在这里一并回答一下,并且稍微说说原理。因为我对地图方面其实是一窍不通,如果有说的不对的地方,还请指正。

其实这些现象都是使用了作弊地图导致的。本来魔兽争霸是有一个地图验证的,如果你跟主机的图不同,是进不去的(要下载地图)。但是魔兽对地图中的war3map.j文件是进行bcc(block check character)校验的,bcc不同于md5,bcc一般只是用来排错的,并不是加密算法。所以就有人写出了这样的代码,可以在b文件末尾添加上一些不起作用的字串,来让b文件的bcc校验码等于a文件(具体代码我就不贴出来了,很容易搜到)。于是呢,我们就可以做到随意修改地图中的war3map.j ,然后再处理一下,使之跟原来的war3map.j的bcc校验码相同。再把改过并处理后的war3map.j文件替换原来的,这样做出来的作弊地图,暴雪的验证会因为bcc校验相同,而把它认为和原版图是相同的。达到的效果就是,别人用正版图建主机,你可以进入,你用盗版图建主机,别人用正版图也可以进入。但是别以为可以为所欲为的修改war3map.j ,虽然你突破了验证这一关进入了游戏,但是魔兽的联机机制是没有办法突破的。

在这里稍微谈一下魔兽的联机机制,没兴趣的请略过这一段。魔兽联机时,一直有个同步机制,每个联机的玩家都会同时计算所有数据,一旦有不一致,就会导致掉线,这也是为什么用金山游侠之类的游戏修改器单机时可以改钱,联机时一改就掉线。因为你只能修改你自己的机器上的数据,而无法改别人的,单方面修改的结果就是造成你跟其他人不同,你就会掉线。当然,如果所有人同时修改的话,仍然是不会掉线的,所以现在有一些联机修改器,参加游戏的几个玩家一起开这个修改器,可以在玩rpg时改钱什么的,我几个同学就老是用这种修改器来通关一些很难打的rpg图。顺便说一下,这样玩下来保存的replay是无法正常播放的,因为replay只记录动作,你使用修改器的改动不会被记录,播放replay时会因为你并未像你游戏时那样修改数据,造成replay不合逻辑而出错。再顺便说一下吧,为什么所谓的人品外挂并不能实现。曾有人发帖抱怨,怎么蓝胖子次次放招都多重施法,怎么某人每次都暴击,他们是不是用了人品挂。其实这是不可能的,有人以为魔兽中的随机数据都是由主机计算的,这样主机就可以找到办法来修改随机数,造成每次都对他有利的结果。但是实际中并非如此,随机数也是所有人一起计算的,也就是说魔兽里的随机是个伪随机。在一局游戏一开始时,主机会发给每个玩家一个随机数种子(这个种子很有可能就是主机从建立主机到游戏开始所经历的毫秒数),之后的一整局中,所有的随机数都根据这个随机数种子,依照事先定好的算法计算出来,这样也就保证了所有人计算出同样的“随机”结果。另外,这个随机种子也会记录进replay,这也从一个侧面说明了魔兽里的随机是伪随机,如果是真的随机,replay就无法重现了。说的有点多了,下面回到正题。

因为魔兽联机机制的存在,你要是随意改了war3map.j,例如改成给自己增加10000的钱,但是别人是按照的没有修改的war3map.j,在别人机器中你是没有那么多钱的。这时你买一个8000的物品,在你自己机器上是可以的,因为你有10000的钱,但是在其他人机器上,你钱却根本不够!这样的不合理动作就会造成你跟其他人断开连接。
也就是说,你只能修改那些不会造成冲突的地方。例如有些作弊图可以显示出地图全开的效果,因为这些显示的东西只是在你本地机器上显示出来的,并不会对其他玩家照成冲突。类似这样的修改都是可行的,不会掉线。
那么,为什么会出现这种有变态技能效果的dota作弊图呢?我刚开始也很困惑,这么夸张的改动怎么竟然没有掉线?我跟朋友要了个作弊图玩的replay,在我的机器上,用正版dota地图播放,竟然完全再现了那些变态效果!因为我对地图方面并不了解,所以开始上网找资料,并通过qq向某些搞地图的高人请教,又下载了那个变态版dota作弊图和某平台私自山寨的所谓“原版”dota图,提取出来war3map.j来进行对比。经过n久的努力,总算搞明白他是怎么改出来这种效果的了。
原来是因为dota使用到了game cache,而作弊图是单方面修改了game cache中的数据,然后通过函数同步给了所有的玩家。通俗点说,game cache相当于一个池子,所有玩家共享这块区域,任意一个玩家都可以修改这个池中的数据,也可以发出通知,让所有人都来同步这个池子,这样就变相修改了其他人的数据。举个例子,例如dota里黑曜石的放逐技能,它可以减少一个人的智力,一分钟后再归还给他,dota里关于这个技能的函数,把目标和要归还的智力值记录在game cache中,1分钟之后会再从game cache取出目标和智力值,给目标加上相应的智力值,就完成了归还这个人的智力的过程。但是在作弊图中,这里增加了代码,先进行一个判断,如果黑曜石是本机玩家,会把game cache中记录的目标改成本方随机的一个队友,然后把game cache中记录的智力值改为500,然后通知所有玩家同步game cache中的这两个值,这样就完成了对所有人game chche中这两个值的修改。1分钟一到,dota就会向这个目标“归还”智力,这样,本方的一个玩家就凭空增加了500智力。(那个被减少智力的倒霉玩家就无法被归还了,可怜)
大致的原理就是这样了,具体细节我就不详细叙述了。不过dota用到game cache的地方其实并不多,所以能改的地方也就那几个。这也是为什么作弊图要专门改这几个地方,而不是改成例如加钱或者加攻击力或者直接胜利之类的,不是不想改,而是无法实现。另外,暴雪官方的地图是不会这样使用game cache的,所以不用担心对战地图被改(另外对战图还有暴雪标志的保护)。其他的rpg地图,如果本身没有用到game cache的,也就改不出来什么花样,最多显示个全图之类。

暴雪将会在1.23修补这个地图验证漏洞,目前1.23的补丁已经在测试中了,相信升级之后,这种改图作弊将不复存在。只是不知国内玩家到时是不是还要继续死守bug频出的1.20呢?
强行插入广告一则:浩方平台会再对地图进行自己的验证,md5验证,作弊图是无法通过的。
至于做山寨dota图的某平台嘛,就我目前来看,它是没有任何地图验证的,唉。

应广大群众强烈要求,这里给出山寨版dota 6.57c的作弊图链接地址,请大家自行围观(话说我参照这个做出了58b和59c的作弊图,活活活):
http://sc2dota.com/news/310.html

新浪科技讯 北京时间1月10日消息,据国外媒体报道,由于大量用户争相排队下载Windows 7 Beta导致服务器不堪重负,微软周五下午宣布延迟发布Windows 7 Beta。
  微软CEO史蒂夫·鲍尔默(Steve Ballmer)周三在消费电子展(CES)上发表主题演讲时宣布,将于本周五面向公众发布Windows 7 Beta。但由于排队下载的用户过多,导致微软服务器被挤爆。
  微软在Windows 7官方博客中称:“由于用户对Windows 7 Beta热情较高,导致服务器超负荷运转。在发布Beta之前,我们将对Microsoft.com网站增加额外的硬件支持。”
  微软还称:“我们将确保为用户提供最佳的下载体验,一旦发布Beta,我们会立即通知用户。”周五早上,在微软上传Beta文件之前,就已经有迹象表明微软服务器不堪重负。按计划,微软只提供250万份Windows 7 Beta下载。

刚下的,每秒1MB多啊(刚刚链接上的速度是10MB多,吓了我一跳,等稳定下来就1MB多)
补充两个windows 7地址:
Windows 7 Beta 32bit(2.44GB):
http://download.microsoft.com/download/6/3/3/633118BD-6C3D-45A4-B985-F0FDFFE1B021/EN/7000.0.081212-1400_client_en-us_Ultimate-GB1CULFRE_EN_DVD.iso
Windows 7 Beta 64bit(3.15GB):
http://download.microsoft.com/download/6/3/3/633118BD-6C3D-45A4-B985-F0FDFFE1B021/EN/7000.0.081212-1400_client_en-us_Ultimate-GB1CULXFRE_EN_DVD.iso
语言包:http://dl.pconline.com.cn/download/53202-1.html 没找到官方下载地址,就用这个吧.

还有几个Server服务器的版本:(cd-key)
标准版/企业版/数据中心版(2850.0MB):
http://download.microsoft.com/download/0/E/7/0E76E1CE-BFB2-475B-B14C-3445E6C27B4E/7000.0.081212-1400_server_en-us-GB1SXFRE_EN_DVD.iso
Web版(2724.3MB):
http://download.microsoft.com/download/0/E/7/0E76E1CE-BFB2-475B-B14C-3445E6C27B4E/7000.0.081212-1400_serverweb_en-us-GB1WXFRE_EN_DVD.iso
安腾版(2512.2MB):
http://download.microsoft.com/download/0/E/7/0E76E1CE-BFB2-475B-B14C-3445E6C27B4E/7000.0.081212-1400_serverenterpriseia64_en-us-GB1SIAIFRE_EN_DVD.iso
此次发布的测试版可免费试用30天,输入以下相应序列号后可以一直使用到今年8月1日。
标准版:2T88R-MBH2C-M7V97-9HVDW-VXTGF
业版:TFGPQ-J9267-T3R9G-99P7B-HXG47
数据中心版:GQJJW-4RPC9-VGW22-6VTKV-7MCC6
Web版:GT8BY-FRKHB-7PB8W-GQ7YF-3DXJ6
腾版:CQ936-9K2T8-6GPRX-3JR9T-JF4CJ
如果不能下的话,就是之前有个高手猜测为什么微软要提前泄露的原因,服务器不能承受全世界的人下载的原因,我刚才截取的图。

downlaod form microsoft

omg

未获取函数指针就调用函数(如直接连接mswsock..lib并直接调用AcceptEx)的消耗是很大的,因为AcceptEx 实际上是存在于Winsock2结构体系之外的。每次应用程序常试在服务提供层上(mswsock之上)调用AcceptEx时,都要先通过WSAIoctl获取该函数指针。如果要避免这个很影响性能的操作,应用程序最好是直接从服务提供层通过WSAIoctl先获取这些APIs的指针。  

奇迹世界 network 类里面就进行指针获取

void MsWinsockUtil::LoadExtensionFunction( SOCKET ActiveSocket )
{
//AcceptEx 窃荐 啊廉坷扁 (dll俊辑..)
GUID acceptex_guid = WSAID_ACCEPTEX;
LoadExtensionFunction( ActiveSocket, acceptex_guid, (void**) &m_lpfnAccepteEx);

//TransmitFile 窃荐 啊廉坷扁 (dll俊辑..)
GUID transmitfile_guid = WSAID_TRANSMITFILE;
LoadExtensionFunction( ActiveSocket, transmitfile_guid, (void**) &m_lpfnTransmitFile);

//GetAcceptExSockaddrs 窃荐 啊廉坷扁
GUID guidGetAcceptExSockaddrs = WSAID_GETACCEPTEXSOCKADDRS;
LoadExtensionFunction( ActiveSocket, guidGetAcceptExSockaddrs, (void**) &m_lpfnGetAcceptExSockAddrs);

//DisconnectEx 窃荐 啊廉坷扁
GUID guidDisconnectEx = WSAID_DISCONNECTEX;
LoadExtensionFunction( ActiveSocket, guidDisconnectEx, (void**) &m_lpfnDisconnectEx );
}

bool MsWinsockUtil::LoadExtensionFunction( SOCKET ActiveSocket, GUID FunctionID, void **ppFunc )
{
DWORD dwBytes = 0;

if (0 != WSAIoctl(
   ActiveSocket,
   SIO_GET_EXTENSION_FUNCTION_POINTER,
   &FunctionID,
   sizeof(GUID),
   ppFunc,
   sizeof(void *),
   &dwBytes,
   0,
   0))
{
   return false;
}

return true;
}

LPFN_ACCEPTEX     MsWinsockUtil::m_lpfnAccepteEx     = NULL;
LPFN_TRANSMITFILE    MsWinsockUtil::m_lpfnTransmitFile    = NULL;
LPFN_GETACCEPTEXSOCKADDRS MsWinsockUtil::m_lpfnGetAcceptExSockAddrs = NULL;
LPFN_DISCONNECTEX    MsWinsockUtil::m_lpfnDisconnectEx    = NULL;

 

收包和发包循环:

服务器需要进行的连接如下:

1、 与其他服务器连接

2、监听绑定端口

这个2个内容都封装进SESSION内里面,通过NETWORKOBJECT对象判断该进行哪部分的包处理

if( !pIOCPServer->Init( &desc, 1 ) )
根据参数&desc ,对完成端口进行设置

内容有:创建 io_thread(工作者线程), accept_thread(绑定端口),connect_thread(连接其他服务器), send_thread(收包线程),并根据连接的最大数目分配好session pool。

if( !pIOCPServer->StartListen( CLIENT_IOHANDLER_KEY, "127.0.0.1", 6000 ) )
{
   printf( "监听出错" );
   return 0;
}

pIOCPServer->Connect( CLIENT_IOHANDLER_KEY, pNetObj, "127.0.0.1", 7000 );

收包:

pIOCPServer->Update()      ---------》 IOHANDLER_MAP_ITER it->second->Update()    ----------》

VOID IoHandler::Update()
{
ProcessActiveSessionList();

if( !m_pAcceptedSessionList->empty() )
{
   ProcessAcceptedSessionList();
}

if( !m_pConnectSuccessList->empty() )
{
   ProcessConnectSuccessList();
}

if( !m_pConnectFailList->empty() )
{
   ProcessConnectFailList();
}

KickDeadSessions();
}   

收包循环

    if( !pSession->ProcessRecvdPacket( m_dwMaxPacketSize ) )
    {
     pSession->Remove();
    }

发包循环

unsigned __stdcall send_thread( LPVOID param )
{
IOCPServer *pIOCPServer = (IOCPServer*)param;
IOHANDLER_MAP_ITER it;
while( !pIOCPServer->m_bShutdown )
{
   Sleep( 10 );

   for( it = pIOCPServer->m_mapIoHandlers.begin(); it != pIOCPServer->m_mapIoHandlers.end(); ++it )
   {
    it->second->ProcessSend();
   }
}

return 0;
}

d、接受SOCKET连接并进行完成端口绑定

VOID IoHandler::ProcessAcceptedSessionList()
{
SESSION_LIST_ITER   it;
Session      *pSession;

// 立加俊 己傍茄 技记甸阑 罐酒敌 烙矫 府胶飘肺 颗辫
m_pAcceptedSessionList->Lock();
m_pTempList->splice( m_pTempList->end(), *m_pAcceptedSessionList );//将m_pAcceptedSessionList 合并到TEMPLIST
m_pAcceptedSessionList->Unlock();

// 立加俊 己傍茄 技记俊 措茄 贸府
for( it = m_pTempList->begin(); it != m_pTempList->end(); ++it )
{
   pSession = *it;

   // 弥绊悼立荐甫 檬苞窍绰 版快 角菩
   if( m_numActiveSessions >= m_dwMaxAcceptSession )
   {
    printf( "connection full! no available accept socket!\n" );
    m_pTempList->erase( it-- );
    ReuseSession( pSession );
    continue;
   }

   // IOCP绑定
   CreateIoCompletionPort( (HANDLE)pSession->GetSocket(), m_hIOCP, (ULONG_PTR)pSession, 0 );

   // Recv俊 角菩窍绰 版快 贸府
   if( !pSession->PreRecv() )
   {
    m_pTempList->erase( it-- );
    ReuseSession( pSession );
    continue;
   }

   //--------------------------------
   // 己傍利栏肺 立加等 技记 贸府
   //--------------------------------

   // 匙飘亏 坷宏璃飘 积己 夸没
   NetworkObject *pNetworkObject = m_fnCreateAcceptedObject();
   assert( pNetworkObject );

   // 匙飘亏 坷宏璃飘 官牢爹
   pSession->BindNetworkObject( pNetworkObject );

   // 立加矫 檬扁拳 棺 NetworkObject肺 立加 烹瘤
   pSession->OnAccept();

   // 悼立荐 刘啊
   ++m_numActiveSessions;
}

if( !m_pTempList->empty() )
{
   // 立加俊 己傍茄 技记甸阑 ActiveSessionList俊 眠啊
   m_pActiveSessionList->Lock();
   m_pActiveSessionList->splice( m_pActiveSessionList->begin(), *m_pTempList );
   m_pActiveSessionList->Unlock();
}
}

PreRecv() 的动作判断SOCKET是否继续有效

BOOL Session::PreRecv()
{
WSABUF wsabuf;

m_pRecvBuffer->GetRecvParam( (BYTE**)&wsabuf.buf, (int&)wsabuf.len );

ZeroMemory( &m_recvIoData, sizeof(OVERLAPPEDEX) );

m_recvIoData.dwOperationType = RECV_POSTED;

int ret = WSARecv( GetSocket(), &wsabuf, 1, &m_recvIoData.dwIoSize, &m_recvIoData.dwFlags, &m_recvIoData, NULL );

if( ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING )
{
   return FALSE;
}

return TRUE;
}

b、代码实现连接

连接每个服务器都用继承自ServerSession 的类实现

有如下类

AgentServerSession

BattleServerSession

FieldServerSession

GameDBProxySession

GuildServerSession

MasterServerSession

基类ServerSession 有 update 实现心跳连接

VOID ServerSession::Update()
{
if( IsForConnect() )
{
   // heartbeat 焊郴扁
   DWORD dwCurTick = GetTickCount();
   if( dwCurTick - m_dwLastHeartbeatTick > 10000 )
   {
    m_dwLastHeartbeatTick = dwCurTick;

    MSG_HEARTBEAT msg;
    msg.m_byCategory   = 0;
    msg.m_byProtocol   = SERVERCOMMON_HEARTBEAT;
    Send( (BYTE*)&msg, sizeof(MSG_HEARTBEAT) );
   }
}
}

每个Session要连接服务器的时候

VOID GameDBProxySession::OnConnect( BOOL bSuccess, DWORD dwSessionIndex )
{
ServerSession::OnConnect( bSuccess, dwSessionIndex );

if( bSuccess )
{
   ServerSession::SendServerType();

   g_pGameServer->ConnectTo( AGENT_SERVER );
}
else
{
   //SUNLOG( eFULL_LOG, "Can't connect to game DB proxy." );
}
}

VOID GameServer::ConnectTo( eSERVER_TYPE eServerType )
{
switch( eServerType )
{
case MASTER_SERVER:
   ConnectToServer( m_pMasterServerSession,
    (char*)m_pMasterServerSession->GetConnectIP().c_str(), m_pMasterServerSession->GetConnectPort() );
        break;

case GAME_DBPROXY:
   ConnectToServer( m_pGameDBProxySession,
    (char*)m_pGameDBProxySession->GetConnectIP().c_str(), m_pGameDBProxySession->GetConnectPort() );
   break;

case AGENT_SERVER:
   ConnectToServer( m_pAgentServerSession,
    (char*)m_pAgentServerSession->GetConnectIP().c_str(), m_pAgentServerSession->GetConnectPort() );
   break;
case GUILD_SERVER:
   ConnectToServer( m_pGuildServerSession,
    (char*)m_pGuildServerSession->GetConnectIP().c_str(), m_pGuildServerSession->GetConnectPort() );
   break;

default:
   ASSERT( !"弊繁 辑滚 鸥涝篮 绝绢夸" );
}
}

DWORD GameServer::ConnectToServer( NetworkObject * pNetworkObject, char * pszIP, WORD wPort )
{
return m_pIOCPServer->Connect( SERVER_IOHANDLER, pNetworkObject, pszIP, wPort );
}

DWORD IOCPServer::Connect( DWORD dwIoHandlerKey, NetworkObject *pNetworkObject, char *pszIP, WORD wPort )
{
if( pNetworkObject == NULL ) return 0;

IOHANDLER_MAP_ITER it = m_mapIoHandlers.find( dwIoHandlerKey );

assert( it != m_mapIoHandlers.end() );

return it->second->Connect( pNetworkObject, pszIP, wPort );
}

c、代码实现监听

VOID GameServer::StartListen()
{
SERVER_ENV * pServerEnv = m_pFileParser->GetServerEnv();

if( !m_pIOCPServer->IsListening( SERVER_IOHANDLER ) )
{
   DISPMSG( "[GameServer::StartListen] Starting listen(%s:%d)...\n", pServerEnv->ServerIoHandler.szIP, pServerEnv->ServerIoHandler.wPort );
   if( !m_pIOCPServer->StartListen( SERVER_IOHANDLER, pServerEnv->ServerIoHandler.szIP, pServerEnv->ServerIoHandler.wPort ) )
   {
    DISP_FAIL;
    return ;
   }
   DISP_OK;
}
}

BOOL IOCPServer::StartListen( DWORD dwIoHandlerKey, char *pIP, WORD wPort )
{
IOHANDLER_MAP_ITER it = m_mapIoHandlers.find( dwIoHandlerKey );

assert( it != m_mapIoHandlers.end() );

return it->second->StartListen( pIP, wPort );
}

1、服务器内容

a、不同机器上的分为

   DBProxy //数据库

Guild //公会数据

Master //主服务器 Agent //副本服务器

4种服务器,代码提供了很清晰的每个服务器的HANDLER FUNC TABLE(HASH)。

class PacketHandler : public Singleton<PacketHandler>
{
typedef VOID (*fnHandler)( CScence * pScence, GamePackHeader * pMsg, WORD wSize );
//typedef VOID (*fnHandler_CG)( Player * pPlayer, GamePackHeader * pMsg, WORD wSize );

public:
PacketHandler();
~PacketHandler();

BOOL       RegisterHandler_DG();
//BOOL       RegisterHandler_CG();
BOOL       RegisterHandler_GM();
BOOL       RegisterHandler_AG();
BOOL       RegisterHandler_Actor();

VOID       ParsePacket_DG( CScence * pScence, GamePackHeader * pMsg, WORD wSize );
//VOID       ParsePacket_CG( Player * pPlayer, GamePackHeader * pMsg, WORD wSize );
VOID       ParsePacket_GM( CScence * pScence, GamePackHeader * pMsg, WORD wSize );
VOID       ParsePacket_AG( CScence * pScence, GamePackHeader * pMsg, WORD wSize );
VOID       ParsePacket_Actor( CScence * pScence, GamePackHeader * pMsg, WORD wSize );

private:

BOOL       AddHandler_DG( BYTE category, BYTE protocol, fnHandler fnHandler );
//BOOL       AddHandler_CG( BYTE category, BYTE protocol, fnHandler_CG fnHandler );
BOOL       AddHandler_GM( BYTE category, BYTE protocol, fnHandler fnHandler );
BOOL       AddHandler_AG( BYTE category, BYTE protocol, fnHandler fnHandler );
BOOL       m_FunctionMap_Acotr( BYTE category, BYTE protocol, fnHandler fnHandler );

struct FUNC_DG : public BASE_FUNC
{
   fnHandler     m_fnHandler;
};
struct FUNC_GM : public BASE_FUNC
{
   fnHandler     m_fnHandler;
};
struct FUNC_AG : public BASE_FUNC
{
   fnHandler     m_fnHandler;
};
struct FUNC_ACTOR : public BASE_FUNC
{
   fnHandler     m_fnHandler;
};

FunctionMap      m_FunctionMap_DG;
FunctionMap      m_FunctionMap_CG;
FunctionMap      m_FunctionMap_GM;
FunctionMap      m_FunctionMap_AG;
FunctionMap      m_FunctionMap_Actor;
};

CPP。

#include "PacketHandler.h"

PacketHandler::PacketHandler()
{

}

PacketHandler::~PacketHandler()
{
}

BOOL PacketHandler::RegisterHandler_DG()
{
//#define HANDLER_DG( c, p ) if( !AddHandler_DG( c, p, Handler_DG_CHARINFO::On##p ) ) return FALSE

return TRUE;
}

BOOL PacketHandler::RegisterHandler_Actor()
{
#define HANDLER_GZ( c, p ) if( !AddHandler_Actor( c, p, Handler_GZ_GUILD::On##p ) ) return FALSE

return TRUE;
}

BOOL PacketHandler::RegisterHandler_GM()
{
//if( !AddHandler_GM( GM_CONNECTION, GM_CONNECTION_SERVER_INFO_CMD, Handler_GM::OnGM_CONNECTION_SERVER_INFO_CMD ) )
// return FALSE;
//if( !AddHandler_GM( GM_OPERATION, GM_RELOAD_DATA_CMD, Handler_GM::OnGM_RELOAD_DATA_CMD ) )
// return FALSE;
//if( !AddHandler_GM( SERVERCOMMON, SERVERCOMMON_SERVERSHUTDOWN_REQ, Handler_GM::OnSERVERCOMMON_SERVERSHUTDOWN_REQ ) )
// return FALSE;

return TRUE;
}

BOOL PacketHandler::RegisterHandler_AG()
{
// CG_CHARINFO
//if( !AddHandler_AG( CG_CHARINFO, CG_CHARINFO_SELECT_INFO_SYN, Handler_CG_CHARINFO::OnCG_CHARINFO_SELECT_INFO_SYN))
//       return FALSE;

return TRUE;
}

VOID PacketHandler::ParsePacket_Actor( CScence * pScence, GamePackHeader * pMsg, WORD wSize )
{
if( 0xff == pMsg->m_byCategory )
{
}

FUNC_GZ * pFuncInfo = (FUNC_GZ *)m_FunctionMap_GZ.Find( MAKEWORD( pMsg->m_byCategory,pMsg->m_byProtocol ) );

if( NULL == pFuncInfo )
{
   //SUNLOG( eCRITICAL_LOG, "[PacketHandler::ParsePacket_GZ] PacketType Error GZ!!"); 
   return ;
}

pFuncInfo->m_fnHandler( pScence, pMsg, wSize );
}
VOID PacketHandler::ParsePacket_DG( CScence * pScence, GamePackHeader * pMsg, WORD wSize )
{
if( 0xff == pMsg->m_byCategory )
{
}

FUNC_DG * pFuncInfo = (FUNC_DG *)m_FunctionMap_DG.Find( MAKEWORD( pMsg->wType,pMsg->m_byProtocol ) );

if( NULL == pFuncInfo )
{
   //SUNLOG( eCRITICAL_LOG, "[PacketHandler::ParsePacket_DG] PacketType Error DG!!"); 
   return ;
}

pFuncInfo->m_fnHandler( pScence, pMsg, wSize );
}

VOID PacketHandler::ParsePacket_GM( CScence * pScence, GamePackHeader * pMsg, WORD wSize )
{
if( 0xff == pMsg->m_byCategory )
{
}

FUNC_GM * pFuncInfo = (FUNC_GM *)m_FunctionMap_GM.Find( MAKEWORD( pMsg->m_byCategory,pMsg->m_byProtocol ) );

if( NULL == pFuncInfo )
{
   //SUNLOG( eCRITICAL_LOG, "[PacketHandler::ParsePacket_GM] PacketType Error!! GM");
   return ;
}

pFuncInfo->m_fnHandler( pScence, pMsg, wSize );
}

VOID PacketHandler::ParsePacket_AG( CScence * pScence, GamePackHeader * pMsg, WORD wSize )
{
if( 0xff == pMsg->m_byCategory )
{
}

FUNC_AG * pFuncInfo = (FUNC_AG *)m_FunctionMap_AG.Find( MAKEWORD( pMsg->m_byCategory,pMsg->m_byProtocol ) );

if( NULL == pFuncInfo )
{
   //SUNLOG( eCRITICAL_LOG, "[PacketHandler::ParsePacket_AG] PacketType Error!! AG Category[%d] Protocol[%d] ", pMsg->m_byCategory,pMsg->m_byProtocol);
   return ;
}

pFuncInfo->m_fnHandler( pScence, pMsg, wSize );
}

BOOL PacketHandler::AddHandler_Acotr( BYTE category, BYTE protocol, fnHandler fnHandler)
{
FUNC_ACTOR * pFuncInfo    = new FUNC_ACTOR;
pFuncInfo->m_dwFunctionKey   = MAKEWORD( category, protocol );
pFuncInfo->m_fnHandler    = fnHandler;
return m_FunctionMap_Actor.Add( pFuncInfo );
}

BOOL PacketHandler::AddHandler_DG( BYTE category, BYTE protocol, fnHandler fnHandler)
{
FUNC_DG * pFuncInfo     = new FUNC_DG;
pFuncInfo->m_dwFunctionKey   = MAKEWORD( category, protocol );
pFuncInfo->m_fnHandler    = fnHandler;
return m_FunctionMap_DG.Add( pFuncInfo );
}

BOOL PacketHandler::AddHandler_GM( BYTE category, BYTE protocol, fnHandler fnHandler)
{
FUNC_GM * pFuncInfo     = new FUNC_GM;
pFuncInfo->m_dwFunctionKey   = MAKEWORD( category, protocol );
pFuncInfo->m_fnHandler    = fnHandler;
return m_FunctionMap_GM.Add( pFuncInfo );
}
BOOL PacketHandler::AddHandler_AG( BYTE category, BYTE protocol, fnHandler fnHandler)
{
FUNC_AG * pFuncInfo     = new FUNC_AG;
pFuncInfo->m_dwFunctionKey   = MAKEWORD( category, protocol );
pFuncInfo->m_fnHandler    = fnHandler;
return m_FunctionMap_AG.Add( pFuncInfo );
}

值得注意的是此类是singleton,这样只能实例化一次,带来的好处就是没有多个实例造成的代码泛滥

b、代码实现

上次已经绘制过基本图元了, 这次只不过要贴张图而已.....

本来我想用Graphics的Model渲染流程来做, 不过这一层太高级了, 都是什么场景管理资源映射之类的

做低级的事情, 就要用低级的API嘛

图形渲染的底层是CoreGraphics, 这个层我不打算再单独写(翻译)一篇了, 因为都是Direct3D概念的一些抽象. 也就是说D3D用熟了基本上一看就明白(用GL的我就不清楚啦, 嘿嘿, N3的作者都放弃用GL去实现@_@).

还记得D3D Tutorial中的Textured例子不? 需要的东西有带纹理坐标的点, 纹理. N3中也一样, 不过, 这里没法用固定管线了.

N3的设计的时候就放弃了固定管线(多么明智呀, 别喷我-_-, 我只会shader.......), 所以在这之前我们要先写一个shader来进行绘制.

因为我们只是进行简单的演示, 就尽量简单了, 写一个2D的纹理绘制, 你可以用来做UI:

  1. //------------------------------------------------------------------------------
  2. //  texture2d.fx
  3. //  texture shader for 2D(UI)
  4. //  (C) xoyojank
  5. //------------------------------------------------------------------------------
  6. float2 halfWidthHeight  : HalfWidthHeight;
  7. texture diffMap     : DiffMap0;
  8. sampler diffMapSampler = sampler_state
  9. {
  10.     Texture = <diffMap>;
  11.     AddressU = Clamp;
  12.     AddressV = Clamp;
  13.     MinFilter = Point;
  14.     MagFilter = Point;
  15.     MipFilter = None;
  16. };
  17. struct VS_INPUT
  18. {
  19.     float3 pos  : POSITION;
  20.     float2 uv       : TEXCOORD;
  21. };
  22. struct VS_OUTPUT
  23. {
  24.     float4 pos  : POSITION;
  25.     float2 uv       : TEXCOORD;
  26. };
  27. //------------------------------------------------------------------------------
  28. /**
  29. */
  30. VS_OUTPUT
  31. VertexShaderFunc(VS_INPUT input)
  32. {
  33.     VS_OUTPUT output;
  34.     output.pos.xy = float2(input.pos.x - halfWidthHeight.x, halfWidthHeight.y - input.pos.y) / halfWidthHeight;
  35.     output.pos.zw = float2(input.pos.z, 1.0f);
  36.     output.uv = input.uv;
  37. return output;
  38. }
  39. //------------------------------------------------------------------------------
  40. /**
  41. */
  42. float4
  43. PixelShaderFunc(float2 uv : TEXCOORD0) : COLOR
  44. {
  45. return tex2D(diffMapSampler, uv);
  46. }
  47. //------------------------------------------------------------------------------
  48. /**
  49. */
  50. technique Default
  51. {
  52.     pass p0
  53.     {
  54.         ColorWriteEnable  = RED|GREEN|BLUE|ALPHA;
  55.         ZEnable           = False;
  56.         ZWriteEnable      = False;
  57.         StencilEnable     = False;
  58.         FogEnable         = False;
  59.         AlphaBlendEnable  = True;
  60.         SrcBlend          = SrcAlpha;
  61.         DestBlend         = InvSrcAlpha;
  62.         AlphaTestEnable   = False;
  63.         ScissorTestEnable = False;
  64.         CullMode          = CW;        
  65.         VertexShader = compile vs_3_0 VertexShaderFunc();
  66.         PixelShader = compile ps_3_0 PixelShaderFunc();
  67.     }
  68. }

值得一提的是CullMode = CW, 为什么? 因为N3用的右手坐标系, 这点又跟D3D不一样了........为什么呢? 难道写MAYA跟MAX的插件的时候比较省事?

还是要跟上一次一样设置顶点格式并载入VertexBuffer:

  1. // vertex
  2.             Array<VertexComponent> vertexComponents;
  3.             vertexComponents.Append(VertexComponent(VertexComponent::Position, 0, VertexComponent::Float3));
  4.             vertexComponents.Append(VertexComponent(VertexComponent::TexCoord, 0, VertexComponent::Float2));
  5. float vertex[4][5] = {
  6.                 {0.0f,  0.0f,   0.0f,   0.0f, 0.0f},
  7.                 {0.0f,  256.0f, 0.0f,   0.0f, 1.0f}, 
  8.                 {256.0f,0.0f,   0.0f,   1.0f, 0.0f}, 
  9.                 {256.0f,256.0f, 0.0f,   1.0f, 1.0f}
  10.             };
  11.             vertexBuffer = VertexBuffer::Create();
  12.             Ptr<MemoryVertexBufferLoader> vbLoader = MemoryVertexBufferLoader::Create();
  13.             vbLoader->Setup(vertexComponents, 4, vertex, 4 * 5 * sizeof(float));
  14.             vertexBuffer->SetLoader(vbLoader.upcast<ResourceLoader>());
  15.             vertexBuffer->Load();
  16.             vertexBuffer->SetLoader(NULL);

纹理的创建其实跟顶点差不多, 因为它都是属于资源的一种, 详见Nebula3资源子系统

  1. // texture
  2.             texture = Texture::Create();
  3.             texture->SetResourceId(ResourceId("bin:razor.jpg"));
  4.             texture->SetLoader(StreamTextureLoader::Create());
  5.             texture->Load();
  6.             texture->SetLoader(NULL);

shader的加载跟上一次一样, 只是参数不同:

  1. // shader
  2. this->shaderInstance = this->shaderServer->CreateShaderInstance(ResourceId("shd:texture2d"));
  3.             Ptr<ShaderVariable> halfWidthHeight = this->shaderInstance->GetVariableBySemantic(ShaderVariable::Semantic("HalfWidthHeight"));
  4.             float2 halfWH = float2(this->renderDevice->GetDefaultRenderTarget()->GetWidth(), this->renderDevice->GetDefaultRenderTarget()->GetHeight()) * 0.5f;
  5.             halfWidthHeight->SetFloatArray(&halfWH.x(), 2);
  6.             Ptr<ShaderVariable> diffMap = this->shaderInstance->GetVariableBySemantic(ShaderVariable::Semantic("DiffMap0"));
  7.             diffMap->SetTexture(texture);

绘制嘛, 当然改成矩形了, 图片可贴不到一跟线上:

  1. this->renderDevice->BeginFrame();
  2. this->renderDevice->BeginPass(this->renderDevice->GetDefaultRenderTarget(), this->shaderInstance);
  3.         PrimitiveGroup primGroup;
  4.         primGroup.SetBaseVertex(0);
  5.         primGroup.SetNumVertices(4);
  6.         primGroup.SetPrimitiveTopology(PrimitiveTopology::TriangleStrip);
  7. this->renderDevice->SetVertexBuffer(this->vertexBuffer);
  8. this->renderDevice->SetPrimitiveGroup(primGroup);
  9. this->renderDevice->Draw();
  10. this->renderDevice->EndPass();
  11. this->renderDevice->EndFrame();
  12. this->renderDevice->Present();

上图:

图形子系统是渲染层中图形相关子系统的最高层. 它基本上是Mangalore图形子系统的下一个版本, 但是现在整合进了Nebula, 并且与低层的渲染代码结合得更加紧密. 最基本的思想是实现一个完全自治的图形”世界”, 它包含模型, 灯光, 还有摄像机实体, 而且只需要与外部世界进行最少的通信. 图形世界的最主要操作是加入和删除实体, 还有更新它们的位置.
因为Mangalore的图形子系统跟Nebula2的完全分界线从Nebula3中移除了, 很多设想都可以用更少的代码和交互来实现.
图形子系统也会为了异步渲染而多线程化, 它和所有的底层渲染子系统都会生存在它们自己的fat-thread中. 这本应是Nebula3层次结构中更高级的东西, 但是我选择了这个位置, 因为这是游戏跟渲染相关通信最少的一部分代码. 正是因为图形代码有了更多的”自治权”, 游戏相关的代码可以跟图形以完全不同的帧率来运行, 不过这需要实践来证明一下. 但是我一定会尝试, 因为完全没有必要让游戏逻辑代码运行在10帧以上(格斗游戏迷们可能会反对吧).
图形子系统中最重要的公有类有:

  • ModelEntity
  • CameraEntity
  • LightEntity
  • Stage
  • View

一个ModelEnity表示了一个可见的图形对象, 它包括位置, 包围体和内嵌的Model资源. 一个Model资源是一个完全的3D模型, 包括几何体, 材质, 动画, 层级变换等…(后面会提到).
一个CameraEntity描述了图形世界中的一个视景体, 为渲染提供View和Project矩阵.
一个LightEntity描述了一个动态光源. Nebula3的光源属性还没有最终确定, 但是我的目标是一个相对灵活地近似(最后一个光源不会超过几个shader参数).
Stage和View是Nebula3图形子系统新增的内容. 在Mangalore中, 图形实体是生存在一个单独的图形Level类里, 任何时候只能有一个Level和一个摄像机. 这对于只需要渲染一个世界到帧缓存(frame buffer)的情况来说还是不错的. 但许多游戏程序需要更复杂的渲染, 如在GUI中渲染一个使用单独灯光的3D对象, 而它又跟其它的图形世界是隔离的. 还有反射或像监视器之类的东西都需要一个额外的视口, 诸如此类. 在Mangalore中, 这个问题通过OffscreenRenderer类得到解决, 虽说比较容易使用, 但是具有一些使用限制并且需要更多事后的思考.
Nebula3提供了一个基于State和View的更加简洁的解决方案. 一个Stage就是一个图形实体的容器, 表示一个图形世界. 同一时间可能存在多个Stage, 但是它们之间是互相隔绝的. 每个实体在一个时刻只连接到了一个Stage(虽说克隆一个已有实体是一件很简单的事情). 除了简单地把实体组织到一起外, Stage的主要工作是根据它们之间的关系来加速可见性查询. 应用程序可以派生Stage的子类来实现完全不同的可见性查询方案.
一个View对象通过一个CameraEnity渲染stage到一个RenderTarget. 任何stage都可以连接任意数量的View对象. View对象可能会互相依赖(也可能是连接到不同stage的View), 所以更新一个View会首先强制更新另一个View的RenderTarget(这在一个View渲染需要使用另一个View的RenderTarget做为纹理时很方便). View对象完全实现了自己的渲染循环. 应用程序可以在View的子类中方便地实现它自己的渲染策略(如每个light一个pass VS 每个pass多个light, 渲染到cubemap, 等等).
总而言之, 一个Stage完全控制了可见性查询流程, 而一个View则完全控制了渲染流程.
图形子系统的一个最主要的工作就是根据可见性查询的结果来决定哪些实体需要被渲染. 一个可见性查询在实体间建立了一个双向的链接, 它有两种形式: 摄像机链接和灯光链接. 摄像机链接把一个摄像机和在它视景体内的模型连接到了一起. 因为链接是双向的, 所以摄像机知道所有的在它视景体范围内的模型, 而模型也知道所有可以看到它的摄像机. 灯光链接在灯光与模型之间建立了相似的关系, 一个灯光具有所有受它影响的模型的链接, 一个模型也知道所有影响它的灯光.
加速可见性查询最重要的类就是Cell类. 一个Cell是一个图形实体和子Cell的可见性容器, 它必须遵循2条简单的规则:

  1. 如果一个Cell是完全可见的, 那么它所有的图形实体和子Cell都必须可见.
  2. 如果一个Cell是完全不可见的, 那么它所有的图形实体和子Cell都必须不可见.

Cell是附属于Stage的, 它们形成了一棵有根Cell的树形层次结构. 标准的Cell支持简单的空间划分方案, 如四叉树和八叉树, 但如果像其它的可见性方案, 如portal, 就需要派生Cell的子类来实现了. 子类唯一的功能限制就是上面标出的那两条规则.
当一个图形体连接到一个Stage时, 它会被插入”接受” (通常仅仅是容纳)它的最低级的Cell中. 当更新图形实体的变换信息或改变包围体时, 它会根据需要改变在Cell层次中的位置.
Stage居住在StageBuilder类当中, 应用程序应当派生StageBuilder来创建一个Stage的初始状态(通过加入Cell和实体). Nebula3会提供一些标准的StageBuilder集合, 这应该能够满足大多数应用程序的需要了.
这只是图形子系统的一个粗略的概述. 因为当前只有一个最基本的实现, 很多细节接下来可能会有所更改.

Nebula3的代码运行在两种根本不同的方案中. 第一种方案我称之为”Fat Thread”. 一个Fat Thread在一个线程中运行一个完整的子系统(如渲染, 音频, AI, 物理, 资源管理), 并且基本上锁定在一个特定的核心上.

第二种类型的线程我叫它”Job”. 一个job是一些数据和用于处理这些数据的包装成C++对象的代码. 工作调度程序掌管了Job对象, 并且把工作分配给低负载的核心来保持它们一直处于忙碌状态.

显然, 挑战就是设计一个经过全面考虑的系统, 以保持所有的核心一直均匀地忙碌着. 这不但意味着连续的活动需要在游戏每帧的空闲时期内轮流交替, 而且要求job对象不得不事先(如每帧前)创建好, 这样才能在各种Fat Thread空闲时填充当前帧的空白.

这是我希望进行更多试验和调整的地方.

第二个挑战就是让程序员的工作尽量的简单. 一个游戏应用程序员(逻辑程序员)在任何时候都不应该关心他运行在一个多线程的环境中, 不应该担心会产生死锁或改写了其它线程的数据, 也不应该瞎搞一些临界区, 事件和信号量. 同样, 整个引擎的架构也不应该是”脆弱的”. 大部分传统的多线程代码在一定程度上都会发生紊乱, 或者忘记了临界区而打乱数据.

当线程间需要进行数据共享和通信时, 多线程就变得很棘手. 像两个临界区这样的解决方案也会导致脆弱代码问题.

从大的角度来说, Nebula3通过一个”并行Nebula”的概念解决了这个两个问题. 其思想就是运行了一个完整子系统的”Fat Thread”都有自己的最小Nebula运行库, 这个最小运行库刚好包含了这个子系统需要的部分. 因此, 如果这个运行在它自己线程中的子系统需要进行文件访问, 它会有一个跟其它Fat Thread完全分离的文件服务器(file server). 这个解决方案的优点是, 大部分Nebula中的代码都不需要知道它运行在一个多线程的环境中, 因为在fat thread之间没有数据进行共享. 运行着的每个最小Nebula内核是跟其它Nebula内核完全隔离的. 缺点就是, 重复的数据会浪费一些内存, 但是我们只是占用几KB, 而不是MB.

这些数据冗余消除了细密的锁定, 并且解决把程序员从思考每一行代码的多线程安全性中解放了出来.

当然, 从某种意义上说Fat Thread间的通信是肯定会发生的, 要不然这整个思想就没有意义了. 方法就是建立一个且只有一个的标准通信系统, 并且保证这个通信系统是可靠而快速的. 这就是消息系统的由来. 要跟一个Fat Thread通信的话只有发送一个消息给它. 消息是一个简单的C++对象, 它包含了一些带有get/set方法的数据. 通过这个标准的通信手段, 实际上只有消息子系统才需要是线程安全的(同样, 访问跟消息相关的资源时, 如内存缓冲区, 必须受到约束, 因们它们代表了共享数据). (xoyojank: 我说咋那么多Message…)

这样虽然解决了Fat Thread方案中大多数的多线程问题, 但没有解决Job对象的任何事情. Nebula3很有可能需要约束一个Job对象能做什么和不能做什么. 最直接的行为就是限制job做内存缓冲区的计算. 那样的话, job中就不能存在复杂的运行库(不能文件I/O, 不能访问渲染等等). 如果这样还不够的话, 必须定义一个”job运行时环境”, 就像Fat Thread中的那样. 因为一个job不会发起它自己的线程, 而且还会被调度到一个已经存在的线程池中. 就这个方面来说, 这不存在什么问题.

到现在为止(xoyojank: 2007/01/21, 最新版本已经实现了多数子系统的多线程化), 只有IO子系统作为概念证明在Fat Thread中得到实现, 并且它运行得很今人满意. 在做传统的同步IO工作时, 一个Nebula3程序可以直接调用本地线程的IO子系统. 所以像列出文件夹的内容或删除一个文件, 只会调用一个简单的C++方法. 对于异步IO工作, 定义了一些常见的IO操作消息(如ReadStream, WriteStream, CopyFile, DeleteFile, 等等). 进行异步IO只需要几行代码: 创建一个消息对象, 填充数据, 并发送这个消息到一个IOInterface单件. 如果必要的话, 这可能会需要等待和轮询异步操作.

这样的好处就是, 整个IO子系统没有一行多线程意义上的代码, 因为各个在不同的Fat Thread中的IO子系统是完全隔离的(当然, 同步肯定会发生在一些IO操作上, 但那都留给操作系统了).

跟N2比起来, N3的资源子系统更加开放, 给予了程序员对资源的创建和管理更多的控制. 

Nebula3的资源有下面向个属性:

  • 包装了一些其它Nebula子系统需要的数据
  • 可以用ResourceId共享
  • 可以在任何时候加载(初始化)和卸载
  • 可以同步或异步加载

例如典型的图形资源有网格和纹理, 但资源子系统并不局限于图形资源. 

资源子系统有两个操作层次( 可能以后会把他们放入两个不同的命名空间, 现在他们都是在Resources命名空间下 ):

低层提供了真正的资源对象, 处理资源的共享, 加载和(次要的)保存. 低层的资源类有:

  • ResourceId
  • Resource
  • ResourceLoader
  • ResourceSaver
  • SharedResourceServer. 

高层资源子系统提供了资源管理, 这意味着根据用户的反馈动态的加载和卸载资源. 高层资源子系统的类有:

  • ResourceProxy (又名: ManagedResource)
  • ResourceProxyServer (又名: ResourceManager)
  • ResourceMapper

下面说明资源子系统的各个类是怎么协同工作的:

一个ResourceId是一个唯一的资源标识符. ResourceId用来共享和定位磁盘上的数据(或者资源保存在哪). ResouceId是一些原子字符串(string atoms). Atom是一个常量字符串的唯一32-bit标识符, 这可以大大加快拷贝和比较, 并且可以减少内存占用, 因为标识符字符串只保存一份. 为了定位磁盘上的数据, ResourceId常常分解成一个合法的URL(例如一个ResourceId “texture:materials/granite.dds”, 会在运行时被分解成”file:///C:/Programme/[AppName]/export/textures/materials/granite.dds”. 

一个Resource对象实际上是资源数据的容器. 像纹理和网格这样特定的资源类型都是Resource类的子类, 并且实现了特定的接口. Resource子类通常都是平台相关的(如D3D9Texture), 但是通过有条件的类型定义使其变成平台无关的. 并不像Nebula2那样, 资源对象并不知道怎样去组织, 加载或保存自己. 取而代之的是, 一个合适的ResourceLoader或ResourceSaver必须附属于Resource对象. 因为Nebula程序很少输出数据, ResourceSaver只 是为了完整性而存在的. 换句话说, ResourceLoader是必须的, 因为他们是启用Resource对象的唯一途径. ResourceLoader具有整个资源装载过程的完全控制. 它们可以是平台相关的, 而且也许会依赖于相关联的特定平台的Resource类. 这使得程序员可以对资源的装载过程相比Nebula2有更多的控制. 典型的资源加载类有StreadTextureLoader, MemoryVertexBufferLoader和MemoryIndexBufferLoader(从内存中加载顶点缓存和索引缓存).

Resource类也提供了一个共同的接口用来同步和异步的资源加载. 同步加载可以这样做:

  1. res-> SetResourceId("tex:system/white.dds");
  2. res-> SetLoader(StreamTextureLoader::Create());
  3. res-> SetAsyncEnabled(false)
  4. res-> Load()
  5. if (res-> IsValid()) ... 这时资源加载已经成功了, 否则LoadFailed会返回true.

异步资源加载也很相似:

  1. res->SetResourceId("tex:system/white.dds");
  2. res->SetLoader(StreamTextureLoader::Create());
  3. res->SetAsyncEnabled(true);
  4. res->Load();
  5. 资源这时进入等待状态...
  6. 只要 IsPending() return true, 就要重复地调用Load()... 当然真正的程序会在这时做一些其他的事情
  7. 接下来的某个调用Load()后时刻, 资源的状态要么是Valid(资源已经准备好了), Failed(资源加载失败)或者Cancelled(等待中的资源被取消加载了)

一个应用程序甚至是Nebula3的渲染代码通常都不需要关心这些, 因为资源管理层会处理他们, 并把异步加载的这些细节隐藏到资源代理后面. 

SharedResourceServer单件通过ResourceId来共享资源. 通过SharedResourceServer创建资源确保了每个资源只在内存中加载了一份, 而不管客户端的数目. 如果客户端的数目降低到了0, 资源会被自动卸载(这并不是合适的资源管理, 而应该是ResourceProxyServer应该关心的). 资源共享完全可以直接通过标准的Nebula3的创建机制来绕过. 

ResourceProxy(或ManagedResource)是对于实际资源对象的资源管理包装. 它的思想是包含的资源对象会受资源用途反馈的控制. 例如, 一个纹理代理会在被请求的纹理在后台加载时提供一个占位纹理, 屏幕上所有使用这个资源的物体都很小的话会被提供一张低分辨率的纹理, 一个X帧没有被绘制的纹理会被卸载, 等等. 

ResourceProxyServer(或ResourceManager)单件是资源管理系统的前端. 除了管理附属于它的ResourceMapper的工作外, 它还是ResourceProxy的工厂, 并且把ResourceMapper跟Resource类型联系到了一起. 

ResourceMapper是一个有趣的东西. 一个ResourceMapper跟一种资源类型(如纹理或网格)相关联, 并被应用程序依附到ResourceProxyServer. 它负责从渲染代码的使用反馈来加载/卸载资源. ResourceMapper的子类可以实现不同的资源管理策略, 也可以通过派生特定的ResourceMapper和ResourceLoader来创建一个完全定制的平台和应用相关的资源管理方案. 目标是显而易见的, Nebula3提供了一些好用的ResourceMapper来加载需要的任何东西. 

资源使用反馈是由渲染代码写入ResourceProxy对象的, 而且应该包含这个资源的一些信息:是否会在不久后用到, 是否可见, 并估计物体占用的屏幕空间大小. 特定的反馈依赖于ResourceProxy的子类, ResourceProxy中没有公有的反馈方法. 

基于资源的使用反馈, 一个ResourceMapper应该实现下面的一些操作(这取决于具体的mapper):

  • Load: 根据level-of-detail异步加载资源(如跳过不需要的高分辨率mipmap层次)
  • Unload: 完全卸载资源, 释放珍贵的内存
  • Upgrade: 提高已加载资源的level-of-detail(如加载高分辨率的mipmap层次纹理)
  • Degrade: 降低已加载资源的level-of-detail(如跟上面相反的情况)

Nebula2的脚本系统实现了一个面向C++的脚本接口, 它把脚本命令直接映射到了C++方法. 从技术角度来说, 这是一个简捷的思路, 但是对于需要把游戏逻辑和行为脚本化的关卡设计师来说, Nebula2的脚本系统太底层和透明了.

关卡逻辑脚本一般来说构架于比C++接口更高级的层次上, 直接把脚本命令映射到C++方法会把脚本层次弄得错综复杂. Bug甚至会比同样的C++代码更多, 因为脚本语言一般缺少强类型检查和”编译时”的错误检测, 所以在本应在C++编译时发现的Bug会在脚本运行时才发现(这对于不同的脚本语言有所不同). 这是我们从Project Nomads中得出的经验, 它就是用Nebula2的脚本系统驱动的.

所以教训就是: 把你的脚本架构在一个正确的抽象层上, 并且: 把你的C++接口映射到一种脚本语言是没有意义的, 因为那样你不如从一开始直接用C++来做这些东西.

相应的, 新的Nebula3脚本哲学为关卡设计师提供一些在”正确的抽象层”的(大多是限于特定应用)积木. 当然, “正解的抽象层” 很难来定义, 因为这要在灵活性跟易用性之间找到一个平衡( 例如, 一个”Pickup” 命令是不是应该把角色移动到拾取范围内呢? )

除了太底层以外, Nebula2的脚本系统也有一些其它的缺点:

  • C++方法必须遵循可以转化为脚本的原则( 只有简单数据类型才可以做为参数 )
  • 给程序员带来麻烦. 每个C++方法都需要额外的脚本接口代码( 每个方法几行 )
  • 只有派生自nRoot的类可以脚本化
  • 对象关联到脚本系统( 思路简单, 但是增加的依赖性会使重构非常困难 )

下面是Nebual3的底层脚本的大概:

  • 脚本系统的基础是Script::Command类
  • Script::Command是一个完全脚本语言无关的, 它包含了一个命令名称, 一些输入参数的集合还有一些输出参数的集合.
  • 一个新的脚本命令通过派生Script::Comand类来创建, 脚本的C++功能代码可以写入子类的OnExecute()方法
  • ScriptServer类是脚本系统中仅有一个脚本语言相关的类, 它会把Command对象注册成新的脚本命令, 并且把命令参数在脚本语言和C-API之间做翻译.

这个观念比Nebula2更为简单, 最重要的是, 它不会跟Nebula3的其它部分交织在一起. 甚至可以通过改变一个#define来编译一个没有脚本支持的Nebula3.

当然, 书写脚本命令的C++代码跟Nebula2一样烦人, 这是NIDL的由来. NIDL的是全称是”Nebula Interface Definition Language”. 基本思想是通过为脚本命令定义一个简单的XML schema并把XML描述编译成派生了Script::Command的C++代码, 来尽量减少书写脚本命令的重复性工作.

对于一个脚本命令必不可少的信息有:

  • 命令的名称
  • 输入参数的类型和名称
  • 输出参数的类型和名称
  • 对应的C++代码( 通常只有一行 )

还有一些非必须, 但是可以带来便利性的信息:

  • 关于命令的作用和每个参数的意义的描述, 这可以作为运行时的帮助系统
  • 一个唯一的FourCC(四字符码), 可以更快的通过二进制通道传输

大部分的脚本命令翻译成了大约7行的XML-NIDL代码. 这些XML文件再用”nidlc”NIDL编译器工具编译为C++代码. 这个预处理是VisualStudio完全集成的, 所以使用NIDL文件不会为程序员代来任何困难.

为了减少乱七八糟的文件(编译生成的), 相关的脚本命令被组织到一个叫作库的集合中. 一个库由一个单独的NIDL-XML文件表示, 并且它只会被翻译一个C++头文件和一个C++源代码文件. 脚本库可以在程序启动时注册到ScriptServer, 所以如果你的应用程序不需要脚本访问文件的话, 仅仅不注册IO脚本库就可以了. 这会减小可执行文件的体积, 因为连接器会把没有用到的脚本库丢弃掉.

最后, Nebula3放弃了TCL作为标准的脚本语言, 而采用了运行时代码更加小巧的LUA. LUA已经成为游戏脚本的准规范, 这也使得寻找熟练的LUA关卡设计师更加容易.

N3的场景管理最为核心的一个类是GrphicsServer, 它包含一些"stage"和"View".

Stage把图形实体(模型, 摄像机, 灯光)进行分类渲染. 它的主要工作是在连接的图形实体间加速可见性查询. 不同的可见性查询由不同的Stage子类来实现. N3会提供了一些不同用途的Stage子类, 但你也可以根据程序需要自己来实现可见性查询机制.

可见性查询适用于这些实体:

  • Camera->Light: 查找对于指定摄像机可见的所有灯光
  • Camera->Model: 查找对于指定摄像机可见的所有模型
  • Light->MOdel: 查找被指定光源照射到的所有模型

这些可见性查询在图形实体间建立了一些所谓的"可见性链接", 再利用低级的渲染子系统来加速渲染.

要渲染一个Stage的内容, 需要至少一个View对象. 一个View对象通过绑定一个摄像机实体把Stage渲染到一个render target. 可以并存任意数目的View, 也可能都被绑定到任意Stage. 此外, View对象之间可能存在依赖关系(结果就是一个View对象会在渲染自身时首先请求它所依赖的View对象).

图形实体表示了可以被连接到Stage的一个最小图形对象, 它分为以下三种:

  • ModelEntity: 一个可见的模型实例
  • LightEntity: 一个光源
  • CameraEntity: 一个摄像机

可见性查询使图形实体间形成一种双向的链接关系. 一个CameraEntity链接到所有对于这个摄像机来说可见的ModelEntity和LightEntity. 因为可见性链接是双向的, 所以ModelEntity和LightEntity也知道它们对于哪个摄像机可见. LightEntity有它们影响到的ModelEntity的链接, ModelEntity也知道它们被哪个光源照亮.

==========================================================

N3 画个东西真简单, 想画个模型, 创建出来设置一下位置扔给Stage就好了

  1. this->model = ModelEntity::Create();
  2. this->model->SetTransform(matrix44::translation(0.0f, 3.0f, 0.0f));
  3. this->model->SetResourceId(ResourceId("mdl:examples/eagle.n2"));
  4. this->stage->AttachEntity(this->model.upcast<GraphicsEntity>());

模型是黑的? 再往场景里扔个灯就好了:

  1. // attach a light entity
  2.             matrix44 lightTransform = matrix44::multiply(matrix44::scaling(100.0f, 100.0f, 100.0f), matrix44::lookatrh(point(20.0f, 20.0f, 20.0f), point::origin(), vector::upvec()));
  3. this->lightEntity = SpotLightEntity::Create();
  4. this->lightEntity->SetCastShadows(true);
  5. this->lightEntity->SetTransform(lightTransform);
  6. this->lightEntity->SetColor(float4(4.0f, 2.0f, 1.0f, 1.0f));        
  7. this->stage->AttachEntity(this->lightEntity.upcast<GraphicsEntity>());

想控制的话, 再扔个摄像机进去就OK了.......

  1.         GraphicsServer* gfxServer = GraphicsServer::Instance();
  2. // setup the camera util object
  3. this->mayaCameraUtil.Setup(point(0.0f, 0.0f, 0.0f), point(0.0f, 0.0f, 10.0f), vector(0.0f, 1.0f, 0.0f));
  4. // setup a stage
  5. this->stage = gfxServer->CreateStage(StringAtom("DefaultStage"), SimpleStageBuilder::Create());
  6. // attach a camera to the stage
  7. this->cameraEntity = CameraEntity::Create();
  8.         cameraEntity->SetTransform(this->mayaCameraUtil.GetCameraTransform());
  9. this->stage->AttachEntity(cameraEntity.upcast<GraphicsEntity>());
  10. // setup a default view
  11. this->view = gfxServer->CreateView(View::RTTI, StringAtom("DefaultView"), true);
  12. this->view->SetStage(this->stage);        
  13. this->view->SetFrameShader(FrameServer::Instance()->GetFrameShaderByName(ResourceId(DEFAULT_FRAMESHADER_NAME)));
  14. this->view->SetCameraEntity(cameraEntity);

别忘了处理输入事件:

可以参考ViewerApplication::OnProcessInput().

相对于其他的子系统来说, 输入系统是比较简单的. 很多游戏根本就没有对这一块进行封装, 而直接采用了Win32的消息机制.

不过经过封装的输入系统使用起来很方便, 呵呵.

N3中有三种输入设备, 键盘, 鼠标, 手柄. 分别是基于Win32消息, DirectInput, XInput实现的. 这里有一个继承图能够很好的说明输入系统的组织结构:

基本的消息处理机制是这样的一个流程:

InputServer里有默认的一个键盘, 一个鼠标, 一个手柄的"handler", 在每帧开始时InputServer会检测当前的输入消息,  得到一个InputEvent, 由相应的InputHandler来处理.  各个InputHandler都保存着当前帧各种输入状态的缓存(如鼠标左键是否按下), 因此, 在程序运行过程中, 我们只要在绘制结束前检测各个InputHandler的状态就相当于知道当前用户是怎样输入的了.

一般只需要关心这么几个函数就够了:

  1. ////////////////////// Mouse////////////////////////////
  2. /// return true if button is currently pressed
  3. bool ButtonPressed(Input::MouseButton::Code btn) const;
  4. /// return true if button was down at least once in current frame
  5. bool ButtonDown(Input::MouseButton::Code btn) const;
  6. /// return true if button was up at least once in current frame
  7. bool ButtonUp(Input::MouseButton::Code btn) const;
  8. /// return true if a button has been double clicked
  9. bool ButtonDoubleClicked(Input::MouseButton::Code btn) const;
  10. /// return true if mouse wheel rotated forward
  11. bool WheelForward() const;
  12. /// return true if mouse wheel rotated backward
  13. bool WheelBackward() const;
  14. /// get current absolute mouse position (in pixels)
  15. const Math::float2& GetPixelPosition() const;
  16. /// get current screen space mouse position (0.0 .. 1.0)
  17. const Math::float2& GetScreenPosition() const;
  18. /// get mouse movement
  19. const Math::float2& GetMovement() const;
  1. //////////////////////Keyboard//////////////////////
  2. /// return true if a key is currently pressed
  3. bool KeyPressed(Input::Key::Code keyCode) const;
  4. /// return true if key was down at least once in current frame
  5. bool KeyDown(Input::Key::Code keyCode) const;
  6. /// return true if key was up at least once in current frame
  7. bool KeyUp(Input::Key::Code keyCode) const;
  8. /// get character input in current frame
  9. const Util::String& GetCharInput() const;

GamePad先略过, 原理相同

测试例子, 在上一次的代码中添加一段:

  1. void OnRenderFrame()
  2.     {
  3. if (this->inputServer->GetDefaultMouse()->ButtonDown(MouseButton::LeftButton))
  4.         {
  5.             MessageBoxA(this->displayDevice->GetHwnd(), "Left Button Down", NULL, 0);
  6.         }
  7. //...//
  8.     }

效果:

概述

  • 一些为了兼容Nebula2的代码所做的修改, 主要是一些宏的名字受到影响(DeclareClass -> __DeclareClass, ImplementSingleton -> __ImplementSingleton etc...)
  • 着手删除#ifndef/#define/#endif 这些防止重复include的宏, 因为几乎所有的编译器(VStudio, GCC, Codewarrior) 都支持#pragma once 
  • 把同的样Win32 和Xbox360 代码移动到一个共同的Win360 命名空间来消除代码冗余
  • 加入了一个新的Toolkit层, 它包含了一些导出工具和辅助类
  • 加入和整理了一些 Doxygen(文档) 页面

编译系统

  • 重新组织了 VStudio解决方案的结构, 让所有的依赖工程都在一个解决方案中, 这样就不用再同时打开多个VStudio了
  • 现在可以通过.epk编译脚本来导入VStudio工程(对于不在Nebula3 SDK目录下的工程很有用)
  • 新的"projectinfo.xml" 文件为一些有用的导出工具定义了工程和平台特有的属性
  • 把 export.zip 档案文件分割到一个独立的平台无关文件和几个特定平台的文件 (export.zip 包含所有平台无关的文件, export_win32.zip, export_xbox360.zip, export_wii.zip 包含特定平台的文件)
  • 加入一个统一的多平台支持到 asset-pipeline (如 "msbuild /p:Platform=xbox360" 来生成XBOX360的东西)
  • 一个新的命令行生成工具 (有代码):
    • audiobatcher3.exe (包装了音频导出)
    • texturebatcher3.exe (包装了纹理导出)
    • shaderbatcher3.exe (包装了 shader 编译)
    • buildresdict.exe (生成资源词典文件)
    • 这些工具大部分只是调用其它的生成工具(像xactbld3.exe, nvdxt.exe, 还有其它命令下的生成工具)
  • 注意公开的N3-SDK因为法律原因只包含Win32平台的支持

基础层

  • 修正Core::RefCounted 和Util::Proxy 引用计数线程不安全的BUG
  • 加入 WeakPtr<> 类用于更好地处理环形引用
  • 在 Ptr<>中加入类型转换的方法
  • 简化System::ByteOrder 类接口
  • 加入平台相关的面向任务的"virtual CPU core id" (如 MainThreadCode, RenderThreadCore, 等等...)
  • 加入一个 System::SystemInfo 类
  • 加入 Threading::ThreadId 类型和 Threading::Thread::GetMyThreadId()静态方法
  • 现在可以在VStudio调试器和其它的高度工具中看到线程的固有名称了
  • SetThreadIdealProcessor() 现在用于在Win32平台上把线程分配给可用CPU核心
  • 新的线程子系统的HTTP 调试页面(现在只列出Nebula3的活动线程)
  • MiniDump支持: 崩溃, n_assert()和 n_error() 现在在Win32平台上会生成 MiniDump 文件
  • 新的 Debug 子系统用于代码分析:
    • 提供 DebugTimer 和 DebugCounter 对象
    • HTTP 调试页面允许在运行时检查DebugTimers和 DebugCounters
  • 新的Memory::MemoryPool 类来分配同样大小的内存块(加快分配速度和减少内存碎片)
  • Math::matrix44在中的一些新的和改名的方法
  • Http 子系统现在运行在它自己的线程里
  • 把 SVG 支持加入到 Http 子系统(Http::SvgPageWriter 和Http::SvgLineChartWriter) (xoyojank:难道是Scalable Vector Graphics?这样的话可以输出图表了)
  • 加入 IO::ExcelXMLReader 流读取类, 允许读取XML模式的MS Excel电子表格文件
  • 在Messaging::AsyncPort加入行为方式, 定义了处理线程怎样去等待新的消息:
    • WaitForMessage: 在消息到达前一直阻塞
    • WaitForMessageOrTimeOut: 在消息到达或超时前一直阻塞
    • DoNotWait: 不等待消息
  • 加入 Remote 子系统, 允许通过TCP/IP连接远程控制N3应用程序

渲染层

  • 把渲染移动了它自己的线程 (InternalGraphics子系统在渲染线程这边,  Graphics 前端子系统在主线程这边)
  • 加入了 CoreAnimation 和 Animation 子系统 (构造中)
  • 为简单的用户界面加入了UI子系统 (构造中) (xoyojank: 这个不错^_^)
  • 加入CoreAudio和 Audio 子系统(构造中):
    • CoreAudio 是后台的, 运行在自己的线程里
    • Audio 是前台的"客户端", 运行在主线程里 (或者其它任何线程)
    • 围绕XACT的概念设计
    • 提供 XACT 的包装实现
  • 加入 CoreGraphics::TextRenderer 和 CoreGraphics::ShapeRenderer 类, 打算用于渲染调试信息
  • 加入调试渲染子系统(现在在Debug命名空间下)
  • Frame 子系统: FramePostEffect 现也也许会包含 FrameBatch
  • Input 子系统: 断开 XInput 游戏手柄接口现在对于连接中的设备每隔0.5秒才检测一次
  • Resources 子系统: 加入 ResourceAllocator/ResourceLump 系统为Console平台真正的资源流做准备

应用层和插件:

  • 删除了 CoreFeature (这东西不得不进入GameApplication类来阻止鸡生蛋问题)
  • 加入 NetworkFeature (构造中)
  • 加入 UIFeature (构造中)
  • 加入 CoreNetwork 和 Multiplayer 插件(RakNet的包装)

可能是还在开发当中的缘故, 我感觉Nebula3中的lua脚本系统不是很完善. 所有的调用都是封装成Command来执行的, 并不像LuaBind那样直接绑定到C++类对象; 而且, 对于C++调用脚本的接口也不是很方便, 只有一个Eval()来执行一个字符串. 如果要实际进行应用的话, 我想最好是自己扩展一下, 这里有一篇不错的文章: Integrating Lua into C++. 当然, 对于需求更高的用户来说, 可以选择使用LuaBind等第三方库来整合脚本系统.

Command(命令)
可以这么说, 脚本中调用的, 都是一个个的Command. 一个新的Command定义了一个脚本语言独立的新的脚本命令, 你可以通过派生一个Command的子类并注册到脚本服务器来实现. 也就是说, 新的命令不依赖于你具体使用的脚本系统, 可以是lua, 也可以是python等等.

view plaincopy to clipboardprint?

  1. class Print : public Scripting::Command   
  2. {   
  3.     DeclareClass(Print);   
  4. public:   
  5. virtual void OnRegister();   
  6. virtual bool OnExecute();   
  7. virtual Util::String GetHelp() const;   
  8. private:   
  9. void Callback(const Util::String& str);   
  10. };<PRE></PRE> 
class Print : public Scripting::Command

{

DeclareClass(Print);

public:

virtual void OnRegister();

virtual bool OnExecute();

virtual Util::String GetHelp() const;

private:

void Callback(const Util::String&amp; str);

};

ScriptServer(脚本服务器)
ScriptServer是语言无双的, 也就是说你可以自己派生一个相应语言的子来来支持一种脚本言. Nebula3里已经实现了一个LuaServer, 不过个感觉没有LuaBind方便. 所有的脚本执行都是通过LuaServer::Eval(const String& str)来完成的. 脚本要调用C++代码的话, 需要封装一个Command, 然后用LuaServer::RegisterCommand()来注册就可以用了. 具体可以参考Command命名空间里的相关代码.

view plaincopy to clipboardprint?

  1. scriptServer->RegisterCommand("print", Print::Create());<PRE></PRE> 
    scriptServer->RegisterCommand("print", Print::Create());

应用实例
其实App::ConsoleApplication里就有LuaServer, 并且已经注册了一些IO命名. 我们派生一个从命令行读取脚本命令执行的来做测试:

view plaincopy to clipboardprint?

  1. class ScripTestApp : public App::ConsoleApplication   
  2. {   
  3. public:   
  4. ScripTestApp(void);   
  5. /// open the application
  6. virtual bool Open();   
  7. /// run the application, return when user wants to exit
  8. virtual void Run();   
  9. };   
  10. ScripTestApp::ScripTestApp(void)   
  11. {   
  12. }   
  13. bool ScripTestApp::Open()   
  14. {   
  15. if (ConsoleApplication::Open())   
  16. {   
  17. return true;   
  18. }   
  19. return false;   
  20. }   
  21. void ScripTestApp::Run()   
  22. {   
  23. Util::String input;   
  24. while (true)   
  25. {   
  26.   input = IO::Console::Instance()->GetInput();   
  27. if (!input.IsEmpty())   
  28.   {   
  29. this->scriptServer->Eval(input);   
  30.   }   
  31. }   
  32. }<PRE></PRE> 
class ScripTestApp : public App::ConsoleApplication

{

public:

ScripTestApp(void);

/// open the application

virtual bool Open();

/// run the application, return when user wants to exit

virtual void Run();

};

ScripTestApp::ScripTestApp(void)

{

}

bool ScripTestApp::Open()

{

if (ConsoleApplication::Open())

{

return true;

}

return false;

}

void ScripTestApp::Run()

{

Util::String input;

while (true)

{

input = IO::Console::Instance()->GetInput();

if (!input.IsEmpty())

{

this->scriptServer->Eval(input);

}

}

}

运行结果:

Nebula3的网络子系统提供了基于TCP协议的简单C/S通信模式. 它并没有打算做成大厅,会话管理还有玩家数据同步的面向游戏的高级通信. 这些以后会在更高层的Nebula3子系统中出现.

使用IP地址

  一个IpAddress对象通过主机名字或TCP/IP地址加一个端口号定义了一个通信端点. IpAddress对象可以通过多数方式建立:

1: // 从 TCP/IP 地址和端口号:

2: IpAddress ipAddr("192.168.0.2",1234);

3:

4: // 从主机名和端口号:

5: IpAddress ipAddr("www.radonlabs.de",1234);

6:

7: // 从本机(127.0.0.1) 和端口号:

8: IpAddress ipAddr("localhost",1234);

9:

10: // 从"any" 地址 (0.0.0.0) 和端口号:

11: IpAddress ipAddr("any",1234);

12:

13: // 从广播地址 (255.255.255.255) 和端口号:

14: IpAddress ipAddr("broadcast",1234);

15:

16: // 从主机的第一个合法网络适配器的地址和端口号

17: IpAddress ipAddr("self",1234);

18:

19: // 从主机的第一个连接到互联网的网络适配器的地址和端口号:

20: IpAddress ipAddr("insetself",1234);

21:

22: // 从一个定义了主机名的URI和端口号:

23: IpAddress ipAddr(IO::URI("http://www.radonlabs.de:2100"));

  一个IpAddress对象可以用于从主机名查找TCP/IP地址:

1: IpAddress ipAddr("www.radonlabs.de",0);

2: String numericalAddr = ipAddr.GetHostAddr();

建立一个客户端/服务器系统

  网络子系统用TcpServer和TcpClient类实现了一个易用的基于TCP协议的C/S系统. 一个TcpServer可以为任意数量的TcpClient服务.

  建立一个服务器可以这么做:

1: using namespace Net;

2:

3: Ptr<TcpServer> tcpServer = TcpServer::Create();

4: tcpServer->SetAddress(IpAddress("any",2352));

5: if(tcpServer->Open())

6: {

7: // TcpServer successfully opened

8: }

  这样会建立一个在2352端口监听客户端连接请求的服务器.

  为了跟TcpServer通信, 需要在客户端建立一个TcpClient对象:

1: using namespace Net;

2:

3: Ptr<TcpClient> tcpClient = TcpClient::Create();

4: tcpClient->SetBlocking(false);

5: tcpClient->SetAddress(IpAddress("localhost",2352));

6: TcpClient::Result res = tcpClient->Connect();

  这里假设服务端和客户端运行在同一台机器上(因为客户端连接到了”localhost”).

  像上面那样非阻塞的情况, Connect()方法不是返回TcpClient::Success(这意味着连接建立好了)就是TcpClient::Connecting, 如果这样的话, 应用程序需要继续调用Connect()方法. 如果连接错误, 会返回一个TcpClient::Error的返回值.

  如果是阻塞的, Connect()方法直到连接建立(结果是TcpClient::Success)或发生错误才会返回.

  注意:一个交互式应用程序不应该在网络通信时阻塞, 而应不断地为用户提供反馈.

  一旦连接建立, 服务端会为每个客户机建立一个TcpClientConnection对象. TcpClientConnection在服务器上表示客户机, 并且负责从客户机收发数据.

  要进行接收和发送数据的话, 需使用IO::Stream对象. 在通信流上连接IO::StreamReader和IO::StreamWriter对象后, 从流中编码和解码数据是一件非常容易的事情.

  注意:发送数据并不是即时的, 而是在Send()方法被调用之前会一直保存在发送流当中.

  要客户端给服务器发送一些文本数据话, 只要从发送流获取一个指针, 向其中写入数据后调用Send()方法就可以了:

1: using namespace Net;

2: using namespace IO;

3:

4: // obtain pointer to client's send stream and attach a TextWriter

5: const Ptr<Stream>& sendStream = tcpClient->GetSendStream();

6: Ptr<TextWriter> textWriter = TextWriter::Create();

7: textWriter->SetStream(sendStream);

8: textWriter->Open())

9: textWriter->WriteString("Hello Server");

10: textWriter->Close();

11:

12: // send off the data to the server

13: if(this->tcpClient->Send())

14: {

15: // data has been sent

16: }

  在服务器端接收客户端数据, 应用程序需要要频繁地(每帧一次)缓存带有客户羰数据的TcpClientConnection. 可能不只一个TcpClientConnection在等待处理, 因此处理循环应该像这样:

1: // get array of client connections which received data since the last time

2: Array<Ptr<TcpClientConnection>> recvConns = tcpServer->Recv();

3: IndexT i;

4: for(i =0; i < recvConns.Size(); i++)

5: {

6: // get receive stream from current connection, attach a text reader and read content

7:      Ptr<TextReader> textReader = TextReader::Create();

8:      textReader->SetStream(recvConns[i]->GetRecvStream());

9:      textReader->Open();

10:      String str = textReader->ReadString();

11:      textReader->Close();

12:

13: // process received string and send response back to client

14: // create a TextWriter and attach it to the send stream of the client connection

15:      Ptr<TextWriter> textWriter = TextWriter::Create();

16:      textWriter->SetStream(recvConns[i]->GetSendStream());

17:      textWriter->Open();

18:      textWriter->WriteString("Hello Client");

19:      textWriter->Close();

20:

21: // finally send the response back to the client

22:      recvConns[i]->Send();

23: }

  在客户端获得服务器的应答, 调用TcpClient::Recv()方法会在数据到达之前一直阻塞(在阻塞模式下), 或者立即返回(在非阻塞模式下), 并在有服务器数据时返回true:

1: // check if data is available from the server

2: if(tcpClient->Recv())

3: {

4: // yep, data is available, get the recv stream and read the data from it

5: const Ptr<Stream>& recvStream = tcpClient->GetRecvStream();

6:      Ptr<TextReader> textReader = TextReader::Create();

7:      textReader->SetStream(recvStream);

8:      textReader->Open();

9:      String responseString = textReader->ReadString();

10:      n_printf("The server said: %s\n", responseString.AsCharPtr());

11:      textReader->Close();

12: }

  客户端也应该通过调用IsConnected()访求检查连接是否有效. 如果因为某些原因使连接断开, 这个方法会返回false.

  注意:

TcpServer和TcpClient并没有为能够跟不相关的客户端和服务器端而实现一个潜在的通信协议(例如, 一个TcpServer可以跟标准的Web浏览器客户端一起工作, 还有一个TcpClient类可以跟一个标准的HTTP服务器通信).

  现实世界的情况是, 一个应用程序应该实现自己的健壮的通信协议, 它至少会编码负载数据的长度. 如果负载比最大包大小还要大, 数据会以多个包发送并在客户端接收. 客户端应该把数据解码成一个完整的消息, 否则需要等待消息的数据接收完毕.

字节次序问题

  服务器和客户端可能运行在不同字节次序的的CPU上. 如果二进制数据通过网络发送, 数据必需转换成两个客户端都一致的”网络字节顺序”. Nebula3在IO::BinaryReader和IO::BinaryWriter类中提供字节顺序的自动转换. 只需要简单地调用下面的方法在网络通信流上读写就可以了:

1: binaryReader->SetStreamByteOrder(System::ByteOrder::Network);

2: binaryWriter->SetStreamByteOrder(System::ByteOrder::Network);

Socket

  网络子系统提供了一个把传统socket函数包装成C++接口的Socket类. 一般情况下应用程序不直接使用Socket类, 而是使用更高级的像TcpServer这样的类. 但也不是不可能在有的时候直接使用socket函数比Socket类更方便.

上一次熟悉了IO系统后, 写个程序来练练手.

正好这次看到App命名空间, 正好熟悉一下ConsoleApplication的用法. 因为Nebula3内置了ZipFileSystem, 但不支持压缩, 只支持解压缩, 就试着写了一个命令行的unzip.exe, 算是对之前所学的一个总结.

没想解压缩就像拷贝文件一样简单! 因为当zip文件挂载到IO系统后, 可以像本地文件一样使用其中的文件, 呵呵.

 1: /********************************************************************

2: created: 2008/07/08

3: created: 8:7:2008 16:15

4: filename: UnZip.cpp

5: author: xoyojank

6:

7: purpose: zip file extract test

8: *********************************************************************/

9:

10: #include “stdneb.h”

11: #include “UnZipApp.h”

12:

13: using namespace Util;

14:

15: //——————————————————————————

16: /**

17: */

18: void __cdecl

19: main(int argc, const char** argv)

20: {

21: CmdLineArgs args(argc, argv);

22: UnZipApp app;

23: app.SetCompanyName(“Xoyojank”);

24: app.SetAppName(“UnZip”);

25: app.SetCmdLineArgs(args);

26: if (app.Open())

27: {

28: app.Run();

29: app.Close();

30: }

31: system(“pause”);

32: app.Exit();

33: }

 1: /********************************************************************

2: created: 2008/07/08

3: created: 8:7:2008 16:16

4: filename: UnZipApp.h

5: author: xoyojank

6:

7: purpose: UnZip Application

8: *********************************************************************/

9: #pragma once

10: #include "stdneb.h"

11: #include "app/consoleapplication.h"

12:

13: class UnZipApp : public App::ConsoleApplication

14: {

15: public:

16: UnZipApp(void);

17:

18: /// open the application

19: virtual bool Open();

20: /// run the application, return when user wants to exit

21: virtual void Run();

22:

23: private:

24: /// a recursion method to unzip the files under "dir"

25: void UnZipDir(Util::String& dir);

26: private:

27: Util::String zipFileName;

28: Util::String sourcePath;

29: Util::String targetPath;

30: };

 1: /********************************************************************

2: created: 2008/07/08

3: created: 8:7:2008 16:19

4: filename: UnZipApp.cpp

5: author: xoyojank

6:

7: purpose: UnZip Application

8: *********************************************************************/

9: #include "UnZipApp.h"

10:

11:

12: UnZipApp::UnZipApp(void)

13: {

14: }

15:

16: bool UnZipApp::Open()

17: {

18: if (ConsoleApplication::Open())

19: {

20: // help info

21: if (this->args.HasArg("-help"))

22: {

23: n_printf("-file: the .zip file to unzip.\n");

24: n_printf("-path: where are the files unzip to, if this args is omitted, the file will be unzip into current directory.\n");

25: return false;

26: }

27:

28: Util::String zipFile;

29: zipFile = this->args.GetString("-file");

30: // current .exe directory

31: this->sourcePath = Util::String("bin:") + zipFile;

32: bool fileValid = this->ioServer->MountZipArchive(this->sourcePath);

33: if (!fileValid)

34: {

35: // absolute path

36: this->sourcePath = Util::String("file:///") + zipFile;

37: fileValid = this->ioServer->MountZipArchive(this->sourcePath);

38: if (!fileValid)

39: {

40: n_error("Cannot open zip file.\n");

41: return false;

42: }

43: }

44: this->zipFileName = zipFile.ExtractFileName();

45: this->zipFileName.StripFileExtension();

46: this->sourcePath = this->sourcePath.ExtractDirName() + "/";

47:

48: // target directory

49: this->targetPath = this->args.GetString("-path");

50: if (this->targetPath.Length() <= 1 || this->targetPath[1] != ':')

51: {// relative path

52: this->targetPath = Util::String("bin:") + this->targetPath;

53: }

54: else

55: {// absolute path

56: this->targetPath = Util::String("file:///") + this->targetPath;

57: }

58: this->targetPath += "/";

59: if (this->sourcePath == this->targetPath)

60: {

61: n_printf("the source diretory cannot be the same with the destination!");

62: return false;

63: }

64: return true;

65: }

66: return false;

67: }

68:

69: void UnZipApp::Run()

70: {

71: UnZipDir(this->zipFileName);

72: }

73:

74: void UnZipApp::UnZipDir( Util::String& dir )

75: {

76: // create a new directory

77: this->ioServer->CreateDirectory(this->targetPath + dir);

78: // unzip the files in this directory

79: Util::Array<Util::String> listFile = this->ioServer->ListFiles(this->sourcePath + dir, "*");

80: for (IndexT i = 0; i < listFile.Size(); i++)

81: {

82: Util::String curFile = this->targetPath + dir + "/" + listFile[i];

83: this->ioServer->CopyFile(this->sourcePath + dir + "/" + listFile[i], curFile);

84: n_printf("%s\n", curFile.AsCharPtr());

85: }

86: // unzip the sub directories

87: Util::Array<Util::String> listDir = this->ioServer->ListDirectories(this->sourcePath + dir, "*");

88: for (IndexT i = 0; i < listDir.Size(); i++)

89: {

90: Util::String curDir = dir + "/" + listDir[i];

91: n_printf("%s\n", (this->targetPath + curDir).AsCharPtr());

92: UnZipDir(curDir);

93: }

94: }

调试参数:

运行结果:

IO子系统

Nebula3的IO系统相对于Nebula1和2是一个巨大的进步, 新系统的主要设计目标有:

  • 使用更标准的机制, 如用URI来定位资源, 用MIME类型来区分数据格式
  • 一个灵活的流模型, 它不关心数据是来自文件, 内存, HTTP连接还是其它地方
  • 从流读写不数据的数据类型也更方便, 例如要读取的XML格式数据来自文件/内存/网络都没问题
  • 另外, 新的流和读写类可以在运行时注册到IO系统中
  • 相对于系统平台的特定IO函数, 像fopen()这样的C Lib函数会有额外的性能或内存损失. 所以在保证可移植性的前提下不损失性能, 必须使用特定平台的IO函数

IO子系统的一些主要概念:

  • 一个中枢的IO::Console 对象连接控制台处理器(console handler)来进行文本的输入和输出. 这保证了所有的Nebula3的文本输出都通过一个集中的进出通道. 特定的控制台处理器可以用特定的方式处理文本输出(例如输出到stdout, 游戏控制台, 日志文件或网络连接).
  • 重定向符做为路径别名. 大体的功能跟Nebula1和2差不多, 除了从AmigaOS 的重定向符得到的灵感. Nebula3重定向符的一个新特性就是它们可以做为URI的别名. 例如, 重定向符”textures:”可以定义为 "http://www.radonlabs.de/textures", 这样简化的资源路径"textures:mytexture.dds"就会解释成这个绝对路径: "http://www.radonlabs.de/textures/mytexture.dds" (太NB了, 把纹理放到网站上加载? 哈哈, 拿来做内置广告肯定很爽)
  • 流(Stream)做为基本的数据进出通道. 它提供了基本的API函数 Open()/Close()/Read()/Write(), 但是可能完全隐藏了传输和存储通道. 典型的例子有IO::FileStream, IO::MemoryStream, 或 Net::HttpStream
  • Stream reader 和 writer 是连接到流上并且实现了简单易用的接口来读写数据格式. 例如你可以把IO::XmlReader连接到IO::FileStream来从文件系统读取XML格式的数据, 或者连接到IO::HttpStream来从HTTP连接读取XML格式的数据.

这里有个很好的代码例子可以反映出Nebula3输入输出系统的强大:

1: IO::FileServer::Instance()->CopyFile("http://www.radonlabs.de/index.html", "temp:index.html");

这一行代码从HTTP服务器拷贝了一个文件到当用户的临时目录里去. 再多加几行代码, 你可以创建一个流对象指向HTTP服务器上的HTML文件, 连接一个XML reader到这个流上, 然后就可以在不存储中间文件的基础上进行解析HTML了.

标准重定向符

Nebula3初始化了以下几个重定向符:

  • home: 指向应用程序目录, 一般在” C:\Program Files “下. Nebula3把这个目录当成只读的, 为的是不需要管理员权限就能运行.
  • user: 这个指向当前登录的用户目录, 一般是指” C:\Documents and Settings\[username] “. Nebula3会自动创建一个本地目录来避免不同程序覆写掉它们的数据. 所以说一般情况下把数据写入用户目录是安全的. 这个地方可以用于保存游戏数据和配置, 或者程序需要调用的持久性数据.
  • temp: 这个指向当前用户的临时目录, 一般是可写的, 但是不要假设下一次启动程序时数据还存在.
  • bin: 这个指向应用程序可执行文件的目录. 它可以跟home相同, 也可能不同. 这个目录应该也当成是只读的来对待.

其它重定向符可以在程序运行时进行定义. 通常情况下会定义一些抽象资源路径, 如textuers, sound, data等等. 这样的话资源的路径就可以只更改重定向符的定义而是不是去替换所有的路径. 重定向符的另一个好处就是减少了路径字符串的长度, 在一定程序上节省了内存占用.

URI(统一资源定位符)

在Nebula3中的资源位置通常都是用URI定义的. URI一般包括下面这几部, 有一些是可选的:

  • 模式(协议?), 如"http:", "file:", 等... Nebula3 没有硬编码任何模式, 而跟流类绑定在一起注册到IO::StreamServer 单件
  • 一个可选的用户信息字段, 这是一个用户名和密码用于HTTP或FTP主机的身份验证
  • 一个主机名, 如"www.radonlabs.de"
  • 一个在主机名后可选的端口号
  • 一个本地路径, 指向主机上的一个资源
  • 一个可选的片段, 通常指向资源内部的一个位置
  • 一个可选的查询部分, 一般包含一个PHP脚本或其它相似的动态响应机制的参数

IO::URI类用来传递URI并且解析URI字符串到它的各个部分中. 值得注意的是URI对象比字符串占用更多的内存, 所以有时把URI保存在字符串中, 并在需要分割的时候才使用IO::URI类会更好一些.

这里有一些URI的例子:

1: file:///c:/temp/bla.txt

2: file://samba/temp/bla.txt

3: http://www.radonlabs.de/index.html

4: http://user:password@www.myserver.com:8080/index.html#main

通过使用重定位符会大大简化路径名称. 要引用一个程序目录的文件你可以使用”home:bla.txt”, 等价于file:///c:/Program Files/[myapp]/bla.txt.

Stream, Reader 和 Writer

流(Stream)提供了用于储存和传输原始数据的接口. 一个流对象提供了传统的Open()/Close()/Read()/Write()/Seek()接口, 其中有些还提供内存映射, 这样数据的读写可以直接通过内存访问来实现. Stream对象用一个IO::URI对象来定义它们的资源位置. 通常情况下, 一个URI格式映射到一个特定的流对象. 例如”http:”URI格式一般映射到Net::HttpStream类, 而”file:”格式则映射到IO:FileStream类. 这个映射由StreamServer构造一个流对象并匹配一个URI. 一个Nebula3应用程序通过StreamServer::Register()方法来注册这个映射关系, 这也是新的流对象和URI格式的注册方法.

让我们来看看有哪些重要的类:

  • IO::FileStream: 提供了访问主机文件系统的功能
  • IO::MemoryStream: 一个具有流接口的动态内存缓冲
  • IO::HttpStream: 提供了一个流接口来访问HTTP服务器文件

Stream reader和writer类提供了一些舒适的接口专门处理特定的数据格式. 这里有一些stream reader和writer:

  • IO::BinaryReader/IOBinaryWriter: 读写二进制数据
  • IO::TextReader/IOTextWriter: 读写文本数据
  • IO::XmlReader/IOXmlWriter: 读写XML格式的数据
  • Messaging::MessageReader/MessagingMessageWriter: 消息序列化

这里有一个用XmlReader从HTTP服务器访问文件的简单例子

1:     using namespace IO;

2:

3:     Ptr<Stream> stream = StreamServer::Instance()->CreateStream("http://www.radonlabs.de/index.html");

4:     Ptr<XmlReader> xmlReader = XmlReader::Create();

5:     xmlReader->SetStream(stream);

6: if (xmlReader->Open())

7: {

8: // parse content here using the XmlReader interface

9: }

File Server(文件服务器)

Nebula3 IO::FileServer类提供了一个单件用于访问主机的文件系统进行一些全局操作, 像定义重定向符, 复制, 删除和检查文件是否存在, 列出目录内容, 等等.

这个代码片断介绍FileServer的一些有用的方法:

using namespace IO;

using namespace Util;

FileServer* fs = FileServer::Instance();

// check if a file or directory exists

bool fileExists = fs->FileExists("home:bla.txt");

bool dirExists = fs->DirectoryExists("temp:bla/blub");

// resolve a path with assigns into an absolute filesystem

// path, this is sometimes necessary to interface with

// 3rd party libraries which don't understand Nebula3 paths directly

String absPath = fs->ResolveAssings("user:myapp/savegames");

// create a directory, note that all missing subdirectories will

// be created as well

fs->CreateDirectory("user:myapp/savegames");

// copy and delete files

fs->CopyFile("home:movie.mpg", "temp:movie.mpg");

fs->DeleteFile("temp:movie.mpg");

// list files in a directory matching a pattern

Array<String> files = fs->ListFiles("temp:", "*.txt");

// list all subdirectories in temp:

Array<String> dirs = fs->ListDirectories("temp:", "*");

控制台

一般不直接调用IO::Console, 直接n_printf(), n_error(), n_dbgout(), n_warning()@_@