RSS 2.0 Feed

Sunday, February 17, 2008

我在《软件设计精要与模式》第19 章中介绍了职责链模式在实际项目中的应用,由于引入了该模式,使得对象在职责划分上有了更清晰的结构,然而由于项目场景的诸多限制,总有几分“为模式而模 式”的生涩感觉。最近在开发WCF的相关项目时,又一次应用了职责链模式,一方面加深了自己对设计模式的进一步理解,也积累了一些心得,可以与各位分享。
 
在 该项目中,我希望实现对Endpoint的合法性检验,其中对于绑定而言,则包含了许多约束,例如绑定与URI样式的约束,例如绑定与服务契约设计的约 束。绑定不同,则向对应的约束也不相同。为了更好地体现Endpoint,我在项目中定义了属于自己的Endpoint类:
    public class ServiceEndpoint
    
{
        
public Uri Address
        
{
            
get;
            
set;
        }


        
public Binding Binding
        
{
            
get;
            
set;
        }


        
public Type ContractType
        
{
            
get;
            
set;
        }

    }

如果考虑最简单的实现方式,我们完全可以通过分支语句,判断不同的绑定类型,然后执行对绑定约束的检查,例如:

public class BindingConstraint

{

public bool Constraint(ServiceEndpoint endpoint)

{

bool flag = false;

switch (endpoint.Binding.Name)

{

case "BasicHttpBinding":

//check the BasicHttpBinding;

 

flag = true;

break;

case "NetTcpBinding":

//check the NetTcpBinding;

break;

case "NetPeerTcpBinding":

//check the NetPeerTcpBinding;

break;

//...Other bindings' constraint;

}

 

return flag;

}

}

这 是一个合适的设计,无可厚非。然而,我们所面临的关于绑定的约束性检查,远非几行代码就可以实现。例如,对于WSDualHttpBinding绑定而 言,我们就需要判断传递进来的契约类型是否具有回调契约。当然,我们可以分别将这些约束性检查放入到专门的方法,甚至是专门的类中,但不可避免的是,我们 会让Constraint方法的switch语句变得越来越长。
 
这还不是关键的,最主要的是我们需要检查的绑定存在扩展的可能,因为除 了WCF自身提供的绑定类型之外,我们还可以提供CustomBinding或其它自定义绑定,一旦可能增加绑定,就需要修改这里的Constraint ()方法。这样的设计就难免捉襟见肘了。事实上,在.NET Framework 3.5中,微软就为WCF新增加了几个绑定,例如WebHttpBinding。
 
此时,我们就可以考虑采用职责链模式,由于我们是针对绑定类型进行约束性检查,我们可以为每个绑定类型定义一个约束对象,然后对其进行抽象,设计类图如下所示:
chainofresponsibility.gif

首先,我们需要定义一个基类BindingConstraint,如下所示:
    public abstract class BindingConstraint//:IEndpointConstraint
    {
        
#region protected fields

        
protected BindingConstraint m_bindingConstraint;
        
protected bool m_hasNextConstraint = false;        

        
#endregion


        
#region public methods

        
public void AddConstraint(BindingConstraint constraint)
        
{
            m_bindingConstraint 
= constraint;
            m_hasNextConstraint 
= true;
        }


        
#endregion
        

        
#region public abstract methods

        
public abstract bool Constraint(ServiceEndpoint endpoint);

        
#endregion


    }

这 是一个抽象类,包含了两个protected字段,其中m_bindingConstraint即形成职责链的关键,通过AddConstraint() 方法可以将BindingConstraint对象加入到职责链中,而字段m_hasNextConstraint则用来标识当前对象之下是否还存在 BindingConstraint对象。抽象方法Constraint()则实现约束性检查的业务逻辑,同时还包括对职责链是否存在下一个职责对象的判 断。例如BasicHttpBindingConstraint的定义如下:
    public class BasicHttpBindingConstraint:BindingConstraint
    
{
        
public override bool Constraint(ServiceEndpoint endpoint)
        
{
            
if (endpoint.Binding.Name == "BasicHttpBinding")
            
{
                
//check BasicHttpBinding Constraint;
                BasicHttpBinding binding = endpoint.Binding as BasicHttpBinding;
                //...

                
//check binding and address's scheme                if (endpoint.Address.Scheme.ToLower() != "http" && endpoint.Address.Scheme.ToLower() != "https")
                
{
                    
throw new BindingConstraintException("the binding is mismatch with address schema.");
                }
                

                
return true;
            }

            
else
            
{
                
if (m_hasNextConstraint)
                
{
                    
return m_bindingConstraint.Constraint(endpoint);
                }

                
else
                
{
                    
return false;
                }

            }

        }

    }

根 据传入的endpoint对象,判断绑定的名称是否为"BasicHttpBinding",如果是,则执行该约束对象的约束性检查,如果不是,则去寻找 职责链中的其它对象(根据m_hasNextConstraint判断)。正是通过这样的实现方式,当客户端调用BindingConstraint的 Constraint()方法时,就可以根据传入的ServiceEndpoint对象,智能地找到符合自己条件的BindingConstraint对 象,并执行Constraint()方法进行约束性检查。
 
在我的《软件设计精要与模式》一书中,在应用职责链模式时,我的建议是最好提供专门创建职责链的工厂类,在本例中自然也不例外:
    public class BindingConstraintFactory
    
