(转载)

假如,我们现在利用Select语句从数据库查询数据,Oracle数据库是如何运作的呢?从中我们可以领悟到什么呢?下面,就结合一条简单的select语句,看看Oracle数据库后台的运作机制。这对于我们之后的系统管理与故障排除非常有帮助。

  第一步:客户端把语句发给服务器端执行。

  当我们在客户端执行select语句时,客户端会把这条SQL语句发送给服务器端,让服务器端的进程来处理这语句。也就是说,Oracle客户端是不会做任何的操作,他的主要任务就是把客户端产生的一些SQL语句发送给服务器端。虽然在客户端也有一个数据库进程,但是,这个进程的作用跟服务器上的进程作用事不相同的。服务器上的数据库进程才会对SQL语句进行相关的处理。不过,有个问题需要说明,就是客户端的进程跟服务器的进程是一一对应的。也就是说,在客户端连接上服务器后,在客户端与服务器端都会形成一个进程,客户端上的我们叫做客户端进程;而服务器上的我们叫做服务器进程。所以,由于所有的SQL语句都是服务器进程执行的,所以,有些人把服务器进程形象地比喻成客户端进程的“影子”。

  第二步:语句解析。

  当客户端把SQL语句传送到服务器后,服务器进程会对该语句进行解析。同理,这个解析的工作,也是在服务器端所进行的。虽然这只是一个解析的动作,但是,其会做很多“小动作”。

  1、查询高速缓存。服务器进程在接到客户端传送过来的SQL语句时,不会直接去数据库查询。而是会先在数据库的高速缓存中去查找,是否存在相同语句的执行计划。如果在数据高速缓存中,刚好有其他人使用这个查询语句的话,则服务器进程就会直接执行这个SQL语句,省去后续的工作。所以,采用高速数据缓存的话,可以提高SQL语句的查询效率。一方面是从内存中读取数据要比从硬盘中的数据文件中读取数据效率要高,另一方面,也是因为这个语句解析的原因。

  不过这里要注意一点,这个数据缓存跟有些客户端软件的数据缓存是两码事。有些客户端软件为了提高查询效率,会在应用软件的客户端设置数据缓存。由于这些数据缓存的存在,可以提高客户端应用软件的查询效率。但是,若其他人在服务器进行了相关的修改,由于应用软件数据缓存的存在,导致修改的数据不能及时反映到客户端上。从这也可以看出,应用软件的数据缓存跟数据库服务器的高速数据缓存不是一码事。

  2、语句合法性检查。

  当在高速缓存中找不到对应的SQL语句时,则数据库服务器进程就会开始检查这条语句的合法性。这里主要是对SQL语句的语法进行检查,看看其是否合乎语法规则。如果服务器进程认为这条SQL语句不符合语法规则的时候,就会把这个错误信息,反馈给客户端。在这个语法检查的过程中,不会对SQL语句中所包含的表名、列名等等进行SQL他只是语法上的检查。

  3、语言含义检查。

  若SQL语句符合语法上的定义的话,则服务器进程接下去会对语句中的字段、表等内容进行检查。看看这些字段、表是否在数据库中。如果表名与列名不准确的话,则数据库会就会反馈错误信息给客户端。

  所以,有时候我们写select语句的时候,若语法与表名或者列名同时写错的话,则系统是先提示说语法错误,等到语法完全正确后,再提示说列名或表名错误。若能够掌握这个顺序的话,则在应用程序排错的时候,可以节省时间。

  4、获得对象解析锁。

  当语法、语义都正确后,系统就会对我们需要查询的对象加锁。这主要是为了保障数据的一致性,防止我们在查询的过程中,其他用户对这个对象的结构发生改变。对于加锁的原理与方法,我在其他文章中已经有专门叙述,在这里就略过不谈了。

  5、数据访问权限的核对。

  当语法、语义通过检查之后,客户端还不一定能够取得数据。服务器进程还会检查,你所连接的用户是否有这个数据访问的权限。若你连接上服务器的用户不具有数据访问权限的话,则客户端就不能够取得这些数据。故,有时候我们查询数据的时候,辛辛苦苦地把SQL语句写好、编译通过,但是,最后系统返回个“没有权限访问数据”的错误信息,让我们气半死。这在前端应用软件开发调试的过程中,可能会碰到。所以,要注意这个问题,数据库服务器进程先检查语法与语义,然后才会检查访问权限。

  6、确定最佳执行计划。

  当语句与语法都没有问题,权限也匹配的话,服务器进程还是不会直接对数据库文件进行查询。服务器进程会根据一定的规则,对这条语句进行优化。不过要注意,这个优化是有限的。一般在应用软件开发的过程中,需要对数据库的sql语言进行优化,这个优化的作用要大大地大于服务器进程的自我优化。所以,一般在应用软件开发的时候,数据库的优化是少不了的。

  当服务器进程的优化器确定这条查询语句的最佳执行计划后,就会将这条SQL语句与执行计划保存到数据高速缓存。如此的话,等以后还有这个查询时,就会省略以上的语法、语义与权限检查的步骤,而直接执行SQL语句,提高SQL语句处理效率。

 第三步:语句执行。

  语句解析只是对SQL语句的语法进行解析,以确保服务器能够知道这条语句到底表达的是什么意思。等到语句解析完成之后,数据库服务器进程才会真正的执行这条SQL语句。

  这个语句执行也分两种情况。一是若被选择行所在的数据块已经被读取到数据缓冲区的话,则服务器进程会直接把这个数据传递给客户端,而不是从数据库文件中去查询数据。若数据不在缓冲区中,则服务器进程将从数据库文件中查询相关数据,并把这些数据放入到数据缓冲区中。

  这里仍然要注意一点,就是Oracle数据库中,定义了很多种类的高速缓存。像上面所说的SQL语句缓存与现在讲的数据缓存。我们在学习数据库的时候,需要对这些缓存有一个清晰的认识,并了解各个种类缓存的作用。这对于我们后续数据库维护与数据库优化是非常有用的。

  第四步:提取数据。

  当语句执行完成之后,查询到的数据还是在服务器进程中,还没有被传送到客户端的用户进程。所以,在服务器端的进程中,有一个专门负责数据提取的一段代码。他的作用就是把查询到的数据结果返回给用户端进程,从而完成整个查询动作。

  从这整个查询处理过程中,我们在数据库开发或者应用软件开发过程中,需要注意以下几点:

  一是要了解数据库缓存跟应用软件缓存是两码事情。数据库缓存只有在数据库服务器端才存在,在客户端是不存在的。只有如此,才能够保证数据库缓存中的内容跟数据库文件的内容一致。才能够根据相关的规则,防止数据脏读、错读的发生。而应用软件所涉及的数据缓存,由于跟数据库缓存不是一码事情,所以,应用软件的数据缓存虽然可以提高数据的查询效率,但是,却打破了数据一致性的要求,有时候会发生脏读、错读等情况的发生。所以,有时候,在应用软件上有专门一个功能,用来在必要的时候清除数据缓存。不过,这个数据缓存的清除,也只是清除本机上的数据缓存,或者说,只是清除这个应用程序的数据缓存,而不会清除数据库的数据缓存。

  二是绝大部分SQL语句都是按照这个处理过程处理的。我们DBA或者基于Oracle数据库的开发人员了解这些语句的处理过程,对于我们进行涉及到SQL语句的开发与调试,是非常有帮助的。有时候,掌握这些处理原则,可以减少我们排错的时间。特别要注意,数据库是把数据查询权限的审查放在语法语义的后面进行检查的。所以,有时会若光用数据库的权限控制原则,可能还不能满足应用软件权限控制的需要。此时,就需要应用软件的前台设置,实现权限管理的要求。而且,有时应用数据库的权限管理,也有点显得繁琐,会增加服务器处理的工作量。因此,对于记录、字段等的查询权限控制,大部分程序涉及人员喜欢在应用程序中实现,而不是在数据库上实现

posted @ 2008-09-11 00:39 InsistYML 阅读(146) | 评论 (0)编辑
(转载)摘要:本文深入探讨了 SQL Server 体系结构的工作原理。其中介绍了数据库引擎的增强功能及其使用技巧,并提供了相关信息的链接。深入了解 SQL Server 的引擎有助于数据库管理员(数据库系统工程师)在设计、构建或改进数据库系统时充分利用 SQL Server 的优势。虽然本文主要面向数据库专业人士,但也可用于教学或营销目的。 

  一. 简介

  本文描述 Microsoft? SQL Server? 2000 中新增的存储引擎功能,并提供相关的使用技巧,同时探讨存储引擎的工作原理。初步掌握存储引擎的工作原理可使您最大限度地发挥 SQL Server 的性能。

  当今,人们的注意力集中在应用程序的高可扩展性方面。数据库的设计和实施周期不断缩短,而且由于开发需求的不断变更以及产品使用的增长,又使得数据库在不断地演变发展。为满足可扩展性、可用性和易于使用等方面的要求,需要有一种具有应变能力和灵活性的数据存储引擎。

  SQL Server 2000 的不同版本可支持不同规模的系统,其范围从用于 Pocket PC 的小型移动系统到运行在群集 Windows? 2000 Datacenter Server 上容量达数 TB 的高可用性事务处理或决策支持系统。所有这些系统都满足使命关键的业务系统所要求的灵活性、安全性和可靠性要求。

  由于存储引擎操作的智能化和自动化,因而可以在各种用途和规模的项目中部署 SQL Server 2000 应用程序。高度完善的体系结构改善了性能、可用性和可伸缩性。

  可用性

  由于在与物理文件进行交互时采用了新的算法,可靠性和并发能力得到了增强。这些算法减轻了日常维护工作,使您不必再运行数据库控制台命令 (DBCC)。然而,DBCC 仍旧可以使用,并且新增的 DBCC CHECK 命令的运行不会干扰联机处理工作。

  可扩展性

  存储子系统(由物理数据库文件及其在磁盘上的布局组成)既支持小型数据库,也支持超大型数据库。SQL Server 当前可支持最高达 64 GB 物理内存 (RAM) 和 32 颗处理器。

  易于使用

  增强的管理功能可帮助数据库管理员 (DBA) 实现服务器的自动管理和集中管理。这也使得对远程服务器和应用程序的维护变得容易,DBA 无需访问每个站点。由复杂算法管理的服务器配置可动态对服务器使用方案的变化作出反应,从而使 DBA 可以将精力集中在数据库管理和优化等任务上。

  二. 存储引擎的增强功能

  SQL Server 2000 的关系数据库服务器包括两个主要部分:关系引擎和存储引擎。两个引擎独立工作,它们通过本地数据访问组件(如 OLE DB)进行交互。关系引擎提供访问存储引擎的接口,而存储引擎由与基本数据库存储组件和功能进行交互的服务构成。

  存储引擎的主要任务包括: 
  提供改善管理存储组件易用性的功能 
  管理数据缓冲和对物理文件的所有 I/O 操作 
  控制并发、管理事务、锁定和日志记录 
  管理用于存储数据的文件和物理页 
  恢复系统故障 

  SQL Server 2000 中的存储引擎提供概念简单而实际操作比较灵活的新增功能,同时减少了详细规划容量和优化性能的工作。SQL Server 2000 可以对其环境作出反应,并准确而快速地适应数据库使用上的变化。这种技术上的突破已将数据库管理的重点转移到作为服务的数据简化上。SQL Server 2000 DBA 可以将注意力集中到设计一个可对数据流和数据使用作出响应的系统上,而不再需要将时间浪费在优化个别参数上。 

 

2003-6-7 19:21:00    
 查看评语???     

 2003-6-7 19:23:01      SQL Server 2000 中的变化建立在体系结构的增强上。这种增强是在 SQL Server 7.0 中引入的,其目的是为后来的改进和创新提供基础。存储引擎小组的主要目的是减少花在定期优化服务器上的时间和精力。由于绝大多数的调优参数设置是基于数据库使用的,所以引擎使用自适应算法动态调整这些设置,使其适合数据库环境。调优参数现在可以按这种方式自动调整,而在早期版本中,它们需要不断调整和测试。您仍可以手动调整调优功能,但 SQL Server 2000 可以完成更多的工作。只有很少的 SQL Server 客户才需要对调优参数进行调整;这种调整工作需要进行仔细的测试,并且需要经验丰富的数据库管理员进行监督。

  下表总结了 SQL Server 2000 存储引擎的主要增强功能。本文的后面将对这些内容进行详细阐述。


功能 描述及益处 
应用程序锁定管理器 如果需要控制对应用程序定义的资源(如表单)的并发访问,新增的存储过程允许您使用 SQL Server 的应用程序锁定管理器锁定这些资源。 
数据库控制台命令 (DBCC) DBCC CHECK 命令可以在联机处理过程中运行,且不会中断更新。新增的功能允许校验物理页的一致性,以检测硬件引起的错误。在 SQL Server 2000 企业版中,DBCC 可以在多个处理器上以并行方式运行。 
数据库选项 所有的数据库选项都可使用 ALTER DATABASE 进行修改。此功能简化了管理工作。 
差异备份 在 SQL Server 2000 中,由于改进后的功能可以在更广的层次上跟踪数据库的更改,差异备份的速度更快。 
动态调优 通过使用动态自适应算法,服务器可以自动调整以前是静态不变的配置设置。管理控制仍可用于管理系统范围的资源,但以后您不必使用它们。手动设置参数可以在它们的约束边界内动态调整。 
行内文本 在包含较小且使用频繁的文本列的表中,较小的文本值可以与标准数据行存储在同一页中,而不必存储在文本值页中。如果表中包含这种被频繁访问的文本数据,此功能可减少大量磁盘 I/O 操作。 
并行建立索引 在企业版中,索引建立过程自动使用为并行处理配置的所有处理器,减少了建立索引所需的时间;例如,在一台八处理器的服务器中,时间缩短到原来的六分之一。索引建立过程还可利用内存和 tempdb 中的可用资源。 
预读索引 读取索引的功能得到增强,提高了扫描索引的性能。 
重组索引 对 DBCC SHOWCONTIG 进行的改进提供了有关索引碎片的详细信息。新增的 DBCC 命令 INDEXDEFRAG 可联机重组索引页,且不会中断数据库服务,也不会导致数据库一致性或故障恢复方面的问题。 
降序排列索引中的键列 索引中的各个键列可单独指定为升序或降序。 
KILL 命令 此命令现在报告完成的进度。如果此命令正在等待另一个进程(例如回滚),则可以查看命令执行的进度。改进后的命令可以用于停止 Microsoft 分布式事务协调器 (MS DTC) 事务,而这些事务并不与特定会话相关联。 
对高内存量的支持 Windows 2000 中的技术改进了使用大量内存的企业版系统的性能。通过使用 Windows 2000 的 AWE 扩展,SQL Server 2000 可至多支持 64 GB 物理内存 (RAM)。 
锁定 改进后的锁定管理器可探测到其它资源(如线程和内存)的死锁情况。并发能力的改善同时也降低了死锁的发生,从而进一步加强了 SQL Server 2000 的可扩展性。 
逻辑日志标记 Transact-SQL 命令可在日志中创建书签,使数据库可以恢复到书签所示的时点。此功能还可同步恢复用于同一应用程序的多个数据库。 
联机索引重组 对 DBCC SHOWCONTIG 进行的改进提供了有关索引碎片的详细信息。新增的 DBCC 命令 INDEXDEFRAG 可联机重组索引页,且不会中断数据库服务,也不会导致数据库一致性或故障恢复方面的问题。 
优化的预读 I/O 操作 对于扫描所涉及的每个文件,SQL Server 2000 都会同时发出多个连续的、预读读取操作。为提高性能,查询优化器在扫描表和索引时使用连续的预读 I/O 操作。 
密码保护备份 可使用密码保护备份媒体和单独的备份。这样可以防止未授权的用户恢复备份并访问数据库。 
故障恢复模式 通过使用故障恢复模式,可以选择数据库的日志记录级别。这样事务日志管理更加灵活。故障恢复模式可联机更改,以适应一天当中不断变化的数据库使用。 
共享表扫描 在企业版中,对某个表的多次扫描可以利用其他进行中的对该表的扫描,减少了对磁盘的实际 I/O 操作。 
收缩日志 收缩日志命令可在更多的情况下立即运行。即使不能立即收缩日志,SQL Server 也会提供建议性的反馈,说明在继续或完成收缩操作之前必需完成的操作。 
快照备份 对第三方供应商提供的快照备份的支持进一步得到加强。快照备份采用存储技术,可以在几秒内备份或恢复整个数据库。如今,可以将这些备份与常规事务日志及差异备份相结合,为 OLTP 数据库提供完整的保护。此功能对于中型或大型数据库是非常有益的,因为在这种环境中可用性是非常重要的。 
节省空间的空表和索引 SQL Server 2000 不为空表和索引分配磁盘页。SQL Server 7.0 会给空表和索引分配多达三个磁盘页。 
前 n项排序 此新功能可优化前 n 项值的检索(例如,SELECT TOP 5 * FROM tablename)。 
Xlock SQL Server 2000 提供这种新的 Transact-SQL 锁定提示。它可用于明确调用互斥的、事务级别的页或表锁。 

  SQL Server 2000 中增加了许多功能,这些功能使数据交互更为有效,使管理更加灵活。以下部分将详细介绍这些增强功能,并提供相关的使用技巧。
