WCF 的通信选项 - 第一部分






4.84/5 (85投票s)
WCF 通信选项 - 第 1 部分。
引言
我印象深刻!这可不容易。我已经试用了 CTP(2006 年 2 月)版本一段时间了,信不信由你,我觉得我们可能真的有所收获。我还没有时间去探索 WinFX 的其他“基础”成员,但从我看到的 Windows Communication Foundation (WCF) 来看,它确实是一个非常不错的套件。是的,我知道每次有新技术发布时,总会有一些*很棒*的功能。但在过去,我总觉得我们付出的(复杂性)远远大于我们获得的(功能),你还记得 COM 吗?这一次,我*感觉*我们是免费获得了这一切。这可能只是我天真的看法,但嘿,这都是关于我的。
我喜欢在文章开头加上免责声明。本文内容仅代表我个人的印象和理解,应在此背景下阅读。即使行文有时可能显得我好像知道我在说什么,但与事实的任何相似之处都可能仅仅是巧合。享受这段旅程。
为什么我们不能好好交流?
我们一离开 DOS 时代,就意识到我们不再孤单,我们需要学会与虚拟世界中的其他居民和睦相处并进行交流。DDE、OLE、COM、DCOM 和 Remoting 一直是提供两个应用程序之间通信机制的尝试。还记得 OLE 和 COM 最初被介绍时是如何描述的吗?“所有未来产品的基石”。回过头来看,我们可以看到它们真的只是蹒跚学步的婴儿。每一个都只解决了整个问题的一小部分。所以,如果它们是蹒跚学步的婴儿,那么 WCF 绝对是巨大的飞跃。WCF 为通信问题提供了一个完整的解决方案。而且它做到了优雅*和*简洁。你能看出我有一点*点*的兴奋吗?
无论你的需求是与同一台机器上的另一个模块通信,还是与用不同语言实现的另一个模块通信,或者你需要与世界上另一个机器上的模块通信,或者你想与运行在不同平台上的模块通信,甚至与一个*没有*运行的模块通信!是的,你可以在 WCF 下做到。
在我看来,WCF 之美在于它是一个“全包”的解决方案。它是第一个提供完整端到端解决方案,涵盖了问题范围和深度。从程序员的角度来看,它也是最简单的;你总是只需要进行一次方法调用。是的,我确信在底层有很多神奇之处来支持这种易用性。
现在,我还没有探索每一个角落、选项或可能性,但从我所见来看,这是一个绝佳的解决方案。至少对我来说,正如我之前所说的……
什么是服务?
让我们看看我能惹上多少麻烦。我认为,在其最根本的形式下,服务只是在一个*外部*进程中运行的某些功能(代码),它以*标准方式*提供给其他进程。这就是核心,除了*标准方式*还包括平台和语言的中立性。
因此,根据上述定义,服务实际上有两个部分。第一部分是必须在某个地方运行的代码,以便为客户端提供某些功能。第二部分,必须有一个*通用机制*,任何进程都可以使用,无论平台、语言或位置如何,都可以访问该服务。这个*通用机制*最终是 XML 和 SOAP。当然,客户端还需要一些额外的设施才能*知道*(或*发现*)服务提供了哪些功能。但我认为那些是支持技术。
还需要一些粘合剂来将服务的两个部分连接起来。这个粘合剂就是支持服务和客户端之间通信媒介(传输)的代码。我懒得…我是说,聪明,我们也想出了一些通用粘合剂。这样,每个服务实现都不必重新发明轮子。对于 Web 服务,通用粘合剂是 Web 服务器。因此,Web 服务器为使用 HTTP 作为其传输机制的服务提供了一个*托管环境*。我还想说,Web 服务是上述*服务*的一种特殊实现。
以下是我们将在本文其余部分中探讨的内容。如何定义服务?如何实现服务?如何托管服务?如何访问和使用服务?一旦我们掌握了基础知识,我们将研究 WCF 所支持的一些更复杂的通信选项。
那么 WCF 是什么?
对我来说,WCF 是这样的
WCF 是一个应用程序间通信平台。它提供了一个通用的 API,可以*隐藏*底层的通信媒介。它可以是平台中立的,并提供*位置无关性*。基本上,在 WCF 下,我,作为一个程序员,不需要知道或关心对方*在哪里*,*实际通信*是如何进行的。对我来说,这很美!
WCF 是*服务*为基础的技术。它以 Web 服务为根基,因此 XML 和 SOAP 是其核心技术。WCF 吸收了 Web 服务的概念并对其进行了超级充电。WCF 的很多外观和感觉与传统的 Web 服务相似。事实上,我喜欢把 WCF 服务想象成“注射了类固醇”的 Web 服务。你定义 WCF 服务的方式与定义 Web 服务非常相似。你可以用与 Web 服务相同的协议来查询已知服务的方法。并且你拥有与 Web 服务非常相似的基础设施要求。主要区别在于,WCF 扩展了可用的传输,除了 HTTP 之外,还包括 TCP/IP、IPC(命名管道)和消息队列。我认为 Web 服务的重点是解决*互操作性*问题,而 WCF 的重点是解决更广泛的*通信*问题。而且它在保持统一 API 的同时,还提供了更有效的机制。
从开发者的角度来看,最重要的功能是你不必关心你正在通信什么或如何通信。代码是相同的,无论最终的传输机制或服务的实际位置如何。
服务协定 - 暴露功能
我们的 WCF 之旅始于服务如何定义它们暴露的功能。在 WCF 下实现服务所需的基础设施大部分是使用*声明式编程*指定的。这意味着,使用属性来指定功能。以下显示了如何声明一个将被公开为服务的接口
[ServiceContract]
public interface ILocalTime
{
[OperationContract]
string GetLocalTime();
}
public class LocalTimeService : ILocalTime
{
...
}
ServiceContract
属性指定了该接口定义了服务的函数。OperationContract
用于装饰每个要作为服务一部分公开的方法。这就是创建 WCF 服务所需的所有内容。实际部署服务还需要稍多一点,我们稍后将介绍。
顺便说一句,实现服务时不必使用接口,就像定义类不必使用接口一样。你*确实*需要明确指定你想通过服务公开什么。你可以在接口中定义任何你想要或需要的其他内容,但只有被 [OperationContract]
装饰的方法才会被服务公开。
数据协定 - 暴露数据类型
WCF 还允许你暴露自定义数据类型,这样你就不局限于 CLR 的简单数据类型。这些是简单的结构,没有与之相关的方法。这有时会有点令人困惑,因为服务和 CLR 定义都使用了相同的语法。以下是我们将在下面使用的 DataContract
的示例。
[DataContract]
public class SensorTemp
{
[DataMember]
public int probeID;
[DataMember]
public int temp;
}
DataContract
指定了你正在暴露的数据类型,而 DataMember
指定了构成数据类型的成员。与 ServiceContract
一样,你必须使用 DataMember
显式声明哪些成员要暴露给外部客户端。这意味着你可以在类定义中包含任何你想要(或需要)的其他内容,但只有被 DataMember
装饰的成员才能被客户端看到。
WCF 中的编码选项
正如我们在上面看到的,在 WCF 中指定功能的一种方法是使用属性。属性会被编译器转换,生成 WCF 所需的大部分基础设施,以便我们创建和使用服务。
指定许多选项的第二种方法是通过配置文件。这允许你在不重新编译的情况下进行更改。许多 WCF 类会自动从配置文件中使用默认值。以下是一个使用配置数据指定的终结点示例(*终结点*稍后将进行描述)。首先是配置文件,然后是引用了配置数据代码语句
<endpoint name ="LocalTimeService"
address="net.pipe:///LocalTimeService"
binding="netNamedPipeBinding"
contract="ILocalTime" />
LocalTimeProxy proxy = new LocalTimeProxy("LocalTimeService");
最后,编码功能的第三种方法当然是编程方式。你可以通过属性或配置文件完成的许多事情也可以通过编程方式完成。这是前面使用编程方式定义的终结点
Uri baseAddress = new Uri(ConfigurationManager.AppSettings["basePipeTimeService"]);
baseAddress = new Uri(ConfigurationManager.AppSettings["basePipeTimeService"]);
serviceHost.AddServiceEndpoint(typeof(ILocalTime),
new NetNamedPipeBinding(), baseAddress);
终结点
终结点是服务的“身份”。它们定义了我们需要的所有信息,以便成功地与服务建立联系并进行通信。终结点由三部分信息组成:*地址*、*绑定*和*协定*。*地址*显然是服务的位置,例如“net.pipe:///LocalTimeService”。*绑定*指定了安全选项、编码选项和传输选项,这意味着很多选项!幸运的是,WCF 提供了一系列预定义的绑定,我们可以使用它们来简化我们的生活。最后,*协定*是服务实现的实际接口。
实现 WCF 服务
所以,服务只不过是一个用一些特殊属性装饰的普通类。这些属性随后会被编译器转换,生成特殊的代码基础设施,以便将该类作为服务公开给世界。在下面的代码中,我们首先定义一个接口,其中有一个返回服务部署位置的本地时间的方法。然后 LocalTimeService
类实现该接口,从而将功能暴露给世界,至少是对任何感兴趣的人。
[ServiceContract]
public interface ILocalTime
{
[OperationContract]
string GetLocalTime();
}
[ServiceBehavior(InstanceContextMode =
InstanceContextMode.PerCall)]
public class LocalTimeService : ILocalTime
{
public string GetLocalTime()
{
return DateTime.Now.ToShortTimeString();
}
}
这就是创建 WCF 服务所需的一切。如果你将上述代码编译成 DLL(一个库),你就创建了一个服务。当然,要有一个可用的东西还需要更多东西。我们需要另外两样东西来完成服务。我们需要一个能够在客户端请求服务功能时加载服务 DLL 的东西。我们需要一个能够监听通信端口,并查看收到的所有内容,以确定它是否与我们负责的服务协定匹配的东西。
部署 WCF 服务
有几种方法可以部署我们的服务。首先,如果我们实现的服务支持 HTTP,那么我们可以像普通的 Web 服务一样使用 IIS 部署我们的服务。如果我们想支持其他一些传输,那么我们可以使用 Windows Activation Service (WAS) 部署服务,这是 IIS 7.0 中的一项增强功能。如果这些都不合适,或者我们想要更多地控制服务,那么另一种解决方案是使用 ServiceHost
构建我们自己的托管环境。ServiceHost
是 WCF 中用于托管服务的一个类,几乎就像一个迷你 IIS,只是为了我们的服务。我们可以在 Windows 下的任何托管环境中实现 ServiceHost
,在控制台应用程序、Windows 可执行文件或 Windows 服务(以前称为 NT 服务)中。
ServiceHost
将在我们为服务指定的任何通道上,使用我们指定的任何协议进行监听,并在客户端请求我们特定服务时调用我们的服务。这只需要几行代码就能获得很多效果。我们只需要告诉 ServiceHost
它负责的终结点以及在收到匹配消息时应实例化的类。以下是托管 LocalTimeService
在控制台应用程序中所需的代码
class TimeService
{
static void Main(string[] args)
{
Uri baseAddress = new Uri(
ConfigurationManager.AppSettings["basePipeTimeService"]);
ServiceHost serviceHost = new ServiceHost(typeof(LocalTimeService),
baseAddress);
serviceHost.Open();
Console.WriteLine("Service is running....press any key to terminate.");
Console.ReadKey();
serviceHost.Close();
}
}
你现在可以编译服务了。但是,如果你尝试运行它,你会得到一个错误消息,表明你没有为 ServiceHost
提供终结点。正如我们上面所看到的,你可以通过编程方式或使用配置文件来指定终结点。使用配置的好处是你可以随时更改它,而无需重新编译。正如我们稍后将看到的,你可以为服务指定多个终结点,具体取决于你想支持的客户端。而且,如果以后你决定不再支持某个特定的传输,你只需要编辑配置文件。
以下是我们为 LocalTimeService
所需的配置文件
<configuration>
<appSettings>
<add key="basePipeTimeService"
value="net.pipe:///LocalTimeService" />
</appSettings>
<system.serviceModel>
<services>
<service name="LocalTimeService">
<endpoint
address=""
binding="netNamedPipeBinding"
contract="ILocalTime"
/>
</service>
</services>
</system.serviceModel>
</configuration>
让我们检查一下配置文件中的条目。你应该注意到,条目可以有多少个就有多少个。例如,服务可能支持多个终结点(不同的传输)。此示例中也只指定了一个服务,但同一个托管环境中可以提供多个服务。你可以看到终结点有三个属性:*地址*、*绑定*和*协定*。指示的绑定引用了 WCF 中提供的标准 netNamedPipeBinding
。对于每种传输,都有各种默认绑定类。你可以在文档中查看每个选项。
我在此说明,你最终也会遇到“*零应用程序(非基础设施)终结点*”异常。不会有很多关于具体哪里不匹配的线索,所以你需要仔细审查文本。确保你指定了正确的命名空间。
现在你可以执行该应用程序了,服务将可供任何知道如何与其通信的客户端使用。
代理
仅仅说我们要去纽约,并且我们要坐汽车去,并不能让我们到达那里。我们需要一辆汽车才能真正到达那里。拥有一个完全定义的终结点是不够的。我们需要一些(代码)能够真正地将终结点作为参数,并允许我们做我们想做的事情,调用一个方法。而那个东西就是一个代理。
就像过去 COM 一样,代理负责所有的底层连接(序列化和打包我们的参数),所以我们只需要进行调用。我们不关心它们是如何通过“龙头”推出去的,也不关心它们是如何在另一边被拉出来的。
正如我们过去一样,有一个实用工具可以为我们创建代理。但是,这是 WCF 需要改进的一个领域。我猜测在未来的版本中,这个功能将集成到 Visual Studio 中。至少,我希望如此。要创建一个代理,你需要使用命令行实用工具 **svcutil**,它有很多开关,并非所有开关都得到很好地记录。但嘿,我并不抱怨,这只是为了获得大量的重大改进而付出的微小不便。而且它仍然只是 Beta 版。
所以,你对你的服务 DLL 运行 **svcutil**,然后*砰*!你得到了你的代理类。还有其他选项,比如如果服务有一个 MEX 终结点,你可以指向它,它会动态地从服务中提取服务信息。这与 Studio 中创建 Web 服务时提供的功能基本相同,我们使用“添加 Web 引用”对话框。我真正想要的是 Visual Studio 自动生成代理,因为它一开始就有源代码中的所有信息!但正如我所说,我并不抱怨。;)
因此,目前创建代理是一个两步过程。首先,你对你的服务 DLL 运行 **svcutil**,它会创建模式(XSD)和 WSDL 文件。然后,你*再次*调用 **svcutil**,但这次你针对它刚刚创建的输出(* \*.xsd*、* \*.wsdl*)运行它。
**svcutil** 会生成“*output.cs*”作为默认文件名,除非你另有指定,我通常只是重命名它。还有选项可以只生成 DataContracts
或只生成 ServiceContracts
,还有一个选项可以生成客户端配置文件。这是 LocalTimeService
的代理文件,其中部分内容为了可读性已编辑。里面内容不多,因为所有的魔法都发生在 ClientBase
中。
[ServiceContract]
public interface ILocalTime
{
[OperationContract]
string GetLocalTime();
}
public interface ILocalTimeChannel : ILocalTime,
System.ServiceModel.IClientChannel
{
}
public partial class LocalTimeProxy :
System.ServiceModel.ClientBase<ILocalTime>, ILocalTime
{
public LocalTimeProxy()
{
}
public string GetLocalTime()
{
return base.InnerProxy.GetLocalTime();
}
}
消耗 WCF 服务
那么,下一步逻辑就是构建一个知道如何消耗所提供服务的客户端。客户端代码只需要两样东西:允许它与服务通信的代理,以及服务的终结点。这是消耗 LocalTimeService
的一种客户端版本
class Client
{
public bool keepClocking = true;
LocalTimeProxy proxy = null;
public Client()
{
proxy = new LocalTimeProxy();
}
public void ClockingThread()
{
while (keepClocking)
{
Console.WriteLine(proxy.GetLocalTime());
Thread.Sleep(1000);
}
proxy.Close();
}
static void Main(string[] args)
{
Client client = new Client();
//Create a separate thread to request the time once a second
Thread thread = new Thread(new ThreadStart(client.ClockingThread));
thread.Start();
Console.WriteLine("Service is running....press" +
" any key to terminate.");
Console.ReadKey();
client.keepClocking = false;
//wait 2 seconds
Thread.Sleep(2000);
}
}
这个客户端没什么特别的。它只是每秒调用一次服务方法 GetLocalTime()
。正如你所看到的,客户端代码没有任何关于方法调用另一端是什么或在哪里,或者实际使用了什么机制来建立连接的指示。它只是一个简单的类方法调用!当我们查看其他示例时,你将继续看到 WCF 所提供的编程的简洁性。以下是指定客户端所需的指向服务终结点的配置文件。
<configuration>
<system.serviceModel>
<client>
<endpoint name ="LocalTimeService"
address="net.pipe:///LocalTimeService"
binding="netNamedPipeBinding"
contract="ILocalTime" />
</client>
</system.serviceModel>
</configuration>
编译并运行客户端。启动多个实例。只需确保在启动客户端之前先启动服务,否则将没有人监听。
以上就是让服务监听和消耗服务的入门基础。在第 2 部分,我们将构建一些示例来演示 WCF 对各种通信模式的支持。下载包含了本文所述所有示例应用程序的源代码。