{
        
public static BindingConstraint CreateConstraint()
        
{
            BindingConstraint basicHttp 
= new BasicHttpBindingConstraint();
            BindingConstraint netTcp 
= new NetTcpBindingConstraint();
            BindingConstraint netPeerTcp 
= new NetPeerTcpBindingConstraint();
            BindingConstraint netNamedPipe 
= new NetNamedPipeBindingConstraint();
            BindingConstraint wsHttp 
= new WSHttpBindingConstraint();
            BindingConstraint wsFederationHttp 
= new WSFederationHttpBindingConstraint();
            BindingConstraint wsDualHttp 
= new WSDualHttpBindingConstraint();
            BindingConstraint netMsmq 
= new NetMsmqBindingConstraint();
            BindingConstraint msmqIntegration 
= new MsmqIntegrationBindingConstraint();

            netMsmq.AddConstraint(msmqIntegration);
            wsDualHttp.AddConstraint(netMsmq);
            wsFederationHttp.AddConstraint(wsDualHttp);
            wsHttp.AddConstraint(wsFederationHttp);
            netNamedPipe.AddConstraint(wsHttp);
            netPeerTcp.AddConstraint(netNamedPipe);
            netTcp.AddConstraint(netPeerTcp);
            basicHttp.AddConstraint(netTcp);

            
return basicHttp;
        }

    }

通过CreateConstraint()方法,就可以像穿珠子一般,形成一条隐藏的职责链(在创建这样的职责链时,需要注意职责对象的先后次序),然后在客户端代码中,可以非常优雅的实现对绑定的约束性检查:

private static BindingConstraint m_bindingConstraint = BindingConstraintFactory.CreateConstraint();

 

public void AddServiceEndpoint(ServiceEndpoint endpoint)

{

if (m_bindingConstraint.Constraint(endpoint))

{

m_endpointsList.Add(endpoint);

}

}


不管传入的endpoint是何种类型的绑定,只要在职责链中创建了相对应的绑定约束对象,都可以进行约束性检查,且逻辑清楚,职责明确。如果对 绑定进行了扩展,我们只需要新增对应的绑定约束对象,然后修改BindingConstraintFactory工厂类的工厂方法即可。
 
使用职责链模式必须恰到好处,否则会成为模式滥用的反面教材。根据我对职责链模式的理解,可以认定只要同时符合下列三个条件,就可以引入职责链模式:
1、当一个方法的传入参数将成为分支语句的判断条件时;
2、当每一个分支的职责相对独立,且逻辑较为复杂时;
3、当分支条件存在扩展的可能时。
 
至于职责链模式的最佳实践,则包含以下内容:
1、应尽量将职责链模式的抽象定义为抽象类,而不要定义为接口。这样有利于一些公共逻辑的重用。
2、应在实现职责链模式的同时,提供创建职责链的工厂类。

本文已在IT168发表:http://tech.it168.com/msoft/2008-01-31/200801310937911.shtml

posted @ | Feedback (0) | Filed Under [ 软件设计 ]

Thursday, January 31, 2008

shupi.jpg

《Programming WCF Services》是Juval Lowy 的大作,其人其书可以到www.idesign.net网站上去查阅。书的质量要看书的内容来说话,这里用不着我来饶舌。本书的翻译由我和徐宁合作,历时半载,从初稿到再稿,直到三稿,可谓历尽艰辛,力求翻译出原文所表达的韵味,真实传达作者的思想。现在,终于可以与各位读者见面了。其中滋味,难与人说。

机 械工业出版社的编辑同志效率很高,态度认真,甚至为了保证本书的质量,不惜推迟出版日期,放弃抢先占领市场的先机,同意我的要求,使得我能够有更多的时间 来仔细校阅自己的译稿。好在不辱使命,这本书仍然能够作为国内介绍WCF的第一本出现在国内技术界,可以说是译者的幸运。

译书不是一件讨好的事情,我也预备着迎接一片骂声了。并非我不自信,实在是众口难调,我很难有信心说自己的翻译能够百分百的达到原著的水平。我只能说,我尽力了!

书 能够在过年之前出版,我很高兴。我希望本书的阅读能够给大家带来新年的好心情。虽然钱锺书先生说过,所谓献书,“其实只仿佛魔术家玩的飞刀,放手而并没有 脱手。随你怎样把作品奉献给人,作品总是作者自已的。”但我还是预备将本书赠给我的父母,以及我的妻子,感谢他们在我译书过程中对我的支持与关心。

本书的中文名为《WCF服务编程》,这是出版社的同志为我确定的。我本来想照着刘如鸿先生翻译的Juval Lowy《Programming .NET Components》,将本书定名为《WCF服务程序设计》,这样就可以使得Juval Lowy的 书籍在中文版方面能有一个统一的体例与延续,如今看来,竟不能如愿了。不过,O'Reilly的书籍有一个好的习惯,不管是何种语言版本的书籍,都需得与 原版保持基本一致的装帧风格,所以,阅读本书,至少在装帧方面能够带给你阅读原版的感觉。我只期望大家阅读本书内容,也能有此感,则万分庆甚!

本书在China-Pub的链接地址:http://www.china-pub.com/38123
本书在华储网的链接地址:http://www.huachu.com.cn/itbook/itbookinfo.asp?lbbh=10072934#