三. 与数据进行交互

 

  在 SQL Server 2000 中,存储引擎的功能得到增强,在与数据进行交互时可提供更好的可扩展性及性能。了解这些增强的功能有助于更有效地使用 SQL Server。

  无论是通过用户界面还是自动执行的任务,数据交换都从查询开始。数据请求先被传递到关系引擎,然后关系引擎与存储引擎进行交互以获取数据,并将其传递给用户。无论是从用户还是 DBA 的角度来看,存储和关系引擎的功能是无法区分的。

  更有效地读取数据

  数据通过一系列事务在服务器和用户之间传递。应用程序或用户启动任务后,数据库将其传递给查询处理器进行处理,然后返回最终结果。查询处理器通过接收、解释和执行 SQL 语句来完成任务。

  例如,当用户会话发出 SELECT 语句时,将会执行以下步骤: 

  关系引擎将语句进行编译和优化后,将其纳入执行计划(获取数据所需的一系列步骤)。然后,关系引擎运行执行计划。执行步骤包括通过存储引擎访问表和索引。 

  关系引擎解释执行计划,调用存储引擎以收集所需的信息。 

  关系引擎将存储引擎返回的所有数据组合到最终的结果集中,然后将结果集返回给用户。 

  为提高此过程的性能,进行了两项改进。在 SQL Server 2000 中,关系引擎将核准查询谓词的工作交由存储引擎完成,这样在该过程中这些谓词能尽早得到处理,因而提高了存储和关系引擎之间数据交换的效率。此项改进使核准查询的效率显著提高。

  增强的 Top n 功能

  另一项改进是存储引擎处理从结果集中选择前 n 个记录的方式。在 SQL Server 2000 中,新的 Top n 引擎将分析类似以下语句的最佳操作路径:
SELECT top 5 * from orders order by date_ordered desc
在本例中,如果必须搜索整个表,则引擎会分析数据并只跟踪高速缓存中的前 n 项数值。这种方式将大幅提高上述 SELECT 语句的性能,因为只有前 n 项值需要排序,而非整个表。

  共享扫描

  在 SQL Server 2000 企业版中,两个或多个查询可共享正在进行的表扫描,此项功能可改善大型 SQL Server 2000 数据库的性能。例如,当查询使用无序扫描查询一个很大的表时,高速缓存中的页面将被清空,以便为流入数据腾出空间。如果另一个查询已经开始,对同一表的第二次扫描就会使磁盘 I/O 再次检索这些页面。在频繁进行表扫描的环境中,当两个查询搜索相同的数据页时,这将导致磁盘颠簸。 


图 1:共享扫描效果

  优化的进程可减少由此类数据访问模式造成的磁盘 I/O 操作。对表的第一个无序扫描将从磁盘中读取数据;后续的对同一表的无序扫描不必再读取硬盘,而只需使用已在内存中的信息。参见图 1。在对同一个表同时进行多个扫描操作时,此同步过程可将性能提高至多八倍。此项改进的效果在大型决策支持查询中更加明显,因为整个表的大小远远大于高速缓存的大小)。 

  当查询没有更有效的执行计划时,存储引擎将使用共享扫描功能协助查询。此功能的目的是提高频繁读取大型表的性能。当查询处理器确定最佳执行计划中包含表扫描时,将调用此功能。然而,尽管可以使用查询或索引优化强制进行共享扫描时,但强制进行表扫描并不会提高性能。此时使用状态良好的索引完成同样的工作效果不会差,而且可能会更好。

  并发

  为了在多个用户进行数据交互的同时维护事务的一致性,存储引擎会锁定资源以管理行、页、键、键范围、索引、表和数据库的依存性。通过在更改资源时将其锁定,引擎可防止多个用户同时更改同一数据。SQL Server 中的锁可在不同粒度级别上动态应用,以选择事务所需的限制最小的锁。

  在 SQL Server 2000 中,并发方面的改进进一步减少了死锁,避免了对资源不必要的锁定。例如,增强的锁定管理器可了解被竞用的其它资源(如线程和内存)。这种新的功能可帮助数据库管理员确定更广范围内的设计或硬件限制。 

 
 2003-6-7 19:30:13    锁定管理器中新增的 Transact-SQL 接口支持在编程代码中使用自定义的锁定逻辑。业务逻辑所需的锁可通过在 Transact-SQL 批处理中调用 sp_getapplock 命令来创建,这将允许您指定要锁定的应用程序定义的资源(例如锁定表单,而非数据行)、要使用的锁定模式、超时值以及锁的范围是事务还是会话。当使用新的应用程序锁管理器创建锁后,它们接受 SQL Server 的常规锁管理,如同它们是由存储引擎创建的一样,因此,不必担心当调用事务终止时,应用程序创建的锁仍处于打开状态。

  在 SQL Server 2000 中,用于获取锁的进程将考虑页中的数据是否都已提交。例如,若要对某个表运行 SELECT 语句,而该表中的数据在最近未发生变化(如 pubs 数据库中的表),则该进程不会产生锁,因为最近没有活动事务对表进行过更新。存储引擎是通过将数据页上的日志序列号与当前活动事务相比较来实现上述功能的。如果数据库中的绝大多数数据都早于最早的活动事务,则对于这样的数据库,这一功能将显著减少锁定操作,从而使性能大幅提高。 

  在使用锁保护事务中数据的同时,另一个进程 latching 控制对物理页的访问。闩锁是一种非常轻型的、短期同步对象,它保护事务生存期内不需要锁定的操作。当存储引擎扫描某页时,它先锁住该页,读取行,将行返回给关系引擎,然后再解除对页面的锁定,以使其它进程可以访问同一数据。存储引擎使用称为 lazy latching 的进程优化对数据页的访问,即只在另一个活动进程请求某页时,才释放对该页的锁存。如果没有活动进程请求同一数据页,则在对该页的整个操作过程中,单个闩锁将始终有效。

  为改进系统的并发性能,应将精力集中在数据库系统的设计以及与其相关的代码对象上。SQL Server 2000 支持 TB 级的数据存储,其扩展能力可线性增长,且不受限制。数据库管理员的任务是管理数据库生命周期,即所有数据库组件(从代码到磁盘上的数据存储)的设计和优化周期,以确保设计始终满足服务级协议的要求。

  四. 表和索引

  在物理数据结构方面也进行了改进,提高了设计和维护的灵活性。

  随着表或索引的增长,SQL Server 以八个为一组分配新的数据页;这些数据页称为扩展。虽然 text、ntext 或 image 类型的列可存储在不同的页中,但一行数据不能超出一页,所以它只能拥有 8 KB 数据。拥有聚集索引的表按键的顺序存储在磁盘上。堆是不带聚集索引的表,它们是无序的。记录按插入的顺序存储。

  SQL Server 2000 支持索引视图,在其它数据库产品中常常称为实体视图。在某个视图上创建聚集索引时,该视图将从派生对象转为存储在数据库中的基本对象,并且其结构与带有聚集索引的表相同。索引视图可用于存储预先计算的值或复杂联接的结果,但前提是维护开销不能超过性能上的收益。在 SQL Server 2000 企业版中,只要索引视图可以优化查询计划,查询处理器就会自动使用它。对于很少更改但又经常作为复杂联接或计算查询组成部分的数据,索引视图可改善查询速度。

  行内文本

  行内文本可用于在主页面中存储小文本数据。例如,如果某个表中有一文本列,但文本值通常小到可与行中的其余内容放在同一普通页中,则可以在文本列中设置阈值。阈值用来确定可存储在主页面而非单独的文本页上的最大文本长度。如果大多数数据可放在主页面上,而只有小部分数据比较大,需要创建文本页,采取这种做法就可使性能获得提高。

  若要确定在何种情况下使用此新功能,则需要权衡存储密度(或每个数据页上存储的行数)以及 I/O 性能的改善。例如,某个表中的文本列用于存放注释。该列中有 20% 的文本值较长,而其它 80% 的文本值都小于 100 个字节。对于这种情形,似乎可以采用行内文本解决方案;但是,只有在这样的列中的数据被频繁访问时才应考虑使用行内文本。如果用户频繁访问此表,但只在进行特殊搜索时才查看此注释列,则使用行内文本未必是最好的做法。由于每页存储的行数少,所以存储密度会降低;并且由于表包含更多的页,所以表扫描响应时间也会增加。所以,实现行内文本的最好情况是,存在需要频繁访问的文本列,并且该列的许多值都小于 8 K,可以存储在行中。

  新增数据类型

  SQL Server 2000 引入了三种新的数据类型。bigint 是 8 字节整数类型。sql_variant 可以存储不同数据类型的数据值。第三种数据类型 table 可用于优化性能。Table 变量使 tempdb 的使用效率更高,并且比临时表更加快速。与其它变量一样,它们的作用范围是声明它们的批处理。table 变量的功能类似于临时表,但其性能要高于临时表或游标,并且可更加有效地利用服务器资源。通常,在创建与数据库交互的代码时,一定要考虑利用服务器上可用资源的最佳方法。 

  索引

  通过使用索引,可以优化对数据的访问。因为是否建立索引取决于使用情况,所以不正确的索引是造成数据库缓慢最主要的原因。标准的索引维护工作应该包括周期性校验当前索引方案以及通过适当增删索引使其适应当前的系统使用情况。

  SQL Server 2000 中的几个新增功能使索引维护更加有效,索引管理更加容易。这些增强功能减少了磁盘 I/O 操作,从而增加了索引扫描的性能。在范围扫描可以使用辅助索引时,这一功能尤其有用。 

 
 2003-6-7 19:31:33    建立索引

  建立索引时,存储引擎对行进行采样,并计算使用服务器资源建立索引的最佳方法。通过使用选项,可以控制建立索引的方式,因而可选择控制系统资源的分配方式。可以使用这些选项,并结合您在特定数据库系统方面的知识,平衡对于整体系统的性能是至关重要的进程中的资源,从而使建立索引的操作对事务处理的影响最小。

资源 命令 选项 说明 
内存 sp_configure(高级)
index create memory
指定建立索引操作可以使用的内存总量。 
TempDB create index
sort_in_tempdb
从 tempdb 中分配在索引建立期间用于排序的磁盘空间。如果 tempdb 在单独的磁盘上,此命令会产生更高的 I/O 带宽;并且如果数据库所在空间不是非常连续,该命令还可以使索引页的布局在物理上更加连续。 
CPU sp_configure(高级)
最大并行程度
限制在并行操作中(服务器范围)可使用的处理器(CPU) 的个数。 

  大型系统的另一个可扩展性功能是并行索引建立。SQL Server 2000 企业版具有此功能。在发出单个 CREATE INDEX 语句时,此过程将被自动调用。存储引擎计算数据的要求,然后创建单独的线程,每个线程建立一段索引。 


图 2:并行索引优化

  索引建立也可以使用共享表扫描,从而使这一过程进一步优化。

  整理索引碎片

  SQL Server 2000 支持联机索引重组,相比于以前的版本,这是一个非常大的进步。联机索引重组对事务的吞吐量影响非常小,并且可随时停止并重新启动,而不会影响其运行效果。索引重组操作按较小增量进行,并且可完全恢复。

  随着在表中插入、删除和更新信息,聚集和非聚集索引页最终将变得零碎,从而降低对数据的范围查询的效率。因此,定期整理索引碎片是非常有益的。可以使用 DBCC SHOWCONTIG(该命令在 SQL Server 2000 中已有所改进)分析并报告碎片。

  如果确定索引已变为碎片,就可以使用 DBCC INDEXDEFRAG 命令对其进行重组。该命令以逻辑键的顺序记录页,同时压缩可用空间,移动已建立的扩展中的行以满足填充因子设置。通过提高页面中内容的密度以减少数据扫描时读取的页数,从而提高读取性能。如果索引经常得到维护并且其分布不是完全散碎的,那么运行 DBCC INDEXDEFRAG 对联机性能的影响要远远小于重建索引。

  DBCC INDEXDEFRAG 是众多长期运行的管理操作中的一个,它们内部都使用短小的事务。这些短小的事务可最大限度提高服务器中的并发操作,允许操作停止而不影响工作,并且这些事务被全部记录以便在发生故障时进行恢复。
五. 日志记录和故障恢复 

 

  事务日志是一个记录流,它记录了从数据库创建到当前时点对数据库所做的更改。每个记录的操作都创建一个日志记录。日志记录由事务生成,并在事务提交时写入磁盘。相反,被事务修改的数据页不会立即写入磁盘,而是先保留在 SQL Server 的缓冲区高速缓存中,稍后再写入磁盘。推迟将数据写入磁盘可最大限度地提高对数据页进行多路访问的性能,并避免中断扫描。在提交时强制将日志写入磁盘是为了确保在服务器关机时不会丢失已完成的工作。

  故障恢复可确保在将数据库变为联机状态之前保持其在事务上的一致性。如果数据库在事务上是一致的,则所有提交的工作都已生效,而任何未提交的工作都变为无效。日志总是定义数据库的正确视图。简而言之,故障恢复就是将数据与事务日志在某一给定时点保持一致的过程。 

  当 SQL Server 启动时,当数据库被连接时,或在从备份恢复数据库的最后一步时,故障恢复将自动执行。在 SQL Server 启动时执行的故障恢复称为重新启动故障恢复或启动故障恢复。使用备份进行故障恢复通常是由于磁盘发生故障。此类故障恢复称为媒体故障恢复。

  重新启动故障恢复是自动进行的,通常可恢复到最近的时点。在使用备份进行故障恢复时,DBA 可以选择恢复到较早的时点。这种故障恢复需要满足一些限制条件。

  每当启动一个 SQL Server 实例时,启动故障恢复会自动运行,它将回滚上次关闭实例时尚未完成的所有事务。在使用备份进行故障恢复时,DBA 可以选择恢复到较早的时点。这种故障恢复需要满足一些限制条件。无论何种情况,故障恢复操作都基于此目标时点。 

 
 2003-6-7 19:32:58    故障恢复分为两个阶段: 

  恢复所有更改,直到达到事务日志中的目标时点。 

  撤消由在恢复停止点仍处于活动状态的事务所执行的所有操作。

  SQL Server 使用检查点加速重新启动故障恢复。检查点强制将当前缓冲区高速缓存中所有已修改的数据页保存到磁盘上。这将为故障恢复的恢复阶段创建一起点。由于检查点的开销非常大,所以 SQL Server 自动对检查点进行管理,以保证在尽量缩短重新启动所花时间的同时尽可能提高性能。

  在 SQL Server 2000 中,成功完成的写入必须可持久存储在磁盘中。如果使用写缓存磁盘存储,请与您的存储设备供应商联系,确认高速缓存是否容错。容错能力表示高速缓存可不受电源故障或操作员操作的影响。如果缓存没有容错能力,则应不使用。

  逻辑日志标记

  在 SQL Server 7.0 中,已经可以恢复到任何指定时点。如果出现硬件故障,则恢复过程是相当简单的。然而,对数据库的另一种威胁可能是输入了无效的数据,或者有效数据被用户操作所破坏。在这种情况下,需要确定问题发生的开始时间。在 SQL Server 7.0 中,解决这种问题的唯一方法是将日志恢复成数据库副本,直到问题重现,然后再对产品映像执行恢复操作,直到在所发现的问题出现时刻之前的时点。

  在 SQL Server 2000 中,可以在日志中标记事务。之后,如果需要恢复,就可以参考执行时使用的标记,而不必使用规定的时刻。为此,请使用 BEGIN TRANSACTION 的语句和 WITH MARK [说明] 子句。标记存储在 msdb 中。故障恢复可以包括包含标记的事务,也可以恰恰在包含标记的事务前停止。例如,如果某个进程以批处理方式运行并且更改了许多记录,那么可以使用此功能以确保,当进程运行在错误环境中时可以将数据回滚到执行命令的时点。

  标记名称不必唯一。若要指定所需的事务,请指定 datetime 值。该操作的语法为:
