WCF (Windows Communication Foundation) 示例
WCF (Windows Communication Foundation) 的一个简单示例:一个简单的客户端/服务器解决方案。
引言
发布这篇文章的原因是我认为很多人都会使用新的 .NET 3.0 WCF。它易于使用,并且看起来非常强大。我花了大量时间调试这个程序,希望这篇文章能让其他人避免遇到和我一样的头痛,因为我总是会遇到一个奇怪的安全异常。直接使用代码或将其作为灵感。我假设读者熟悉客户端/服务器编程,因此不会深入细节。据说 WCF 在传输方式上非常动态,可以配置为使用几乎任何通信标准,这使其适用于许多客户端/服务器应用程序。无论您使用 HTTP、TCP 等传输数据,可选的 SSL/加密功能都使 WCF 更适合大规模解决方案。
背景
这个项目很简单,真的很简单。源代码包含一些我用于开发应用程序的文件。源代码包含 ServiceLibrary,它是编译成简单 DLL 的契约。不要混淆契约和代理的区别。契约是一种通信方式,而代理是一种访问另一个远程契约的方式。另外两个库几乎不言自明,分别是 WCFClient 和 WCFService。目标是创建一个动态的客户端服务器解决方案,它不受配置文件等限制,而是更加动态。另外,不要低估编写代码的乐趣 :) 感谢丹麦的 Gedebuk。
Proxy.cs 代码由 svcutil.exe 自动生成。如果需要重新创建 Proxy.cs,请启动服务并运行命令:"C:\Program Files\Microsoft SDKs\Windows\v6.0\Bin\svcutil.exe" https://:8001/MyService /out:Proxy.cs。
使用代码
代码包含两个文件夹,它们被加载到一个项目文件中。
我是如何创建项目的?
在本节中,我将描述我是如何创建项目的。步骤按顺序排列并编号。1) 创建 ServiceLibrary.cs
我通过从 ServiceLibrary
开始创建项目。此库被编译成 DLL,在客户端(或多个客户端)上使用。在 WCF 术语中,这是客户端为了能够与服务器通信而必须遵守的契约。这个契约实际上是一个客户端(或客户端)通过它进行通信的接口。ServiceLibrary
包含客户端可以调用的所有方法。在此示例中,它还包含服务的实现。DataContract
带有 [DataContract]
标记,表示这是一个可以传输的对象。在 WCF 中,无论传输的是字符串等简单类型,还是复杂对象等复杂类型,都没有关系。以下是我契约的复制粘贴内容。
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
using System.Runtime.Serialization;
namespace ServiceLibrary
{
// You have created a class library to
// define and implement your WCF service.
// You will need to add a reference to this
// library from another project and add
// the code to that project to host the
// service as described below. Another way
// to create and host a WCF service is by
// using the Add New Item, WCF Service
// template within an existing project such
// as a Console Application or a Windows
// Application.
[ServiceContract()]
public interface IService1
{
[OperationContract]
string MyOperation1(string myValue);
[OperationContract]
string MyOperation2(DataContract1 dataContractValue);
[OperationContract]
string HelloWorld(string str);
}
public class service1 : IService1
{
public string MyOperation1(string myValue)
{
return "Hello: " + myValue;
}
public string MyOperation2(DataContract1 dataContractValue)
{
return "Hello: " + dataContractValue.FirstName;
}
public string HelloWorld(string str)
{
return "Helloworld from " + str;
}
}
[DataContract]
public class DataContract1
{
string firstName;
string lastName;
[DataMember]
public string FirstName
{
get { return firstName; }
set { firstName = value; }
}
[DataMember]
public string LastName
{
get { return lastName; }
set { lastName = value; }
}
}
}
这些代码是 Visual Studio 项目为我生成的。我只添加了此代码的 HelloWorld 部分。VS 环境还生成了关于添加服务引用的操作指南。我跳过了这一点并删除了那些部分。您可以从 Visual Studio 的配置文件进行程序配置,也可以在代码中添加配置,或者编写配置。我选择在代码中配置服务。有人可能会争论,当客户端/服务器关系发生变化时,更改 XML 文件比重新编译项目更容易,但我选择这样做是为了使程序能够在将来的实现中自行配置。这段代码将作为相当大的客户端/服务器解决方案中的核心功能,其中计算机将通过 WCF 随机联系其他计算机。这要求程序能够更改运行时,这也是这段代码由代码控制的主要原因。
2) 创建服务宿主应用程序
服务宿主应用程序是包含具有实际对象的服务的程序。如果您愿意这样称呼它,这就是服务器。
我创建了一个 Windows Forms 项目并添加了以下代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Description;
namespace WCFService
{
public partial class MainForm : Form
{
private ServiceHost host = null;
private string urlMeta, urlService = "";
public MainForm()
{
InitializeComponent();
Append("Program started ...");
}
void host_Opening(object sender, EventArgs e)
{
Append("Service opening ... Stand by");
}
private void button1_Click(object sender, EventArgs e)
{
try
{
// Returns a list of ipaddress configuration
IPHostEntry ips = Dns.GetHostEntry(Dns.GetHostName());
// Select the first entry. I hope it's this maschines IP
IPAddress _ipAddress = ips.AddressList[0];
// Create the url that is needed to specify
// where the service should be started
urlService = "net.tcp://" +
_ipAddress.ToString() + ":8000/MyService";
// Instruct the ServiceHost that the type
// that is used is a ServiceLibrary.service1
host = new ServiceHost(typeof(ServiceLibrary.service1));
host.Opening += new EventHandler(host_Opening);
host.Opened += new EventHandler(host_Opened);
host.Closing += new EventHandler(host_Closing);
host.Closed += new EventHandler(host_Closed);
// The binding is where we can choose what
// transport layer we want to use. HTTP, TCP ect.
NetTcpBinding tcpBinding = new NetTcpBinding();
tcpBinding.TransactionFlow = false;
tcpBinding.Security.Transport.ProtectionLevel =
System.Net.Security.ProtectionLevel.EncryptAndSign;
tcpBinding.Security.Transport.ClientCredentialType =
TcpClientCredentialType.Windows;
tcpBinding.Security.Mode = SecurityMode.None;
// <- Very crucial
// Add a endpoint
host.AddServiceEndpoint(typeof(
ServiceLibrary.IService1), tcpBinding, urlService);
// A channel to describe the service.
// Used with the proxy scvutil.exe tool
ServiceMetadataBehavior metadataBehavior;
metadataBehavior =
host.Description.Behaviors.Find<ServiceMetadataBehavior>();
if (metadataBehavior == null)
{
// This is how I create the proxy object
// that is generated via the svcutil.exe tool
metadataBehavior = new ServiceMetadataBehavior();
metadataBehavior.HttpGetUrl = new Uri("http://" +
_ipAddress.ToString() + ":8001/MyService");
metadataBehavior.HttpGetEnabled = true;
metadataBehavior.ToString();
host.Description.Behaviors.Add(metadataBehavior);
urlMeta = metadataBehavior.HttpGetUrl.ToString();
}
host.Open();
}
catch (Exception ex1)
{
Console.WriteLine(ex1.StackTrace);
}
}
private void button2_Click(object sender, EventArgs e)
{
host.Close();
}
void host_Closed(object sender, EventArgs e)
{
Append("Service closed");
}
void host_Closing(object sender, EventArgs e)
{
Append("Service closing ... stand by");
}
void host_Opened(object sender, EventArgs e)
{
Append("Service opened.");
Append("Service URL:\t" + urlService);
Append("Meta URL:\t" + urlMeta + " (Not that relevant)");
Append("Waiting for clients...");
}
private void Append(string str)
{
textBox1.AppendText("\r\n" + str);
}
}
}
有趣的部分当然是 button1_Click
,它创建服务并将服务公开给其他客户端。现在,其他客户端要连接到此服务,服务就需要一个契约。契约不是我们自己编写的,这就是为什么我使用 scvutil.exe 工具。在我的 PC 上,该工具位于 C:\Program Files\Microsoft SDKs\Windows\v6.0\Bin\svcutil.exe。为了使契约对象(实际上是客户端连接到的代理),我们需要生成 Proxy.cs(文件名完全不重要,也可以是 foo.cs)。这将在第 3 步中完成。
3) 构建供客户端使用的代理对象
代理对象是从位于端口 8001 的服务描述(请查看代码)构建的。它读取服务的元数据,并构建客户端在与服务通信时所需的契约。
- 首先,启动服务并点击“启动服务”按钮。这将创建服务。所有元信息都已发布,供其他客户端或工具读取。
- 其次,运行 "C:\Program Files\Microsoft SDKs\Windows\v6.0\Bin\svcutil.exe" https://:8001/MyService /out:Proxy.cs。这将创建两个文件。一个 Proxy.cs 文件和一个 XML 配置文件。我丢弃了配置文件,因为我将在程序内部配置服务。您应该考虑将 "C:\Program Files\Microsoft SDKs\Windows\v6.0\Bin\svcutil.exe" https://:8001/MyService /out:Proxy.cs 放入批处理文件中,以便您自己更轻松地操作。我已包含我的批处理文件供您查看。它位于“ProxyGenerator”文件夹中。
接下来,我创建一个简单的客户端并添加相关代码。
4) 创建客户端应用程序并添加相关代码
现在,我们创建一个简单的标准客户端程序作为 Windows Forms 应用程序。一旦文件生成(Proxy.cs 和 output.config <- 可以抑制),我将 Proxy.cs 添加到我的客户端程序中,该程序称为 WCFClient。现在,客户端知道了契约,并且能够遵守它以创建与服务的通信通道。可以将 Proxy.cs 编译成 DLL,然后简单地将 DLL 添加到客户端(我认为这是一种更干净的方式来添加像通信契约或接口这样关键的东西)。为了演示,我们保留 Proxy.cs 不变并将其添加到客户端项目中。接下来,也是最后一步:我们需要添加一些简单的客户端代码来检索服务代理对象。我的客户端代码如下所示。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.ServiceModel;
namespace WCFClient
{
public partial class Form1 : Form
{
string endPointAddr = "";
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
if (textBox1.Text != "")
{
endPointAddr = "net.tcp://" + textBox2.Text +
":8000/MyService";
NetTcpBinding tcpBinding = new NetTcpBinding();
tcpBinding.TransactionFlow = false;
tcpBinding.Security.Transport.ProtectionLevel =
System.Net.Security.ProtectionLevel.EncryptAndSign;
tcpBinding.Security.Transport.ClientCredentialType =
TcpClientCredentialType.Windows;
tcpBinding.Security.Mode = SecurityMode.None;
EndpointAddress endpointAddress =
new EndpointAddress(endPointAddr);
Append("Attempt to connect to: " + endPointAddr);
IService1 proxy =
ChannelFactory<IService1>.CreateChannel(
tcpBinding, endpointAddress);
using (proxy as IDisposable)
{
Append("Message from server: " +
(proxy.HelloWorld(textBox1.Text) +
" back to you :)"));
}
}
}
private void Append(string str)
{
textBox3.AppendText("\r\n" + str);
关注点
嗯,我真的很享受编写代码和文章的乐趣。在我看来,最令人印象深刻的是传输层可以非常轻松地修改为使用 HTTP 而不是 TCP。我没有在文章中指出这一点,但这也是一件好事。能够通过一些简单的代码将传输层从 TCP(安全 SSL)切换到 HTTP,这真是太棒了!不过,我确实遇到了一些问题。tcpBinding.Security.Mode = SecurityMode.None;
在双方都非常关键。我不确定它的作用,但根据我的参考资料,它并没有完全关闭安全。仍然存在 SSL 加密,但级别较低。可以向连接添加证书,这也能使连接更安全。
历史
- 2006 年 12 月 13 日 - 项目完成。