照例,我还要为自己的著作《软件设计精要与模式》打个广告,谢谢!

posted @ | Feedback (5) | Filed Under [ WCF & SOA ]

Tuesday, January 08, 2008

这里所谓的“读书时代”,并不能与“学生时代”划上等号,盖因读书的时节并非一定要在作为莘莘学子的时候,只要有闲暇时,未尝不可以读书。

我素来对文字报有极高地热忱,我不敢想象仓颉造字鬼神哭泣的境况,但却知道如若没有文字,则世界不知会黯淡几分?孩童之时,喜读连环画,很多人会沉迷于图 画中的各种人物模样与风景,但我往往会因为阅读画下的文字,而忽略对所谓“画”的领悟。所以,对于我在无知幼童之时,竟然喜欢抱着繁体字的西游记吃力地阅 读,也就不足为怪了。当时的四大名著,《西游记》给我的冲击主要是一种让人激动的幻想,以及为那大闹天宫,打得众天兵天将屁滚尿流的威风凛凛的齐天大圣的 风采所折服。其实我最爱的还是那本“诲盗”的《水浒传》,那群傲啸水泊梁山的108众好汉,在一个男孩子的心理,完全是内心盼望着在长大之后也要成为这样 的英雄。也许,幼时的我,会认为这样才是一种挥洒方遒的写意人生吧!

《三国演义》对于儿时的我来说,其实太过于深奥了。但我记得那个时候有一个书系,唤作“小图书馆系列”,现在想来对我影响颇大。我始终觉得编写这一系列的 作者们必然是一些高超的小说家,能够在不失去原著原味的基础上,写得那么的浅显易懂。我阅读的三国演义,其实就是这一系列的三国演义改编版。我对这个小图 书馆系列始终抱有好感,我记得自己还阅读了《大卫·科波菲尔》、《汤姆大叔的小屋》以及《汤姆·索亚历险记》等国内外名著的改编版本。

《红楼梦》在我幼小的心灵里,充满了胭脂水粉的柔腻,在那时还不懂得什么是真正的文学作品,总会天真的认为《红楼梦》应该是女孩儿读的书,因而那时的我对于这样一本惊天之作,实际上是不屑一顾的。或许,每个男人的幼年心理中,都应该有一个英雄的梦吧。

我没有机会去看《封神演义》、《七侠五义》,也不曾看过《说岳全传》,因为那个时候,所谓通俗小说实际上是金庸先生的时代。“凡有井水处,皆能歌柳词”, 其实有井水之处,皆在读金庸小说,似乎也能说得通顺。我已经不记得自己阅读的第一本武侠小说是什么了,但还能清晰地记得在阅读残本的《射雕英雄传》时候的 那种眉飞色舞。都说武侠小说是成人的童话,其实在孩童的心理,又何尝不是童话呢?

初中的时候,我最喜欢翻弄舅父的书架。舅父是教授中文的,是以他的书架对于我而言,无异于宝山。也正是在翻弄书架的同时,我知道了罗曼·罗兰,列夫·托尔 斯泰,知道了契珂夫,屠格涅夫,还知道了拜伦,雪莱,济慈,当然,我更知道了周作人、胡适、钱钟书、沈从文、梁实秋、林语堂、郁达夫。我像一个饥饿的人, 阅读着约翰·克里斯多夫的故事,也为复活中玛斯洛娃的悲惨命运落泪,我背诵着拜伦如白银一般闪光的篇章,为济慈最后写在水上的名字而神伤。我像膜拜缪斯之 神一般,学着朱自清的锦绣华章,揣摩沈从文凤凰一般清艳的文字,为郁达夫的愁绪而感伤不已。

我还尝试着去阅读《诗经》、《离骚》,但最喜欢的还是李太白的豪放与瑰奇,尤爱他的乐府长诗,诵读出来也倍觉豪迈与旷达。我喜欢曹孟德的诗文,激赏曹植飘飘如仙的《洛神赋》,尤喜唐宋八大家,突然觉得读这些大家之作,则其余人的文字再无足观矣。

高中的时候,我终于体会到了《红楼梦》的超凡之处,虽然只是一鳞半爪,却也不得不为之而倾倒。如果说命运安排我流放到一个渺无人烟的荒岛,假设只允许我带 一本书的话,我会毫不犹豫地选择《红楼梦》。读《红楼梦》一遍是万万不够的,实质上,取出《红楼梦》来,不管翻至何页,你都可以饶有趣味地阅读下去,而每 读一遍,你又会有新的体会,新的感触。小说的魅力竟至于此,或者也可以说是一种灵魂的旅游吧。

对于进入大学的我们而言,才真正可以算是脱缰的野马,终于体会到了无拘无束的自由。至少,我可以不用偷偷摸摸地看金庸小说了。在将“飞雪连天射白鹿,笑书 神侠倚碧鸳”一一梳理个遍之后,我们慢慢发现了一个读书的宝库,那就是校图书馆的样本书库,除了馆藏的古籍与工具书外,全馆的书籍都要在此留存一本的。或 许是为了增加效益,样本书库的部分书籍以收费方式对学生公开。于是,我和几个室友利用课余时间帮助图书馆老师打打杂、跑跑腿,终于赚来了可以自由出入书库 的权利,从而开始了长达四年的读闲书生涯。