RESTORE LOG WITH [ STOPBEFOREMARK|STOPAFTERMARK ] = @TaggedTransaction AFTER @datetime
也可以对分布式事务使用标记(称为分布式标记),以支持将多个相关数据库恢复到事务上一致的状态。相关数据库可以位于 SQL Server 的同一或不同实例上。可以定期对一组数据库设置分布式标记(例如,每五分钟一次)。如果其中某个数据库的事务日志被损坏,则必须将这组数据库恢复到更早的时点。分布式标记可提供这一时点。使用分布式标记,就可以在对多个相关数据库进行备份时不用费心地确定备份的时刻。

  收缩事务日志

  SQL Server 7.0 中不能立即执行日志收缩操作。该操作被推迟到下一次备份或删节事务日志。这种方式使许多 SQL Server 7.0 客户非常烦恼。SQL Server 2000 可以立即收缩日志,并且可在日志备份后指出是否可以进行进一步收缩。这时,可以在日志备份完成后再次运行收缩命令。

  日志大小取决于当前故障恢复模式以及应用程序设计。如果发现需要定期收缩日志,请查明造成问题的原因。应该进一步调查日志添满的原因,而不要只是一味地使用收缩命令维护日志。

  故障恢复模式

  使用 SQL Server 2000 中增加的故障恢复模式可以方便到实施数据保护计划。这些模式都在性能、日志空间要求和媒体(磁盘)故障保护之间进行了取舍。共有三种模式,它们是:简单故障恢复、完全故障恢复和大容量记录。

  选择故障恢复模式时,应考虑数据库的使用情况和可用性要求,同时选择的模式应有助于确定适当的备份和恢复过程。这些故障恢复模式只适用于媒体故障恢复,即使用备份进行故障恢复。重新启动故障恢复所有提交的工作。

  故障恢复模式间的转换非常容易。例如,在大型数据库中,既可使用完全模式,也可以使用大容量记录模式,或同时使用这两种模式。可以在白天使用完全模式,而在夜晚或在包含大容量插入以及重建索引的数据装载过程中使用大容量记录模式。也可以在运行数据装载时切换到大容量记录模式,然后再切换回完全模式,运行事务日志备份,而且能够恢复到模式切换时点,而不必运行完全数据库备份。此功能可更有效地进行大容量处理操作;而所要做的只是将以前的事务日志进行备份。

  要更改故障恢复模式,请使用以下语法:
  ALTER DATABASE SET RECOVERY RecoveryModel

  简单故障恢复模式

  简单故障恢复模式通常需要很少的日志空间,但如果数据或日志文件被损坏,则它造成的潜在工作损失是最大的。因为在这种模式下只记录基本故障恢复所需的事件。使用简单故障恢复模式时,只能进行完全数据库备份和差异数据库备份。在出现故障时,必须重新完成自从上次备份后所有提交的工作。此模式对管理员是最简单的,但并不适用于关键性任务的应用程序,因为这种程序通常不允许丢失已提交的工作。

  此模式类似于 SQL Server 7.0 及以前版本中的 truncate log on checkpoint 选项。 

 
 2003-6-7 19:34:47    完全故障恢复模式

  在完全故障恢复模式中,所有一切都被记录。完全故障恢复模式提供了全面的保护,以防止损坏的数据文件对工作造成损失。如果事务日志被损坏,则从最近一次日志备份后提交的工作都将丢失,并且必须重新手动完成。

  即使使用完全故障恢复模式,也最好使用容错磁盘存储事务日志,以防止数据丢失。完全故障恢复模式还允许恢复到指定的时点。

  大容量记录故障恢复模式

  大容量记录故障恢复模式为大容量操作提供了最高的性能。而且,这些操作在该模式下占用的日志空间要小于在完全故障恢复模式下占用的空间。例如,新页的分配将被记录,而插入页中的数据则不被记录。在 SQL Server 2000 中,大容量操作由大容量装载(BCP 和 BULK INSERT,包括当他们在 DTS 包中运行时)、SELECT INTO、CREATE INDEX、WRITETEXT 和 UPDATETEXT 组成。

  与完全故障恢复模式相比,大容量记录故障恢复模式减少了对大容量操作的日志记录。请记住,在需要进行故障恢复时,如果日志被损坏或在最近一次日志备份后又进行了大容量操作,则在最后一次日志备份后对数据库进行的更改将会丢失。

  此模式不支持恢复到指定的时点,但它允许恢复到包含大容量更改的事务日志备份的末尾。使用大容量记录故障恢复模式进行的事务日志备份包含由大容量操作修改的扩展。此功能改善了对日志传送的支持,因为不用担心备份在大容量操作后会变为无效。SQL Server 维护映射以跟踪修改的数据扩展,这样做,可优化 SQL Server 用于标识更改的进程。

  改善的备份功能

  除引入故障恢复模式以简化常规数据保护外,SQL Server 2000 还改善了管理特性:快照技术、差异备份和安全性都已得到加强。 

  事务日志备份链永远不会断开。在 SQL Server 7.0 中,某些操作(如向数据库中添加文件)会中断日志链,并且要求以后进行完全数据库备份。 

  备份操作不会与应用程序或其他管理操作发生冲突。例如,备份可与大容量操作(如创建索引和批处理装载)同时进行。 

  日志和文件备份可以同时进行。 

  无论系统正在进行何种活动,SQL Server 2000 都对无人值守备份操作实现良好的支持。

  SQL Server 支持与独立硬件和软件供应商共同完成的快照备份和恢复技术。快照备份使得在进行备份时占用的系统资源最少,甚至可以不占用资源。这种技术对于中型或大型数据库尤其有益,因为在这种环境中可用性是非常重要的。这种技术的主要优势在于: 

  可在非常短的时间(通常可以秒计)内创建备份,基本上不会对服务器造成任何影响。 

  可以使用磁盘备份非常快地恢复数据库。 

  另一台主机可创建备份,且不会对生产系统造成影响。 

  可以立即创建生产数据库的副本,以用于报告或测试目的。 

  快照备份和恢复技术是与第三方硬件和/或软件供应商协作共同完成的,这些供应商使用了 SQL Server 2000 为实现该技术而提供的特定功能。备份技术通常使用拆分磁盘镜像集的方法,创建要备份的数据的即时副本。在恢复时,原有数据就可立即投入使用。基本磁盘的同步是在后台进行的,因此几乎可以实现即时恢复。

  差异数据库备份的运行时间与上次完全备份后数据更改的总量成正比。数据更改越少,备份越快。SQL Server 2000 使用映射跟踪自最近一次数据库或文件备份后发生更改的数据扩展,以确保可更有效地定位这些扩展。此外,SQL Server 2000 支持文件差异备份。

  备份仍会收集自最近一次完全备份后对数据库进行的更改,运作方式与故障恢复相同。然而,这种备份是非常快的,因为它们只记录少部分更改过的信息,尤其是当数据库非常大而更改的数据又非常少时。

  为确保安全,可以使用密码保护备份媒体和备份集。这样就可防止未授权的用户在备份中添加数据或恢复数据库。

  六. 增强的管理功能

  在 SQL Server 2000 中,存储引擎的若干管理功能得到了加强。

  数据库验证

  DBCC 提供了各种管理能力,包括验证数据库一致性的 CHECK 命令。

  使用 SQL Server 7.0 和 SQL Server 2000 的经验表明,数据库的不一致性是由硬件问题引起的,但是数据库引擎或应用程序在正常操作中不一定能检测到这种问题。这种情况更可能出现在不经常访问的数据上。为解决这种问题,SQL Server 2000 引入一种检查模式 Physical_Only,可以探测到绝大部分由硬件引发的问题。探测过程非常快,速度与磁盘扫描速度相当,并且其对资源的消耗也很小。

  由于 SQL Server 存储引擎在基础体系结构上的改进(从 SQL Server 7.0 开始),已不必在常规维护时进行数据库验证。然而,Microsoft 仍然将数据库验证工具作为管理任务关键数据的重要组成部分。Microsoft 建议: 

  根据对基本硬件(特别是磁盘子系统)的信心,不定期运行 Physical_Only 检查。 

 
 2003-6-7 19:36:16    在关键时刻,如在硬件或软件升级时,或怀疑出现任何问题时,进行完整数据库检查。 

  Microsoft 不推荐在进行常规维护时执行完整数据库检查。

  SQL Server 2000 还在数据库验证方面作出了如下重大改进: 

  默认情况下,检查可以在联机状态下完成。联机检查对事务工作负载的影响很小。这种影响的大小取决于系统负载、硬件配置和 tempdb 的速度。Microsoft 的实验结果表明,对于中等 OLTP 工作负载(50% 的 CPU 使用率),这种影响为 15% 到 20%。提供的 TABLOCK 选项会强制检查索取共享表锁,这可以使检查的运行速度更快,但会妨碍更新。 

  检查操作在对称的多处理器 (SMP) 计算机中是以并行方式完成的,它受限于在该 SQL Server 实例中设置的最大并行程度。 

  SQL Server 2000 检查命令继续支持 SQL Server 7.0 中引入的修复功能。在某些情形,脱机修复可以替代备份恢复。

  数据库状态控制

  SQL Server 2000 对 ALTER DATABASE 语句进行了改进,改进后的语句允许通过 Transact-SQL 对数据库状态实现更多的控制。现在,所有数据库选项都可通过 ALTER DATABASE 命令进行灵活修改;在以后的版本中,将不再更新 sp_dboption 和 databaseproperty()。Transact-SQL 命令 sp_helpdb 和 DatabasePropertyEx() 提供有关数据库状态的更多信息。

  下表列出了数据库状态选项。


选项类型 可用设置 
用户权限 SINGLE_USERRESTRICTED_USERMULTI_USER 
可用性 ONLINEOFFLINE 
更新能力 READ_ONLYREAD_WRITE 


  SQL Server 还根据数据库中的条件设置以下状态:复原 (restoring)、恢复 (recovering) 和待定 (suspect)。数据库选项可通过以下方式进行设置:ALTER DATABASE 语句的 SET 子句、sp_dboption 系统存储过程或 SQL Server Enterprise Manager(在某些情况下)。

  在数据库状态发生变化后,更改数据库状态的会话仍然保持连接,而与新状态不一致的会话可被终止,并且其事务将被回滚。会话终止选项如下: 

  立即终止 

  在指定时间后终止 

  允许活动进程正常完成 

  检查活动,如果发现活动用户会话,则忽略状态更改 

  如下为语法的两个示例:

  alter database accting set read_only with rollback immediate
  alter database accting set single_user with rollback after 60 seconds

  系统进程 ID 和工作单元

  管理方面的另一个改进是 KILL 命令,该命令在停止进程时使用。改进后的 KILL 命令具有状态反馈。所以,如果要了解 KILL 命令的状态,请运行以下命令:

  KILL SPID WITH STATUSONLY

  在试图停止已由其它 KILL 命令停止的系统进程 ID (SPID) 时,系统将返回相同的状态信息。

  在 SQL Server 2000 中,MS DTC 事务可以在没有相关连接或 SPID 的情况下 存在。因此,在等待事务或工作单元完成之前,连接可由其他进程使用。当 MS DTC 事务管理器发送消息声明任务已完成时,您可以提交事务,也可以回滚事务。这就叫做一个工作单元 (UOW),它是 MS DTC 用于事务的事务标识符。UOW 没有 SPID。 

 
 2003-6-7 19:37:22    动态调优

  在 SQL Server 2000 中,基于使用的性能优化是动态管理的,无需手动调整。静态参数已被去除,但仍然保留了对某些资源的管理控制(例如,设置 SQL Server 可以使用的最大内存数)。相对于根据平均值和估计值进行手工计算的系统,这种方法更加精确,反应更加快捷。这样,您可以将注意力集中在数据库管理的设计方面。传统的数据库系统需要大量的手工管理和调优工作。例如,为了根据使用情况来优化系统,DBA 必须监视系统,不断记录大量的统计数据,以便选择可提供最佳系统性能的静态设置。然后,DBA 要重新评估系统以确定新设置的效果,接着又从头开始调优过程。

  SQL Server 2000 在存储引擎中引入了动态算法,它可主动监视服务器的使用情况,并在内部调整设置。SQL Server 2000 中的动态反馈和分析可将设置保持在绝对优化值的 10% 以内(参见图 3),从而使系统的性能更优,适应性更强。
七. 数据存储组件

 

  SQL Server 2000 协同 Windows 2000 操作系统平衡所有可用 CPU 的工作量。如果正在运行一个特定的 SQL Server 实例,并且其他应用程序未占用相同的资源,请将处理器相关设置保持为默认值,以便使全部的处理器得到充分利用。SQL Server 可利用多个处理器上的并行处理能力执行查询、索引建立、DBCC 和其他操作。SQL Server 2000 标准版最多可支持四个处理器和 2 GB 物理内存 (RAM)。企业版提高到新的水平,支持多达 32 个处理器和 64 GB 物理内存 (RAM)。

  SQL Server 实例的主内存源称为它的内存池。在 SQL Server 实例中几乎所有使用内存的数据结构都是从内存池分配的。从内存池分配的对象示例包括缓冲区高速缓存(其中存储最近读取的数据)和过程高速缓存(其中存储最近的执行计划)。 

  内存池中的分配是高度动态的。为优化性能,SQL Server 不断调整分配给不同区域的内存池大小。例如,当存储的执行计划的数量很少时,会通过将更多可用内存分配给数据高速缓存来调整内存池,从而优化资源的使用。

  SQL Server 2000 尽可能使用内存以减少磁盘 I/O。为此,SQL Server 在物理内存 (RAM) 中使用缓冲区高速缓存装载最近引用的数据,这样,这些数据可被重复使用。减少磁盘 I/O 和提供数据库系统速度的潜在方法是增加 SQL Server 可用的物理内存 (RAM)。

  通常,内存设置不需要任何调整。然而,在某些情况下可以对它们进行控制。例如,当在同一服务器上运行 SQL Server 的多个实例时,特别是在使用故障转移群集时,需要特别关注内存。如果在运行 SQL Server 的服务器上运行其他应用程序,也需要监视内存的使用情况。 

 
 2003-6-7 19:38:17      SQL Server 2000 利用 Windows 2000 的新功能,可寻址超过 3GB 的物理内存 (RAM) 。参见图 4。SQL Server 2000 企业版可以使用 Windows 2000 Advanced Server 或 Windows 2000 Datacenter Server 所允许的内存量。

  文件、文件组和磁盘

  SQL Server 在磁盘文件中存储数据和日志。在基本安装中,默认情况下,创建的数据和日志文件保存在服务器配置中指定的默认位置。然而,为了获得最优的性能和管理能力,可以应用以下几条基本原则: 
尽可能将数据分布到多个磁盘、信道和控制器中。 

  通常,磁盘越多(无论其单个容量),访问磁盘(控制器和信道)的速度以及存储引擎读写数据的速度也就越快。系统使用量越大,数据文件与日志文件的分离程度(将它们存储在不同的物理驱动器上)也就越重要。此外,由于 tempdb 的使用已发生变化,所以应该将 tempdb 存储在大磁盘集上;例如,与数据文件存放在一起或一组磁盘上。 

  使用文件组,使企业数据库更易于管理。 

  每个数据库都以一个默认的文件组开始。由于 SQL Server 2000 可在不附加文件组的情况下高效工作,因此许多系统都无需添加用户定义的文件组。然而,随着系统的增长,使用附加的文件组可提供更高的管理能力,当然这要求由称职的 DBA 实施和维护。

  在 SQL Server 2000 中,如果将数据库上的特定文件组设置为只读,则该文件组中的数据不能更改,但仍可以管理诸如权限等目录信息。

  注意:在 SQL Server 2000 中,数据库引擎中的异步 I/O 操作数实现了动态管理,并且不受使用的文件或文件组个数的影响,这一点与 SQL Server 7.0 相同。

  实施或优化数据库设计时,数据库管理员(数据库系统工程师)需要考虑数据库存储组件的配置,尤其是物理和逻辑磁盘的布局、数据库文件在磁盘中的排列。

  八. 总结

  灵活性的增强和性能控制的提高,使数据库管理员可以在掌握数据库技术的使用技巧和丰富数据库的使用经验时,将精力集中在管理数据库代码、设计和存储组件等方面,并以此作为数据库系统管理的最佳途径。SQL Server 2000 数据库引擎为各种数据库实现提供了通用的可扩展性和灵活性。 

作者:TerryLee
出处:http://terrylee.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
posted @ 2008-08-18 11:19 InsistYML 阅读(90) | 评论 (0)编辑
     摘要: Compilation and Deployment in ASP.NET 2.0By Rick Strahlhttp://www.west-wind.com/Last Update: February 12, 2008Compilation and deployment in ASP.NET 2.0 has brought some of the biggest changes to the A... 阅读全文
posted @ 2008-08-04 16:05 InsistYML| 编辑

(转载)在本篇文章的第一部分:[原创]深入剖析ASP.NET的编译原理之一:动态编译(Dynamical Compilation),详细讨论了ASP.NET如何进行动态编译的,现在我们来谈谈另外一种重要的编译方式:预编译(Precompilation)。

1.为什么要进行预编译

ASP.NET 2.0的编译方式大体可以分成两种:动态编译和预编译,要回答为什么要进行预编译,我们先要看看动态编译有什么不好的地方。我们回顾一下上一篇介绍的ASP.NET进行动态编译的简单的流程:当来自Brower的一个基于aspx的Http request抵达Web server,IIS handle这个request,通过分析注册在IIS中的Application Mapping,将Request 传给aspnet_isapi.dll ISAPI extension。ISAPI extension通过HttpRuntime进入Http Runtime Pipeline,HttpRuntime为每个Request创建一个单独的HttpContext对象,用于保存request的Context信息。在Http Runtime Pipeline中,Http request会被注册的一系列的Http module处理,比如OutputCache Module,Session Module,Authentication Module,Authorization,ErrorHandler Module等等。在Pipeline的终端,ASP.NET需要需要根据request创建对应的HttpHandler对象来处理该Request,并生成结果Response到Client。对于一个基于Aspx的Http request,对应的Http handler对象一般就是一个System.Web.UI.Page对象。

ASP.NET会先判断对应的Page type是否存在于被Cache的Assembly中,如果存在,直接创建Page对象,否则ASP.NET会先对该Page的相关的Source code (包括code behind,html等等) 进行编译,我们也说过这种编译是一Directory为单位的,也就是说,处于同一个Directory下的需要编译的文件会被编译成到同一个Assembly中。编译生成的Assembly会被Cache,用于后续的Request。

