使用 Topshelf 作为宿主,log4net 作为日志工具,HTML 作为输出的 WCF 服务
如何使用 Topshelf 作为主机运行简单的 WCF 服务,
引言
最近,我需要一种简单的方式通过浏览器显示(来自数据库的)数据信息(以 HTML 格式),但又不想创建 Web 项目并在 IIS 下运行(也不想安装 IIS)。我发现使用 WCF 服务可以解决这个问题,尤其是在服务器上运行着 Topshelf 服务宿主的情况下。我曾试图查找关于如何在 Topshelf 宿主下运行 WCF 服务的信息,但没有成功,所以我写这篇文章,希望可以帮助其他人避免我犯过的错误。我还使用了 log4net 来记录事件,并将 XML 输出转换为 HTML。
背景
Topshelf 是一个服务宿主,它使得安装和运行服务变得非常容易。您可以在 这里 获取所有信息。
WCF Web 服务提供了一种简单易用的方式,通过 HTTP 协议获取数据。您(或您的程序)可以请求 WCF Web 服务来显示从数据库中获取的数据,或者执行其他操作。简单地说,您发送一个 HTTP 请求,并得到 JSON 或 XML 格式的响应。就我的目标而言,我不喜欢 XML 格式,我想要 HTML。下面是我所做的工作。
Using the Code
我们将整个任务分成几个步骤。它们是:
- 创建 WCF Web 服务
- 使用 log4net 添加日志记录
- 准备并部署项目到服务器
必备组件
开始之前,您需要安装一些工具
- VisualStudio 2010
- .NET 4.0
- Topshelf 已安装在服务器上
- 下载 log4net
我们开始吧!
1. 创建 WCF Web 服务
- 打开 VS2010,创建一个新的项目 - WCF 服务应用程序。
- 删除默认创建的 Service1.svc 和 IService1.cs,并将您的 WCF 服务添加到项目中。
- 开始编程!在本例中,我不想真正连接到数据库,所以我们只创建一个本地对象并对其进行操作。首先,创建一个测试对象类,称之为
person
。public class Person { public string FirstName { get; set; } public string LastName { get; set; } public string BirthYear { get; set; } public string Nickname { get; set; } public int Id { get; set; } }
打开 CoolService.cs,并创建一个构造函数,在这里我们创建
Person
类的两个实例。public CoolService() { Person Jack = new Person() { FirstName = "Jack", LastName = "Nicolson", BirthYear = "1976", Nickname = "Goose", Id = 1 }; Person Jane = new Person() { FirstName = "Jane", LastName = "Corino", BirthYear = "1971", Nickname = "Mary Poppins", Id = 2 }; persons = new List<person>() { Jack, Jane }; }
我们希望以 HTML 格式获取人员信息,但结果可以是 XML 或 JSON,所以我们尝试将人员信息转换为 XML,然后再转换为 HTML。
让我们创建一个 xslt 文件,它将帮助我们以美观的方式展示人员信息。右键单击项目,选择“添加项...”,然后选择(Visual C# -> 数据)XSLT 文件,将其重命名为 Style 并单击“添加”。将以下 xslt 代码添加到其中:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<html>
<head>
<title>
Person info
</title>
<style type="text/css">
h1 {
text-align: center;
}
h2
{
text-indent: 0px;
}
h3
{
text-indent: 20px;
}
body {
font-family: Calibri;
margin:0px;
padding:0px;
background: #fff;
}
td
{
padding: 5px;
border:1px solid #A4B1BF;
}
</style>
</head>
<body>
<xsl:apply-templates select="Person" />
</body>
</html>
</xsl:template>
<xsl:template match="Person">
<h1>
<xsl:value-of select="FirstName" /> - <xsl:value-of select="LastName" />
</h1>
<table class="report-table" cellpadding="0" cellspacing="0">
<tr>
<td>Birth year</td>
<td>
<xsl:value-of select="BirthYear" />
</td>
</tr>
<tr>
<td>Nick:</td>
<td>
<xsl:value-of select="Nickname" />
</td>
</tr>
</table>
</xsl:template>
</xsl:stylesheet>
这很简单,不需要任何解释。
现在,我们将此文件添加到资源中 - 以便可以直接从代码中调用它。右键单击项目,选择“添加项...”,然后选择(在 Visual C# -> 常规选项卡中)资源文件,将其命名为 Resource.resx。您将在左上角看到一个组合框,单击它并选择“文件”。
将 Style.xslt 拖放到任意位置,然后您就可以将其作为变量访问该资源了。
将以下命名空间包含到项目中:
using System.IO;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;
using System.Xml.Xsl;
using System.Xml.XPath;
将 DoWork()
方法重命名为 Stream GetPersonInfo(string id)
(别忘了重命名对应的接口)。以下代码完成了我们所需的所有转换(将其添加到 GetPersonInfo
方法中):
StringReader reader = new StringReader(Resource.Style);
XmlReader xreader = XmlReader.Create(reader);
XslCompiledTransform xslt = new XslCompiledTransform(false);
xslt.Load(xreader);
XmlSerializer serializer = new XmlSerializer(typeof(Person));
XDocument doc = new XDocument();
using (XmlWriter writer = doc.CreateWriter())
{
serializer.Serialize(writer, persons);
}
StringWriter outputWriter = new StringWriter();
xslt.Transform(doc.CreateNavigator(), null, outputWriter);
string htmlEmailText = outputWriter.ToString();
一切都很好,但问题是我们需要向客户端返回一些数据,它不能只是一个 string
,因为在这种情况下,string
会被编码,结果将只是一组编码后的标签 - 这与我们实际想要的有点不同(您可以尝试一下)。所以,我们返回 Stream
。
System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
MemoryStream stream = new MemoryStream(encoding.GetBytes(htmlEmailText));
return stream;
现在我们需要定义终结点。Endpoint
是我们向服务请求信息时要调用的地址。在我们的例子中,CoolService
服务就是终结点。将以下代码添加到您的 Web.config 文件中:
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="ServiceBehaviour">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
<endpointBehaviors>
<behavior name="web">
<webHttp/>
</behavior>
</endpointBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="ServiceBehaviour" name="WcfTestService">
<endpoint behaviorConfiguration="web" binding="webHttpBinding"
contract="WcfTestService.ICoolService" address="" />
</service>
</services>
<serviceHostingEnvironment multipleSiteBindingsEnabled="false" />
</system.serviceModel>
您可以看到我们使用了 webHttpBinging
,因为我们想通过浏览器调用我们的服务。但我们还需要在接口中添加一些信息,所以在 GetPersonInfo
的 [OperationContract]
属性的后面添加以下代码:
[WebGet(UriTemplate = "/id/{id}", ResponseFormat = WebMessageFormat.Xml)]
这意味着,首先,我们将使用模板 "/id/{id}" 来调用我们的服务,以获取指定 ID 的人员信息,然后只有这种格式才会被识别;其次,输出将是 XML 格式。因此,如果我们想获取特定人员的信息,我们需要在方法中指定 "id
" 参数(我们已经完成了)。
现在我们需要为 topshelf 创建一个“入口点”。您可以在官网上找到所有信息,这里我只展示您需要做的具体操作。创建一个新类,称之为 CoolServiceBootstrapper.cs,然后添加以下命名空间:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Topshelf;
using Topshelf.Shelving;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Web;
using log4net;
让该类继承自 Bootstrapper<WebServiceHost>
,该类的最终版本是:
public class CoolServiceBootstrapper : Bootstrapper<WebServiceHost>
{
private ILog log4netLogger = LogManager.GetLogger(typeof(CoolServiceBootstrapper));
public static WebServiceHost webService = null;
public void InitializeHostedService
(Topshelf.Configuration.Dsl.IServiceConfigurator<WebServiceHost> cfg)
{
log4net.Config.XmlConfigurator.Configure();
cfg.Named("WcfTestService.CoolService");
cfg.HowToBuildService(n =>
{
if (webService != null)
{
webService.Close();
}
webService = new WebServiceHost(typeof(CoolService));
log4netLogger.Info("Test cool service created");
log4netLogger.Info("Test cool service endpoint: " +
webService.Description.Endpoints.Select
(e => e.ListenUri.ToString()).Aggregate((e, ne) => e + ", " + ne));
return webService;
});
cfg.WhenStarted(s =>
{
s.Open();
log4netLogger.Info("Test cool service started");
});
cfg.WhenStopped(s =>
{
s.Close();
s = null;
});
}
}
正如您所见,这里有一些供 topshelf 参考的指针,说明了在 init
、start
和 stop
时应该做什么。我们还添加了一些日志功能,以确保一切正常工作。
2. 使用 log4net 添加日志记录
好的,现在我们想添加一些日志记录。因为服务是不可见的,我们总是想知道发生了什么,即使我们看不到屏幕,就像在“黑客帝国”里一样
好的,我想您已经下载并解压了 log4net 库。只需将 DLL 复制到工作文件夹,添加引用,并在代码中添加 `using log4net;` 命名空间。
然后创建如下的 private
变量:
private ILog log4netLogger = LogManager.GetLogger(typeof(CoolService));
然后您就可以开始记录日志了。例如,您可以记录从数据库中获取的数据:
log4netLogger.Info("Persons count is " + persons.Count);
但是日志引擎如何知道将日志保存在哪里呢?很简单 - 只需将此信息添加到您的 Web.config 文件中(configSection
- 如果您没有这个节,请创建它):
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,
log4net" requirePermission="false"/>
在这里,您只定义了节,但我们需要实际将其添加到 config 文件中:
<log4net>
<appender name="LogFileAppender"
type="log4net.Appender.RollingFileAppender" >
<param name="AppendToFile"
value="true" />
<file type="log4net.Util.PatternString" value="..\..\log\Topshelf.Host.%appdomain.log" />
<rollingStyle value="Size" />
<maxSizeRollBackups value="4" />
<maximumFileSize value="10MB" />
<staticLogFileName value="true" />
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern"
value="%-5p %d{yyyy-MM-dd hh:mm:ss} - %m%n" />
</layout>
</appender>
<root>
<level value="INFO" />
<appender-ref ref="LogFileAppender" />
</root>
</log4net>
正如您所见,"file
" 字符串描述了文件名。它假设您的服务位于比服务本身深两层的位置。一旦服务开始工作,topshelf 就会创建一个以服务命名的文件。我曾尝试将日志文件定义在服务的文件夹中,但没有成功。我遇到了一个异常,即 TopShelf
试图访问日志文件,而它已被另一个程序打开。使用这个文件名解决了这个问题。
3. 准备并部署项目到服务器
好了,现在我们几乎可以部署我们的服务并查看它的工作情况了……但等等,TopShelf 怎么知道要运行什么?如果您阅读过有关 TopShelf 的信息,您现在就知道它有一个叫做“Shelving”的强大功能。这意味着 TopShelf 会遍历其 Services 目录中找到的所有服务,读取 config 文件,并运行可以找到的继承自 Bootstrapper
类的特定代码(您可以阅读它实际如何工作 这里 和 这里)。
所以,我想您现在已经安装并运行了 TopShelf(如果没有,下载 topshelf 二进制文件,解压它们,阅读“Using Shelving.txt”文件并按其说明操作)。您需要做的是为您的服务创建一个新文件夹,并将其放在 Topshelf/Services/ 文件夹中。然后,您需要将所有文件复制到这里 - 在本例中,将是 log4net.dll、Topshelf.dll 和 WcfTestService.dll。此外,您还需要复制 Web.config 文件。
下一步是准备 TopShelf 的 config 文件。您可能知道 topshelf 会爬取所有文件夹并查找同名的配置文件。例如,如果您的文件夹名为 TestService,那么 config 文件应该命名为 TestService.config。这个 config 文件包含附加信息 - 哪个 DLL 应该运行以及如何运行它等。只需将 web.config 复制到同一个文件夹并重命名。这个文件将由 Topshelf 本身用作服务宿主。
必须为 Topshelf 添加信息:
<section name="ShelfConfiguration" type="Topshelf.Shelving.ShelfConfiguration, TopShelf" />
以及节本身:
<ShelfConfiguration Bootstrapper="WcfTestService.CoolServiceBootstrapper, WcfTestService" />
请确保您使用正确的数据:Boostrapper
值应为 "<Bootstrapper class name>, <Assembly name>" 格式。
然后您需要更新 serviceModel
节。
与 Web.config 的主要区别在于,我们显式指定了 Web 服务的地址:
Web.config
<system.web>
<compilation debug="true" targetFramework="4.0" />
</system.web>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="ServiceBehaviour">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
<endpointBehaviors>
<behavior name="web">
<webHttp/>
</behavior>
</endpointBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="ServiceBehaviour" name="WcfTestService.CoolService">
<endpoint behaviorConfiguration="web" binding="webHttpBinding"
contract="WcfTestService.ICoolService" address="" />
</service>
</services>
<serviceHostingEnvironment multipleSiteBindingsEnabled="false" />
</system.serviceModel>
Testservice.config
<system.web>
<compilation debug="true" targetFramework="4.0" />
</system.web>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="ServiceBehaviour">
<serviceMetadata httpGetEnabled="true"
httpGetUrl="https:///testservice/"/>
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
<endpointBehaviors>
<behavior name="web">
<webHttp/>
</behavior>
</endpointBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="ServiceBehaviour" name="WcfTestService.CoolService">
<endpoint behaviorConfiguration="web" binding="webHttpBinding"
contract="WcfTestService.ICoolService"
address="https:///testservice/CoolService.svc" />
</service>
</services>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>
复制文件后,重新启动 Topshelf。据我所知,它应该会自动检测到更改,但出于某些原因,这并不总是有效。所以我更喜欢手动重启。转到控制面板\系统和安全\管理工具\服务,找到 Topshelf,右键单击并选择“重新启动”。
为了确保 Topshelf 可以看到您的服务,请转到 Topshelf/log/Topshelf.Host.Topshelf.Host.exe.log。
这里有关于 Topshelf 如何与托管服务交互的全部信息。如果一切正常,在这里的 log 文件夹中,您将找到由我们的服务创建的日志,名为 Topshelf.Host.TestService.log。
现在我们可以测试我们的服务是如何工作的了。打开您喜欢的浏览器,在地址栏输入:https:///testservice/coolservice.svc/id/1。
如果一切正常,您应该会看到类似这样的内容:

如果我们查看日志文件,我们会看到类似这样的内容:
INFO 2012-04-11 06:09:38 - Topshelf v2.2.2.0, .NET Framework v4.0.30319.261
INFO 2012-04-11 06:09:40 - Test cool service created
INFO 2012-04-11 06:09:40 - Test cool service endpoint: https:///testservice/CoolService.svc
INFO 2012-04-11 06:09:40 - [TestService] Create complete
INFO 2012-04-11 06:09:40 - <testservice> Created
INFO 2012-04-11 06:09:40 - <testservice> Starting
INFO 2012-04-11 06:09:40 - Test cool service started
INFO 2012-04-11 06:09:40 - [TestService] Start complete
INFO 2012-04-11 06:09:40 - <testservice> Running
INFO 2012-04-11 06:10:15 - Persons count is 2