既然是闲书,也就无所谓书的类型和品味,但求兴之所致。随笔、小说、诗歌……无论体裁;文学、历史、政治……无论课目;凡是能给与我趣味的满足,能有一字 之得,统统都不放过。这样的闲读生涯,荒废了我本来的专业,却也开拓了视野,博览了群书,所失与所得,很难辨得清是与非,功与过。

在样本书库晃悠的时期内,最令我雀跃不已的是图书馆进新书的时候。由于是样本书库,但凡馆藏的新书到了,都得放一本到样本书库中。而我们则负责帮助老师对 这些新书进行分类,然后放入各自科目的书架上。有我们这些超级书虫在,可苦了那些秋水望穿期待着新书的其他同学们。正所谓“权利腐败”,在我们手上就产生 了书籍的“贪污”,为新书分类的时候就将自己感兴趣的书籍直接过滤了,他人若要阅读这些书籍,那就抱歉,只有等我看完之后才有机会拜读了。

如果说大学四年有什么值得记忆的时刻,或许就是我们在图书馆度过的快乐时光了。今后,也不可能再有这样的机会,蠹鱼一般钻进书山之中,忘我的阅读吧。

工作以及读研的几年,几乎就要失去读书的乐趣了。闲适的时光固然多多,然而闲适的心情却寥寥。由于受到一个人的影响,我开始阅读余光中。诗是诗人之诗,文 是学者之文,虽偏居海外孤岛,心却包容世界。这个时期的我,或许是因为工作的负累,更愿意阅读一些轻松悠闲的小品文章,最爱董桥的英式幽默与中式诗意,王 小波的嬉笑怒骂与诗人情怀,以及他冷眼旁观的静默人生;为杜拉斯的《情人》而忧伤,在博尔赫斯的《小径分岔的花园》中迷失;然后在李商隐的《锦瑟》中找到 自己惘然的一叹,最后在贝多芬的金铁铿音中找到慰籍的力量。

其余,就是阅读专业书的日子了。我终于发现了软件设计艺术的乐趣,这些乐趣都集萃在我的著作《软件设计精要与模式》中,在该书的序中已经记录了阅读的足迹。

posted @ | Feedback (5) | Filed Under [ 随笔与随想 ]

Wednesday, January 02, 2008

若要公开WCF服务,需要提供一个运行服务的宿主环境。就像.NET CLR需要创建宿主环境以托管代码一般,WCF的宿主环境同样运行在进程的应用程序域中。在应用程序域中可以创建一个或多个ServiceHost实例,其关系如图一所示:
Host_ServiceHost.gif 
图一  托管ServiceHost

WCF并不推荐在应用程序域中创建多个ServiceHost实例。如果要托管多个服务,完全可以在一个宿主中通过多个Endpoint公开多个WCF服务。由于应用程序域对安全进行了隔离,如果需要提供不同的安全上下文,则有必要创建多个ServiceHost实例。

WCF的典型宿主包括以下四种:
1、"Self-Hosting" in a Managed Application(自托管宿主)
2、Managed Windows Services(Windows Services宿主)
3、Internet Information Services(IIS宿主)
4、Windows Process Activation Service(WAS宿主)

以下将通过一个具体的实例分别介绍这几种宿主的托管方式及其相关的注意事项。在这样的一个实例中,我们定义了如下的服务契约:
namespace BruceZhang.WCF.DocumentsExplorerServiceContract
{
    [ServiceContract]
    public interface IDocumentsExplorerService
    {
        [OperationContract]
        [FaultContract(typeof(DirectoryNotFoundException))]
        DocumentList FetchDocuments(string homeDir);
        
        [OperationContract]
        Stream TransferDocument(Document document);        
    }    
}