正是因为对资源的首次访问会导致一次编译(这样说不太准确,因为动态编译是以directory为单位进行的,应该对对某个Directory下的资源进行首次访问),这样会严重降低Web Application的响应速度。所以我们为了避免这种情况,需要预先对web site进行编译,所以提高web site的响应是进行预编译的最重要的原因。

同时动态编译就以为着Web server上放置的是Source code,而且他们是可被修改的。而对于一个开发完毕的Web Application,我们更希望以Binary Assembly的方式进行部署,这样Server上部署的都是Binary Assembly,不怕被别人篡改而导致系统的崩溃,从知识产权来讲,也更利于保护商业秘密。这也是我们为什么要进行预编译的另一个原因。

下面我们就来讲讲如何进行预编译,以及与编译背后的原理。同时在这里我需要特别提出的是,在上一部分讲的一些术语和原理,比如Preservation file,FastObjectFactory,同样适用于预编译,重复的内容,在这里就不必再介绍了。同时我也将沿用上一部的Sample。如果想看看相关的内容,请参阅[原创]深入剖析ASP.NET的编译原理之一:动态编译(Dynamical Compilation)。

2.In Place Pre-compilation V.S. Pre-compilation for Deployment

对于预编译,有可以分为In Place Pre-compilation和Pre-compilation for Deployment,In Place Pre-compilation很简单,实际上就是把整个Web site编译到我们一个临时的目录下面,这个临时目录也就是我们在介绍动态编译提到的那个临时目录。而且这个编译的方式,包括生成的文件也和动态编译完全一样,唯一不同就是编译的时间:预先编译,编译的范围:整个Web site。这种编译就是你常用的在VS的build。这种编译方式一般用于开发阶段。

为了部署为目的的编译是我们今天讨论的重点,下面我们就着重来讨论Pre-compilation for Deployment。

注:在ASP.NET的编译都是通过一个叫做aspnet_compiler的工具执行的,该工具随ASP.NET 2.0一起发布,你完全可以利用此工具以命令行的方式的执行编译,并通过传递不同的命令行开关设置不同的编译选项。该工具被置于了VS中,使你可以利用VS进行可视化的编译。

3.Non-updatable Pre-compilation V.S. Updatable Pre-compilation

ASP.NET 2.0为我们提供了几种不同方式的预编译和部署。为了弄清楚这些预编译和部署方式,我们先来回顾一下ASP.NET 1.x下的编译方式。我们知道在ASP.NET 1.x时代对整个Web site进行编译,实际上我们只会对所有C#和VB.NET等后台代码进行编译,并生成一个单一的Assembly。而Web page的aspx是不会参与编译的。所以当我们访问一个Web page的时候,ASP.NET必须对aspx进行动态编译。

这一切之所以能够进行是因为Web page采用的是aspx + code behind的模式。

<%@ Page Language="C#" AutoEventWireup="false" 
    Codebehind
="Default.aspx.cs"
    Inherits
="Default" %>

 

 

从上面我们可以看到aspx和Code behind是一种继承的关系,aspx继承和它对应的Code Behind。ASP.NET可以把Code behind和aspx分开进行编译,把它们编译到不同的Assembly中。我们就是上面的Code为例,
我们现在若对该Web site进行编译的话,Default.aspx.cs会被编译到一个Assembly中,假设这个Assembly为App_Web.dll. 我们把该Dll和aspx部署到Production Server上。如果我们现在访问defaut.aspx。ASP.NET
会对aspx进行动态编译,生成的Assembly可以暂时成为App_Web_aspx.dll。对于Default.aspx,如果我们如C#代码来描述的话,应该像下面一样定义:

 这种编译方式,我自己把它叫做对asXx的动态编译。在ASP.NET2.0 中也沿用了这种编译方式。这种编译方式的主要特征是对Code behind和所有的后台代码进行预编余,aspx(确切地说应该是asXx:asax,asmx,asax等)原样部署。由于这种方式的预编译,asXx是可以修改的(当然这种修改是有一定限制的,因为code behind已经编译好了,所以这种修改只可能是和code behind无关的修改),所以又叫做Updatable Pre-compilation。

除了Updatable Pre-compilation之外,ASP.NET还提供另外一种高效的预编译方式,Non-updatable Pre-compilation,之所以叫做不可修改的预编译,这是因为:这种编译方式把asXx、Code behind、后台代码甚至是部分Resource都进行预编译,从而避免了运行时对asXx的动态编译,从而最大程度地提高了整个Web site的响应。在部署的时候,我们除了把生成的Assembly进行部署之外,所有的通过编译生成的asXx也必须进行部署。 不过需要特别说明的是,此时的asXx文件仅仅是一个占位的文件而已,它里面不具有任何HTML。

4.Partial class

在ASP.NET 1.x,由于采用的aspx + code behind的机制,对于任何一个Web page或者其他ASP.NET 基于axXx的对象来说,都是由两个文件、两个class组成。两个文件是指axXx和code behind,两个class是指Code behind定义的继承自System.Web.UI.Page的class,和一个继承自它的由axXx生成的class。

对于使用过ASP.NET 1.x来说,一定会很熟悉这样一种情况:对于每个在aspx中通过HTML定义的Server Control,在Code behind中必须具有一个对应的protected成员,否则你不能通过编程的方式访问这个Server control。以不同方式呈现的同一个Server control通过ID关联起来,如果在Code behind中改了Server control的ID,Server control的Server端的Event handler将会失去原有的作用。

但是在ASP.NET 2.0来说,这种情况发生了改变,在aspx中的Server control在Code behind中却没有相应的成员变量,但是我们可以毫无障碍地访问到每个Server control。这使得我们的code behind更加简洁,通过避免了Server control在aspx和code bebind中的不匹配的问题。这一切都得益于.NET Framework 2.0提供的partial class的机制:把同一个class分布于不同文件中进行定义。有了这个概念,我们来看ASP.NET 2.0的code behind机制。

比如我们有这样的一个Page:

 

Code behind如下:

 

 而实际上,ASP.NET会为我们创建一个隐藏的.cs文件(这个文件有人 把它称之为Sibling partial class):

 

 这个文件会随着aspx文件的改变而动态变化,所以code behind中的Server control永远和aspx中的Server control是完全匹配的。所以我们说ASP.NET 2.0的Page是由3个文件、两个class组成的。

5.编译的粒度和Assembly的命名

到现在为止,我们所讲的ASP.NET的预编译都是以Directory为单位的,同一个Directory下的所有需要编译的文件被编译到同一个Assembly中。ASP.NET还支持以Page为单位的预编译,也就是每个Page编译成一个Assembly。

在默认的情况下,ASP.NET预编译生成的Assembly名称是随机生成的,也就是每次生成的Assembly都具有不同的name。所以我们在部署Web site的时候,一般需要把原来的Assembly删掉,再部署新的Assembly。不过ASP.NET为我们提供了另外一种选择,使得每次编译生成的Assembly具有相同的名称,这样我们部署的时候就可以直接把新的Assembly 拷贝到Production Server上,自动覆盖掉同名的Assembly。

6.  Sample

我们沿用上一部份是用的Sample,我们通过采用不同的预编译方式看看程序将如何运行。

    6.1 Non-updatable Pre-compilation

我们采用如上图所示的默认的发布方式,ASP.NET 将会进行Non-updatable Pre-compilation。浏览目标文件夹,我们会发现 如下的文件结构.


除了多了一个Bin目录和PrecompiledApp.config之外,整个结构和Source code中的结构完全一样。通过上面的分析,我们知道这种预编译方式是将asXx、code behind、后台代码已经Resource一起编译成Assembly。我们说过对于这样的预编译方式,aspx仅仅是一个站位的文件而以,其中HTML已经没有任何意义了,那么对于编译后的aspx中到底是什么东西呢。我们来一探究竟。打开每个aspx都是一段如下如下一样文字,并无任何HTML。

 

This is a marker file generated by the precompilation tool, and should not be deleted!

 

PrecompiledApp.config里面具有一段简短的configuration,表明version和是否可以进行进一步的修改。

 

<precompiledApp version="2" updatable="false"/>

 

所有的Assembly被编译到Bin目录中,我们来看看到底生成了一些什么样的文件在Bin目录中。

在Bin目录由两类文件构成:Assembly和以complied作为扩展名的Preservation file。Preservation file的内容和作用在第一部分已经详细介绍过了,相信大家不会感到陌生。Preservation file在这里和动态编译所起的作用一样。唯一有一点不同的是,他的结构更加简洁,去掉的Dependence file的列表,因为对于Non-updatable Pre-compilation来说,每个Page的以来的文件都是不可更改的。

<preserve resultType="6" virtualPath="/Artech.ASPNETDeployment/App_Code/" hash="439abe7d" filehash="" flags="140000" assembly="App_Code" />

我们来运行以下程序,和动态编译情况下的输出结果比较,看看有什么不同。我们照例先运行Default Page。

 

输出的结果印证我们前面的讨论:处于同一个目录下的Default 和Default2被编译到同一个Assembly中,关注于处理逻辑的code behind的class name为Default和Default2,关注与可视化界面render的aspx对应的class name被加上的_aspx后缀,如果对default_aspx和default2_aspx进行Reflect的话,你会发现他们分别继承Default和Default2,而后者直接继承自System.Web.UI.Page。所以default_aspx和default2_aspx是真正的意义上基于Web page的Http handler。像动态编译一样,预编译生成一个基于Assembly的FastObjectFactory Type,对该对象的描述请参照第一部分。

有了前面的理论基础,相信大家已经猜到这时候,我浏览Part I下的Page1和Page2时的输出是什么 样子,由于预编译是以目录为单位的,我们对Part I下的任何一个page的访问,都会加载相同的Assembly,所以此时对这两个Page的访问会得到一样的输出结果:


   6.2 Updatable Pre-compilation

接下来我们来对Web Page进行Updatable Pre-compilation,相关的编译设置如下:选择Allow this precompiled site to be updatable。


生成的文件及其结构和进行Non-updatable Pre-compilation,不同的又一下3点:

  • PrecompiledApp.config:updatable被设置为true。
    <precompiledApp version="2" updatable="true"/>
  • asXx和我们进行开发时内容一样,你如aspx包含的就是HTML,我们可以在部署之后对他们进行和code behind无关的修改。
  • Preversation file中有加上了Page对应的dependence file列表。
    <?xml version="1.0" encoding="utf-8"?>

    <preserve resultType="9" virtualPath="/Artech.ASPNETDeployment/App_GlobalResources/" hash="439abe7d" filehash="ff21249472dbf6cb" flags="140000" assembly="App_GlobalResources" resHash="1cba48dd56e28538" />

我们来运行一下Web site,看看现在的输出结果又有何不同。首先打开Default Page:


通过上图,我们发现此时加载了两个相关的Assembly。我们来分析一下为什么会这样。在分析Updatable Pre-compilation时,我们说过:asXx是不会才与编译的,只有他们的code behind, 所有的后台代码,资源文件才会本编译。对于一个page 来说,page的code behind被编译到Assembly中,aspx则不会。Aspx在运行时实行动态编译,所以aspx是可被修改的。在本例中,我们访问Default Page,ASP.NET先对aspx进行编译,其对应的class name为default_aspx,由于default_aspx继承与Default,并且Default存在于预编译生成的Assembly中,所以这个Assembly被加载进来。

由于同一个page最重本编译到两个不同的Assembly中,所以我们此时访问Part I中的Page1或者Page2,又会有两个Assembly被加载进来:


     6.3 以Page为单位进行预编译

前面我们进行的都是以directory为单位的预编译,现在我们缩小编译的粒度,以Page为单位进行编译。我们选择了“Use fixed naming and single page assemblies”选项。那么现在进行的是 基于单个page的non-updatable pre-compilation。通时由于采用的是fixed naming的编译方式,每次进行编译生成的Assembly的名称都是一样的。



现在我们来看看,编译之后生成的Assembly:

我们看到编译器为每个Page生成了一个单独的Assembly。此时运行程序,你看到的又将不同。如果此时你访问Default Page,你将看到:


是不是和上面不同,Assembly只有Default对应的两个Type,没有了处于同一个目录下的Default2的Type。因为Default2有它独自的Assembly .所以你该会想到,如果我们现在每访问一个没有被访问过的Page,就会有一个新的Assembly被加载。比如我么访问Part I下的Page1:


     6.4编译强类型的Assembly

我们知道可以通过一个Public key/Private key pair对Assembly进行签名,进而把它部署到GAC中,我们来看看如何做。

首先我通过SN.exe生成Public key/Private key pair并保存到一个文件中(比如D:\MyKey.keys),然后进行如下的编译设置


那么我们进行编译就会生成强类型的Assembly。我们可以运行我们的程序来证明:


每个Assembly有具有一样的PublicKeyToken,因为我们使用的一样的Public key/Private key pair进行对每个Assembly签名的。

 

 

 

 

posted @ 2008-07-29 15:36 InsistYML 阅读(129) | 评论 (0)编辑

(转载)

 

C#中提供了三种类型的计时器:
1、基于 Windows 的标准计时器(System.Windows.Forms.Timer)
2、基于服务器的计时器(System.Timers.Timer)
3、线程计时器(System.Threading.Timer)
下面我就通过一些小实验来具体分析三种计时器使用上面的异同点,特别是和线程有关的部分。
实验例子截图:

一、基于 Windows 的标准计时器(System.Windows.Forms.Timer)
首先注意一点就是:Windows 计时器是为单线程环境设计的
此计时器从Visual Basic 1.0 版起就存在于该产品中,并且基本上未做改动
这个计时器是使用最简单的一种,只要把工具箱中的Timer控件拖到窗体上,然后设置一下事件和间隔时间等属性就可以了

实验出来的结果也完全符合单线程的特点:
1、当启动此计时器后,会在下方子线程ID列表中显示子线程ID,并且和主线程ID相同

        private void formsTimer_Tick(object sender, EventArgs e)
        
{
            i
++;
            lblSubThread.Text 
+= "子线程执行,线程ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString() + "\r\n";
        }


2、当单击主线程暂停5秒后,子线程会暂停执行,并且当5秒之后不会执行之前被暂停的子线程,而是直接执行后面的子线程(也就是会少输出几行值)
            System.Threading.Thread.Sleep(5000);
3、在子进程的事件中暂停5秒会导致主窗口相应无响应5秒
4、定义一个线程静态变量:
        [ThreadStatic]
        private static int i = 0;

在子线程事件中每次加一,再点击线程静态变量值会得到增加后的i值

二、基于服务器的计时器(System.Timers.Timer)
System.Timers.Timer不依赖窗体,是从线程池唤醒线程是传统的计时器为了在服务器环境上运行而优化后的更新版本
在VS2005的工具箱中没有提供现成的控件,需要手工编码使用此计时器
使用方式有两种,
1、通过SynchronizingObject属性依附于窗体

            System.Timers.Timer timersTimer = new System.Timers.Timer();
            
            timersTimer.Enabled 
= false;
            timersTimer.Interval 
= 100;
            timersTimer.Elapsed 
+= new System.Timers.ElapsedEventHandler(timersTimer_Elapsed);

            timersTimer.SynchronizingObject 
= this;

通过这种方式来使用,实验效果几乎和基于 Windows 的标准计时器一样,只是在上面的第二条实验中,虽然也会暂停子线程的执行,不过在5秒之后把之前排队的任务都执行掉(也就是不会少输出几行值)
2、不使用SynchronizingObject属性
这种方式就是多线程的方式了,即启动的子线程和主窗体不在一个线程。不过这样也存在一个问题:由于子线程是单独的一个线程,那么就不能访问住窗体中的控件了,只能通过代理的方式来访问:

        delegate void SetTextCallback(string text);
        .
        .
        
void timersTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        
{
            
//使用代理
            string text = "子线程执行,线程ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString() + "\r\n";
            SetTextCallback d 
= new SetTextCallback(SetText);
            
this.Invoke(d, new object[] { text });
            i
++;
        }


        
private void SetText(string text)
        
{
            lblSubThread.Text 
+= text;
        }

这样我们再次实验就会得到如下的结果:
1、当启动此计时器后,会在下方子线程ID列表中显示子线程ID,并且和主线程ID不相同

2、当单击主线程暂停5秒后,子线程会一直往下执行(界面上可能看不出来,不过通过在子线程输出文件的方式可以很方便的看出来)
3、在子进程的事件中暂停5秒不会导致主窗口无响应
4、在子线程事件中每次给线程静态变量加一,再点击线程静态变量值得到的值还是0(不会改变主窗口中的线程静态变量)

 三、线程计时器(System.Threading.Timer)
 线程计时器也不依赖窗体,是一种简单的、轻量级计时器,它使用回调方法而不是使用事件,并由线程池线程提供支持。
对消息不在线程上发送的方案中,线程计时器是非常有用的。
使用方法如下:

        System.Threading.Timer threadTimer;
        
public void ThreadMethod(Object state)
        
{
            
//使用代理
            string text = "子线程执行,线程ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString() + "\r\n";
            SetTextCallback d 
= new SetTextCallback(SetText);
            
this.Invoke(d, new object[] { text });
            i
++;
        }


        
private void Form1_Load(object sender, EventArgs e)
        
{
            threadTimer 
= new System.Threading.Timer(new System.Threading.TimerCallback(ThreadMethod), null-1-1);
        }



暂停代码:
                threadTimer.Change(-1, -1);

