摘要: 介绍了Microsoft用于构建分布式面向服务架构系统的新一代框架WCF的体系结构与技术要素,并通过开发一套即时通信软件展现了基于WCF构架开发分布式应用程序的编程步骤与技巧。 关键词:WCF;SOA;分布式;即时通信一、前言 自从在微软提出.NET战略以来,就针对建立企业级的分布式应用先后推出了一系列产品和技术,包括:ASP.NET Web服务、.NET Remoting、Message Queuing以及Enterprise Service等。这些技术为基于微软技术的软件研发人员开发分布式应用提供了很大的便利,同时也各自存在着一些不足。WCF(Windows Communication Foundation)作为微软基于SOA所推出的.NET平台下的新一代框架产品集成了现有技术的优点,代表了未来软件架构设计与开发的发展方向。因此,掌握并能在未来应用中合理运用WCF技术,对于程序员特别是基于微软技术开发的程序员而言是十分必要的。基于此,文章通过介绍一套即时通信软件的具体开发过程来展现基于WCF技术的分布式软件研发的基本步骤与高级技巧。
二、WCF概述 对于一个好的分布式系统来讲,设计时应当考虑到异构性、开放性、安全性、可扩展性、故障处理、并发性以及透明性等问题。基于SOAP的Web Service可以实现异构环境的互操作性,保证了跨平台的通信。利用WSE(Web Service Enhancements)可以为ASMX提供安全性的保证。.NET Remoting具有丰富的扩展功能,可以创建定制的信道、格式化器和代理程序。Enterprise Service(COM+)提供了对事务的支持,其中还包括分布式事务,可实现故障的恢复。MSMQ可以支持异步调用、脱机连接、断点连接等功能,利用消息队列支持应用程序之间的消息传递。从功能角度来看,WCF整合了ASMX、.Net Remoting、Enterprise Service、WSE以及MSMQ等现有技术的优点,它提供了一种构建安全可靠的分布式面向服务系统的统一的框架模型,使软件研发人员在开发分布式应用时变得更加轻松。 1. 面向服务 既然WCF是一套面向服务的框架,服务自然便是WCF中最为重要的概念。服务是指暴露在外的一系列功能的集合,面向服务则是指一套构建“面向服务程序”的抽象原则以及最优方法。对于业务逻辑的理解,传统的编程方式认为应将业务逻辑封装为对象,该对象提供了与业务相关的一些功能;而基于WCF的程序设计却更多的是考虑如何提供服务以及消费服务。与面向组件服务程序类似,基于SOA 的应用程序将服务封装到了单个逻辑程序当中,如图1所示。
图1封装服务的SOA应用程序逻辑图 2. WCF体系结构 WCF拥有一个非常灵活的分层体系结构,分布式应用程序可以使用高级API或者低级API编写。高级API或者服务层可以用于调用方法和事件。服务层把这些高级的抽象代码转换为消息,以使用低级API上的信道和端口。图2中显示了WCF应用程序的各个层。
图2 WCF体系结构图 WCF提供了对可靠性、事务性、并发管理、安全性以及实例激活等技术的有力支持,而这些支持均依赖于如图3所示的WCF构架。 在客户端,分布式应用通过一个代理来转发对宿主端所提供服务的调用,而代理拥有和服务相同的操作接口,另外还有一些附加的代理管理方法。这也就意味着客户端从来不会直接调用服务,即便这个服务就在本机的内存中。当客户端代理接收到来自客户端的调用请求后,它将消息通过信道链向下传递。每个信道都会执行相应的消息的调用前处理,例如对消息的编码、提供可靠的会话、对消息进行加密等。客户端的最后一个信道则是传输信道,根据配置的传输方式发送消息给宿主。 在宿主端,消息同样通过信道链进行传输。与客户端信道相对应,宿主端信道也会对消息执行相应的宿主端的调用前处理,例如对消息的解码、提供会话管理、对消息进行解密等。宿主端的最后一个信道则负责将消息发送给消息分发器(Dispatcher),由分发器负责调用服务的实例。
图3 WCF构架示意图 3. WCF的基本技术要素 作为基于SOA的一个框架产品,WCF实际上是构建了一个在互联系统中实现各个应用程序之间通信的分布式框架。它使得系统构架师与开发人员在构建分布式系统时,能将更多的精力投入到与系统的业务逻辑本身的设计上来,而无需过多的考虑底层通信的实现及相关问题。WCF最核心的部分是能够快捷的创建一个服务,一个WCF服务端框架由宿主、端点以及服务类三部分所组成,如图4所示。
图4 WCF服务框架 宿主(Host),即承载WCF Service运行的环境。可用的宿主环境包括: (1) 自承载方式:在控制台应用程序与基于WinForm的应用程序中都可以使用这种方式; (2) 系统服务方式:服务可以随着操作系统的启动而自动启动; (3) IIS方式:与Web Services 的部署方式类似,由请求消息来激活服务,但只支持HTTP方式的绑定; (4) WAS(Windows Process Activation Service)方式:这个宿主是 IIS7 的一部分,只有 Windows Vista 和 Windows Server 2008提供默认支持,它支持几乎所有的通讯协议并提供了应用程序池、循环回收、空闲管理、身份管理、隔离等强大的功能。 服务类(Service Class)是指一个标记了一些WCF特有的属性的类,它包含了对服务的业务逻辑的具体实现。 端点(Endpoints)是WCF实现通信的核心要素,客户端和服务端都通过端点来交换消息,WCF 允许我们为服务添加多个绑定和端点。端点由地址(Address)、绑定(Binding)以及契约(Contract)三部分组成,如图5所示。在WCF中,类ServiceEndpoint代表了一个Endpoint,在类中包含的EndpointAddress,Binding,ContractDescription类型分别对应端点中的地址、绑定以及契约。
图5 端点构成图 地址:每个服务都会关联到一个唯一的地址,因此地址定位和唯一标志了一个端点,其主要提供了两个重要信息: 服务位置以及传送协议。在WCF 中,地址由System.ServiceModel.EndpointAddress对象来表示,其包括URI、Identity、Headers三个要素。 绑定:绑定提供了一种可设置的方式来选择传输协议、消息编码、通讯模式、可靠性、安全性、事务传播以及交互方式等。例如在传输协议上可以选择HTTP/HTTPS、TCP、P2P、IPC甚至是MSMQ等方式。消息编码上可以选择使用纯文本方式来确保互操作能力,或者选择二进制编码来优化性能,或者使用 MTOM来提高负载能力,甚至是自定义编码方式。 WCF中提供了BasicHttpBinding、NetTcpBinding、NetPeerTcpBinding、NetNamedPipeBinding、WSHttpBinding、WSFederationHttpBinding、WSDualHttpBinding 、NetMsmqBinding以及MsmqIntegrationBinding九种标准类型的绑定。 契约:契约是用来描述服务功能的一种平台中立的标准方式,WCF 所有服务都需要实现一个或多个契约。WCF 定义了四种类型的契约: (1) 服务契约(Service Contracts): 定义了客户端可以使用哪些服务操作。 (2) 数据契约(Data Contracts): 定义了服务传输的数据类型。WCF 定义了一些隐式数据契约,比如 int、string 等,但更多时候需要使用 DataContractAttribute 显式定义那些自定义类型数据的数据契约。 (3) 错误处理契约(Fault Contracts): 定义了服务引发的错误信息,以及如何将这些异常传递给客户端。 (4) 消息契约(Message Contracts ): 允许直接操控服务的消息内容和格式。 一般情况下,应当用接口来定义服务契约,尽管我们也可以使用类。将服务契约定义为接口基于如下几点优点: (1) 便于契约的继承,不同根的类型可以*实现相同的契约; (2) 同一服务类型可以同时实现多个契约; (3) 类似于接口隔离原则,可以随时修改服务类型的实现而不影响其它实现; (4) 便于制定版本升级策略,新、旧版本的服务契约可以同时使用而互不影响。 在WCF中,对于自承载的服务,端点的相关的信息可以有代码实现与配置文件两种定义方式。而对于IIS承载服务,端点的相关的信息一般定义在虚拟根目录下的Web.Config文件中。一般来讲,使用配置文件来定义端点相关信息是更为灵活、更为推荐的一种方式,其可以在不修改代码、不重新发布系统的情况下对服务的地址、绑定和契约等参数进行修改(因为修改config类型文件的内容是不需要重新编译和重新部署的)。 在下面的代码中具体说明了如何定义宿主端的端点相关信息。其中地址为http://localhost:8080/HelloService,契约为WCFServiceHello命名空间下的IHello接口,绑定采用的是WSHttpBinding方式。值得注意的是,代码中的HelloService为相对地址,http://localhost:8080/提供的是基址,当然去掉基址直接将address设为http://localhost:8080/HelloService也是可以的。代码中还添加了名为MyServiceTypeBehaviors的行为配置,其将serviceMetadata节中的httpGetEnabled属性设为了true,目的是为了自动透过 HTTP-GET发布服务的元数据。WCF提供的另外一种发布元数据的方式是使用专门的 MEX 端点。 <configuration> <system.serviceModel> <services> <service name="WCFServiceHello.HelloWorld" behaviorConfiguration="MyServiceTypeBehaviors" > <endpoint contract="WCFServiceHello.IHello" binding="wsHttpBinding" address="HelloService"/> <host> <baseAddresses > <add baseAddress="http://localhost:8080/"/> </baseAddresses> </host> </service> </services> <behaviors> <serviceBehaviors> <behavior name="MyServiceTypeBehaviors" > <serviceMetadata httpGetEnabled="true" /> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel> </configuration> 在接下来的宿主代码中,只需要简单的创建ServiceHost类型的对象,并利用其实例方法Open启动服务应用程序即可,简要代码如下所示: using(ServiceHost host=new ServiceHost(typeof(WCFServiceHello.HelloWorld))) { Console.WriteLine("HelloService has been started..."); host.Open(); Console.ReadKey(); } 客户端和服务之间通过消息交换来完成方法调用和数据传递,而在 WCF 中定义了3种消息交换模式,如图6所示。 (1) OneWay:这种消息交换模式在调用方法后会立即返回而不需要等待服务端的消息返回。 (2) Request/Reply:这种消息交换模式属于同步调用。在调用服务方法后需要等待服务端的消息返回。 (3) Duplex:这种消息交换模式具有客户端与服务端双向通信的功能,同时它的实现还可以使消息交换具有异步回调的作用。
图6 WCF 中的3种消息交换模式 在设置完宿主端端点之后,同样也必须为分布式应用程序定义客户端的端点,而且只有当客户端的端点与宿主端的某个端点相互匹配时,客户端的请求才能被宿主端所监听到。如果服务提供了发布元数据,那么利用.NET Framework 3.0 SDK所提供的SvcUtil.exe工具可以很轻松的自动生成与宿主端对应的客户端代理以及客户端配置文件。比如,运行宿主端应用程序,然后打开Visual Studio 2005命令提示符,键入SvcUtil http://localhost:8080,便可以在当前目录下得到客户端代理文件HelloWorld.cs与客户端配置文件output.config。另外一种简便直观的可视化工具是SDK 所附带的SvcConfigEditor.exe(C:/Program Files/Microsoft SDKs/Windows/v6.0A/bin目录下,XP系统),使用这个工具可以非常方便地创建或修改宿主端和客户端的配置文件。 生成好客户端代理与配置文件后,在代码中直接使用客户端代理对象即可。 using (HelloClient client = new HelloClient()) { client.Hello(); } Console.ReadKey(); 另外一种创建客户端代理的方式是使用ChannelFactory动态的来创建。虽然WCF提供了这种方式,但是在实际开发中并不推荐使用它,毕竟 ChannelFactory 直接依赖于契约,而这恰恰违背了 SOA 中边界隔离的原则。利用服务器端与客户端之间的Channel来创建客户端代理的代码举例如下: ServiceEndpoint httpendpoint = new ServiceEndpoint(ContractDescription.GetContract(typeof(IHello)), new WSHttpBinding(), new EndpointAddress("http://localhost:8080/HelloService")); using (ChannelFactory<IHello> factory =new ChannelFactory<IHello>(httpendpoint)) { IHello service = factory.CreateChannel(); service.Hello(); } Console.ReadKey();
三、软件的分析与设计 软件主要的功能是初步实现基于WCF的局域网内的实时通信,以面向服务为指导思想将具体开发过程分为即时通信服务的设计与实现、宿主的设计与实现以及即时通信客户端的设计与实现三部分,使得应用程序具有较好的安全性、并发性、可扩展性以及可维护性。 1. 服务的设计 服务的设计包括了通用模块Common.dll的设计、服务契约IChat的定义与实现、客户端的回调接口IChatCallback的定义三个部分。 通用模块Common.dll主要包括对聊天者类型Person的定义,其包括name(聊天者的名称或代号)以及ImageURL(聊天者选择的头像图片的存储路径)两个私有字段及相应的属性访问器。由于Person 类型是自定义数据类型,因此必须加上DataContractAttribute 来显式定义它。Person类的实现核心代码如下: [DataContract] public class Person { private string imageURL; private string name; public Person(string imageURL, string name) { this.imageURL = imageURL; this.name = name; } [DataMember] public string ImageURL { get { return imageURL; } set { imageURL = value; } } [DataMember] public string Name { get { return name; } set { name = value; } } } 服务契约IChat的定义中主要包括Join、Say、Whisper以及Leave四个基本方法。其中Join表示:进入聊天;Say表示向所有用户广播消息;Whisper 表示对指定的用户发送消息;Leave表示离开聊天。 需添加ServiceContract属性将IChat接口标记为服务契约,由于要实现客户端与服务端双向通信的功能,因此还必须设置CallbackContract参数,其参数IChatCallback为Duplex 模式下的客户端回调类型。 对于IChat接口中的方法来讲,必须标记OperationContract属性。其中IsOneWay指示在该消息交换模式下调用方法后是否会立即返回而不需要等待服务端的消息返回;IsInitiating指示服务方法是否启动一个 Session;IsTerminating指示服务方法调用完成是否结束 Session。 IChat的定义代码如下: [ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IChatCallback))] interface IChat { [OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = false)] void Say(string msg);
[OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = false)] void Whisper(string to, string msg);
[OperationContract(IsOneWay = false, IsInitiating = true, IsTerminating = false)] Person[] Join(Person name);
[OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = true)] void Leave(); } 客户端的回调接口IChatCallback的定义包括了分别对应Join、Say、Whisper以及Leave的UserEnter、Receive、ReceiveWhisper以及UserLeave四个基本方法。UserEnter表示当有新聊天用户加入时所有聊天用户接收到一个相应的通知;Receive表示接收用户广播的消息;ReceiveWhisper 表示接收相关用户发来的消息;UserLeave表示当有聊天用户离开时所有聊天用户接收一个相应的通知。 注意在接口定义中,每个服务方法的消息转换模式均设置为One-Way。此外,回调接口是被本地调用,因此不需要定义[ServiceContract]属性。 回调接口IChatCallback的定义的代码如下: interface IChatCallback { [OperationContract(IsOneWay = true)] void Receive(Person sender, string message);
[OperationContract(IsOneWay = true)] void ReceiveWhisper(Person sender, string message);
[OperationContract(IsOneWay = true)] void UserEnter(Person person);
[OperationContract(IsOneWay = true)] void UserLeave(Person person); } 定义了服务契约以及客户端回调接口之后,仍需要定义一些消息类型用作客户端与服务之间的交互,具体代码及解释如下: // 定义说明消息类型的枚举类型MessageType public enum MessageType { Receive, UserEnter, UserLeave, ReceiveWhisper }; // ChatEventArgs继承至EventArgs,作为传递的消息参数, //其包括消息的类型msgType、消息发送者person以及消息的主体内容message public class ChatEventArgs : EventArgs { public MessageType msgType; public Person person; public string message; } 最后是对服务契约中IChat接口的实现类ChatService的设计。值得注意的是ChatService继承了IChat,不再需要像服务契约IChat一样添加ServiceContract属性。另外设置了其ServiceBehavior属性的InstanceContextMode参数来决定实例化方式(PerSession方式或者PerCall方式)。PerSession表明服务对象的生命周期存活于一个会话期间,在同一个会话期间对于服务的不同操作的调用都会施加到同一个客户端代理类型的对象上;PerCall则表示服务对象是在方法被调用时创建,结束后即被销毁。ServiceBehavior.ConcurrencyMode 参数的设置则用于控制具体服务对象的并发行为,其包括三种行为: Single: 为默认方式。服务实例是单线程的,不接受重入调用(reentrant calls)。也就是说对于同一个服务实例的多个调用必须排队,直到上一次调用完成后才能继续。 Reentrant: 和 Single 一样,也是单线程的,但能接受重入调用,至于针对同一服务对象的多个调用仍然需要排队。因为在 Single 模式下,当方法调用另外一个服务时,方法会阻塞,直到所调用的服务完成。如果方法不能重入,那么调用方会因无法接受所调用服务的返回消息,无法解除阻塞状态而陷入死锁。Reentrant 模式解决了这种不足。 Multiple: 允许多个客户端同时调用服务方法。不再有锁的问题,不再提供同步保障。因此使用该模式时,必须自行提供多线程同步机制(比如在本例中给static类型的syncObj对象加锁)来保证数据成员的读写安全。 类ChatService的实现代码及解析如下: [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Multiple)] public class ChatService : IChat { //用于保障多线程同步而设置的对象 private static Object syncObj = new Object(); //客户端的回调接口对象 IChatCallback callback = null; //用于广播事件的委托 public delegate void ChatEventHandler(object sender, ChatEventArgs e); public static event ChatEventHandler ChatEvent; private ChatEventHandler myEventHandler = null; //利用字典对象chatters来保存聊天者对象以及绑定的对应事件委托 static Dictionary<Person, ChatEventHandler> chatters = new Dictionary<Person, ChatEventHandler>(); //当前聊天者对象 private Person person; // 判断具有相应名字的聊天者是否存在于字典对象中 private bool checkIfPersonExists(string name) { foreach (Person p in chatters.Keys) { if (p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } // 在字典对象中搜索判断其中是否包含了相应名字的聊天者, // 如果有则返回其对应的委托ChatEventHandler;否则返回空 private ChatEventHandler getPersonHandler(string name) { foreach (Person p in chatters.Keys) { //不区分大小写 if (p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) { ChatEventHandler chatTo = null; chatters.TryGetValue(p, out chatTo); return chatTo; } } return null; } // 在字典对象中搜索判断其中是否包含了相应名字的聊天者, // 如果有则返回聊天者对象;否则返回空 private Person getPerson(string name) { foreach (Person p in chatters.Keys) { if (p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) { return p; } } return null; }
// 加入聊天,如果字典对象chatters中没有同名的聊天者 public Person[] Join(Person person) { bool userAdded = false; //创建新的ChatEventHandler类型委托,其指向MyEventHandler()方法 myEventHandler = new ChatEventHandler(MyEventHandler); //执行关键区域,判断是否存在同名聊天者, //如果不存在则向字典中添加该对象并将MyEventHandler委托作为其value, //以待后面来触发 lock (syncObj) { if (!checkIfPersonExists(person.Name) && person != null) { this.person = person; chatters.Add(person, MyEventHandler); userAdded = true; } } //如果新的聊天者添加成功,获得一个回调的实例来创建一个消息, //并将其广播给其他的聊天者 //读取字典chatters的所有聊天者对象并返回一个包含了所有对象的列表 if (userAdded) { callback = OperationContext.Current.GetCallbackChannel<IChatCallback>(); ChatEventArgs e = new ChatEventArgs(); e.msgType = MessageType.UserEnter; e.person = this.person; //广播有新聊天者加入的消息 BroadcastMessage(e); //将新加入聊天者对象的委托加到全局的多播委托上 ChatEvent += myEventHandler; Person[] list = new Person[chatters.Count]; //执行关键区域,将字典chatters的所有聊天者对象 //拷贝至一个聊天者列表上,用于方法返回 lock (syncObj) { chatters.Keys.CopyTo(list, 0); } return list; } else { return null; } } // 广播当前聊天者输入的消息给所有聊天者 public void Say(string msg) { ChatEventArgs e = new ChatEventArgs(); e.msgType = MessageType.Receive; e.person = this.person; e.message = msg; BroadcastMessage(e); } //在字典对象chatters中查找具有相应名称的聊天者对象, //并异步地触发其对应的ChatEventHandle委托 public void Whisper(string to, string msg) { ChatEventArgs e = new ChatEventArgs(); e.msgType = MessageType.ReceiveWhisper; e.person = this.person; e.message = msg; try { ChatEventHandler chatterTo; //执行关键区域,获取具有相应名称的聊天者对象在chatters中所对应的委托 lock (syncObj) { chatterTo = getPersonHandler(to); if (chatterTo == null) { throw new KeyNotFoundException("The person whos name is " + to + " could not be found"); } } //异步执行委托 chatterTo.BeginInvoke(this, e, new AsyncCallback(EndAsync), null); } catch (KeyNotFoundException) { } } /// 当聊天者离开时,从字典chatters中移除包含该对象的项。 /// 并从全局的多播委托上移除该对象对应的委托 public void Leave() { if (this.person == null) return; //获得该聊天者对象所对应的ChatEventHandler委托 ChatEventHandler chatterToRemove = getPersonHandler(this.person.Name); //执行关键区域,从从字典chatters中移除包含该聊天者对象的项。 lock (syncObj) { chatters.Remove(this.person); } //从全局的多播委托上移除该对象对应的委托 ChatEvent -= chatterToRemove; ChatEventArgs e = new ChatEventArgs(); e.msgType = MessageType.UserLeave; e.person = this.person; this.person = null; //将消息广播给其他聊天者 BroadcastMessage(e); } // 当chatters中的聊天者对象所对应的ChatEventHandler委托被触发时, // MyEventHandler方法将执行。 // 该方法通过检查传递过来的ChatEventArgs参数类型来执行相应的客户端 //的回调接口中的方法。 private void MyEventHandler(object sender, ChatEventArgs e) { try { switch (e.msgType) { case MessageType.Receive: callback.Receive(e.person, e.message); break; case MessageType.ReceiveWhisper: callback.ReceiveWhisper(e.person, e.message); break; case MessageType.UserEnter: callback.UserEnter(e.person); break; case MessageType.UserLeave: callback.UserLeave(e.person); break; } } catch { Leave(); } } // 异步地触发字典chatters中所有聊天者对象所对应的ChatEventHandler委托。 // BeginInvoke 方法可启动异步调用。第一个参数是一个 AsyncCallback 委托, //该委托引用在异步调用完成时要调用的方法。 // 第二个参数是一个用户定义的对象,该对象可向回调方法传递信息。 //BeginInvoke 立即返回,不等待异步调用完成。 //BeginInvoke 会返回 IAsyncResult,这个结果可用于监视异步调用进度。 private void BroadcastMessage(ChatEventArgs e) { ChatEventHandler temp = ChatEvent; if (temp != null) { foreach (ChatEventHandler handler in temp.GetInvocationList()) { handler.BeginInvoke(this, e, new AsyncCallback(EndAsync), null); } } } // EndInvoke 方法检索异步调用的结果。 //调用 BeginInvoke 后可随时调用 EndInvoke 方法; //如果异步调用尚未完成,EndInvoke 将一直阻止调用线程, //直到异步调用完成后才允许调用线程执行。 private void EndAsync(IAsyncResult ar) { ChatEventHandler d = null; try { //System.Runtime.Remoting.Messaging.AsyncResult封装了异步委托上 //的异步操作的结果。 //AsyncResult.AsyncDelegate 属性可用于获取在其上调用 //异步调用的委托对象。 System.Runtime.Remoting.Messaging.AsyncResult asres = (System.Runtime.Remoting.Messaging.AsyncResult)ar; d = ((ChatEventHandler)asres.AsyncDelegate); d.EndInvoke(ar); } catch { ChatEvent -= d; } } }
2. 宿主端的设计 设计好服务之后,宿主端的设计相对简单,只是一个基于控制台的应用程序。首先建立一个ServiceHost类型的对象host,然后调用ServiceHost类型的实例方法Open即可启动宿主监听。需要注意的是在程序末尾应当调用Abort以及Close将资源释放掉,或者利用using语句来自动释放所占用的资源。下面是宿主端的实现代码以及配置文件内容: static void Main(string[] args) { Uri uri = new Uri(ConfigurationManager.AppSettings["addr"]); ServiceHost host = new ServiceHost(typeof(Chatters.ChatService), uri); host.Open(); Console.WriteLine("Chat service Host now listening on Endpoint {0}", uri.ToString()); Console.WriteLine("Press ENTER to stop chat service..."); Console.ReadLine(); host.Abort(); host.Close(); } } 配置文件中,地址为net.tcp://localhost:10001/chatservice;绑定为netTcpBinding,并且提供可靠性传输、不保障安全性,发送超时时间限定为1秒;契约为Chatters.IChat接口。另外设置了名称为DuplexBinding的绑定配置,其对服务的分流作了限制,并发的Session个数的上限为10000。 <?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="addr" value="net.tcp://localhost:10001/chatservice" /> </appSettings> <system.serviceModel> <services> <service name="Chatters.ChatService" behaviorConfiguration="MyBehavior"> <endpoint address="" binding="netTcpBinding" bindingConfiguration="DuplexBinding" contract="Chatters.IChat" /> </service> </services> <behaviors> <serviceBehaviors> <behavior name="MyBehavior"> <serviceThrottling maxConcurrentSessions="10000" /> </behavior> </serviceBehaviors> </behaviors> <bindings> <netTcpBinding> <binding name="DuplexBinding" sendTimeout="00:00:01"> <reliableSession enabled="true" /> <security mode="None" /> </binding> </netTcpBinding> </bindings> </system.serviceModel> </configuration>
3. 客户端的设计 客户端的设计主要包括实现客户端代理与配置文件、客户端的回调接口契约的实现以及客户端登录、主窗体、聊天窗体界面的设计与代码逻辑实现这三部分。表示客户端的各个对象之间行为关系的序列图如图7所示。
图7 客户端应用的序列图 首先是利用svcutil.exe工具自动生成的客户端代理以及客户端的配置文件,并做出相应的更改。打开Visual Studio 2005 命令提示,转到ChatService所在目录,依次输入以下命令可以生成同步的客户端代理。 svcutil ChatService.exe svcutil *.wsdl *.xsd /language:C# /out:MyProxy.cs /config:app.config 输入以下命令可以创建异步的客户端代理: svcutil *.wsdl *.xsd /a /language:C# /out:MyProxy2.cs /config:app.config 因为用户加入聊天这个过程是异步执行的,所以可以利用自动生成的异步客户端代理文件中的BeginJoin 与EndJoin 方法来替换同步的Join方法。 客户端代理的实现代码如下所示: using Common;
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")] [System.ServiceModel.ServiceContractAttribute(CallbackContract = typeof(IChatCallback), SessionMode = System.ServiceModel.SessionMode.Required)] public interface IChat { [System.ServiceModel.OperationContractAttribute(AsyncPattern = true, Action = "http://tempuri.org/IChat/Join", ReplyAction = "http://tempuri.org/IChat/JoinResponse")] System.IAsyncResult BeginJoin(Person name, System.AsyncCallback callback, object asyncState);
Person[] EndJoin(System.IAsyncResult result);
[System.ServiceModel.OperationContractAttribute(IsOneWay = true, IsInitiating = false, Action = "http://tempuri.org/IChat/Leave")] void Leave();
[System.ServiceModel.OperationContractAttribute(IsOneWay = true, IsInitiating = false, Action = "http://tempuri.org/IChat/Say")] void Say(string msg);
[System.ServiceModel.OperationContractAttribute(IsOneWay = true, IsInitiating = false, Action = "http://tempuri.org/IChat/Whisper")] void Whisper(string to, string msg); }
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")] public interface IChatCallback {
[System.ServiceModel.OperationContractAttribute(IsOneWay = true, Action = "http://tempuri.org/IChat/Receive")] void Receive(Person sender, string message);
[System.ServiceModel.OperationContractAttribute(IsOneWay = true, Action = "http://tempuri.org/IChat/ReceiveWhisper")] void ReceiveWhisper(Person sender, string message);
[System.ServiceModel.OperationContractAttribute(IsOneWay = true, Action = "http://tempuri.org/IChat/UserEnter")] void UserEnter(Person person);
[System.ServiceModel.OperationContractAttribute(IsOneWay = true, Action = "http://tempuri.org/IChat/UserLeave")] void UserLeave(Person person); }
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")] public interface IChatChannel : IChat, System.ServiceModel.IClientChannel { }
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")] public partial class ChatProxy : System.ServiceModel.DuplexClientBase<IChat>, IChat { public ChatProxy(System.ServiceModel.InstanceContext callbackInstance) : base(callbackInstance) { }
public ChatProxy(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName) : base(callbackInstance, endpointConfigurationName) { }
public ChatProxy(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, string remoteAddress) : base(callbackInstance, endpointConfigurationName, remoteAddress) { }
public ChatProxy(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : base(callbackInstance, endpointConfigurationName, remoteAddress) { }
public ChatProxy(System.ServiceModel.InstanceContext callbackInstance, System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : base(callbackInstance, binding, remoteAddress) { }
public System.IAsyncResult BeginJoin(Person name, System.AsyncCallback callback, object asyncState) { return base.Channel.BeginJoin(name, callback, asyncState); }
public Person[] EndJoin(System.IAsyncResult result) { return base.Channel.EndJoin(result); }
public void Leave() { base.Channel.Leave(); }
public void Say(string msg) { base.Channel.Say(msg); }
public void Whisper(string to, string msg) { base.Channel.Whisper(to, msg); } } 在客户端配置文件中,地址、绑定、契约分别与宿主端配置所一一对应。 <?xml version="1.0" encoding="utf-8"?> <configuration> <system.serviceModel> <client> <endpoint name="" address="net.tcp://localhost:10001/chatservice" binding="netTcpBinding" bindingConfiguration="DuplexBinding" contract="IChat" /> </client> <bindings> <netTcpBinding> <binding name="DuplexBinding" sendTimeout="00:00:20" > <reliableSession enabled="true" /> <security mode="None" /> </binding> </netTcpBinding> </bindings> </system.serviceModel> </configuration> 对于客户端的回调接口契约的实现,首先需要定义一些消息类型作为客户端与服务交互的消息,代码及解释如下: //定义了消息类型的枚举 public enum CallBackType { Receive, ReceiveWhisper, UserEnter, UserLeave }; //代理事件参数 public class ProxyEventArgs : EventArgs { //当前的在线用户列表 public Person[] list; } // 客户端回调方法所绑定的事件的参数定义 public class ProxyCallBackEventArgs : EventArgs { //回调的类型 public CallBackType callbackType; //收到的消息内容 public string message=""; //发送消息的人 public Person person=null; } 然后是对实现了客户端回调接口契约IChatCallback的Proxy_Singleton类的设计。该类采用了将构造函数设置为私有并通过GetInstance()返回static的Proxy_Singleton类型的对象来实现单例设计模式,它提供了该唯一对象的唯一全局访问点,这样使得在同一个Session中对于ChatService的不同操作的调用都是对于同一个Proxy_Singleton类型的对象的操作。其保障了程序正确无误的执行而无需担心每次操作是否会重新初始化新的Proxy_Singleton类型的对象或者同一个Session中不同操作是否会是对于不同Proxy_Singleton类型的对象的操作调用。从这点来讲,这也与服务中设置的InstanceContextMode = InstanceContextMode.PerSession的语义一致。为了保障了多线程环境下的线程安全,代码中通过给readonly的静态对象加锁的方式来保障这点。 下面的代码是对Proxy_Singleton类的具体实现: public sealed class Proxy_Singleton : IChatCallback { private static Proxy_Singleton singleton = null; private static readonly object singletonLock = new object(); private ChatProxy proxy; private Person myPerson;
private delegate void HandleDelegate(Person[] list); private delegate void HandleErrorDelegate(); // ProxyEventHandler与ProxyCallBackEventHandler事件用于窗体订阅。 public delegate void ProxyEventHandler(object sender, ProxyEventArgs e); public delegate void ProxyCallBackEventHandler(object sender, ProxyCallBackEventArgs e); public event ProxyEventHandler ProxyEvent; public event ProxyCallBackEventHandler ProxyCallBackEvent; private Proxy_Singleton() { } // 接收发来的广播消息 public void Receive(Person sender, string message) { Receive(sender, message, CallBackType.Receive); } // 接收发来的消息 public void ReceiveWhisper(Person sender, string message) { Receive(sender, message, CallBackType.ReceiveWhisper); } // 根据callbacktype参数的值来封装消息,并触发ProxyCallBackEvent事件 private void Receive(Person sender, string message, CallBackType callbackType) { ProxyCallBackEventArgs e = new ProxyCallBackEventArgs(); e.message = message; e.callbackType = callbackType; e.person = sender; OnProxyCallBackEvent(e); } // 新的聊天者加入 public void UserEnter(Person person) { UserEnterLeave(person, CallBackType.UserEnter); } // 聊天者离开 public void UserLeave(Person person) { UserEnterLeave(person, CallBackType.UserLeave); } // 被UserEnter()以及UserLeave()调用, // 根据callbackType参数的值来封装消息,并触发ProxyCallBackEvent事件 private void UserEnterLeave(Person person, CallBackType callbackType) { ProxyCallBackEventArgs e = new ProxyCallBackEventArgs(); e.person = person; e.callbackType = callbackType; OnProxyCallBackEvent(e); } // 异步的Join操作,首先调用BeginJoin,该操作完成后将调用OnEndJoin。 public void Connect(Person p) { InstanceContext site = new InstanceContext(this); proxy = new ChatProxy(site); IAsyncResult iar = proxy.BeginJoin(p, new AsyncCallback(OnEndJoin), null); } // 作为异步调用中BeginInvoke的回调方法, // 获取当前在线用户的列表 private void OnEndJoin(IAsyncResult iar) { try { Person[] list = proxy.EndJoin(iar); HandleEndJoin(list); } catch (Exception e) { MessageBox.Show(e.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // 如果在线者列表不为空,则触发ProxyEvent事件 private void HandleEndJoin(Person[] list) { if (list == null) { MessageBox.Show("Error: List is empty", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); ExitChatSession(); } else { ProxyEventArgs e = new ProxyEventArgs(); e.list = list; OnProxyEvent(e); } } // 触发ProxyCallBackEvent事件 protected void OnProxyCallBackEvent(ProxyCallBackEventArgs e) { if (ProxyCallBackEvent != null) { ProxyCallBackEvent(this, e); } } // 触发ProxyEvent事件 protected void OnProxyEvent(ProxyEventArgs e) { if (ProxyEvent != null) { ProxyEvent(this, e); } } // 单例模式,返回Proxy_Singleton类型的唯一对象 public static Proxy_Singleton GetInstance() { lock (singletonLock) { if (singleton == null) { singleton = new Proxy_Singleton(); } return singleton; } } //通过布尔值 pvt来决定是调用客户端代理的广播方法还是发送消息给 //指定目标的方法 public void SayAndClear(string to, string msg, bool pvt) { if (!pvt) proxy.Say(msg); else proxy.Whisper(to, msg); } // 先调用客户端代理对象的Leave方法 // 最后调用AbortProxy()来释放客户端代理对象 public void ExitChatSession() { try { proxy.Leave(); } catch { } finally { AbortProxy(); } } // 调用客户端代理的Abort与Close方法 public void AbortProxy() { if (proxy != null) { proxy.Abort(); proxy.Close(); proxy = null; } } } 登录界面Login主要完成打开PickForm窗体以及选择用户的头像并显示,验证头像图片URL、用户名称的合法性并将它们作为参数创建一个聊天者类型的对象,最终将此对象传递给新创建的主窗体并显示主窗体。登录界面Login执行效果图如图8所示。
图8 登录界面执行效果图 登录界面Login主要功能的实现代码及详细解析如下: // 当关闭选择图像的窗体时,在Login窗体中加载所选择的图片 private void ShowFace(object sender, EventArgs e) { if (pickfaceform.Visible == false) { this.pbFace.Image = Image.FromFile(pickfaceform.ImageURL); } } // 当用户选择好图像且输入了自己的名称时创建一个聊天者类型的对象, // 将此对象传递给新创建的主窗体并显示主窗体 private void btnLogin_Click(object sender, EventArgs e) { imgurl = this.pickfaceform.ImageURL; if ((txtName.Text.ToString() != "") && (imgurl != null)) { this.Visible = false; currentPerson = new Person(imgurl, txtName.Text); MainWindow mainwindow = new MainWindow(currentPerson); mainwindow.Show(); } else { MessageBox.Show("Please choose a face and then Enter your name before continue!", "Retry", MessageBoxButtons.OK,MessageBoxIcon.Warning); } } private void btnPickface_Click(object sender, EventArgs e) { pickfaceform.Show(); }
选择头像界面完成的功能相对单一,只是加载指定路径下的所有头像图片并在ListView控件中显示出它们,执行效果图如图9所示。
图9 选择头像执行效果图 选择用户头像界面PickForm主要功能实现代码及详细解析如下: // 加载指定路径下各种类型的图片 private void LoadImage() { string path = AppDomain.CurrentDomain.BaseDirectory + @"Faces"; DirectoryInfo di = new DirectoryInfo(path); //获得的各种类型文件存放的路径存放于FileInfo类型的数组中 FileInfo[] jpgfis = di.GetFiles("*.jpg"); FileInfo[] pngfis = di.GetFiles("*.png"); FileInfo[] giffis = di.GetFiles("*.gif"); FileInfo[] bmpfis = di.GetFiles("*.bmp"); string[] faces_path = new String[jpgfis.Length + pngfis.Length + giffis.Length + bmpfis.Length]; int i = 0, j = 0; foreach(FileInfo jpgisitem in jpgfis) { faces_path[i++] = jpgfis[j++].FullName; } j = 0; foreach(FileInfo giffisitem in giffis) { faces_path[i++] = giffis[j++].FullName; } j = 0; foreach(FileInfo pngfisitem in pngfis) { faces_path[i++] = pngfis[j++].FullName; } j = 0; foreach (FileInfo bmpfisitem in bmpfis) { faces_path[i++] = bmpfis[j++].FullName; } //i值记录相应图片在ImageList中的索引值 i = 0; ImageList largeimageList = new ImageList(); ImageList smallimageList = new ImageList(); largeimageList.ImageSize = new Size(40, 40); smallimageList.ImageSize = new Size(20,20); foreach (string facepath in faces_path) { ListViewItem item = new ListViewItem("", i); //将图片路径存储到Tag属性里 item.Tag = facepath as object; lstFaces.Items.AddRange(new ListViewItem[] { item }); largeimageList.Images.Add(Bitmap.FromFile(facepath)); smallimageList.Images.Add(Bitmap.FromFile(facepath)); i++; } //设置lstFaces的LargeImageList与SmallImageList属性, //用于控件显示为大、小图标时使用 lstFaces.SmallImageList = smallimageList; lstFaces.LargeImageList = largeimageList; } // 确定选择的头像后,隐藏窗体并将选中头像的Tag值 //(存储了该头像图片的路径)赋值给imageurl private void btnOK_Click(object sender, EventArgs e) { if (lstFaces.SelectedItems.Count != 0) { imageurl = lstFaces.SelectedItems[0].Tag.ToString(); this.Visible = false; } else { MessageBox.Show("Please click one face at least before press OK!", "Retry", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } }
主界面主要是利用单例模式创建的Proxy_Singleton类型的对象完成对于新用户加入、用户退出、接收广播消息、接收消息的处理以及显示聊天窗体的功能,执行效果如图10所示。
图10 主界面执行效果图 值得注意的是,在程序中窗体对象订阅了Proxy_Singleton类型对象的事件,因此当调用UI控件的方法,那么必须使用控件的一个 Invoke 方法来将调用封送到适当的线程(Proxy_Singleton对象的事件处理方法所在线程)。核心代码及各个模块功能的详细解释如下所示: public partial class MainWindow : StyledForms.GoogleTalkForm { Person currPerson; ImageList largeimageList=new ImageList(); ImageList smallimageList = new ImageList(); ChatControl chatcontrol=new ChatControl(); private Proxy_Singleton ProxySingleton = Proxy_Singleton.GetInstance(); public delegate void ListViewDoubleClickHandler(object sender, MouseEventArgs e); public MainWindow() { InitializeComponent(); this.Closed += delegate { Window_Closed(); }; } public MainWindow(Person currentPerson) { InitializeComponent(); largeimageList.ImageSize = new Size(40, 40); smallimageList.ImageSize = new Size(20, 20); lstChatters.Items.Clear(); currPerson = currentPerson; //当前用户加入聊天 ProxySingleton.Connect(currPerson); ProxySingleton.ProxyEvent += new Proxy_Singleton.ProxyEventHandler(ProxySingleton_ProxyEvent); //ChatControl窗体订阅ProxyCallBackEvent事件 ProxySingleton.ProxyCallBackEvent += new Proxy_Singleton.ProxyCallBackEventHandler( this.chatcontrol.ProxySingleton_ProxyCallBackEvent); //MainWindow窗体订阅ProxyCallBackEvent事件 ProxySingleton.ProxyCallBackEvent += new Proxy_Singleton.ProxyCallBackEventHandler( this.ProxySingleton_ProxyCallBackEvent); this.lblcurrPerson.Text += currPerson.Name.ToString(); this.Closed += delegate { Window_Closed(); }; } private void Window_Closed() { //用户退出聊天 ProxySingleton.ExitChatSession(); Application.Exit(); } private delegate void ProxySingleton_ProxyEvent_Delegate(object sender, ProxyEventArgs e); //该方法作为ProxyEvent事件绑定的委托所引用的方法 //通过ProxyEventArgs参数获得所有参与聊天的用户的信息并显示他们的名称 private void ProxySingleton_ProxyEvent(object sender, ProxyEventArgs e) { //如果Windows 窗体中的控件被绑定到特定的线程,则不具备线程安全性。 //因此,如果从另一个线程调用控件的方法, //那么必须使用控件的一个 Invoke 方法来将调用封送到适当的线程。 //InvokeRequired属性可用于确定是否必须调用 Invoke 方法。 if (this.InvokeRequired) { this.Invoke(new ProxySingleton_ProxyEvent_Delegate(ProxySingleton_ProxyEvent), sender, e); return; } lstChatters.View=View.LargeIcon; ImageList imageListLarge = new ImageList(); ImageList imageListSmall = new ImageList(); imageListSmall.ImageSize = new Size(20, 20); imageListLarge.ImageSize = new Size(40, 40); int i = 0; foreach (Person person in e.list) { ListViewItem item1 = new ListViewItem(person.Name,i++); //利用Tag属性来保存聊天者对象 item1.Tag = person as object; lstChatters.Items.AddRange(new ListViewItem[] { item1}); //向ImageList中添加相应图像 imageListSmall.Images.Add(Bitmap.FromFile(person.ImageURL)); imageListLarge.Images.Add(Bitmap.FromFile(person.ImageURL)); } lstChatters.SmallImageList = imageListSmall; lstChatters.LargeImageList = imageListLarge; } // 该方法作为ProxyCallBackEvent事件绑定的委托所引用的方法, // 负责调用客户端的回调方法与服务通信 private void ProxySingleton_ProxyCallBackEvent(object sender, ProxyCallBackEventArgs e) { switch (e.callbackType) { case CallBackType.Receive: Receive(e.person, e.message); break; case CallBackType.ReceiveWhisper: ReceiveWhisper(e.person, e.message); break; case CallBackType.UserEnter: UserEnter(e.person); break; case CallBackType.UserLeave: UserLeave(e.person); break; } } // 当接收到广播消息时,显示聊天窗体并显示消息内容 private void Receive(Person sender, string message) { showChatWindow(sender); } // 当接收到指定消息时,显示聊天窗体并显示消息内容 private void ReceiveWhisper(Person sender, string message) { showChatWindow(sender); } // 当有新用户加入时,将其添加至字典对象chatters中, // 并在ListView中添加相应的用户头像。 private void UserEnter(Person person) { smallimageList = lstChatters.SmallImageList; largeimageList = lstChatters.LargeImageList; ListViewItem item1 = new ListViewItem(person.Name,smallimageList.Images.Count ); item1.Tag = person as object; lstChatters.Items.AddRange(new ListViewItem[] { item1 }); smallimageList.Images.Add(Bitmap.FromFile(person.ImageURL)); largeimageList.Images.Add(Bitmap.FromFile(person.ImageURL)); lstChatters.SmallImageList = smallimageList; lstChatters.LargeImageList = largeimageList; } // 当有聊天用户离开时,将其从字典对象chatters中移除。 private void UserLeave(Person person) { ListViewItem item = lstChatters.FindItemWithText(person.Name, false, 0); lstChatters.Items.Remove(item); } // 显示聊天窗口 private void showChatWindow(Person otherPerson) { this.chatcontrol.CurrentPerson = currPerson; this.chatcontrol.OtherPerson = otherPerson; this.chatcontrol.Show(); } // 当选择相应的用户头像时,弹出与该用户聊天的窗体 private void lstChatters_MouseClick(object sender, MouseEventArgs e) { Person otherPerson = new Person(); //判断是否选中了相应的聊天者头像 if (!Object.Equals(((ListView)sender).GetItemAt(e.X, e.Y), null)) { otherPerson.Name = lstChatters.SelectedItems[0].Text; otherPerson.ImageURL = (lstChatters.SelectedItems[0].Tag as Person).ImageURL; showChatWindow(otherPerson); } } } 聊天界面主要完成发送消息和接收消息的处理功能。由于在主窗体代码中使用聊天窗体对象chatcontrol订阅了Proxy_Singleton类型对象的ProxProxyCallBackEvent事件,因此当当接收到发来的广播消息或者指定消息时,会自动触发聊天窗体对象chatcontrol的处理方法。聊天界面执行效果如图11所示。
图11 聊天界面执行效果图 核心代码及各个模块功能的详细解释如下所示: public partial class ChatControl :StyledForms2.GoogleTalkForm { Person currentPerson; Person otherPerson; private Proxy_Singleton ProxySingleton = Proxy_Singleton.GetInstance(); public ChatControl() { InitializeComponent(); } // 将聊天者输入的信息添加至RichTextBox中作为聊天记录 public void AppendText(string text) { this.rtxtRecord.AppendText(text); this.Show(); } public void AppendChatRecords(string msg) { this.rtxtRecord.AppendText(msg); this.Show(); } // 返回或设置当前聊天者对象的属性 public Person CurrentPerson { get { return currentPerson; } set { currentPerson = value; } } // 返回或设置当前聊天中另一方对象的属性并设置Label的Text属性 public Person OtherPerson { get { return otherPerson; } set { otherPerson = value; if (otherPerson != null) { this.lblChatTo2.Text= otherPerson.Name; this.Text = "Chatting with " + otherPerson.Name+" ..."; } } } // 当ProxyCallBackEvent事件触发,该方法将被调用 // 它将通过检查ProxyCallBackEventArgs的参数值来判断收到的消息类型, // 然后调用相应的方法 public void ProxySingleton_ProxyCallBackEvent(object sender, ProxyCallBackEventArgs e) { switch (e.callbackType) { case CallBackType.Receive: Receive(e.person.Name, e.message); break; case CallBackType.ReceiveWhisper: ReceiveWhisper(e.person.Name, e.message); break; case CallBackType.UserEnter: break; case CallBackType.UserLeave: break; } } // 该方法将把收到的广播来的消息添加到RichTextBox中作为聊天记录 public void Receive(string senderName, string message) { AppendText(senderName + ": " + message + Environment.NewLine); this.Show(); } // 该方法将把收到的对方发来的消息添加到RichTextBox中作为聊天记录 public void ReceiveWhisper(string senderName, string message) { AppendText(senderName + " Say: " + message + Environment.NewLine); } // 调用ProxySingleton类型的对象的SayAndClear()方法, //并将发送消息框清空 private void SayAndClear(string to, string msg, bool pvt) { if (msg != "") { try { ProxySingleton.SayAndClear(to, msg, pvt); txtMessage.Text = ""; } catch { AbortProxyAndUpdateUI(); AppendText("Disconnected at " + DateTime.Now.ToString() + Environment.NewLine); Error("Error: Connection to chat server lost!"); } } } // 调用ProxySingleton类型的对象的AbortProxy()方法并提示错误消息 private void AbortProxyAndUpdateUI() { ProxySingleton.AbortProxy(); MessageBox.Show("An error occurred, Disconnecting"); } // 调用ProxySingleton类型的对象的ExitChatSession()方法并提示错误消息 private void Error(string errMessage) { ProxySingleton.ExitChatSession(); MessageBox.Show(errMessage, "Connection error", MessageBoxButtons.OK, MessageBoxIcon.Error); } // 调用ProxySingleton类型的对象的SayAndClear()方法来发送消息, // 同时在RichTextBox中添加自己发送的消息内容作为聊天历史记录 private void btnSend_Click(object sender, EventArgs e) { if (txtMessage.Text == "") return; Person to = otherPerson; if (to != null) { string receiverName = to.Name; AppendText("Say to " + receiverName + ": " + txtMessage.Text + Environment.NewLine); SayAndClear(receiverName, txtMessage.Text, true); txtMessage.Focus(); } } // 重写OnClosed方法,将对象引用置为空 protected override void OnClosed(EventArgs e) { this.CurrentPerson = null; this.OtherPerson = null; this.Visible = false; } // 隐藏聊天窗体,并将对象引用置为空 private void btnClose_Click(object sender, EventArgs e) { this.CurrentPerson = null; this.OtherPerson = null; this.Visible = false; } // 隐藏聊天窗体,并将对象引用置为空 private void lblExit_Click(object sender, EventArgs e) { this.CurrentPerson = null; this.OtherPerson = null; this.Visible = false; } }
四、结束语 对于使用 .NET Framework 进行软件研发的开发人员来说,WCF 是分布式编程领域的一个新台阶,关注 WCF 技术的发展趋势并在实际开发项目中合理的运用它是十分必要的。本文简要介绍了SOA的基本思想以及Microsoft用于构建分布式面向服务系统的框架WCF的体系结构与技术要素,并在开发一套即时通信软件中充分利用了WCF基础框架在开发面向服务的分布式应用上带给我们的强大功能与全新体验,展现了基于WCF开发分布式软件系统的编程方法与程序设计技巧。
参考文献 1 WCF Service编程(英文影印版), Juval Lowy, 东南大学出版社, 2007, ISBN:9787564107741 2 学习WCF(英文影印版), MICHELE LEROUX BUSTAMANTE, 东南大学出版社, 2007, ISBN: 9787564109615 3 Professional WCF Programming: .NET Development with the Windows Communication Foundation, Scott Klein, Wiley Publishing, 2007, ISBN: 978-0-470-08984-2 4 MSDN - MSDN Library - .NET Development - Windows Communication Foundation http://msdn2.microsoft.com/en-us/library/ms735119.aspx
给朋友家的店做个宣传(东北口味):沟帮子熏鸡 沟帮子云杉熏鸡 沟帮子郝云杉熏鸡
|