传输:UDP

UDP 传输示例演示如何将 UDP 单播和多播实现为自定义 Windows Communication Foundation (WCF) 传输。 此示例介绍使用通道框架和遵循 WCF 最佳做法在 WCF 中创建自定义传输的建议过程。 创建自定义传输的步骤如下:

  1. 确定 ChannelFactory 和 ChannelListener 将支持哪个通道 消息交换模式 (IOutputChannel、IInputChannel、IDuplexChannel、IRequestChannel 或 IReplyChannel)。 然后确定是否要支持这些接口的会话变体。

  2. 创建支持您的消息交换模式的通道工厂和侦听器。

  3. 确保将任何特定于网络的异常规范化为相应的派生类 CommunicationException

  4. 添加一个<绑定>元素,以将自定义传输添加到通道堆栈中。 有关更多信息,请参阅添加绑定元素

  5. 添加绑定元素扩展部分,以向配置系统公开新的绑定元素。

  6. 添加元数据扩展以将功能传达给其他终结点。

  7. 添加一个绑定,该绑定根据定义完善的配置文件来预配置绑定元素堆栈。 有关详细信息,请参阅 添加标准绑定

  8. 添加绑定节和绑定配置元素,以向配置系统公开绑定。 有关详细信息,请参阅 添加配置支持

消息交换模式

编写自定义传输的第一步是确定传输需要哪些消息交换模式(MEP)。 有三个 MEP 可供选择:

  • 数据报 (IInputChannel/IOutputChannel)

    当使用数据报 MEP 时,客户端使用“启动后不管”交换形式发送消息。 “发后不理”交换形式是一种要求带外确认成功传递的交换形式。 消息可能会在传输过程中丢失,并且永远不会到达服务。 如果发送作在客户端成功完成,则不保证远程终结点已收到消息。 数据报是消息传递的基本构建基块,你可以基于它构建自己的协议,包括可靠的协议和安全协议。 客户端数据报通道实现 IOutputChannel 接口和服务数据报通道实现 IInputChannel 接口。

  • 请求/响应 (IRequestChannel/IReplyChannel)

    在此 MEP 中,将发送一个消息并接收一个答复。 该模式由请求-响应对组成。 请求响应调用的示例包括远程过程调用(RPC)和浏览器 GET。 此模式也称为半双工。 在此 MEP 中,客户端通道实现 IRequestChannel 和服务通道实现 IReplyChannel

  • 双工 (IDuplexChannel)

    通过双工 MEP,客户端可以发送任意数目的消息,并以任意顺序接收消息。 双工 MEP 就像电话通话,所说的每一个字都是一条消息。 由于双方都可以在此 MEP 中发送和接收,因此客户端和服务通道实现的接口是 IDuplexChannel

每个 MEP 还可以支持会话。 会话感知通道提供的附加功能是它将在通道上发送和接收的所有消息相关联。 Request-Response 模式是独立的两条消息会话,因为请求和答复是相关的。 相比之下,支持会话的 Request-Response 模式意味着该通道上的所有请求/响应对相互关联。 这样总共提供了六个 MEP 供您选择:数据报、请求-响应、双工、具有会话的数据报、具有会话的请求-响应以及具有会话的双工。

注释

对于 UDP 传输,唯一支持的 MEP 是 Datagram,因为 UDP 本质上是“触发和忘记”协议。

ICommunicationObject 和 WCF 对象生命周期

WCF 有一个通用状态机,用于管理类似 IChannelIChannelFactoryIChannelListener 这样的用于通信的对象的生命周期。 有五种状态可以存在这些通信对象。 这些状态由 CommunicationState 枚举表示,如下所示:

  • 创建:这是 ICommunicationObject 第一次实例化时的状态。 无输入/输出(I/O)发生在此状态。

  • 打开:当调用Open时,对象会转换为此状态。 此时,属性是不可变的,输入/输出可以开始。 此转换只有从“已创建”状态转换才有效。

  • 已打开:当打开的进程完成时,对象将转换为此状态。 此转换只有从“正在打开”状态转换才有效。 此时,对象完全可用于传输。

  • 关闭:当调用Close进行正常关闭时,对象会转换为此状态。 此转换仅在已打开状态下有效。

  • 已关闭:在“已关闭”状态对象中不再可用。 通常,大多数配置仍可供检查,但无法进行通信。 此状态等效于被处理。

  • 已出错:在出错状态下,可以检查对象,但不再可用。 发生不可恢复的错误时,对象将转换为此状态。 从此状态进行的唯一有效转换是进入 Closed 状态。