实验的效果和基于服务器的计时器(System.Timers.Timer)的第二种方式是一样的,
当然具体的使用方法和原理是不一样的,最主要的就是这种方式使用的是代理的方式而不是事件的方式,并且可以不依赖于窗体和组件而单独执行


下面列出老外总结的一张表(三种方式的区别):

Feature description System.Timers.Timer System.Threading.Timer System.Windows.Forms.Timer
Support for adding and removing listeners after the timer is instantiated. Yes No Yes
Supports call backs on the user-interface thread Yes No Yes
Calls back from threads obtained from the thread pool Yes Yes No
Supports drag-and-drop in the Windows Forms Designer Yes No Yes
Suitable for running in a server multi-threaded environment Yes Yes No
Includes support for passing arbitrary state from the timer initialization to the callback. No Yes No
Implements IDisposable Yes Yes Yes
Supports one-off callbacks as well as periodic repeating callbacks Yes Yes Yes
Accessible across application domain boundaries Yes Yes Yes
Supports IComponent – hostable in an IContainer Yes No Yes

posted @ 2008-07-25 16:32 InsistYML 阅读(42) | 评论 (0)编辑
     摘要: (转载)相信以前用过VB、Delphi,特别是VC的程序员应该对钩子程序都不陌生。在C#中我们同样可以使用钩子程序来实现特殊效果,比如当用户按下某个特殊键时提示,比如关闭应用程序前提示等。当然使用方法相对VC来说要稍微复杂一点,有的地方还不太方便,下面的例子中实现两个基本功能:1、按下Alt+F4时使窗口最小化2、关闭应用程序前提示不过目前只能捕获消息,不能屏蔽消息,我正在实验,也希望知道的高手能... 阅读全文
posted @ 2008-07-25 16:28 InsistYML| 编辑

(转载)

我写的.Net Remoting系列专题:

Microsoft .Net Remoting系列专题之一:.Net Remoting基础篇

Microsoft .Net Remoting系列专题之二:Marshal、Disconnect与生命周期以及跟踪服务

前言:在Remoting中处理事件其实并不复杂,但其中有些技巧需要你去挖掘出来。正是这些技巧,仿佛森严的壁垒,让许多人望而生畏,或者是不知所谓,最后放弃了事件在Remoting的使用。关于这个主题,在网上也有很多讨论,相关的技术文章也不少,遗憾的是,很多文章概述的都不太全面。我在研究Remoting的时候,也对事件处理发生了兴趣。经过参考相关的书籍、文档,并经过反复的试验,深信自己能够把这个问题阐述清楚了。
本文对于Remoting和事件的基础知识不再介绍,有兴趣的可以看我的系列文章,或查阅相关的技术文档。

本文示例代码下载:

Remoting事件(客户端发传真)

Remoting事件(服务端广播)

Remoting事件(服务端广播改进)

应用Remoting技术的分布式处理程序,通常包括三部分:远程对象、服务端、客户端。因此从事件的方向上看,就应该有三种形式:
1、服务端订阅客户端事件
2、客户端订阅服务端事件
3、客户端订阅客户端事件

服务端订阅客户端事件,即由客户端发送消息,服务端捕捉该消息,然后响应该事件,相当于下级向上级发传真。反过来,客户端订阅服务端事件,则是由服务端发送消息,此时,所有客户端均捕获该消息,激发事件,相当于是一个系统广播。而客户端订阅客户端事件呢?就类似于聊天了。由某个客户端发出消息,其他客户端捕获该消息,激发事件。可惜的是,我并没有找到私聊的解决办法。当客户端发出消息后,只要订阅了该事件的,都会获得该信息。

然而不管是哪一种方式,究其实质,真正包含事件的还是远程对象。原理很简单,我们想一想,在Remoting中,客户端和服务端传递的内容是什么呢?毋庸置疑,是远程对象。因此,我们传递的事件消息,自然是被远程对象所包裹。这就像EMS快递,远程对象是运送信件的汽车,而事件消息就是汽车所装载的信件。至于事件传递的方向,只是发送者和订阅者的角色发生了改变而已。

一、 服务端订阅客户端事件
服务端订阅客户端事件,相对比较简单。我们就以发传真为例。首先,我们必须具备传真机和要传真的文件,这就好比我们的远程对象。而且这个传真机上必须具备“发送”的操作按钮。这就好比是远程对象中的一个委托。当客户发送传真时,就需要在客户端上激活一个发送消息的方法,这就好比我们按了“发送”按钮。消息发送到服务端后,触发事件,这个事件正是服务端订阅的。服务端获得该事件消息后,再处理相关业务。这就好比接收传真的人员,当传真收到后,会听到接通的声音,此时选择“接收”后,该消息就被捕获了。

现在,我们就来模拟这个流程。首先定义远程对象,这个对象处理的应该是一个发送传真的业务:
首先是远程对象的公共接口(Common.dll):
public delegate void FaxEventHandler(string fax);
public interface IFaxBusiness
{
    void SendFax(string fax);
}
注意,在公共接口程序集中,定义了一个公共委托。

然后我们定义具体处理传真业务的远程对象类(FaxBusiness.dll),在这个类中,先要添加对公共接口程序集的引用:
public class FaxBusiness:MarshalByRefObject,IFaxBusiness

 public static event FaxEventHandler FaxSendedEvent;

 #region

 public void SendFax(string fax)
 {
  if (FaxSendedEvent != null)
  {
   FaxSendedEvent(fax);
  }
 }

 #endregion

 public override object InitializeLifetimeService()
 {
  return null;
 }
}
这个远程对象中,事件的类型就是我们在公共程序集Common.dll中定义的委托类型。SendFax实现了接口IFaxBusiness中的方法。这个方法的签名和定义的委托一致,它调用了事件FaxSendedEvent。
特殊的地方是我们定义的远程对象最好是重写MarshalByRefObject类的InitializeLifetimeService()方法。返回null值表明这个远程对象的生命周期为无限大。为什么要重写该方法呢?道理不言自明,如果生命周期不进行限制的话,一旦远程对象的生命周期结束,事件就无法激活了。
接下来就是分别实现客户端和服务端了。服务端是一个Windows应用程序,界面如下:


 
我们在加载窗体的时候,注册通道和远程对象:
private void ServerForm_Load(object sender, System.EventArgs e)
{
 HttpChannel channel = new HttpChannel(8080);
 ChannelServices.RegisterChannel(channel);

 RemotingConfiguration.RegisterWellKnownServiceType(
  typeof(FaxBusiness),"FaxBusiness.soap",WellKnownObjectMode.Singleton);
 FaxBusiness.FaxSendedEvent += new FaxEventHandler(OnFaxSended);
}

我们采用的是SingleTon模式,注册了一个远程对象。注意看,这段代码和一般的Remoting服务端有什么区别?对了,它多了一行注册事件的代码:
FaxBusiness.FaxSendedEvent += new FaxEventHandler(OnFaxSended);
这行代码,就好比我们服务端的传真机,一直切换为“自动”模式。它会一直监听着来自客户端的传真信息,一旦传真信息从客户端发过来了,则响应事件方法,即OnFaxSended方法:
public void OnFaxSended(string fax)
{
 txtFax.Text += fax;
 txtFax.Text += System.Environment.NewLine;
}
这个方法很简单,就是把客户端发过来的Fax显示到txtFax文本框控件上。

而客户端呢?仍然是一个Windows应用程序。代码非常简单,首先为了简便其见,我们仍然让它在装载窗体的时候,激活远程对象:
private void ClientForm_Load(object sender, System.EventArgs e)
{
 HttpChannel channel = new HttpChannel(0);
 ChannelServices.RegisterChannel(channel);

 faxBus = (IFaxBusiness)Activator.GetObject(typeof(IFaxBusiness),
  "http://localhost:8080/FaxBusiness.soap");
}
呵呵,可以说客户端激活对象的方法和普通的Remoting客户端应用程序没有什么不同。该写传真了!我们在窗体上放一个文本框对象,改其Multiline属性为true。再放一个按钮,负责发送传真:
private void btnSend_Click(object sender, System.EventArgs e)
{
 if (txtFax.Text != String.Empty)
 {
  string fax = "来自" + GetIpAddress() + "客户端的传真:"
+ System.Environment.NewLine;
  fax += txtFax.Text;
  faxBus.SendFax(fax);
 }
 else
 {
  MessageBox.Show("请输入传真内容!");
 }
}

private string GetIpAddress()
{   
 IPHostEntry ipHE = Dns.GetHostByName(Dns.GetHostName());
 return ipHE.AddressList[0].ToString();   
}

在这个按钮单击事件中,只需要调用远程对象faxBus的SendFax()方法就OK了,非常简单。可是慢着,为什么你的代码有这么多行啊?其实,没有什么奇怪的,我只是想到发传真的客户可能会很多。为了避免服务端人员犯糊涂,搞不清楚是谁发的,所以要求在传真上加上各自的签名,也就是客户端的IP地址了。既然要获得计算机的IP地址,请一定要记得加上对DNS的命名空间引用:
using System.Net;

因为我们严格按照分布式处理程序的部署方式,所以在客户端只需要添加公共程序集(Common.dll)的引用就可以了。而在服务端呢,则必须添加公共程序集和远程对象程序集两者的引用。

OK,程序完成,我们来看看这个简陋的传真机:
客户端:


 
嘿嘿,做梦都想放假啊。好的,传真写好了,发送吧!再看看服务端,great,老板已经收到我的请假条传真了!


 

二、 客户端订阅服务端事件

嘿嘿,吃甘蔗要先吃甜的一段,做事情我也喜欢先做容易的。现在,好日子过去了,该吃点苦头了。我们先回忆一下刚才的实现方法,再来思考怎么实现客户端订阅服务端事件?

在前一节,事件被放到远程对象中,客户端激活对象后,就可以发送消息了。而在服务端,只需要订阅该事件就可以。现在思路应该反过来,由客户端订阅事件,服务端发送消息。就这么简单吗?先不要高兴得太早。我们想一想,发送消息的任务是谁来完成的?是远程对象。而远程对象是什么时候创建的呢?我们仔细思考Remoting的几种激活方式,不管是服务端激活,还是客户端激活,他们的工作原理都是:客户端决定了服务器创建远程对象实例的时机,例如调用了远程对象的方法。而服务端所作的工作则是注册该远程对象。

回忆这三种激活方式在服务端的代码:
SingleCall激活方式:
RemotingConfiguration.RegisterWellKnownServiceType(
  typeof(BroadCastObj),"BroadCastMessage.soap",
  WellKnownObjectMode.Singlecall);
SingleTon激活方式:
RemotingConfiguration.RegisterWellKnownServiceType(
  typeof(BroadCastObj),"BroadCastMessage.soap",
  WellKnownObjectMode.Singleton);
客户端激活方式:
RemotingConfiguration.ApplicationName = “BroadCastMessage.soap”
RemotingConfiguration.RegisterActivatedServiceType(typeof(BroadCastObj));

请注意Register这个词语,它表达的含义就是注册。也就是说,在服务端并没有显示的创建远程对象实例。没有该实例,又如何广播消息呢?

或许有人会想,在注册远程对象之后,显式实例该对象不就可以了吗?也就是说,在注册后加上这一段代码:
BroadCastObj obj = new BroadCastObj();

然而,我们要明白一个事实:就是服务端和客户端是处于两个不同的应用程序域中。因此在Remoting中,客户端获得的远程对象实际是服务端注册对象的代理。如果我们在注册后,人工去创建一个实例,而非Remoting在激活后自动创建的对象,那么客户端获得的对象与服务端人工创建的实例是两个迥然不同的对象。客户端获得的代理对象并没有指向你刚才创建的obj实例。所以obj发送的消息,客户端根本无法捕捉。

那么,我们只有望洋兴叹,束手无策了吗?别着急,别忘了在服务器注册对象方法中,还有一种方法,即Marshal方法啊。还记得Marshal的实现方式吗?
BroadCastObj Obj = new BroadCastObj();
ObjRef objRef = RemotingServices.Marshal(Obj,"BroadCastMessage.soap");

这个方法与前不一样。前面的三种方式,远程对象是根据客户端调用的方式,来自动创建的。而Marshal方法呢?则显式地创建了远程对象实例,然后将其Marshal到通道中,形成ObjRef指向对象的代理。只要生命周期没有结束,这个对象就一直存在。而此时客户端获得的对象,正是创建的Obj实例的代理。

OK,这个问题解决了,我们来看看具体实现。
公共程序集和远程对象与前相似,就不再赘述,只附上代码:
公共程序集:
public delegate void BroadCastEventHandler(string info); 

public interface IBroadCast
{
 event BroadCastEventHandler BroadCastEvent;
 void BroadCastingInfo(string info);
}
远程对象类:
public event BroadCastEventHandler BroadCastEvent;

#region IBroadCast 成员

//[OneWay]
public void BroadCastingInfo(string info)
{
 if (BroadCastEvent != null)
 {
  BroadCastEvent(info);
 }
}

#endregion

public override object InitializeLifetimeService()
{
 return null;
}

下面,该实现服务端了。在实现之前,我还想罗嗦几句。在第一节中,我们实现了服务端订阅客户端事件。由于订阅事件是在服务端发生的,因此事件本身并未被传送。被序列化的仅仅是传递的消息,即Fax而已。现在,方向发生了改变,传送消息的是服务端,客户端订阅了事件。但这个事件是放在远程对象中的,因此事件必须被序列化。而在.Net Framework1.1中,微软对序列化的安全级别进行了限制。有关委托和事件的序列化、反序列化默认是禁止的,所以我们应该将TypeFilterLevel的属性值设置为Full枚举值。因此在服务端注册通道的方式就发生了改变:
private void StartServer()
{
 BinaryServerFormatterSinkProvider serverProvider = new
  BinaryServerFormatterSinkProvider();
 BinaryClientFormatterSinkProvider clientProvider = new
  BinaryClientFormatterSinkProvider();
 serverProvider.TypeFilterLevel = TypeFilterLevel.Full;

 IDictionary props = new Hashtable();
 props["port"] = 8080;
    HttpChannel channel = new HttpChannel(props,clientProvider,serverProvider);
 ChannelServices.RegisterChannel(channel);

 Obj = new BroadCastObj();
 ObjRef objRef = RemotingServices.Marshal(Obj,"BroadCastMessage.soap"); 
}

注意语句serverProvider.TypeFilterLevel = TypeFilterLevel.Full;此语句即设置序列化安全级别的。要使用TypeFilterLevel属性,必须申明命名空间:
using System.Runtime.Serialization.Formatters;

而后面两条语句就是注册远程对象。由于在我的广播程序中,发送广播消息是放在另一个窗口中,因此我将该远程对象声明为公共静态对象:
public static BroadCastObj Obj = null;

然后在调用窗口事件中加入:
private void ServerForm_Load(object sender, System.EventArgs e)
{
 StartServer();
 lbMonitor.Items.Add("Server started!");
}
来看看界面,首先启动服务端主窗口:


 
我放了一个ListBox控件来显示一些信息,例如显示服务器启动了。而BroadCast按钮就是广播消息的,单击该按钮,会弹出一个对话框:


 
BraodCast按钮的代码:
private void btnBC_Click(object sender, System.EventArgs e)
{   
 BroadCastForm bcForm = new BroadCastForm();
 bcForm.StartPosition = FormStartPosition.CenterParent;
 bcForm.ShowDialog();
}

在对话框中,最主要的就是Send按钮:
if (txtInfo.Text != string.Empty)
{  
 ServerForm.Obj.BroadCastingInfo(txtInfo.Text);
}
else
{
 MessageBox.Show("请输入信息!");
}
但是很简单,就是调用远程对象的发送消息方法而已。

现在该实现客户端了。我们可以参照前面的例子,只是把服务端改为客户端而已。另外考虑到序列化安全级别的问题,所以代码会是这样:
private void ClientForm_Load(object sender, System.EventArgs e)
{
 BinaryServerFormatterSinkProvider serverProvider = new
  BinaryServerFormatterSinkProvider();
 BinaryClientFormatterSinkProvider clientProvider = new
  BinaryClientFormatterSinkProvider();
 serverProvider.TypeFilterLevel = TypeFilterLevel.Full;

 IDictionary props = new Hashtable();
 props["port"] = 0;
 HttpChannel channel = new HttpChannel(props,clientProvider,serverProvider);
 ChannelServices.RegisterChannel(channel);

 watch = (IBroadCast)Activator.GetObject(
  typeof(IBroadCast),"http://localhost:8080/BroadCastMessage.soap"); 
 watch.BroadCastEvent += new BroadCastEventHandler(BroadCastingMessage);
}
注意客户端通道的端口号应设置为0,这表示客户端自动选择可用的端口号。如果要设置为指定的端口号,则必须保证与服务端通道的端口号不相同。
然后是,BroadCastEventHandler委托的方法:
public void BroadCastingMessage(string message)
{
 txtMessage.Text += "I got it:" + message;    
 txtMessage.Text += System.Environment.NewLine;   
}
客户端界面如图:


 
好,下面让我们满怀期盼,来运行这段程序。首先启动服务端应用程序,然后启动客户端。哎呀,糟糕,居然出现了错误信息!


 