服务的实现则如下所示:
namespace BruceZhang.WCF.DocumentsExplorerServiceImplementation
{
    [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
    public class DocumentsExplorerService : IDocumentsExplorerService
    {
        #region IDocumentsExplorerService Members

        public DocumentList FetchDocuments(string homeDir)
        {
            //implementation code
        }        

        public Stream TransferDocument(Document document)
        {
            //implementation code
        }

        #endregion     
    }
}
在服务契约的操作中,DocumentList与Document则为自己定义的数据契约:
namespace BruceZhang.WCF.DocumentsExplorerDataContract
{   
    [DataContract]
    public class Document
    {
        //DataMembers
    }
}
namespace BruceZhang.WCF.DocumentsExplorerDataContract
{
    [KnownType(typeof(Document))]    
    [CollectionDataContract]
    public class DocumentList:IList<Document>
    {
        //IList<Document> Methods
    }
}
注意以上定义的服务契约、服务类与数据契约的命名空间。

1、自托管宿主

利 用WCF提供的ServiceHost<T>提供的Open()和Close()方法,可以便于开发者在控制台应用程序,Windows应用 程序乃至于ASP.NET应用程序中托管服务。不管自宿主的环境是何种应用程序,实质上托管服务的方式都是一致的。例如在控制台应用程序中:
using (ServiceHost host = new ServiceHost(typeof(DocumentsExplorerService)))
{
    host.Open();

    Console.WriteLine("The Service had been launched.");
    Console.Read();
}

由 于ServiceHost实例是被创建在应用程序域中,因此我们必须保证宿主进程在调用服务期间不会被关闭,因此我们利用Console.Read()来 阻塞进程,以使得控制台应用程序能够一直运行,直到认为地关闭应用程序。如果是Windows应用程序,则可以将创建ServiceHost实例的代码放 在主窗体的相关代码中,保证服务宿主不会被关闭。

相应地,我们需要配置应用程序的app.config配置文件:
<configuration>
  <system.serviceModel>
    <services>
      <service name="BruceZhang.WCF.DocumentsExplorerServiceImplementation.DocumentsExplorerService" behaviorConfiguration="DocumentExplorerServiceBehavior">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:8008/DocumentExplorerService"/>
          </baseAddresses>
        </host>          
        <endpoint
          address=""
          binding="basicHttpBinding"
          bindingConfiguration="DocumentExplorerServiceBinding"
          contract="BruceZhang.WCF.DocumentsExplorerServiceContract.IDocumentsExplorerService"/>
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>       
      </service>
    </services>
    <bindings>
      <basicHttpBinding>
        <binding name="DocumentExplorerServiceBinding" sendTimeout="00:10:00" transferMode="Streamed"
                  messageEncoding="Text" textEncoding="utf-8" maxReceivedMessageSize="9223372036854775807">          
        </binding>
      </basicHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="DocumentExplorerServiceBehavior">
          <serviceMetadata httpGetEnabled="true"/>          
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

注意,配置文件中的服务名必须包含服务契约以及服务类的命名空间。此外,在配置文件中我通过<baseAddresses>标签为服务添加了基地址,因此在endpoint中,address为""。

此时,调用服务的客户端配置文件也应与服务的配置保持一致:
<configuration>
  <system.serviceModel>
    <client>
      <endpoint        
        address="http://localhost:8008/DocumentExplorerService"
        binding="basicHttpBinding"
        bindingConfiguration="DocumentExplorerServiceBinding"
        contract="IDocumentsExplorerService"/>
      </client>
    <bindings>
      <basicHttpBinding>
        <binding name="DocumentExplorerServiceBinding" sendTimeout="00:10:00" transferMode="Streamed"
                  messageEncoding="Text" textEncoding="utf-8" maxReceivedMessageSize="9223372036854775807">
        </binding>
      </basicHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>

注意,两个配置文件中的服务地址都是一样的,对于绑定的配置也基本一致。

在 通常的企业应用中,我们很少会采用自宿主方式托管服务,这是因为这种方式必须要在应用程序运行下,客户端才能够调用服务,且并不便于随时启动和停止服务。 除了不具有易用性与易管理性之外,在可靠性、性能等诸多方面受到很多限制。但由于它简单、易于实现,因而往往用于开发期间的调试或演示环境。

自托管宿主支持所有的绑定。

2、Windows Services宿主

Windows Services宿主则完全克服了自托管宿主的缺点,它便于管理者方便地启动或停止服务,且在服务出现故障之后,能够重新启动服务。我们还可以通过 Service Control Manager(服务控制管理器),将服务设置为自动启动方式,省去了服务的管理工作。此外,Windows Services自身还提供了一定的安全性以及检测机制和日志机制。

Windows Services宿主的实现也非常简单。我们可以在Visual Studio中创建Windows Services项目。在创建项目之后,就可以创建一个继承了System.ServiceProcess.ServiceBase类的Windows服 务类。Windows服务类继承了ServiceBase类的OnStart()和OnStop()方法,完成Windows服务的启动与停止。我们可以 重写这两个方法,将ServiceHost的启动与关闭对应地放入这两个方法的实现中。例如我们创建的 DocumentsExplorerWindowsService类:
namespace BruceZhang.WCF.DocumentsExplorer
{
    public partial class DocumentsExplorerWindowsService : ServiceBase
    {
        private ServiceHost m_serviceHost = null;

        public static void Main()
        {
            ServiceBase.Run(new DocumentsExplorerWindowsService());
        }

        public DocumentsExplorerWindowsService()
        {
            InitializeComponent();

            ServiceName = "DocumentsExplorerService";
        }

        protected override void OnStart(string[] args)
        {
            if (m_serviceHost != null)
            {
                m_serviceHost.Close();
            }

            m_serviceHost = new ServiceHost(typeof(DocumentsExplorerService));
            m_serviceHost.Open();
        }