每次状态转换都会触发事件。 Abort可以随时调用该方法,并导致对象立即从其当前状态转换为“已关闭”状态。 调用 Abort 将终止任何未完成的工作。

通道工厂和通道侦听器

编写自定义传输的下一步是为客户端通道创建IChannelFactory的实现,为服务通道创建IChannelListener的实现。 通道层使用工厂模式来构造通道。 WCF 为此过程提供基类帮助程序。

在此示例中,工厂实现包含在UdpChannelFactory.cs中,侦听器实现包含在UdpChannelListener.cs中。 实现 IChannel 位于UdpOutputChannel.cs和UdpInputChannel.cs中。

UDP 通道工厂

UdpChannelFactory 派生自 ChannelFactoryBase。 该示例重写 GetProperty 以提供对消息编码器的消息版本的访问。 此示例还重写了 OnClose,因此我们可以在状态机转换时拆开 BufferManager 的实例。

UDP 输出通道

UdpOutputChannel 实现 IOutputChannel。 构造函数根据EndPoint传入的参数验证参数并构造目标EndpointAddress对象。

this.socket = new Socket(this.remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);

通道可以正常关闭或非正常关闭。 如果通道正常关闭,则套接字也将关闭,并调用基类 OnClose 方法。 如果引发异常,则基础设施调用 Abort 以确保清除通道。

this.socket.Close(0);

然后,我们实现 Send()BeginSend()/EndSend()。 这分为两个主要部分。 首先,我们将消息序列化为字节数组。

ArraySegment<byte> messageBuffer = EncodeMessage(message);

然后,我们会在网络上发送生成的数据。

this.socket.SendTo(messageBuffer.Array, messageBuffer.Offset, messageBuffer.Count, SocketFlags.None, this.remoteEndPoint);

UDP通道监听器

该示例实现的UdpChannelListener派生自ChannelListenerBase类。 它使用单个 UDP 套接字来接收数据报。 OnOpen 方法在异步循环中使用 UDP 套接字接收数据。 然后使用消息编码框架将数据转换为消息。

message = MessageEncoderFactory.Encoder.ReadMessage(new ArraySegment<byte>(buffer, 0, count), bufferManager);

由于可以用同一个数据报通道来表示来自多个源的消息,因此 UdpChannelListener 是一个单一实例侦听器。 一次最多只能将一个活动 IChannel 与此侦听器相关联。 仅当由AcceptChannel方法返回的通道随后被销毁时,此示例才会生成另一个通道。 收到的消息将排入这个单一实例通道的队列中。

UdpInputChannel

UdpInputChannel 实现 IInputChannel。 该类包括一个传入消息队列,该队列由 UdpChannelListener 的套接字来填充。 这些消息可以由 IInputChannel.Receive 方法取消排队。

添加绑定元素

现在已经生成了工厂和通道,必须通过绑定将它们公开给 ServiceModel 运行库。 绑定是绑定元素的集合,表示与服务地址关联的通信堆栈。 堆栈中的每个元素都由 <绑定> 元素表示。

在示例中,绑定元素 UdpTransportBindingElement派生自 TransportBindingElement。 它重写以下方法以生成与我们的绑定相关联的工厂。

public IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
{
    return (IChannelFactory<TChannel>)(object)new UdpChannelFactory(this, context);
}

public IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
{
    return (IChannelListener<TChannel>)(object)new UdpChannelListener(this, context);
}

它还包含用于克隆 BindingElement 并返回我们自己的方案 (soap.udp) 的成员。

为传输绑定元素添加元数据支持

若要将传输集成到元数据系统中,必须同时支持策略的导入和导出。 这样,我们就可以通过 ServiceModel 元数据实用工具工具(Svcutil.exe)生成绑定的客户端。

添加 WSDL 支持

绑定中的传输绑定元素负责在元数据中导出和导入寻址信息。 使用 SOAP 绑定时,传输绑定元素还应在元数据中导出正确的传输 URI。

WSDL 导出

若要导出寻址信息,请 UdpTransportBindingElement 实现 IWsdlExportExtension 接口。 该方法 ExportEndpoint 将正确的寻址信息添加到 WSDL 端口。

if (context.WsdlPort != null)
{
    AddAddressToWsdlPort(context.WsdlPort, context.Endpoint.Address, encodingBindingElement.MessageVersion.Addressing);
}