“人之不如意事,十常居八九。”不用沮丧,让我们分析原因。首先看看错误信息,它报告我们没有找到Client程序集。然而事实上,Client程序集当然是有的。那么再来调试一下,是哪一步出现的问题呢?设置好断点,进行逐语句跟踪。前面注册通道一切正常,当运行到watch.BroadCastEvent += new BroadCastEventHandler(BroadCastingMessage)语句时,错误出现了!

也就是说,远程对象的创建是成功的,但在订阅事件的时候失败了。原因是什么呢?原来,客户端的委托是通过序列化后获得的,在订阅事件的时候,委托试图装载包含与签名相同的方法的程序集,也就是BroadCastingMessage方法所在的程序集Client。然而这个装载的过程发生在服务端,而在服务端,并没有Client程序集存在,自然就发生了上面的异常。

原因清楚了,怎么解决?首先BroadCastingMessage方法肯定是在客户端中,所以不可避免,委托装载Client程序集的过程也必须在客户端完成。而服务端事件又是由远程对象来捕获的,因此,在客户端注册的也就必须是远程对象事件了。一个要求必须在客户端,一个又要求必须在服务端,事情出现了自相矛盾的地方。

那么,让我们先想想这样一个例子。假设我们要交换x和y的值,该这样完成?很简单,引入一个中间变量就可以了。
int x=1,y=2,z;
z = x;
x = y;
y = z;
这个游戏相信大家都会玩吧,那么好的,我们也需要引入这样一个“中间”对象。这个中间对象和原来的远程对象在事件处理方面,代码完全一致:
public class EventWrapper:MarshalByRefObject
{
 public event BroadCastEventHandler LocalBroadCastEvent;

 //[OneWay]
 public void BroadCasting(string message)
 {
  LocalBroadCastEvent(message);
 }

 public override object InitializeLifetimeService()
 {
  return null;
 }
}

不过不同之处在于:这个Wrapper类必须在客户端和服务端上都要部署,所以,这个类应该放在公共程序集Common.dll中。

现在再来修改原来的客户端代码:
watch = (IBroadCast)Activator.GetObject(
  typeof(IBroadCast),"http://localhost:8080/BroadCastMessage.soap"); 
watch.BroadCastEvent += new BroadCastEventHandler(BroadCastingMessage);
修改为:
watch = (IBroadCast)Activator.GetObject(
    typeof(IBroadCast),"http://localhost:8080/BroadCastMessage.soap");
EventWrapper wrapper = new EventWrapper(); 
wrapper.LocalBroadCastEvent += new BroadCastEventHandler(BroadCastingMessage);
watch.BroadCastEvent += new BroadCastEventHandler(wrapper.BroadCasting);

为什么这样做就可以了呢?也许画一幅图就很容易说明,可惜我的艺术天分实在很糟糕,我希望以后可以改进这一点。还是用文字来说明吧。

前面说,委托要装载client程序集。现在我们把远程对象委托装载的权利移交给EventWrapper。因为这个类对象是放在客户端的,所以它要装载client程序集丝毫没有问题。语句:
EventWrapper wrapper = new EventWrapper(); 
wrapper.LocalBroadCastEvent += new BroadCastEventHandler(BroadCastingMessage);
实现了这个功能。

不过此时虽然订阅了事件,但事件还是客户端的,没有与服务端联系起来。而服务端的事件是放到远程对象中的,所以,还要订阅事件,这个任务由远程对象watch来完成。但此时它订阅的不再是BroadCastingMessage了,而是EventWrapper的触发事件方法BroadCasting。那么此时委托同样要装载程序集,但此时装载的就是BroadCasting所在的程序集了。由于装载发生的地点是在服务端。呵呵,高兴的是,BroadCasting所在的程序集正是公共程序集(前面已说过,EventWrapper应放到公共程序集Common.dll中),而公共程序集在服务端和客户端都已经部署了。自然就不会出现找不到程序集的问题了。

注意:EventWrapper因为要重写InitializeLifetimeService()方法,所以仍然要继承MarshalByRefObject类。

现在再来运行程序。首先运行服务端;然后运行客户端,OK,客户端窗体出现了:


 
然后我们在服务端单击“BroadCast”按钮,发送广播消息:


 
单击“Send”发送,再来看看客户端,会是怎样?Fine,I got it!


 
怎么样,很酷吧!你也可以同时打开多个客户端,它们都将收到这个广播信息。如果你觉得这个广播声音太吵,那就请你在客户端取消广播吧。在Cancle按钮中:
private void btnCancle_Click(object sender, System.EventArgs e)
{
 watch.BroadCastEvent -= new BroadCastEventHandler(wrapper.BroadCasting);
 MessageBox.Show("取消订阅广播成功!");
}
当然这个时候wrapper对象应该被申明为private对象了:
private EventWrapper wrapper = null;


 
取消后,你试着再广播一下,恭喜你,你不会听到噪音了!

三、 客户端订阅客户端事件

有了前面的基础,再来看客户端订阅客户端事件,就简单多了。而本文写到这里,我也很累了,你也被我啰嗦得不耐烦了。你心里在喊,“饶了我吧!”其实,我又何尝不是如此。所以我只提供一个思路,有兴趣的朋友,可以自己写一个程序。

其实方法很简单,和第二种情况类似。发送信息的客户端,只需要获得远程对象后,发送消息就可以了。而接收信息的客户端,负责订阅该事件。由于事件都是放到远程对象中,因此订阅的方法和第二种情况没有什么区别!

特殊的情况是,我们可以用第三种情况来代替第二种。只要你把发送信息的客户端放到服务端就可以了。当然需要做一些额外的工作,有兴趣的朋友可以去实现一下。在我的示例程序中,已经用这种方法模拟实现了服务端的广播,大家可以去看看。

四、 一点补充

我在前面的事件处理中,使用的都是默认的EventArgs。如果要定义自己的EventArgs,就不相同了。因为该信息是传值序列化,因此必须加上[Serializable],且必须放到公共程序集中,部署到服务端和客户端。例如:
[Serializable]
public class BroadcastEventArgs:EventArgs
{
 private string msg = null;
 public BroadcastEventArgs(string message)
 {
  msg = message;
 }

 public string Message
 {
  get {return msg;}
 }
}

五、持续改进(经Beta的提醒,我改进了我的程序,并对文章进行了修改 2004年12月13日)

也许,细心的读者注意到了,在我的远程对象类和EventWrapper类中,触发事件方法的Attribute[OneWay]被我注释掉了。我看到很多资料上写到,在Remoting中处理事件,触发事件的方法必须具有这个Attribute。这个attribute究竟有什么用?

在发送事件消息的时候,事件的订阅者会触发事件,然后响应该事件。然而当事件的订阅者发生错误的时候呢?例如,发送事件消息的时候,才发现根本没有事件订阅者;或者事件的订阅者出现故障,如断电、或异常关机。此时,发送事件一方会因为找不到正确的事件订阅者,而发生异常。以我的程序为例。当我们分别打开服务端和客户端程序的时候,此时广播信息正常。然而,当我们关闭客户端后,由于该客户端没有取消订阅,此时异常发生,提示信息如图:

(不知道为什么,这个异常与客户端连接服务端出现的异常一样。这个异常容易让人产生误会。)

如果这个时候我们同时打开了多个客户端,那么其他客户端就会因为这一个客户端关闭造成的错误,而无法收到广播信息。那么让我们先做第一步改进:

1、先考虑正常情况。在我的客户端,虽然提供了取消订阅的操作,但并没有考虑用户关闭客户端的情况。即,关闭客户端时,并未取消事件的订阅,所以我们应该在关闭客户端窗体中写入:

        private void ClientForm_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        
{
            watch.BroadCastEvent 
-= new BroadCastEventHandler(wrapper.BroadCasting);
        }

2、仅仅是这样还不够。如果客户端并没有正常关闭,而是因为突然断电而导致客户端关闭呢?此时,客户端还没有来得及取消事件订阅呢。在这种情况下,我们需要用到OneWayAttribute。

前面说到,发送事件一方如果找不到正确的事件订阅者,会发生异常。也就是说,这个事件是unreachable的。幸运的是,OneWayAttribute恰好解决了这个问题。其实从该特性的命名OneWay,大约也能猜到其中的含义。当事件不可到达,无法发送时,正常情况下,会返回一个异常信息。如果加上OneWayAttribute,这个事件的发送就变成单向的了。假如此时发生异常,那么系统会自动抛掉该异常信息。由于没有异常信息的返回,发送信息方会认为发送信息成功了。程序会正常运行,错误的客户端被忽略,而正确的客户端仍然能够收到广播信息。

因此,远程对象的代码就应该是这样:

public event BroadCastEventHandler BroadCastEvent;

IBroadCast 成员

public override object InitializeLifetimeService()
{
 
return null;
}

3、最后的改进

使用OneWay固然可以解决上述的问题,但不够友好。因为对于广播消息的一方来说,象被蒙上了眼睛一样,对于客户端发生的事情懵然不知。这并不是一个好的idea。在Ingo Rammer的Advanced .NET Remoting一书中,Ingo Rammer先生提出了一个更好的办法,就是在发送信息一方时,检查了委托链。并在委托链的遍历中来捕获异常。当其中一个委托发生异常时,显示提示信息。然后继续遍历后面的委托,这样既保证了异常信息的提示,又保证了其他订阅者正常接收消息。因此,我对本例的远程对象进行了修改,注释掉[OneWay],修改了BroadCastInfo()方法:

//[OneWay]
        public void BroadCastingInfo(string info)
        
{
            
if (BroadCastEvent != null)
            
{
                BroadCastEventHandler tempEvent 
= null;

                
int index = 1//记录事件订阅者委托的索引,为方便标识,从1开始。
                foreach (Delegate del in BroadCastEvent.GetInvocationList())
                
{
                    
try
                    
{
                        tempEvent 
= (BroadCastEventHandler)del;
                        tempEvent(info);
                    }

                    
catch
                    
{                        
                        MessageBox.Show(
"事件订阅者" + index.ToString() + "发生错误,系统将取消事件订阅!");
                        BroadCastEvent 
-= tempEvent;
                    }

                    index
++;
                }
                
            }

            
else
            
{
                MessageBox.Show(
"事件未被订阅或订阅发生错误!");
            }

        }

我们来试验一下。首先打开服务端,然后同时打开三个客户端。广播消息:

消息发送正常。

接着关闭其中一个客户端窗口,再广播消息(注意为模拟客户端异常情况,应在ClientForm_Closing方法中把第一步改进的取消订阅代码注释。否则不会发生异常。难道你真的愿意用断电来导致异常发生吗^_^),结果如图:

此时服务端报告了“事件订阅者1发生错误,系统将取消事件订阅”。注意此时另外两个客户端,还是和前面一样,只有两条广播信息。

当我们点击提示框的“确定”按钮后,广播仍然发送:

通过这样的改进后,程序更加的完善,也更加的健壮和友好!

附:
示例代码说明:
1、 Remoting事件(客户端发传真)压缩包:为第一节内容;
2、 Remoting事件(服务端广播)压缩包:为第二节、第三节内容,其中:
第二节代码包含于:
#region 客户端订阅服务端事件
#endregion
第三节代码包含于:
#region 客户端订阅客户端事件
#endregion
如果要实现第二节的程序,请注释掉第三节代码;反之亦然。示例程序默认为第二节程序。
3、 运行示例程序时,请先运行服务端程序,然后运行客户端程序。否则会抛出“基础连接已关闭”的异常。
4、 解决方案均放在Common(或ICommon)文件夹中。

5、改进后的代码放到Remoting事件(服务端广播改进)压缩包中,大家可以比较一下改进后的程序有何不同!

参考资料:
1、 Ingo Rammer,《Advanced .NET Remoting》
2、 吕震宇,《利用Event松耦合远程对象与远程系统
3、 大坏蛋,《.NET Remoting中的事件处理(.NET Framework 2.0)(一)

posted @ 2008-07-25 16:12 InsistYML 阅读(41) | 评论 (0)编辑

(转载)

我写的.Net Remoting系列专题:

Microsoft .Net Remoting系列专题之一:.Net Remoting基础篇

Microsoft .Net Remoting系列专题之三:Remoting事件处理全接触

Microsoft .Net Remoting系列专题之二 

一、远程对象的激活

在Remoting中有三种激活方式,一般的实现是通过RemotingServices类的静态方法来完成。工作过程事实上是将该远程对象注册到通道中。由于Remoting没有提供与之对应的Unregister方法来注销远程对象,所以如果需要注册/注销指定对象,微软推荐使用Marshal(一般译为编组)和Disconnect配对使用。在《Net Remoting基础篇》中我已经谈到:Marshal()方法是将MarshalByRefObject类对象转化为ObjRef类对象,这个对象是存储生成代理以与远程对象通讯所需的所有相关信息。这样就可以将该实例序列化以便在应用程序域之间以及通过网络进行传输,客户端就可以调用了。而Disconnect()方法则将具体的实例对象从通道中断开。

根据上述说明,Marshal()方法对远程对象以引用方式进行编组(Marshal-by-Reference,MBR),并将对象的代理信息放到通道中。客户端可以通过Activator.GetObject()来获取。如果用户要注销该对象,则通过调用Disconnect()方法。那么这种方式对于编组的远程对象是否存在生命周期的管理呢?这就是本文所要描述的问题。

二、生命周期

在CLR中,框架提供了GC(垃圾回收器)来管理内存中对象的生命周期。同样的,.Net Remoting使用了一种分布式垃圾回收,基于租用的形式来管理远程对象的生命周期。

早期的DCOM对于对象生命周期的管理是通过ping和引用计数来确定对象何时应当作为垃圾回收。然而ping引起的网络流量对分布式应用程序的性能是一种痛苦的负担,它大大地影响了分布式处理的整体性能。.Net Remoting在每个应用程序域中都引入一个租用管理器,为每个服务器端的SingleTon,或每个客户端激活的远程对象保存着对租用对象的引用。(说明:对于服务器端激活的SingleCall方式,由于它是无状态的,对于每个激活的远程对象,都由CLR的GC来自动回收,因此对于SingleCall模式激活的远程对象,不存在生命周期的管理。)

1、租用

租用是个封装了TimeSpan值的对象,用以管理远程对象的生存期。在.Net Remoting中提供了定义租用功能的ILease接口。当Remoting通过SingleTon模式或客户端激活模式来激活远程对象时,租用对象调用从System.MarshalByRefObject继承的InitializeLifetimeService方法,向对象请求租用。

ILease接口定义了有关生命周期的属性,均为TimeSpan值。如下:
InitialLeaseTime:初始化有效时间,默认值为300秒,如果为0,表示永不过期;
RenewOnCallTime:调用远程对象一个方法时的租用更新时间,默认值为120秒;
SponsorshipTimeout:超时值,通知Sponsor(发起人)租用过期后,Remoting会等待的时间,默认值为120秒;
CurrentLeaseTime:当前租用时间,首次获得租用时,为InitializeLeaseTime的值。

Remoting的远程对象因为继承了MarshalByRefObject,因此默认继承了InitializeLifetimeService方法,那么租用的相关属性为默认值。如果要改变这些设置,可以在远程对象中重写该方法。例如:
 public override object InitializeLifetimeService()
 {
  ILease lease = (ILease)base.InitializeLifetimeService();
  if (lease.CurrentState == LeaseState.Initial)
  {
   lease.InitialLeaseTime = TimeSpan.FromMinutes(1);
   lease.RenewOnCallTime = TimeSpan.FromSeconds(20);
  }
  return lease;  
 }

也可以忽略该方法,将对象的租用周期改变为无限:
 public override object InitializeLifetimeService()
 {
  return null;
 }

2、租用管理器

如果是前面所说的租用主要是应用在每个具体的远程对象上,那么租用管理器是服务器端专门用来管理远程对象生命周期的管理器,它维持着一个System.Hashtable成员,将租用映射为System.DateTime实例表示每个租用何时应过期。Remoting采用轮询的方式以一定的时间唤醒租用管理器,检查每个租用是否过期。默认为每10秒钟唤醒一次。轮询的间隔可以配置,如将轮询间隔设置为5分钟:LifetimeService.LeaseManagerPollTime = System.TimeSpan.FromMinutes(5);

我们还可以在租用管理器中设置远程对象租用的属性,如改变远程对象的初始有效时间为永久有效:
LifetimeServices.LeaseTime = TimeSpan.Zero;

我们也可以通过配置文件来设置生命周期,如:
&lt;configuration&gt;
 &lt;system.runtime.remoting&gt;
  &lt;application name = "SimpleServer"&gt;
   &lt;lifetime leaseTime = "0" sponsorshipTimeOut = "1M" renewOnCallTime = "1M" pollTime = "30S"/&gt;       
  &lt;/application&gt;
 &lt;/system.runtime.remoting&gt;
&lt;/configuration&gt;

注:配置文件中的pollTime即为上面所说的租用管理器的轮询间隔时间LeaseManagerPollTime。

租用管理器对于生命周期的设置是针对服务器上所有的远程对象。当我们通过配置文件或租用管理器设置租用的属性时,所有远程对象的生命周期都遵循该设置,除非我们对于指定的远程对象通过重写InitializeLifetimeService方法,改变了相关配置。也就是说,远程对象的租用配置优先级高于服务器端配置。

3、发起人(Sponsor)