        protected override void OnStop()
        {
            if (m_serviceHost != null)
            {
                m_serviceHost.Close();
                m_serviceHost = null;
            }
        }
    }
}

在Main 函数中,我们通过ServiceBase.Run()静态方法创建Windows服务实例,并在Windows服务类的构造函数中,调用 ServiceBase类的ServiceName属性指定服务名。在重写的OnStart()方法中,我们首先判断是否已经存在ServiceHost 实例,如果不存在,则创建它。创建ServiceHost实例的方法与自托管宿主方式相同。

为了完成ServiceHost实例的创建,我们同样需要在项目中添加app.config配置文件,配置文件的内容与前完全一样。

如 果在企业应用中要使用WCF技术,最佳的宿主方式我认为就是Windows Services,尤其是服务器的操作系统不是Vista的情况之下。它便于服务的管理,能够维持服务长时期的运行,同时它还支持所有的绑定,因而受到的 限制最小。然而,这种方式唯一的缺点却是对宿主的部署相对比较复杂,必须通过.NET提供的Installutil.exe工具完成对服务宿主的安装(也 可以通过安装包的自定义操作完成)。

若要完成对服务宿主的安装,我们还需要创建它的安装程序。我们可以自定义一个安装类,使其继承自 System.Configuration.Install.Installer类。更简单的办法则是通过Windows服务提供的设计时支持,直接创建 安装类。方法是在Windows服务例如DocumentsExplorerWindowsService的设计器视图下,通过单击右键,在快捷菜单中选 择“Add Installer”,如图二所示:
Host_AddInstaller.gif 
图二   添加安装程序

创建的安装程序ExplorerServiceInstaller如下所示:
namespace BruceZhang.WCF.DocumentsExplorer
{
    //It needs be ran at the command mode
    //Type installutil filename to install the windows service
    //Type services.msc to access the Service Control Manager(SCM) and browse the windows services
    //Type installutil /u filename to uninstall the windows service
    [RunInstaller(true)]
    public partial class ExplorerServiceInstaller : Installer
    {
        private ServiceProcessInstaller m_process;
        private ServiceInstaller m_service;