UdpTransportBindingElement 终结点使用 SOAP 绑定时,该方法的 ExportEndpoint 实现还会导出传输 URI。

WsdlNS.SoapBinding soapBinding = GetSoapBinding(context, exporter);
if (soapBinding != null)
{
    soapBinding.Transport = UdpPolicyStrings.UdpNamespace;
}

WSDL 导入

若要扩展 WSDL 导入系统来处理地址导入,必须将以下配置添加到 Svcutil.exe 配置文件,如 Svcutil.exe.config 文件中所示。

<configuration>
  <system.serviceModel>
    <client>
      <metadata>
        <policyImporters>
          <extension type=" Microsoft.ServiceModel.Samples.UdpBindingElementImporter, UdpTransport" />
        </policyImporters>
      </metadata>
    </client>
  </system.serviceModel>
</configuration>

运行 Svcutil.exe时,有两个选项可用于获取 Svcutil.exe 来加载 WSDL 导入扩展:

  1. 使用 /SvcutilConfig:<file> 使 Svcutil.exe 指向配置文件。

  2. 将配置节添加到与 Svcutil.exe 处于同一目录的 Svcutil.exe.config 中。

UdpBindingElementImporter 类型实现 IWsdlImportExtension 接口。 该方法 ImportEndpoint 从 WSDL 端口导入地址。

BindingElementCollection bindingElements = context.Endpoint.Binding.CreateBindingElements();
TransportBindingElement transportBindingElement = bindingElements.Find<TransportBindingElement>();
if (transportBindingElement is UdpTransportBindingElement)
{
    ImportAddress(context);
}

添加策略支持

自定义绑定元素可以在服务终结点的 WSDL 绑定中导出策略断言,以表达该绑定元素的功能。

策略导出

UdpTransportBindingElement 类型实现 IPolicyExportExtension 以添加对导出策略的支持。 因此,System.ServiceModel.MetadataExporter 在为任何包含它的绑定而生成策略时都包含 UdpTransportBindingElement

IPolicyExportExtension.ExportPolicy中,我们为 UDP 添加断言,如果处于多播模式,则添加另一个断言。 这是因为多播模式会影响通信堆栈的构造方式,因此必须在双方之间协调。

ICollection<XmlElement> bindingAssertions = context.GetBindingAssertions();
XmlDocument xmlDocument = new XmlDocument();
bindingAssertions.Add(xmlDocument.CreateElement(
UdpPolicyStrings.Prefix, UdpPolicyStrings.TransportAssertion, UdpPolicyStrings.UdpNamespace));
if (Multicast)
{
    bindingAssertions.Add(xmlDocument.CreateElement(
        UdpPolicyStrings.Prefix,
        UdpPolicyStrings.MulticastAssertion,
        UdpPolicyStrings.UdpNamespace));
}

由于自定义传输绑定元素负责处理寻址,因此在IPolicyExportExtension上的UdpTransportBindingElement实现还必须处理导出相应的WS-Addressing策略断言,以指示正在使用的WS-Addressing版本。

AddWSAddressingAssertion(context, encodingBindingElement.MessageVersion.Addressing);

策略导入

若要扩展策略导入系统,必须将以下配置添加到 Svcutil.exe 的配置文件,如 Svcutil.exe.config 文件中所示。

<configuration>
  <system.serviceModel>
    <client>
      <metadata>
        <policyImporters>
          <extension type=" Microsoft.ServiceModel.Samples.UdpBindingElementImporter, UdpTransport" />
        </policyImporters>
      </metadata>
    </client>
  </system.serviceModel>
</configuration>

然后,我们从已注册的类(IPolicyImporterExtension)实现UdpBindingElementImporter。 在 ImportPolicy() 中,浏览命名空间中的断言,处理用于生成传输的断言,并检查它是否为多播。 还必须从绑定断言列表中删除我们处理的断言。 同样,在运行 Svcutil.exe时,有两个选项可用于集成:

  1. 使用 /SvcutilConfig:<file> 使 Svcutil.exe 指向配置文件。

  2. 将配置节添加到与 Svcutil.exe 处于同一目录的 Svcutil.exe.config 中。

添加标准绑定

我们的绑定元素可通过以下两种方式使用:

  • 通过自定义绑定:自定义绑定允许用户基于任意绑定元素集创建自己的绑定。

  • 通过使用系统提供的、包含我们的绑定元素的绑定。 WCF 提供了许多系统定义的绑定,例如 BasicHttpBindingNetTcpBindingWsHttpBinding。 这些绑定中的每个绑定与一个准确定义的配置文件相关联。

