REST 和 POX

此示例演示如何在 indigo1 中使用 HTTP 传输发送和接收“纯旧式 XML”(POX) 消息(即仅由 XML 负载组成,没有任何附加 SOAP 信封的消息)。POX 消息可以由多种客户端发送和接收,包括没有为基于 SOAP 的协议提供任何本机支持的客户端,如 Web 浏览器。对于通过 HTTP 交换数据,并且不要求使用 SOAP 和 WS-* 的高级协议功能(如非 HTTP 传输、除请求/响应之外的消息交换模式以及基于消息的安全性、可靠性和事务)的服务而言,POX 是理想的选择。

提示

对 REST 和 POX 样式服务的支持现在直接由 .NET Framework 3.5 版支持。有关更多信息,请参见 .NET Framework 3.5 文档中的 Web Programming Model主题。

实现 POX 服务

此示例中的服务实现一个非常基本的客户数据库。从协定的角度讲,它公开一个称为 ProcessMessage 的操作,该操作采用一个 Message 作为输入并返回一个 Message

[ServiceContract]
public interface IUniversalContract
{
    [OperationContract(Action = "*", ReplyAction = "*")]
    Message ProcessMessage(Message input);
}

但是,此协定的功能不是很完善。我们希望实现一个基本的寻址约定,以便通过能够使用 HTTP 的方式访问以下集合内容:

  • 此集合位于 https://localhost:8100/customers。发送给此 URI 的 HTTP GET 请求以 URI 列表(这些 URI 指向各个条目)的形式返回此集合的内容。
  • 集合中的每个条目具有一个唯一的 URI,其格式是在集合 URI 后面附加客户 ID。例如,https://localhost:8100/customers/1 标识 ID 为 1 的客户。
  • 如果给定了条目 URI,通过向该条目 URI 发出 HTTP GET 请求,可以检索客户的 XML 表示形式。
  • 通过使用 PUT 对现有条目 URI 应用一个新的表示形式,可以修改该条目。
  • 通过使用 HTTP POST 将新条目的内容发送给集合 URI,可以添加条目。新条目的 URI 由服务器响应中的 HTTP 位置头返回。
  • 通过向条目的 URI 发送 DELETE 请求,可以移除该条目。

此体系结构样式称为 REST(具象状态传输),这是使用 HTTP 和 POX 消息进行通信的应用程序的一种设计方式。

若要实现所有这些目的,必须首先创建一个服务,以实现我们希望公开的协定。

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, 
  AddressFilterMode = AddressFilterMode.Prefix)]
class CustomerService : IUniversalContract
{
    Dictionary<string, Customer> customerList;
    public Message ProcessMessage(Message request) { ... }
}

本地 customerList 变量存储数据库的内容。我们希望确保此变量的内容在不同的请求之间保持不变,所以我们使用 ServiceBehavior 属性指定 InstanceContextMode.Single,它告诉 WCF 在不同的请求之间使用同一个物理服务实例。我们还设置了 AddressFilterMode.Prefix,它支持我们希望服务实现的分层寻址结构。AddressFilterMode.Prefix 使服务侦听所有以其终结点地址开头的 URI,而不仅仅是与地址精确匹配的那些 URI。

该服务是使用以下配置进行配置的。

<system.serviceModel>
    <bindings>
        <customBinding>
            <binding name="poxBinding">
                <textMessageEncoding messageVersion="None" />
                <httpTransport />
            </binding>
        </customBinding>
    </bindings>
    <services>
        <service name="Microsoft.ServiceModel.Samples.CustomerService">
          <host>
            <baseAddresses>
              <add baseAddress="https://localhost:8100/" />
            </baseAddresses>
          </host>
            <endpoint address="customers" 
                      binding="customBinding" 
                      bindingConfiguration="poxBinding"
                    contract="Microsoft.ServiceModel.Samples.IUniversalContract" />
        </service>
    </services>
 </system.serviceModel>

