65.9K
CodeProject 正在变化。 阅读更多。
Home

首次了解Windows Web Services API

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (29投票s)

2009 年 7 月 27 日

CPOL

8分钟阅读

viewsIcon

211759

本文展示了如何在 WCF 服务和 WWS 客户端之间进行互操作,以及如何在 WWS 中重写 WCF 服务以保持与现有 WCF 客户端的兼容性。

引言

Windows Web 服务 API 是 SOAP 的原生实现,除了提供完全用纯原生代码实现客户端-服务器的能力外,还可以与现有 WCF 服务和客户端透明地进行互操作。自从今年早些时候在 MVP 峰会上听到 Nikola Dudar 谈论它以来,我一直渴望尝试它。它原生包含在 Windows 7 中,但也可以在较旧的操作系统(如 XP、Vista、2003 和 2008)上安装和使用。您可以使用 WWS 编写纯原生客户端,这些客户端可以连接到现有的托管 WCF 服务,也可以编写 WWS 原生服务,该服务可以由 WCF 客户端使用。它兼容性如此之好,您可以将 WCF 客户端或 WCF 服务替换为 WWS 等效项,而另一方对此一无所知。在本文中,我将讨论一个简单的 WCF 服务及其 WCF 客户端,然后展示如何使用 WWS 编写一个可以使用该 WCF 服务的原生客户端。然后,我将展示如何将 WCF 服务本身透明地替换为等效的 WWS 服务,以及 WCF 和 WWS 客户端如何无需更改代码即可连接到此 WWS 服务。

注意 :这些示例是在运行 VS 2010 Beta 1 的 64 位 Windows 7 RC 机器上编写的。

示例 WCF 服务

要做的第一件事是创建一个非常简单的 WCF 服务。在我们的示例中,我将使用一个字符串反转服务,该服务公开一个接受字符串并返回反转字符串的单个方法。这是服务接口:

    [ServiceContract]
    interface IStringService
    {
        [OperationContract]
        string Reverse(string s);
    }

    class MyStringService : IStringService
    {
        public string Reverse(string s)
        {
            return new string(s.Reverse().ToArray());
        }
    }

以下是展示如何创建和运行服务的代码。

WSHttpBinding binding = new WSHttpBinding();
binding.HostNameComparisonMode = HostNameComparisonMode.StrongWildcard;
binding.Security.Mode = SecurityMode.None;

Uri baseAddress = new Uri("https://:8000/StringService");