发起人是针对客户端而言的。远程对象就是发起人要租用的对象,发起人可以与服务器端签订租约,约定租用时间。一旦到期后,发起人还可以续租,就像现实生活中租方的契约,房东、租房者之间的关系一样。

在.Net Framework中的System.Runtime.Remoting.Lifetime命名空间中定义了ClientSponsor类,该类继承了System.MarshalByRefObject,并实现了ISponsor接口。ClientSponsor类的属性和方法,可以参考MSDN。

客户端要使用发起人机制,必须创建ClientSponsor类的一个实例。然后调用相关方法如Register()或Renewal()方法来注册远程对象或延长生命周期。如:
RemotingObject obj = new RemotingObject();
ClientSponsor sponsor = new ClientSponsor();
sponsor.RenewalTime = TimeSpan.FromMinutes(2);
sponsor.Register(obj);

续租时间也可以在ClientSponsor的构造函数中直接设置,如:
ClientSponsor sponsor = new ClientSponsor(TimeSpan.FromMinutes(2));
sponsor.Register(obj);

我们也可以自己编写Sponsor来管理发起人机制,这个类必须继承ClientSponsor并实现ISponsor接口。

三、跟踪服务

如前所述,我们要判断通过Marshal编组远程对象是否存在生命周期的管理。在Remoting中,可以通过跟踪服务程序来监视MBR对象的编组进程。

我们可以创建一个简单的跟踪处理程序,该程序实现接口ITrackingHandler。接口ITrackingHandler定义了3个方法,MarshalObject、UnmarshalObject和DisconnectedObject。当远程对象被编组、解组和断开连接时,就会调用相应的方法。下面是该跟踪处理类的代码:public class MyTracking:ITrackingHandler
{
 public MyTracking()
 {
  //
  // TODO: 在此处添加构造函数逻辑
  //
 }

 public void MarshaledObject(object obj,ObjRef or)
 {
  Console.WriteLine();
  Console.WriteLine("对象" + obj.Tostring() + " is marshaled at " + DateTime.Now.ToShortTimeString());
 }

 public void UnmarshaledObject(object obj,ObjRef or)
 {
  Console.WriteLine();
  Console.WriteLine("对象" + obj.Tostring() + " is unmarshaled at " + DateTime.Now.ToShortTimeString());
 }

  public void DisconnectedObject(object obj)
 {
  Console.WriteLine(obj.ToString() + " is disconnected at " + DateTime.Now.ToShortTimeString());
 }
}

然后再服务器端创建该跟踪处理类的实例,并注册跟踪服务:
TrackingServices.RegisterTrackingHandler(new MyTracking());

四、测试

1、建立两个远程对象,并重写InitializeLifetimeService方法:

对象一:AppService1
初始生命周期:1分钟

 public class AppService1:MarshalByRefObject
 {
  public void PrintString(string contents)
  {
   Console.WriteLine(contents);   
  }

  public override object InitializeLifetimeService()
  {
   ILease lease = (ILease)base.InitializeLifetimeService();
   if (lease.CurrentState == LeaseState.Initial)
   {
    lease.InitialLeaseTime = TimeSpan.FromMinutes(1);
    lease.RenewOnCallTime = TimeSpan.FromSeconds(20);
   }
   return lease;
   
  }
 }

对象二:AppService2
初始生命周期:3分钟

 public class AppService2:MarshalByRefObject
 {
  public void PrintString(string contents)
  {
   Console.WriteLine(contents);   
  }

  public override object InitializeLifetimeService()
  {
   ILease lease = (ILease)base.InitializeLifetimeService();
   if (lease.CurrentState == LeaseState.Initial)
   {
    lease.InitialLeaseTime = TimeSpan.FromMinutes(3);
    lease.RenewOnCallTime = TimeSpan.FromSeconds(40);
   }
   return lease;
   
  }
 }

为简便起见,两个对象的方法都一样。

2、服务器端

(1) 首先建立如上的监控处理类;

(2) 注册通道:
TcpChannel channel = new TcpChannel(8080);
ChannelServices.RegisterChannel(channel);

(3) 设置租用管理器的初始租用时间为无限:
LifetimeServices.LeaseTime = TimeSpan.Zero;

(4) 创建该跟踪处理类的实例,并注册跟踪服务:
TrackingServices.RegisterTrackingHandler(new MyTracking());

(5) 编组两个远程对象:
ServerAS.AppService1 service1 = new ServerAS1.AppService1();
ObjRef objRef1 = RemotingServices.Marshal((MarshalByRefObject)service1,"AppService1");

ServerAS.AppService2 service2 = new ServerAS1.AppService2();
ObjRef objRef2 = RemotingServices.Marshal((MarshalByRefObject)service2,"AppService2");

(6) 使服务器端保持运行:
Console.WriteLine("Remoting服务启动,按退出..."); 
Console.ReadLine();

3、客户端

通过Activator.GetObject()获得两个远程对象,并调用其方法PrintString。代码略。

4、运行测试

运行服务器端和客户端,由于监控程序将监视远程对象的编组进程,因此在运行开始,就会显示远程对象已经被Marshal:

然后再客户端调用这两个远程对象的PrintString方法,服务器端接受字符串:

一分钟后,远程对象一自动被Disconnect:

此时客户端如要调用远程对象一,会抛出RemotingException异常;

又一分钟后,远程对象二被Disconnect了:

align="center">

用户还可以根据这个代码测试RenewOnCallTime的时间是否正确。也即是说,在对象还未被Disconnect时,调用对象,则从调用对象的这一刻起,其生命周期不再是原来设定的初始有效时间值(InitialLeaseTime),而是租用更新时间值(RenewOnCallTime)。另外,如果这两个远程对象没有重写InitializeLifetimeService方法,则生命周期应为租用管理器所设定的值,为永久有效(设置为0)。那么这两个对象不会被自动Disconnect,除非我们显式指定关闭它的连接。当然,如果我们显式关闭连接,跟踪程序仍然会监视到它的变化,然后显示出来。

五、结论

通过我们的测试,其实结论已经很明显了。通过Marshal编组的对象要受到租用的生命周期所控制。注意对象被Disconnect,并不是指这个对象被GC回收,而是指这个对象保存在通道的相关代理信息被断开了,而对象本身仍然在服务器端存在。

所以我们通过Remoting提供服务,应根据实际情况指定远程对象的生命周期,如果不指定,则为Remoting默认的设定。要让所有的远程对象永久有效,可以通过配置文件或租用管理器将初始有效时间设为0。

posted @ 2008-07-25 16:09 InsistYML 阅读(39) | 评论 (0)编辑

(转载)

   

我写的.Net Remoting系列专题:

Microsoft .Net Remoting系列专题之二:Marshal、Disconnect与生命周期以及跟踪服务

Microsoft .Net Remoting系列专题之三:Remoting事件处理全接触

Microsoft .Net Remoting系列专题之一

一、Remoting基础

什么是Remoting,简而言之,我们可以将其看作是一种分布式处理方式。从微软的产品角度来看,可以说Remoting就是DCOM的一种升级,它改善了很多功能,并极好的融合到.Net平台下。Microsoft® .NET Remoting 提供了一种允许对象通过应用程序域与另一对象进行交互的框架。这也正是我们使用Remoting的原因。为什么呢?在Windows操作系统中,是将应用程序分离为单独的进程。这个进程形成了应用程序代码和数据周围的一道边界。如果不采用进程间通信(RPC)机制,则在一个进程中执行的代码就不能访问另一进程。这是一种操作系统对应用程序的保护机制。然而在某些情况下,我们需要跨过应用程序域,与另外的应用程序域进行通信,即穿越边界。

在Remoting中是通过通道(channel)来实现两个应用程序域之间对象的通信的。如图所示:

首先,客户端通过Remoting,访问通道以获得服务端对象,再通过代理解析为客户端对象。这就提供一种可能性,即以服务的方式来发布服务器对象。远程对象代码可以运行在服务器上(如服务器激活的对象和客户端激活的对象),然后客户端再通过Remoting连接服务器,获得该服务对象并通过序列化在客户端运行。

在Remoting中,对于要传递的对象,设计者除了需要了解通道的类型和端口号之外,无需再了解数据包的格式。但必须注意的是,客户端在获取服务器端对象时,并不是获得实际的服务端对象,而是获得它的引用。这既保证了客户端和服务器端有关对象的松散耦合,同时也优化了通信的性能。

1、Remoting的两种通道

Remoting的通道主要有两种:Tcp和Http。在.Net中,System.Runtime.Remoting.Channel中定义了IChannel接口。IChannel接口包括了TcpChannel通道类型和Http通道类型。它们分别对应Remoting通道的这两种类型。

TcpChannel类型放在名字空间System.Runtime.Remoting.Channel.Tcp中。Tcp通道提供了基于Socket的传输工具,使用Tcp协议来跨越Remoting边界传输序列化的消息流。TcpChannel类型默认使用二进制格式序列化消息对象,因此它具有更高的传输性能。HttpChannel类型放在名字空间System.Runtime.Remoting.Channel.Http中。它提供了一种使用Http协议,使其能在Internet上穿越防火墙传输序列化消息流。默认情况下,HttpChannel类型使用Soap格式序列化消息对象,因此它具有更好的互操作性。通常在局域网内,我们更多地使用TcpChannel;如果要穿越防火墙,则使用HttpChannel。

2、远程对象的激活方式

在访问远程类型的一个对象实例之前,必须通过一个名为Activation的进程创建它并进行初始化。这种客户端通过通道来创建远程对象,称为对象的激活。在Remoting中,远程对象的激活分为两大类:服务器端激活和客户端激活。

(1) 服务器端激活,又叫做WellKnow方式,很多又翻译为知名对象。为什么称为知名对象激活模式呢?是因为服务器应用程序在激活对象实例之前会在一个众所周知的统一资源标识符(URI)上来发布这个类型。然后该服务器进程会为此类型配置一个WellKnown对象,并根据指定的端口或地址来发布对象。.Net Remoting把服务器端激活又分为SingleTon模式和SingleCall模式两种。

SingleTon模式:此为有状态模式。如果设置为SingleTon激活方式,则Remoting将为所有客户端建立同一个对象实例。当对象处于活动状态时,SingleTon实例会处理所有后来的客户端访问请求,而不管它们是同一个客户端,还是其他客户端。SingleTon实例将在方法调用中一直维持其状态。举例来说,如果一个远程对象有一个累加方法(i=0;++i),被多个客户端(例如两个)调用。如果设置为SingleTon方式,则第一个客户获得值为1,第二个客户获得值为2,因为他们获得的对象实例是相同的。如果熟悉Asp.Net的状态管理,我们可以认为它是一种Application状态。

SingleCall模式:SingleCall是一种无状态模式。一旦设置为SingleCall模式,则当客户端调用远程对象的方法时,Remoting会为每一个客户端建立一个远程对象实例,至于对象实例的销毁则是由GC自动管理的。同上一个例子而言,则访问远程对象的两个客户获得的都是1。我们仍然可以借鉴Asp.Net的状态管理,认为它是一种Session状态。

(2) 客户端激活。与WellKnown模式不同,Remoting在激活每个对象实例的时候,会给每个客户端激活的类型指派一个URI。客户端激活模式一旦获得客户端的请求,将为每一个客户端都建立一个实例引用。SingleCall模式和客户端激活模式是有区别的:首先,对象实例创建的时间不一样。客户端激活方式是客户一旦发出调用的请求,就实例化;而SingleCall则是要等到调用对象方法时再创建。其次,SingleCall模式激活的对象是无状态的,对象生命期的管理是由GC管理的,而客户端激活的对象则有状态,其生命周期可自定义。其三,两种激活模式在服务器端和客户端实现的方法不一样。尤其是在客户端,SingleCall模式是由GetObject()来激活,它调用对象默认的构造函数。而客户端激活模式,则通过CreateInstance()来激活,它可以传递参数,所以可以调用自定义的构造函数来创建实例。

二、远程对象的定义

前面讲到,客户端在获取服务器端对象时,并不是获得实际的服务端对象,而是获得它的引用。因此在Remoting中,对于远程对象有一些必须的定义规范要遵循。

由于Remoting传递的对象是以引用的方式,因此所传递的远程对象类必须继承MarshalByRefObject。MSDN对MarshalByRefObject的说明是:MarshalByRefObject 是那些通过使用代理交换消息来跨越应用程序域边界进行通信的对象的基类。不是从 MarshalByRefObject 继承的对象会以隐式方式按值封送。当远程应用程序引用一个按值封送的对象时,将跨越远程处理边界传递该对象的副本。因为您希望使用代理方法而不是副本方法进行通信,因此需要继承MarshallByRefObject。

以下是一个远程对象类的定义:
public class ServerObject:MarshalByRefObject
{
        public Person GetPersonInfo(string name,string sex,int age)
        {
            Person person = new Person();
            person.Name = name;
            person.Sex = sex;
            person.Age = age;
            return person;
        }
}

这个类只实现了最简单的方法,就是设置一个人的基本信息,并返回一个Person类对象。注意这里返回的Person类。由于这里所传递的Person则是以传值的方式来完成的,而Remoting要求必须是引用的对象,所以必须将Person类序列化。

因此,在Remoting中的远程对象中,如果还要调用或传递某个对象,例如类,或者结构,则该类或结构则必须实现串行化Attribute[SerializableAttribute]:
[Serializable]
 public class Person
 {
        public Person()
        {
           
        }

        private string name;
        private string sex;
        private int age;

        public string Name
        {
            get    {return name;}
            set    {name = value;}
        }

        public string Sex
        {
            get {return sex;}
            set {sex = value;}
        }

        public int Age
        {
            get {return age;}
            set {age = value;}
        }
  }
将该远程对象以类库的方式编译成Dll。这个Dll将分别放在服务器端和客户端,以添加引用。

在Remoting中能够传递的远程对象可以是各种类型,包括复杂的DataSet对象,只要它能够被序列化。远程对象也可以包含事件,但服务器端对于事件的处理比较特殊,我将在本系列之三中介绍。

三、服务器端

根据第一部分所述,根据激活模式的不同,通道类型的不同服务器端的实现方式也有所不同。大体上说,服务器端应分为三步:

1、注册通道

要跨越应用程序域进行通信,必须实现通道。如前所述,Remoting提供了IChannel接口,分别包含TcpChannel和HttpChannel两种类型的通道。这两种类型除了性能和序列化数据的格式不同外,实现的方式完全一致,因此下面我们就以TcpChannel为例。

注册TcpChannel,首先要在项目中添加引用“System.Runtime.Remoting”,然后using名字空间:System.Runtime.Remoting.Channel.Tcp。代码如下:
            TcpChannel channel = new TcpChannel(8080);
            ChannelServices.RegisterChannel(channel);

在实例化通道对象时,将端口号作为参数传递。然后再调用静态方法RegisterChannel()来注册该通道对象即可。

2、注册远程对象

注册了通道后,要能激活远程对象,必须在通道中注册该对象。根据激活模式的不同,注册对象的方法也不同。

(1) SingleTon模式

对于WellKnown对象,可以通过静态方法RemotingConfiguration.RegisterWellKnownServiceType()来实现:RemotingConfiguration.RegisterWellKnownServiceType(
                typeof(ServerRemoteObject.ServerObject),
                "ServiceMessage",WellKnownObjectMode.SingleTon);

(2)SingleCall模式

注册对象的方法基本上和SingleTon模式相同,只需要将枚举参数WellKnownObjectMode改为SingleCall就可以了。RemotingConfiguration.RegisterWellKnownServiceType(
                typeof(ServerRemoteObject.ServerObject),
                "ServiceMessage",WellKnownObjectMode.SingleCall);

(3)客户端激活模式

对于客户端激活模式,使用的方法又有不同,但区别不大,看了代码就一目了然。
RemotingConfiguration.ApplicationName = "ServiceMessage";
RemotingConfiguration.RegisterActivatedServiceType(
                typeof(ServerRemoteObject.ServerObject));

为什么要在注册对象方法前设置ApplicationName属性呢?其实这个属性就是该对象的URI。对于WellKnown模式,URI是放在RegisterWellKnownServiceType()方法的参数中,当然也可以拿出来专门对ApplicationName属性赋值。而RegisterActivatedServiceType()方法的重载中,没有ApplicationName的参数,所以必须分开。

3、注销通道

如果要关闭Remoting的服务,则需要注销通道,也可以关闭对通道的监听。在Remoting中当我们注册通道的时候,就自动开启了通道的监听。而如果关闭了对通道的监听,则该通道就无法接受客户端的请求,但通道仍然存在,如果你想再一次注册该通道,会抛出异常。

           //获得当前已注册的通道;
            IChannel[] channels = ChannelServices.RegisteredChannels;

            //关闭指定名为MyTcp的通道;
            foreach (IChannel eachChannel in channels)
            {
                if (eachChannel.ChannelName == "MyTcp")
                {
                    TcpChannel tcpChannel = (TcpChannel)eachChannel;

                    //关闭监听;
                    tcpChannel.StopListening(null);

                    //注销通道;
                    ChannelServices.UnregisterChannel(tcpChannel);
                }
            }
代码中,RegisterdChannel属性获得的是当前已注册的通道。在Remoting中,是允许同时注册多个通道的,这一点会在后面说明。

四、客户端