此配置设置了具有一个终结点(位于 https://localhost:8100/customers)的单一服务(位于 https://localhost:8100)。此终结点使用具有 HTTP 传输和文本编码器的自定义绑定进行通信。该编码器被设置为使用 MessageVersion.None,它允许编码器在读取时接受没有 SOAP 信封的消息,并让编码器在编写响应消息时禁止显示 SOAP 信封。为了简单明确起见,此示例演示了通过不安全传输进行的通信。如果需要安全性,POX 应用程序应使用集成了 HTTP 传输安全 (HTTPS) 的绑定。

实现 ProcessMessage()

我们希望 ProcessMessage() 的实现根据传入请求中出现的不同 HTTP 方法采取不同的操作。为了实现这一点,我们必须访问某些未在 Message 上直接公开的 HTTP 协议信息。但是,我们可以通过 HttpRequestMessageProperty 类访问 HTTP 方法(以及其他有用的协议元素,如请求的头集合)。

public Message ProcessMessage(Message request)
{
    Message response = null;

    //The HTTP Method (for example, GET) from the incoming HTTP request
    //can be found on the HttpRequestMessageProperty. The MessageProperty
    //is added by the HTTP Transport when the message is received.
    HttpRequestMessageProperty requestProperties =
        (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name];
    …
}

获得 HttpRequestMessageProperty 后,我们可以使用它调度不同的内部实现方法。

if (String.Equals("GET", requestProperties.Method,
    StringComparison.OrdinalIgnoreCase))
{
    response = GetCustomer(request);
}
else if (String.Equals("PUT", requestProperties.Method,
    StringComparison.OrdinalIgnoreCase))
{
    response = UpdateCustomer(request);
}
else if (String.Equals("POST", requestProperties.Method,
    StringComparison.OrdinalIgnoreCase))
{
    response = AddCustomer(request);
}
else if (String.Equals("DELETE", requestProperties.Method,
    StringComparison.OrdinalIgnoreCase))
{
    response = DeleteCustomer(request);
}

虽然 GET 和 POST 是最常见的 HTTP 方法(PUT 和 DELETE 适用于更小的范围),但 HTTP 规范还定义了其他几个谓词,如 HEAD 和 OPTIONS,我们不打算在示例服务中支持这些谓词。幸运的是,HTTP 规范还定义了一个状态码(405 不允许的方法)以实现这个特殊的目的。因此,我们在服务的 ProcessMessage 中使用了以下逻辑。

else
{
    //This service does not implement handlers for other HTTP verbs (such as HEAD), so we
    //construct a response message and use the HttpResponseMessageProperty to
    //set the HTTP status code to 405 (Method Not Allowed) which indicates the client 
    //used an HTTP verb not supported by the server.
    response = Message.CreateMessage(MessageVersion.None, String.Empty, String.Empty);
    HttpResponseMessageProperty responseProperty = new HttpResponseMessageProperty();
    responseProperty.StatusCode = HttpStatusCode.MethodNotAllowed;

    response.Properties.Add( HttpResponseMessageProperty.Name, responseProperty );
}

HttpResponseMessagePropertyHttpRequestMessageProperty 非常类似,不同之处在于它携带响应特定的属性,如“状态码”和“状态说明”。创建消息时,默认情况下消息并没有此属性,这也是我们必须在返回响应消息之前将此属性显式添加到 Properties 集合中的原因。

其余的服务实现方法(GetCustomerAddCustomer 等等)以类似的方式使用 HttpRequest/HttpResponse 消息属性,这些方法很简单。

实现 POX 客户端

由于示例服务的 REST 体系结构,此示例的客户端是一个基本的 HTTP 客户端。出于演示目的,此客户端在 WCF HTTP 传输堆栈的上面实现,但它可以通过 HttpWebRequest 实现(例如,如果 WCF 在客户端不可用),甚至可以通过大多数现代 Web 浏览器提供的 XmlHttpRequest 支持来实现。

使用 WCF 发送原始 HTTP 请求涉及到创建消息、在 HttpRequestMessageProperty 上设置适当的值,并用要发送给服务器的数据填充实体正文(可选)。为了简化此过程,我们编写了一个基本的 HTTP 客户端类。

public class HttpClient : ClientBase<IRequestChannel>
{
    public HttpClient( Uri baseUri, bool keepAliveEnabled ) : 
                       this( baseUri.ToString(), keepAliveEnabled )
    {
    }
    
    public HttpClient( string baseUri, bool keepAliveEnabled ) : 
base( HttpClient.CreatePoxBinding( keepAliveEnabled ), 
      new EndpointAddress( baseUri ) )
    {
    }

    //Other members elided for clarity
}

与使用 ServiceModel Metadata Utility Tool (Svcutil.exe) 根据 WSDL 元数据生成的客户端类相似,HttpClient 类继承自 ClientBase<TChannel>。虽然我们的客户端是按照非常普通的协定 (IRequestChannel) 实现的,ClientBase`1<TChannel> 仍然提供了大量有用的功能。例如,它可以自动创建 ChannelFactory<IRequestChannel> 并自动管理其生存期。

与服务中使用的绑定类似,我们的 HTTP 客户端使用自定义绑定进行通信。

private static Binding CreatePoxBinding( bool keepAliveEnabled )
{
    TextMessageEncodingBindingElement encoder = 
new TextMessageEncodingBindingElement( MessageVersion.None, Encoding.UTF8 );

    HttpTransportBindingElement transport = new HttpTransportBindingElement();
    transport.ManualAddressing = true;
    transport.KeepAliveEnabled = keepAliveEnabled;

    return new CustomBinding( new BindingElement[] { encoder, transport } );
}

客户端绑定主要增加了将 HttpTransportBindingElement 上的 ManualAddressing 设置为 true 的步骤。通常,如果 ManualAddressing 设置为 false,通过传输发送的消息将寻址到在创建传输的 ChannelFactory 时提供的 URI。手动寻址允许通过传输发送的各个消息针对不同的请求寻址到不同的 URI,只要这些 URI 与 ChannelFactory 的 URI 具有相同的前缀即可。因为已经选定了我们希望在应用层将消息发送到的 URI,所以将 ManualAddressing 设置为 true 是实现既定用途的不错选择。

HttpClient 上最重要的方法是 Request,此方法可以将消息发送到特定的 URI 并返回服务器的响应。对于不允许在 HTTP 消息中携带任何数据的 HTTP 方法(如 GET 和 DELETE),我们定义了 Request() 的重载,它将 SuppressEntityBody 设置为 true

public Message Request( Uri requestUri, string httpMethod )
{
    Message request = Message.CreateMessage( MessageVersion.None, String.Empty );
    request.Headers.To = requestUri;

    HttpRequestMessageProperty property = new HttpRequestMessageProperty();
    property.Method = httpMethod;
    property.SuppressEntityBody = true;
    request.Properties.Add( HttpRequestMessageProperty.Name, property );
    return this.Channel.Request( request );
}

为了支持允许在请求正文中携带数据的 POST 和 PUT 等谓词,我们还定义了 Request() 的另一个重载,它采用代表整个正文的对象。

public Message Request( Uri requestUri, string httpMethod, object entityBody )
{
    Message request = Message.CreateMessage( MessageVersion.None, String.Empty, entityBody );
    request.Headers.To = requestUri;

    HttpRequestMessageProperty property = new HttpRequestMessageProperty();
    property.Method = httpMethod;

    request.Properties.Add( HttpRequestMessageProperty.Name, property );
    return this.Channel.Request( request );
}

因为 entityBody 对象直接传递给 Message.CreateMessage(),所以默认的 WCF 行为是通过 DataContractSerializer 将此对象实例转换为 XML。如果我们需要其他行为,可以在将此对象传递给 Request() 之前将其包装在 BodyWriter 的实现中。

为了完成 HttpClient 的实现,添加了几个实用工具方法,这些方法可以围绕我们希望支持的谓词创建一个编程模型。

public Message Get( Uri requestUri )
{
    return Request( requestUri, "GET" );
}

public Message Post( Uri requestUri, object body )
{
    return Request( requestUri, "POST", body );
}

public Message Put( Uri requestUri, object body )
{
    return Request( requestUri, "PUT", body );
}

public Message Delete( Uri requestUri )
{
    return Request( requestUri, "DELETE" );
}

现在我们有了这个基本的 HTTP 帮助器类,可以通过编写类似如下的代码使用该帮助器类向服务发出 HTTP 请求。

HttpClient client = new HttpClient( collectionUri );

//Get Customer 1 by doing an HTTP GET to the customer's URI
requestUri = new Uri( collectionUri, "1" );
Message response = client.Get( requestUri );
string statusCode = client.GetStatusCode( response );
string statusDescription = client.GetStatusDescription( response );
Customer aCustomer = response.GetBody<Customer>();

运行示例

若要运行示例,请首先启动 Server 项目。此项目将在控制台应用程序中启动一个自承载服务。该服务器在控制台窗口中报告它收到的任何请求的状态。

当服务项目正在运行并且等待消息时,您可以启动客户端项目。客户端向服务器发出一系列 HTTP 请求,以演示如何使用 HTTP 和 POX 消息修改服务器上的状态。具体而言,客户端将执行以下操作:

  • 检索“Customer 1”(客户 1)并显示数据。
  • 将“Customer 1”(客户 1)的名字从“Bob”更改为“Robert”。
  • 再次检索“Customer 1”(客户 1)以显示服务器状态已被修改。
  • 创建名为 Alice 和 Charlie 的两个新客户。
  • 从服务器上删除“Customer 1”(客户 1)。
  • 再次从服务器上检索“Customer 1”(客户 1)并收到“Endpoint Not Found”(未找到终结点)响应。
  • 获取集合的当前内容,即一个链接列表。
  • 通过对集合中的每个链接发出 GET 请求来检索集合中的每个元素。

每个请求和响应的输出显示在客户端控制台窗口中。

设置、生成和运行示例

  1. 请确保已经执行了 Windows Communication Foundation 示例的一次性安装过程

  2. 若要生成 C# 或 Visual Basic .NET 版本的解决方案,请按照生成 Windows Communication Foundation 示例中的说明进行操作。

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

另请参见

其他资源

How To: Create a Basic Self-Hosted Service
How To: Create a Basic IIS-Hosted Service

Send comments about this topic to Microsoft.
© 2007 Microsoft Corporation. All rights reserved.