此示例在从 SampleProfileUdpBinding 派生的 Binding 中实现配置文件绑定。 包含在SampleProfileUdpBinding中的四个绑定元素最多有:UdpTransportBindingElementTextMessageEncodingBindingElement CompositeDuplexBindingElementReliableSessionBindingElement

public override BindingElementCollection CreateBindingElements()
{
    BindingElementCollection bindingElements = new BindingElementCollection();
    if (ReliableSessionEnabled)
    {
        bindingElements.Add(session);
        bindingElements.Add(compositeDuplex);
    }
    bindingElements.Add(encoding);
    bindingElements.Add(transport);
    return bindingElements.Clone();
}

添加自定义标准绑定导入程序

默认情况下,Svcutil.exe 和 WsdlImporter 类型识别并导入系统定义的绑定。 否则,绑定将作为 CustomBinding 实例被导入。 若要启用 Svcutil.exe 和 WsdlImporter 以导入 SampleProfileUdpBindingUdpBindingElementImporter 还需充当自定义标准绑定导入程序。

自定义标准绑定导入程序在ImportEndpoint接口上实现IWsdlImportExtension方法,以检查从元数据导入的CustomBinding实例,判断其是否可能由特定标准绑定生成。

if (context.Endpoint.Binding is CustomBinding)
{
    Binding binding;
    if (transportBindingElement is UdpTransportBindingElement)
    {
        //if TryCreate is true, the CustomBinding will be replace by a SampleProfileUdpBinding in the
        //generated config file for better typed generation.
        if (SampleProfileUdpBinding.TryCreate(bindingElements, out binding))
        {
            binding.Name = context.Endpoint.Binding.Name;
            binding.Namespace = context.Endpoint.Binding.Namespace;
            context.Endpoint.Binding = binding;
        }
    }
}

通常,实现自定义标准绑定导入程序涉及检查导入的绑定元素的属性,以验证只有标准绑定可能设置的属性已更改,所有其他属性都是其默认值。 实现标准绑定导入程序的基本策略是创建标准绑定的实例,将绑定元素中的属性传播到标准绑定支持的标准绑定实例,并将标准绑定中的绑定元素与导入的绑定元素进行比较。

添加配置支持

若要通过配置来公开我们的传输,必须定义两个配置节。 第一个是 BindingElementExtensionElementUdpTransportBindingElement。 这样, CustomBinding 实现就可以引用绑定元素。 第二个是 ConfigurationSampleProfileUdpBinding

绑定元素扩展元素

UdpTransportElement 节是一个 BindingElementExtensionElement,它向配置系统公开 UdpTransportBindingElement。 通过一些基本重写,我们定义了配置节名称、绑定元素的类型以及如何创建绑定元素。 然后,我们可以在配置文件中注册扩展部分,如以下代码所示。

<configuration>
  <system.serviceModel>
    <extensions>
      <bindingElementExtensions>
        <add name="udpTransport" type="Microsoft.ServiceModel.Samples.UdpTransportElement, UdpTransport" />
      </bindingElementExtensions>
    </extensions>
  </system.serviceModel>
</configuration>

可以从自定义绑定引用扩展,以使用 UDP 作为传输。

<configuration>
  <system.serviceModel>
    <bindings>
      <customBinding>
       <binding configurationName="UdpCustomBinding">
         <udpTransport/>
       </binding>
      </customBinding>
    </bindings>
  </system.serviceModel>
</configuration>

绑定节

SampleProfileUdpBindingCollectionElement 节是一个 StandardBindingCollectionElement,它向配置系统公开 SampleProfileUdpBinding。 批量实现委派给从 SampleProfileUdpBindingConfigurationElement 派生的 StandardBindingElementSampleProfileUdpBindingConfigurationElement 具有与 SampleProfileUdpBinding 上的属性对应的属性,以及从 ConfigurationElement 绑定映射的函数。 最后,在 OnApplyConfiguration 中重写 SampleProfileUdpBinding 方法,如下面的示例代码所示。