        public ExplorerServiceInstaller()
        {
            InitializeComponent();

            m_process = new ServiceProcessInstaller();
            m_process.Account = ServiceAccount.LocalSystem;
            m_service = new ServiceInstaller();
            m_service.ServiceName = "DocumentsExplorerService";
            Installers.Add(m_process);
            Installers.Add(m_service);
        }
    }
}

在ExplorerServiceInstaller 类中,ServiceAccount是一个枚举类型,可以设置为LocalService,LocalSystem,NetworkService以及 User值。其中,LocalService的安全性最低,User值的安全性最高,需要有效的用户账号方才可以安装服务。

对于安装程序而言,也可以直接在设计器视图下设置它的属性。

安装程序直接建立在Windows服务的程序集中,编译之后会获得一个exe文件,例如DocumentsExplorer.exe。然后,我们通过在Visual Studio的Command Prompt模式下运行如下命令:
installutil DocumentsExplorer.exe
即可完成对服务宿主的安装。

打开服务控制管理器(可以在Command Prompt模式下输入Services.msc打开),可以看到名为DocumentsExplorerService的服务:
Host_WindowsServices.gif 
图三  服务控制管理器

如果要卸载该服务宿主,可以通过installutil的/u开关卸载。

在企业应用中,我们往往会将该Windows服务设置为自动启动,可以简化管理员的工作。

3、IIS宿主(说明,这里讲的IIS为IIS 6.0)

若要使用IIS宿主,需要为程序集中添加一个svc文件。我们可以通过为项目添加一个新项的方式添加svc文件:
Host_AddNewItem.gif
图四   添加svc文件

我们也可以直接创建一个WCF Service应用程序作为IIS宿主,它会自动创建一个svc文件,如图五所示:
Host_ServiceApp.gif 
图五   创建WCF Service应用程序

创建的svc文件如图:
Host_svc.gif 
图六  创建的svc文件

WCF Service应用程序创建的svc文件以及通过添加新项获得svc文件,自动会创建WCF服务。因此,如果我们希望在svc文件中嵌入WCF服务的代码,则可以采取这种方式。例如:
<%@ ServiceHost Language="C#" Debug="true" Service="BruceZhang.WCF.DocumentsExplorerServiceImplementation.DocumentsExplorerService"%>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.IO;

using BruceZhang.WCF.DocumentsExplorerDataContract;

namespace BruceZhang.WCF.DocumentsExplorerServiceImplementation
{
    [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
    public class DocumentsExplorerService : IDocumentsExplorerService
    {
        //Service Implementation
    }
}

上述代码中的@ServiceHost指示符只能是在右键单击svc文件后,在View Marckup中才能够看到。

Svc文件通过@ServiceHost指示符指定它所要托管的服务,此外还指定了实现服务的语言、调用模式,还可以设置CodeBehind,指定服务代码。不过,在IIS托管中,服务代码或程序集文件受到一定的限制,它只能放在如下的其中一个位置中:
(1)    svc文件的内嵌代码中;
(2)    放在注册于GAC的单独程序集中;
(3)    驻留于应用程序的Bin文件夹内的程序集中(此时,bin文件夹不必include)
(4)    驻留于应用程序的App_Code文件夹的源代码文件中(根据Language的设置,或者为C#或者为VB);

即使我们将服务代码放在应用程序根目录下,或者其它文件夹中,然后通过CodeBehind指定代码的路径,仍然不能托管服务。

如 果服务契约与服务类是通过引用的方式在宿主应用程序中,则我们可以直接创建一个扩展名为.svc的单个文件,然后include到应用程序根目录下,如图 六中的HostService.svc,该文件没有关联的cs文件。此时,在Visual Studio中直接打开该文件,并不能编写服务代码,而是指定@ServiceHost即可。

注意,上述方式的IIS宿主只能创建ServiceHost实例,如果是自定义的ServiceHost,则需要通过@ServiceHost的Factory来指定创建自定义ServiceHost的工厂类。例如这样的自定义ServiceHost以及相应的工厂类:
using System;
using System.ServiceModel;
using System.ServiceModel.Activation;

namespace BruceZhang.WCF.DocumentsExplorerIISHost
{
    public class CustomServiceHostFactory : ServiceHostFactory
    {
        protected override ServiceHost CreateServiceHost(
           Type serviceType, Uri[] baseAddresses)
        {
            CustomServiceHost customServiceHost =
               new CustomServiceHost(serviceType, baseAddresses);
            return customServiceHost;
        }
    }

    public class CustomServiceHost : ServiceHost
    {
        public CustomServiceHost(Type serviceType, params Uri[]  baseAddresses)
            : base(serviceType, baseAddresses)
        {
        }

        protected override void ApplyConfiguration()
        {
            base.ApplyConfiguration();
        }
    }
}

则@ServiceHost修改为:
<%@ ServiceHost Language="C#" Debug="true" Service="BruceZhang.WCF.DocumentsExplorerServiceImplementation.DocumentsExplorerService" Factory="BruceZhang.WCF.DocumentsExplorerIISHost.CustomServiceHostFactory"%>

在IIS托管应用程序中,我们需要创建web.config(注意,不是app.config),在<system.serviceModel>节中配置服务的相关内容:
  <system.serviceModel>
    <services>
      <service behaviorConfiguration="DocumentExplorerServiceBehavior"
       name="BruceZhang.WCF.DocumentsExplorerServiceImplementation.DocumentsExplorerService">
        <endpoint address="" binding="basicHttpBinding" bindingConfiguration="DocumentExplorerServiceBinding"
         contract="BruceZhang.WCF.DocumentsExplorerServiceContract.IDocumentsExplorerService" />
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
      </service>      
    </services>
    <bindings>
      <basicHttpBinding>
        <binding name="DocumentExplorerServiceBinding" sendTimeout="00:10:00" transferMode="Streamed" messageEncoding="Text" textEncoding="utf-8" maxReceivedMessageSize="9223372036854775807">
        </binding>
      </basicHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="DocumentExplorerServiceBehavior">
          <serviceMetadata httpGetEnabled="true" />
        </behavior>        
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>

注 意,这里的配置文件与之前的宿主配置文件有个别的差异,就是没有指定服务的基地址。这是因为IIS托管会自动将svc文件的地址作为服务的基地址,我们无 法在配置文件中自行指定。Svc文件的地址为svc文件在IIS虚拟目录或站点所设置的路径。例如,我们在IIS中创建一个虚拟目录 DocumentsExplorer指向IIS宿主应用程序DocumentsExplorerIISHost,如图七所示:
Host_IIS.gif 
图七  在IIS站点中为IIS创建虚拟目录

如 果站点的属性没有做任何修改,使用默认的端口号,以及Localhost,则访问服务的基地址为http: //localhost/DocumentsExplorer/HostService.svc。如果在配置文件的服务endpoint中设置地址为 DocumentsService,如:
<endpoint
address="DocumentsService"
binding="basicHttpBinding"
bindingConfiguration="DocumentExplorerServiceBinding"
    contract="BruceZhang.WCF.DocumentsExplorerServiceContract.IDocumentsExplorerService" />

则公开服务的地址则为http://localhost/DocumentsExplorer/HostService.svc/DocumentsServic。

通过IIS启动站点后,不需要做任何操作,服务宿主自动会创建ServiceHost实例或者Factory指定的自定义ServiceHost实例。

由于服务地址发生了变化,因此客户端的配置文件也需要做出相应的修改,必须将服务的地址设置为与之对应的地址。其中,服务的基地址为svc文件在IIS中的地址。

IIS 宿主是一种主要的服务托管方式,这是因为它具有易用性、可维护性、安全性、易于部署等多个优势。然而,它却具有一个致命的阿客流斯之踵,那就是它只支持 HTTP协议的传输绑定。特别对于局域网场景下,如果使用IIS宿主,就无法利用TCP传输的高效率,甚至无法使用MSMQ以及Peer to Peer传输。

IIS 7.0(基于Windows Vista和Windows Server 2007)提供的Windows激活服务(WAS)突破了IIS 6.0对于HTTP的依赖。

4、WAS宿主

WAS是IIS 7.0的一部分,但也可以独立地安装与配置。WAS支持所有可用的WCF传输协议、端口与队列。

利用WAS托管服务与IIS宿主托管服务的方法并没有太大的区别,仍然需要创建svc文件,同时在IIS中需要在站点中创建应有程序指向托管应用程序,还可以设置访问服务的别名与应用程序池。

由于WAS诉诸支持所有的绑定,因此此时的服务绑定并不会受到宿主的限制。

本文已在IT168发表:http://tech.it168.com/msoft/2007-12-11/200712111008739.shtml

posted @ | Feedback (0) | Filed Under [ WCF Tips ]

Monday, December 24, 2007

异常消息与特定技术有关,.NET异常同样如此,因而WCF并不支持传统的异常处理方式。如果在WCF服务中采用传统的方式处理异常,由于异常消息不能被序列化,因而客户端无法收到服务抛出的异常,例如这样的服务设计:
[ServiceContract(SessionMode = SessionMode.Allowed)]
public interface IDocumentsExplorerService
{
    [OperationContract]        
    DocumentList FetchDocuments(string homeDir);
}
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
public class DocumentsExplorerService : IDocumentsExplorerService,IDisposable
{
public DocumentList FetchDocuments(string homeDir)
{
    //Some Codes
 
    if (Directory.Exists(homeDir))
    {
        //Fetch documents according to homedir
    }
    else
    {
        throw new DirectoryNotFoundException(
string.Format("Directory {0} is not found.",homeDir));
    }
}
public void Dispose()
{            
    Console.WriteLine("The service had been disposed.");
}
}

则客户端在调用如上的服务操作时,如果采用如下的捕获方式是无法获取该异常的:
catch (DirectoryNotFoundException ex)
{
    //handle the exception;
}

为了弥补这一缺陷,WCF会将无法识别的异常均当作为FaultException异常对象,因此,客户端可以捕获FaultException或者Exception异常:
catch (FaultException ex)
{
    //handle the exception;
}
catch (Exception ex)
{
    //handle the exception;
}

然 而,这样捕获的异常,却无法识别DirectoryNotFoundException所传递的错误信息。尤为严重的是这样的异常处理方式还会导致传递消 息的通道出现错误,当客户端继续调用该服务代理对象的服务操作时,会获得一个CommunicationObjectFaultedException异 常,无法继续使用服务。如果服务被设置为PerSession模式或者Single模式,异常还会导致服务对象被释放,终止服务。

WCF为异常处理专门提供了FaultContract特性,它可以被应用到服务操作上,指明操作可能会抛出的异常类型。例如前面的服务契约就可以修改为:
[ServiceContract(SessionMode = SessionMode.Allowed)]
public interface IDocumentsExplorerService
{
[OperationContract]
[FaultContract(typeof(DirectoryNotFoundException))]
    DocumentList FetchDocuments(string homeDir);
}

然而,即使通过FaultContract指定了操作要抛出的异常,然而如果服务抛出的异常并非FaultException或者FaultException<T>异常,同样会导致通道发生错误。因此在服务实现中,正确的实现应该如下:
public class DocumentsExplorerService : IDocumentsExplorerService,IDisposable
{
public DocumentList FetchDocuments(string homeDir)
{
    //Some Codes
 
    if (Directory.Exists(homeDir))
    {
        //Fetch documents according to homedir
    }
    else
    {
        DirectoryNotFoundException exception = new DirectoryNotFoundException();
        throw new FaultException<DirectoryNotFoundException>(exception,
new FaultReason(string.Format("Directory {0} is not found.", homeDir)));
    }
}
}

我 们可以将服务所要抛出的异常类型作为FaultException<T>的类型参数,然后创建一个FaultReason对象用以传递错误消 息。客户端在调用服务代理对象时,可以捕获FaultException< DirectoryNotFoundException>异常,并且该异常不会使得通道发生错误,并且客户端可以继续使用该服务代理对象。即使服务 为PerCall服务,客户端仍然可以继续调用服务操作。如果服务为Session服务或Singleton服务,那么即使发生了异常,服务对象也不会被 终结。

如果只是为了让客户端获得异常消息,即使不施加FaultContract特性,或者抛出非FaultException异常,我 们也可以通过ServiceBehavior特性,将服务的IncludeExceptionDetailInFaults设置为true(默认为 false),此时,客户端可以捕获抛出的非FaultException异常信息,但该异常仍然会导致通道出现错误。

但是,在发布服务与部署服务时,我们应避免将服务的IncludeExceptionDetailInFaults设置为true。

如 果不希望使用FaultContract,同时又要保证服务抛出的异常能够被客户端捕获,并且不会导致通道错误,我们还可以通过错误处理扩展的方式实现。 此时,我们可以将服务本身作为错误处理对象,令其实现System.ServiceModel.Dispatcher.IErrorHandler接口:
public class DocumentsExplorerService : IDocumentsExplorerService,IErrorHandler, IDisposable
{…}

在 该接口的ProvideFault()方法中,可以将非FaultContract异常提升为FaultContract<T>异常,例如将 DirectoryNotFoundException异常提升为FaultExceptino< DirectoryNotFoundException>异常:
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
    if (error is DirectoryNotFoundException)
    {
        FaultException<DirectoryNotFoundException> faultException = new FaultException<DirectoryNotFoundException>(
            new DirectoryNotFoundException(), new FaultReason(error.Message));
        MessageFault messageFault = faultException.CreateMessageFault();
        fault = Message.CreateMessage(version,messageFault,faultException.Action);
    }
}

而在该接口的HandleError()方法中,则可以处理异常错误,例如记录日志。

要使得错误处理扩展生效,还需要向服务通道安装错误处理扩展。方法是让服务类实现System.ServiceModel.Description.IServiceBehavior接口:
public class DocumentsExplorerService : IDocumentsExplorerService,IErrorHandler,IServiceBehavior,IDisposable
{…}

然后在ApplyDispatchBehavior()方法中安装错误处理扩展:
public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
    foreach (ChannelDispatcher dispatcher in serviceHostBase.ChannelDispatchers)
    {
        dispatcher.ErrorHandlers.Add(this);
    }
}

通 过这样的处理,即使服务抛出的异常为DirectoryNotFoundException异常,并且在服务契约中没有通过FaultContract特 性指定该异常,客户端同样能够获得异常的错误信息,且该异常不会导致通道发生错误,客户端可以继续调用服务代理对象的操作。

本文已在IT168发表:http://tech.it168.com/msoft/2007-12-03/200712031939709.shtml

posted @ | Feedback (2) | Filed Under [ WCF Tips ]

Friday, December 21, 2007