客户端主要做两件事,一是注册通道。这一点从图一就可以看出,Remoting中服务器端和客户端都必须通过通道来传递消息,以获得远程对象。第二步则是获得该远程对象。

1、注册通道:
TcpChannel channel = new TcpChannel();
ChannelServices.RegisterChannel(channel);

注意在客户端实例化通道时,是调用的默认构造函数,即没有传递端口号。事实上,这个端口号是缺一不可的,只不过它的指定被放在后面作为了Uri的一部分。

2、获得远程对象。

与服务器端相同,不同的激活模式决定了客户端的实现方式也将不同。不过这个区别仅仅是WellKnown激活模式和客户端激活模式之间的区别,而对于SingleTon和SingleCall模式,客户端的实现完全相同。

(1) WellKnown激活模式

要获得服务器端的知名远程对象,可通过Activator进程的GetObject()方法来获得:
ServerRemoteObject.ServerObject serverObj = (ServerRemoteObject.ServerObject)Activator.GetObject(
              typeof(ServerRemoteObject.ServerObject), "tcp://localhost:8080/ServiceMessage");

首先以WellKnown模式激活,客户端获得对象的方法是使用GetObject()。其中参数第一个是远程对象的类型。第二个参数就是服务器端的uri。如果是http通道,自然是用http://localhost:8080/ServiceMessage了。因为我是用本地机,所以这里是localhost,你可以用具体的服务器IP地址来代替它。端口必须和服务器端的端口一致。后面则是服务器定义的远程对象服务名,即ApplicationName属性的内容。

(2) 客户端激活模式

如前所述,WellKnown模式在客户端创建对象时,只能调用默认的构造函数,上面的代码就说明了这一点,因为GetObject()方法不能传递构造函数的参数。而客户端激活模式则可以通过自定义的构造函数来创建远程对象。

客户端激活模式有两种方法:
1) 调用RemotingConfiguration的静态方法RegisterActivatedClientType()。这个方法返回值为Void,它只是将远程对象注册在客户端而已。具体的实例化还需要调用对象类的构造函数。
 RemotingConfiguration.RegisterActivatedClientType(               
                typeof(ServerRemoteObject.ServerObject),
                "tcp://localhost:8080/ServiceMessage");
 ServerRemoteObject.ServerObject serverObj = new ServerRemoteObject.ServerObject();

2) 调用进程Activator的CreateInstance()方法。这个方法将创建方法参数指定类型的类对象。它与前面的GetObject()不同的是,它要在客户端调用构造函数,而GetObject()只是获得对象,而创建实例是在服务器端完成的。CreateInstance()方法有很多个重载,我着重说一下其中常用的两个。
a、 public static object CreateInstance(Type type, object[] args, object[] activationAttributes);

参数说明:
type:要创建的对象的类型。
args :与要调用构造函数的参数数量、顺序和类型匹配的参数数组。如果 args 为空数组或空引用(Visual Basic 中为 Nothing),则调用不带任何参数的构造函数(默认构造函数)。
activationAttributes :包含一个或多个可以参与激活的属性的数组。

这里的参数args是一个object[]数组类型。它可以传递要创建对象的构造函数中的参数。从这里其实可以得到一个结论:WellKnown激活模式所传递的远程对象类,只能使用默认的构造函数;而Activated模式则可以用户自定义构造函数。activationAttributes参数在这个方法中通常用来传递服务器的url。
假设我们的远程对象类ServerObject有个构造函数:
            ServerObject(string pName,string pSex,int pAge)
            {
                name = pName;
                sex = pSex;
                age = pAge;
            }

那么实现的代码是:
            object[] attrs = {new UrlAttribute("tcp://localhost:8080/ServiceMessage")};
            object[] objs = new object[3];
            objs[0] = "wayfarer";
            objs[1] = "male";
            objs[2] = 28;
            ServerRemoteObject.ServerObject = Activator.CreateInstance(
                typeof(ServerRemoteObject.ServerObject),objs,attrs);
可以看到,objs[]数组传递的就是构造函数的参数。

b、public static ObjectHandle CreateInstance(string assemblyName, string typeName, object[] activationAttribute);

参数说明:
assemblyName :将在其中查找名为 typeName 的类型的程序集的名称。如果 assemblyName 为空引用(Visual Basic 中为 Nothing),则搜索正在执行的程序集。
typeName:首选类型的名称。
activationAttributes :包含一个或多个可以参与激活的属性的数组。

参数说明一目了然。注意这个方法返回值为ObjectHandle类型,因此代码与前不同:
            object[] attrs = {new UrlAttribute("tcp://localhost:8080/EchoMessage")};           
            ObjectHandle handle = Activator.CreateInstance("ServerRemoteObject",
                                   "ServerRemoteObject.ServerObject",attrs);
            ServerRemoteObject.ServerObject obj = (ServerRemoteObject.ServerObject)handle.Unwrap();

这个方法实际上是调用的默认构造函数。ObjectHandle.Unwrap()方法是返回被包装的对象。

说明:要使用UrlAttribute,还需要在命名空间中添加:using System.Runtime.Remoting.Activation;

五、Remoting基础的补充

通过上面的描述,基本上已经完成了一个最简单的Remoting程序。这是一个标准的创建Remoting程序的方法,但在实际开发过程中,我们遇到的情况也许千奇百怪,如果只掌握一种所谓的“标准”,就妄想可以“一招鲜、吃遍天”,是不可能的。

1、注册多个通道

在Remoting中,允许同时创建多个通道,即根据不同的端口创建不同的通道。但是,Remoting要求通道的名字必须不同,因为它要用来作为通道的唯一标识符。虽然IChannel有ChannelName属性,但这个属性是只读的。因此前面所述的创建通道的方法无法实现同时注册多个通道的要求。

这个时候,我们必须用到System.Collection中的IDictionary接口:

注册Tcp通道:
IDictionary tcpProp = new Hashtable();
tcpProp["name"] = "tcp9090";
tcpProp["port"] = 9090;
IChannel channel = new TcpChannel(tcpProp,
 new BinaryClientFormatterSinkProvider(),
 new BinaryServerFormatterSinkProvider());
ChannelServices.RegisterChannel(channel);

注册Http通道:
IDictionary httpProp = new Hashtable();
httpProp["name"] = "http8080";
httpProp["port"] = 8080;
IChannel channel = new HttpChannel(httpProp,
 new SoapClientFormatterSinkProvider(),
 new SoapServerFormatterSinkProvider());
ChannelServices.RegisterChannel(channel);

在name属性中,定义不同的通道名称就可以了。

2、远程对象元数据相关性

由于服务器端和客户端都要用到远程对象,通常的方式是生成两份完全相同的对象Dll,分别添加引用。不过为了代码的安全性,且降低客户端对远程对象元数据的相关性,我们有必要对这种方式进行改动。即在服务器端实现远程对象,而在客户端则删除这些实现的元数据。

由于激活模式的不同,在客户端创建对象的方法也不同,所以要分离元数据的相关性,也应分为两种情况。

(1) WellKnown激活模式:

通过接口来实现。在服务器端,提供接口和具体类的实现,而在客户端仅提供接口:
    public interface IServerObject
    {
        Person GetPersonInfo(string name,string sex,int age);
    }

public class ServerObject:MarshalByRefObject,IServerObject
{ ......}
注意:两边生成该对象程序集的名字必须相同,严格地说,是命名空间的名字必须相同。
           
(2) 客户端激活模式:

如前所述,对于客户端激活模式,不管是使用静态方法,还是使用CreateInstance()方法,都必须在客户端调用构造函数实例化对象。所以,在客户端我们提供的远程对象,就不能只提供接口,而没有类的实现。实际上,要做到与远程对象元数据的分离,可以由两种方法供选择:

a、利用WellKnown激活模式模拟客户端激活模式:

方法是利用设计模式中的“抽象工厂”,下面的类图表描述了总体解决方案:

我们在服务器端的远程对象中加上抽象工厂的接口和实现类:
    public interface IServerObject
    {
        Person GetPersonInfo(string name,string sex,int age);
    }

    public interface IServerObjFactory
    {
        IServerObject CreateInstance();       
    }

    public class ServerObject:MarshalByRefObject,IServerObject
    {
        public Person GetPersonInfo(string name,string sex,int age)
        {
            Person person = new Person();
            person.Name = name;
            person.Sex = sex;
            person.Age = age;
            return person;
        }       
    }

    public class ServerObjFactory:MarshalByRefObject,IServerObjFactory
    {
        public IServerObject CreateInstance()
        {
            return new ServerObject();
        }
    }

然后再客户端的远程对象中只提供工厂接口和原来的对象接口:
    public interface IServerObject
    {
        Person GetPersonInfo(string name,string sex,int age);
    }

    public interface IServerObjFactory
    {
        IServerObject CreateInstance();       
    }
我们用WellKnown激活模式注册远程对象,在服务器端:
           //传递对象;
            RemotingConfiguration.RegisterWellKnownServiceType(
                typeof(ServerRemoteObject.ServerObjFactory),
                "ServiceMessage",WellKnownObjectMode.SingleCall);

注意这里注册的不是ServerObject类对象,而是ServerObjFactory类对象。

客户端:
ServerRemoteObject.IServerObjFactory serverFactory =               
                (ServerRemoteObject.IServerObjFactory) Activator.GetObject(
                typeof(ServerRemoteObject.IServerObjFactory),
                "tcp://localhost:8080/ServiceMessage");

ServerRemoteObject.IServerObject serverObj = serverFactory.CreateInstance();

为什么说这是一种客户端激活模式的模拟呢?从激活的方法来看,我们是使用了SingleCall模式来激活对象,但此时激活的并非我们要传递的远程对象,而是工厂对象。如果客户端要创建远程对象,还应该通过工厂对象的CreateInstance()方法来获得。而这个方法正是在客户端调用的。因此它的实现方式就等同于客户端激活模式。

b、利用替代类来取代远程对象的元数据

实际上,我们可以用一个trick,来欺骗Remoting。这里所说的替代类就是这个trick了。既然是提供服务,Remoting传递的远程对象其实现的细节当然是放在服务器端。而要在客户端放对象的副本,不过是因为客户端必须调用构造函数,而采取的无奈之举。既然具体的实现是在服务器端,又为了能在客户端实例化,那么在客户端就实现这些好了。至于实现的细节,就不用管了。

如果远程对象有方法,服务器端则提供方法实现,而客户端就提供这个方法就OK了,至于里面的实现,你可以是抛出一个异常,或者return 一个null值;如果方法返回void,那么里面可以是空。关键是这个客户端类对象要有这个方法。这个方法的实现,其实和方法的声明差不多,所以我说是一个trick。方法如是,构造函数也如此。

还是用代码来说明这种“阴谋”,更直观:

服务器端:
    public class ServerObject:MarshalByRefObject
    {
        public ServerObject()
        {
           
        }

        public Person GetPersonInfo(string name,string sex,int age)
        {
            Person person = new Person();
            person.Name = name;
            person.Sex = sex;
            person.Age = age;
            return person;
        }       
    }

客户端:
    public class ServerObject:MarshalByRefObject
    {
        public ServerObj()
        {
            throw new System.NotImplementedException();
        }

        public Person GetPersonInfo(string name,string sex,int age)
        {
            throw new System.NotImplementedException();
        }       
    }

比较客户端和服务器端,客户端的方法GetPersonInfo(),没有具体的实现细节,只是抛出了一个异常。或者直接写上语句return null,照样OK。我们称客户端的这个类为远程对象的替代类。

3、利用配置文件实现

前面所述的方法,于服务器uri、端口、以及激活模式的设置是用代码来完成的。其实我们也可以用配置文件来设置。这样做有个好处,因为这个配置文件是Xml文档。如果需要改变端口或其他,我们就不需要修改程序,并重新编译,而是只需要改变这个配置文件即可。

(1) 服务器端的配置文件:
&lt;configuration&gt;
  &lt;system.runtime.remoting&gt;
    &lt;application name="ServerRemoting"&gt;
      &lt;service&gt;
        &lt;wellknown mode="Singleton" type="ServerRemoteObject.ServerObject" objectUri="ServiceMessage"/&gt;
      &lt;/service&gt;
      &lt;channels&gt;
         &lt;channel ref="tcp" port="8080"/&gt;
      &lt;/channels&gt;
    &lt;/application&gt;
  &lt;/system.runtime.remoting&gt;
&lt;/configuration&gt;

如果是客户端激活模式,则把wellknown改为activated,同时删除mode属性。

把该配置文件放到服务器程序的应用程序文件夹中,命名为ServerRemoting.config。那么前面的服务器端程序直接用这条语句即可:
RemotingConfiguration.Configure("ServerRemoting.config");

(2) 客户端配置文件

如果是客户端激活模式,修改和上面一样。调用也是使用RemotingConfiguration.Configure()方法来调用存储在客户端的配置文件。

配置文件还可以放在machine.config中。如果客户端程序是web应用程序,则可以放在web.config中。

4、启动/关闭指定远程对象

Remoting中没有提供类似UnregisterWellKnownServiceType()的方法,也即是说,一旦通过注册了远程对象,如果没有关闭通道的话,该对象就一直存在于通道中。只要客户端激活该对象,就会创建对象实例。如果Remoting传送的只有一个远程对象,这不存在问题,关闭通道就可以了。如果传送多个远程对象呢?要关闭指定的远程对象应该怎么做?关闭之后又需要启动又该如何?

我们注意到在Remoting中提供了Marshal()和Disconnect()方法,答案就在这里。Marshal()方法是将MarshalByRefObject类对象转化为ObjRef类对象,这个对象是存储生成代理以与远程对象通讯所需的所有相关信息。这样就可以将该实例序列化以便在应用程序域之间以及通过网络进行传输,客户端就可以调用了。而Disconnect()方法则将具体的实例对象从通道中断开。

方法如下:
首先注册通道:
TcpChannel channel = new TcpChannel(8080);
ChannelServices.RegisterChannel(channel);

接着启动服务:
先在服务器端实例化远程对象。
ServerObject obj = new ServerObject();

然后,注册该对象。注意这里不用RemotingConfiguration.RegisterWellKnownServiceType(),而是使用RemotingServices.Marshal():

ObjRef objrefWellKnown = RemotingServices.Marshal(obj, "ServiceMessage");

如果要注销对象,则:
RemotingServices.Disconnect(obj);

要注意,这里Disconnect的类对象必须是前面实例化的对象。正因为此,我们可以根据需要创建指定的远程对象,而关闭时,则Disconnect之前实例化的对象。

至于客户端的调用,和前面WellKnown模式的方法相同,仍然是通过Activator.GetObject()来获得。但从实现代码来看,我们会注意到一个问题,由于服务器端是显式的实例化了远程对象,因此不管客户端有多少,是否相同,它们调用的都是同一个远程对象。因此我们将这个方法称为模拟的SingleTon模式。

客户端激活模式

我们也可以通过Marshal()和Disconnect()来模拟客户端激活模式。首先我们来回顾“远程对象元数据相关性”一节,在这一节中,我说到采用设计模式的“抽象工厂”来创建对象实例,以此用SingleCall模式来模拟客户端激活模式。在仔细想想前面的模拟的SingleTon模式。是不是答案就将呼之欲出呢?

在“模拟的SingleTon”模式中,我们是将具体的远程对象实例进行Marshal,以此让客户端获得该对象的引用信息。那么我们换一种思路,当我们用抽象工厂提供接口,工厂类实现创建远程对象的方法。然后我们在服务器端创建工厂类实例。再将这个工厂类实例进行Marshal。而客户端获取对象时,不是获取具体的远程对象,而是获取具体的工厂类对象。然后再调用CreateInstance()方法来创建具体的远程对象实例。此时,对于多个客户端而言,调用的是同一个工厂类对象;然而远程对象是在各个客户端自己创建的,因此对于远程对象而言,则是由客户端激活,创建的是不同对象了。

当我们要启动/关闭指定对象时,只需要用Disconnet()方法来注销工厂类对象就可以了。

六、小结

Microsoft.Net Remoting真可以说是博大精深。整个Remoting的内容不是我这一篇小文所能尽述的,更不是我这个Remoting的初学者所能掌握的。王国维在《人间词话》一书中写到:古今之成大事业大学问者,必经过三种境界。“昨夜西风凋碧树,独上高楼,望尽天涯路。”此第一境界也。“衣带渐宽终不悔,为伊消得人憔悴。”此第二境界也。“众里寻他千百度,蓦然回首,那人却在灯火阑珊处。”此第三境界也。如以此来形容我对Remoting的学习,还处于“独上高楼,望尽天涯路”的时候,真可以说还未曾登堂入室。

或许需得“衣带渐宽”,学得Remoting“终不悔”,方才可以“蓦然回首”吧。

posted @ 2008-07-25 16:07 InsistYML 阅读(46) | 评论 (0)编辑
     摘要: 二、ASP.NET Runtime Pipeline(续ASP.NET Http Runtime Pipeline - Part I)现在我们真正进入ASP.NET管辖的范畴,下图基本上囊括整个处理过程涉及的对象,接下来我们一起来讨论这一系列的对象如何相互协作去处理Http Request,并最终生成我们所需的Http Response。HttpContext上面我们介绍了ISAPI在调用ISAP... 阅读全文
posted @ 2008-07-24 10:40 InsistYML| 编辑