using (ServiceHost serviceHost = 
  new ServiceHost(typeof(MyStringService), baseAddress))
{
    // Check to see if it already has a ServiceMetadataBehavior
    ServiceMetadataBehavior smb = 
      serviceHost.Description.Behaviors.Find<ServiceMetadataBehavior>();
    if (smb == null)
        smb = new ServiceMetadataBehavior();

    smb.HttpGetEnabled = true;
    smb.MetadataExporter.PolicyVersion = PolicyVersion.Policy12;
    serviceHost.Description.Behaviors.Add(smb);

    // Add MEX endpoint
    serviceHost.AddServiceEndpoint(
      ServiceMetadataBehavior.MexContractName,
      MetadataExchangeBindings.CreateMexHttpBinding(),
      "mex"
    );

    serviceHost.AddServiceEndpoint(
      typeof(IStringService), binding, baseAddress);
    serviceHost.Open();

    Console.WriteLine("The service is running. Press any key to stop.");
    Console.ReadKey(); 

我不想创建配置文件,所以所有操作都在代码中完成,包括添加 MEX 端点。这没什么好解释的,我使用了标准的 WSHttpBinding 并禁用了安全性(为了保持示例的简单性)。

注意:WWS 支持多种与 WCF 兼容的安全模式——所以这不是问题,但默认的 WCF 安全模式(Message)不受支持!所以请确保不要在这上面犯错。

C# 中的简单 WCF 客户端

这是一个简单的 C# 控制台客户端,它使用 WCF 连接到上面的 WCF 服务并调用 Reverse 方法。

[ServiceContract]
interface IStringService
{
    [OperationContract]
    string Reverse(string s);
}

class Program
{
    static void Main(string[] args)
    {
        WSHttpBinding binding = new WSHttpBinding();
        binding.HostNameComparisonMode = HostNameComparisonMode.StrongWildcard;
        binding.Security.Mode = SecurityMode.None;

        Uri baseAddress = new Uri("https://:8000/StringService");
        EndpointAddress address = new EndpointAddress(baseAddress);
        ChannelFactory<IStringService> channelFactory =
          new ChannelFactory<IStringService>(binding, address);
        IStringService channel = channelFactory.CreateChannel();

        Console.WriteLine(channel.Reverse("This is a test string."));
        

        ((IChannel)channel).Close();
        Console.ReadKey();
    }
}

它没做多少事情,但我想先在这里展示它,因为稍后我将展示这个相同的客户端如何使用将替换上面 WCF 服务的 WWS 服务。此时,我们有一个 WCF 服务和一个使用它的 WCF 客户端。接下来,让我们编写一个简单的 WWS 客户端(原生代码),它将连接到这个 WCF 服务。

编写原生 WWS 客户端

在创建项目之前,您需要安装和配置 Windows 7 RC SDK。(注意:在较旧的操作系统上,您还需要下载并安装 WWS RC API

您还需要正确指向您的 C++ 包含和库搜索目录。在 VS 2010 中,这是按项目级别完成的。有关如何在 VS 2010 中执行此操作的信息,请参阅我写的这篇博客文章:在 VS 2010 中设置 VC++ 目录

现在,在我们开始编写 WWS 原生客户端之前,还需要做两步

使用 svcutil 从 WCF 服务生成 WSDL

运行此命令:svcutil /t:metadata https://:8000/StringService

您将获得以下生成的文件:

  1. schemas.microsoft.com.2003.10.Serialization.xsd
  2. tempuri.org.wsdl
  3. tempuri.org.xsd

以下是 wsdl 文件中显示服务描述的片段:

<wsdl:message name="IStringService_Reverse_InputMessage">
  <wsdl:part name="parameters" element="tns:Reverse" />
</wsdl:message>
<wsdl:message name="IStringService_Reverse_OutputMessage">
  <wsdl:part name="parameters" element="tns:ReverseResponse" />
</wsdl:message>
<wsdl:portType name="IStringService">
  <wsdl:operation name="Reverse">
    <wsdl:input wsaw:Action="http://tempuri.org/IStringService/Reverse" 
      message="tns:IStringService_Reverse_InputMessage" />
    <wsdl:output 
      wsaw:Action="http://tempuri.org/IStringService/ReverseResponse" 
      message="tns:IStringService_Reverse_OutputMessage" />
  </wsdl:operation>
</wsdl:portType>
<wsdl:binding name="WSHttpBinding_IStringService" 
  type="tns:IStringService">
  <wsp:PolicyReference URI="#WSHttpBinding_IStringService_policy" />
  <soap12:binding transport="http://schemas.xmlsoap.org/soap/http" />
  <wsdl:operation name="Reverse">
    <soap12:operation 
      soapAction="http://tempuri.org/IStringService/Reverse" 
        style="document" />
    <wsdl:input>
      <soap12:body use="literal" />
    </wsdl:input>
    <wsdl:output>
      <soap12:body use="literal" />
    </wsdl:output>
  </wsdl:operation>
</wsdl:binding>
<wsdl:service name="MyStringService">
  <wsdl:port name="WSHttpBinding_IStringService" 
    binding="tns:WSHttpBinding_IStringService">
    <soap12:address location="https://:8000/StringService" />
    <wsa10:EndpointReference>
      <wsa10:Address>https://:8000/StringService</wsa10:Address>
    </wsa10:EndpointReference>
  </wsdl:port>
</wsdl:service>

下一步是生成代理文件。

使用 Windows WebServices 编译器工具 (wsutil) 生成代理(c/h 文件)。

运行此命令:wsutil *.xsd *.wsdl

您会看到为传递给 wsutil 的每个文件生成一个 c/h 文件对——由于我们有 3 个文件,它会给我们返回 6 个文件。文件生成后,打开您创建的本地 C++ 控制台项目并添加所有生成的 c/h 文件(并记住禁用 C 文件的预编译头)。为了无错误编译,请按此顺序放置您的 #include-s

#include "stdafx.h"
. . .
#include "WebServices.h"
#include "schemas.microsoft.com.2003.10.Serialization.xsd.h"
#include "tempuri.org.xsd.h"
#include "tempuri.org.wsdl.h"

并且记得在您的附加链接器模块中包含 WebServices.lib。现在要做的第一件事是声明一些变量并指定服务 URL

HRESULT hr = ERROR_SUCCESS;
WS_ERROR* error = NULL;
WS_HEAP* heap = NULL;
WS_SERVICE_PROXY* proxy = NULL;

WS_ENDPOINT_ADDRESS address = {};
WS_STRING url= WS_STRING_VALUE(L"https://:8000/StringService");
address.url = url;

WS_STRING 是一个简单的结构体,它有一个 WCHAR*(指向字符串)和一个 ULONG(表示长度)。我在这里使用了 WS_STRING_VALUE 宏来初始化字符串。

WWS API 通过 WS_ERROR 结构提供丰富的错误信息,因此我们使用 WsCreateError API 调用(使用默认参数)创建一个 WS_ERROR 结构。

hr = WsCreateError(NULL,  0,  &error);
if (FAILED(hr))
{
  //...
}

我们还需要创建一个 WS_HEAP 对象,它表示一个不透明的堆结构(未显示错误处理)

hr = WsCreateHeap(2048, 512, NULL, 0, &heap, error); 

下一步是创建服务代理

WS_HTTP_BINDING_TEMPLATE templ = {};
hr = WSHttpBinding_IStringService_CreateServiceProxy(&templ, NULL, 0, &proxy, error);

WSHttpBinding_IStringService_CreateServiceProxy 是由 wsutil 生成的代理函数。它内部调用 WWS API WsCreateServiceProxyFromTemplate,但这省去了我们正确填充各种参数的麻烦。这是生成的代码:

HRESULT WSHttpBinding_IStringService_CreateServiceProxy(
    __in_opt WS_HTTP_BINDING_TEMPLATE* templateValue,
    __in_ecount_opt(proxyPropertyCount) const WS_PROXY_PROPERTY* proxyProperties,
    __in const ULONG proxyPropertyCount,
    __deref_out_opt WS_SERVICE_PROXY** _serviceProxy,
    __in_opt WS_ERROR* error)
{
    return WsCreateServiceProxyFromTemplate(
        WS_CHANNEL_TYPE_REQUEST,
        proxyProperties,
        proxyPropertyCount,
        WS_HTTP_BINDING_TEMPLATE_TYPE,
        templateValue,
        templateValue == NULL ? 0 : sizeof(WS_HTTP_BINDING_TEMPLATE),
        &tempuri_org_wsdl.policies.WSHttpBinding_IStringService,
        sizeof(tempuri_org_wsdl.policies.WSHttpBinding_IStringService),
        _serviceProxy,
        error);
}

如您所见,代理函数省去了我们指定所有这些属性的麻烦。接下来,我们打开服务代理(连接我们到服务终结点)

hr = WsOpenServiceProxy(proxy, &address, NULL, error);

此时我们已准备好调用服务,为此我们再次使用由 wsutil 生成的代理函数。

WCHAR* result;	

hr = WSHttpBinding_IStringService_Reverse(
        proxy, L"Nishant Sivakumar", &result,
        heap, NULL, 0, NULL, error);

if (FAILED(hr))
{
  // ...
}

wprintf(L"%s\n", result);

WSHttpBinding_IStringService_Reverse 是为我们生成的,使用起来非常简单,它内部封装了对 WsCall WWS API 函数的调用,包括正确封装所有参数和返回值。我在这里没有展示实际的代理函数,但它会在 tempuri.org.wsdl.c 中。

嗯,差不多就是这样。完成后,只需调用所有 close/free API

if (proxy)
{
  WsCloseServiceProxy(proxy, NULL, NULL);
  WsFreeServiceProxy(proxy);
}

if (heap)
{
  WsFreeHeap(heap);
}

if (error)
{
  WsFreeError(error);
}

当我第一次运行控制台应用程序时,它成功连接到 WCF 服务,我感到非常兴奋。原生客户端的代码行数大约是等效托管 WCF 客户端的两倍,但为了能够在纯原生代码中消费 WCF 服务,这笔小的代价是值得的。在下一节中,我将介绍如何用一个相同的 WWS 服务替换 WCF 服务本身(WCF 和 WWS 客户端都将继续正常工作)。

在 WWS 中重写 WCF 服务

一旦我们使用 WWS 重写服务,现有客户端(无论是 WWS、WCF 还是其他)将继续保持相同的行为。我将使用 wsutil 从 wsdl 文件生成的相同的 c/h 文件。它为我们生成了一个函数签名以匹配服务契约方法。在我们的例子中,只有一个 - WSHttpBinding_IStringService_ReverseCallback
typedef HRESULT (CALLBACK* WSHttpBinding_IStringService_ReverseCallback) (
    __in const WS_OPERATION_CONTEXT* _context,
    __in_opt __nullterminated WCHAR* s, 
    __out_opt __deref __nullterminated WCHAR** ReverseResult, 
    __in_opt const WS_ASYNC_CONTEXT* _asyncContext,
    __in_opt WS_ERROR* _error);

所以第一件事是添加一个匹配此签名的方法,它将像 WCF 服务一样反转字符串(只是我们用 C 或 C++ 编写)。

HRESULT CALLBACK Reverse(
	__in const WS_OPERATION_CONTEXT* context,
	__in WCHAR* s,
	__out  WCHAR** reverse,
	__in_opt const WS_ASYNC_CONTEXT* asyncContext,
	__in_opt WS_ERROR* error)
{
	WS_HEAP* heap = NULL;

	HRESULT hr = WsGetOperationContextProperty(
		context,
		WS_OPERATION_CONTEXT_PROPERTY_HEAP,
		&heap,
		sizeof(heap),
		error);

	if (FAILED(hr))
	{
		return hr;
	}

	hr = WsAlloc(
           heap,
           sizeof(WCHAR) * (wcslen(s) + 1),
           (void**)reverse,
           error);

	if (FAILED(hr))
	{
		return hr;
	}

	wcscpy(*reverse, s);
	wcsrev(*reverse);

	return ERROR_SUCCESS;
}

我首先使用 WsGetOperationContextProperty 获取堆,然后使用 WsAlloc 在该堆上为反转字符串分配内存。我们从不使用标准内存分配机制分配内存,除非是我们使用完后可以选择删除/释放的内存。相反,我们使用 WWS 堆,这使我们无需担心内存泄漏——当堆被重置或释放时,内存将被释放。

现在让我们来创建服务。为了节省空间,我将不展示错误处理代码,但每个 HRESULT 返回值都必须在继续之前检查是否成功。首先是创建错误和堆对象,就像我们编写 WWS 客户端时所做的那样。

WS_ERROR* error = NULL;
HRESULT hr = WsCreateError( NULL, 0, &error);
if (FAILED(hr))
{
 // ...
}

WS_HEAP* heap = NULL;
hr = WsCreateHeap( 100000, 0, NULL, 0, &heap, error);
if (FAILED(hr))
{
 // ...
}

下一步是创建服务终结点。

WSHttpBinding_IStringServiceFunctionTable functions = { Reverse };

WS_STRING url = WS_STRING_VALUE(L"https://:8000/StringService");

WS_HTTP_BINDING_TEMPLATE templateValue = {};

WS_SERVICE_ENDPOINT* serviceEndpoint;
hr = WSHttpBinding_IStringService_CreateServiceEndpoint(&templateValue,
    &url, &functions, NULL, NULL, 0,
    heap, &serviceEndpoint, error);
if (FAILED(hr))
{
  // ...
}

注意我们如何使用生成的 WSHttpBinding_IStringServiceFunctionTable 来指定函数列表(在我们的例子中只有一个)。现在我使用代理 WSHttpBinding_IStringService_CreateServiceEndpoint 来创建端点(它内部调用 WsCreateServiceEndpointFromTemplate)。我为其他参数使用了默认值,但可以进行许多自定义配置。我们现在将创建服务宿主

WS_SERVICE_HOST* host;
const WS_SERVICE_ENDPOINT* serviceEndpoints[1];
serviceEndpoints[0]= serviceEndpoint;
hr = WsCreateServiceHost( serviceEndpoints, 1,
    NULL, 0,  &host, error);
if (FAILED(hr))
{
}

我们只有一个端点,但服务宿主可以宿主多个端点。虽然我使用默认参数(基本上是传递 NULL)调用了 WsCreateServiceHost ,但此时可以设置各种服务属性。最后一步是打开服务宿主。

hr = WsOpenServiceHost(host, NULL, error);

以上代码将打开服务并开始在每个终结点上监听(在我们的示例中只有一个)。对于示例控制台应用程序,我们将使用 _getch() 以便应用程序不会退出。

wprintf(L"Press any key to stop the service...\n");
_getch();

应用程序完成后,关闭并释放服务主机

WsCloseServiceHost(host, NULL, error);
WsFreeServiceHost(host);

并释放堆/错误对象

if (heap)
{
  WsFreeHeap(heap);
}

if (error)
{
  WsFreeError(error);
}

运行服务,然后运行 WWS 客户端,它将能够连接到服务并成功调用反转函数。您还可以运行简单的 WCF C# 客户端,它将连接到此服务并执行字符串反转方法。所以现在我们可以将这两个客户端中的任何一个连接到这两个服务中的任何一个——太棒了!

结论

我同意,如果您是从纯 C# 或 VB.NET 世界而来,所有这些代理、必须创建/释放结构、处理 HRESULT 等可能看起来有点陌生。但是,如果您不排斥 C++,并且保持服务或客户端代码原生对您很重要,那么 WWS 确实是一个很好的实现方式。

历史

  • 文章初稿:2009 年 7 月 25 日
  • 文章发表:2009 年 7 月 27 日
© . All rights reserved.