protected override void OnApplyConfiguration(string configurationName)
{
    if (binding == null)
        throw new ArgumentNullException("binding");

    if (binding.GetType() != typeof(SampleProfileUdpBinding))
    {
        throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
            "Invalid type for binding. Expected type: {0}. Type passed in: {1}.",
            typeof(SampleProfileUdpBinding).AssemblyQualifiedName,
            binding.GetType().AssemblyQualifiedName));
    }
    SampleProfileUdpBinding udpBinding = (SampleProfileUdpBinding)binding;

    udpBinding.OrderedSession = this.OrderedSession;
    udpBinding.ReliableSessionEnabled = this.ReliableSessionEnabled;
    udpBinding.SessionInactivityTimeout = this.SessionInactivityTimeout;
    if (this.ClientBaseAddress != null)
        udpBinding.ClientBaseAddress = ClientBaseAddress;
}

若要将此处理程序注册到配置系统,请将以下部分添加到相关配置文件。

<configuration>
  <configSections>
     <sectionGroup name="system.serviceModel">
        <sectionGroup name="bindings">
          <section name="sampleProfileUdpBinding" type="Microsoft.ServiceModel.Samples.SampleProfileUdpBindingCollectionElement, UdpTransport" />
        </sectionGroup>
     </sectionGroup>
  </configSections>
</configuration>

然后,可以从 serviceModel 配置部分引用它。

<configuration>
  <system.serviceModel>
    <client>
      <endpoint configurationName="calculator"
                address="soap.udp://localhost:8001/"
                bindingConfiguration="CalculatorServer"
                binding="sampleProfileUdpBinding"
                contract= "Microsoft.ServiceModel.Samples.ICalculatorContract">
      </endpoint>
    </client>
  </system.serviceModel>
</configuration>

UDP 测试服务和客户端

UdpTestService 和 UdpTestClient 目录中提供了使用此示例传输的测试代码。 服务代码由两个测试组成,一个测试通过代码设置绑定和终结点,另一个测试通过配置完成。 这两个测试都使用两个终结点。 一个终结点使用 SampleUdpProfileBinding,并将 <reliableSession> 设置为 true。 另一个终结点使用具有 UdpTransportBindingElement 的自定义绑定。 这等效于将 SampleUdpProfileBinding<reliableSession> 设置为 false 一起使用。 这两个测试都会创建一个服务,为每个绑定添加一个终结点,打开服务,然后等待用户点击 Enter,然后再关闭服务。

启动服务测试应用程序时,应会看到以下输出。

Testing Udp From Code.
Service is started from code...
Press <ENTER> to terminate the service and start service from config...

然后,可以针对已发布的终结点运行测试客户端应用程序。 客户端应用程序为每个终结点创建一个客户端,并向每个终结点发送五条消息。 以下输出在客户端上。

Testing Udp From Imported Files Generated By SvcUtil.
0
3
6
9
12
Press <ENTER> to complete test.

下面是服务的完整输出。

Service is started from code...
Press <ENTER> to terminate the service and start service from config...
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
   adding 0 + 0
   adding 1 + 2
   adding 2 + 4
   adding 3 + 6
   adding 4 + 8

若要针对使用配置发布的终结点运行客户端应用程序,请在服务上按 Enter,然后再次运行测试客户端。 应该会在服务中看到以下输出。

Testing Udp From Config.
Service is started from config...
Press <ENTER> to terminate the service and exit...

再次运行客户端会生成与上述结果相同的结果。

若要使用 Svcutil.exe重新生成客户端代码和配置,请启动服务应用程序,然后从示例的根目录运行以下 Svcutil.exe。

svcutil http://localhost:8000/udpsample/ /reference:UdpTransport\bin\UdpTransport.dll /svcutilConfig:svcutil.exe.config

请注意,Svcutil.exe 不会为 SampleProfileUdpBinding 生成绑定扩展的配置,因此必须手动添加。

<configuration>
  <system.serviceModel>
    <extensions>
      <!-- This was added manually because svcutil.exe does not add this extension to the file -->
      <bindingExtensions>
        <add name="sampleProfileUdpBinding" type="Microsoft.ServiceModel.Samples.SampleProfileUdpBindingCollectionElement, UdpTransport" />
      </bindingExtensions>
    </extensions>
  </system.serviceModel>
</configuration>

设置、生成和运行示例

  1. 要生成解决方案,请按照生成 Windows Communication Foundation 示例中的说明进行操作。

  2. 若要在单台计算机或跨计算机配置中运行示例,请按照 运行 Windows Communication Foundation 示例中的说明进行操作。

  3. 请参阅前面的“UDP 测试服务和客户端”部分。