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

将 Web 服务器嵌入 Windows 服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (23投票s)

2013年12月10日

CPOL

9分钟阅读

viewsIcon

70421

downloadIcon

3179

使用 NancyFX 为使用 VS 2013 的 Windows 服务提供 Web 界面。

引言

Windows 服务是一个长时间运行的进程,它在后台运行,并在需要时执行。服务不与桌面交互,这会引发许多问题,包括无法比在服务控制面板中简单地单击“启动服务”和“停止服务”更精细地控制服务。本文介绍如何使用 NancyFX 框架为您的 Windows 服务提供 Web 浏览器界面,从而让您能够更精细地控制服务内部的工作和管理服务本身。

(我 Windows 窗体上那张非常可爱的图片来自 NancyFX 网站,是的,使用它可能就像它所说的那样简单!)

基础知识

为了开始,并避免立即启动一个服务,我们将从一个标准的 Windows 窗体项目开始,然后在文章的最后进行转换。我将这个入门项目命名为“NancyWinForm”。

创建 Windows 窗体项目后,第一件事是打开 NuGet 并添加以下包:

Nancy, Nancy.ViewEngines.Razor, Nancy.Hosting.SelfNancy 是核心包,Hosting.Self 对于在 DLL、EXE 等中进行自托管是必需的,而无需依赖其他宿主。这次我们使用 Razor 引擎来解析 HTML 视图页面中的数据。

为了启动并运行 Nancy,我们需要做好一些基本准备。在链接了 NuGet 包之后,下一步是在主窗体顶部添加一些 using 语句。

using Nancy;
using Nancy.Hosting.Self; 
using System.Net.Sockets; 

接下来,我们需要设置一个窗体级别的对象来运行 Nancy

namespace NancyWinForm{
public partial class Main : Form
    {
        NancyHost host;   // < ---- form level object
        public Main()
        {
            InitializeComponent();
        }
    }
}

然后,我们初始化 Nancy 主机对象。

        public Main()       {
            InitializeComponent();
            string URL = "https://:8080";
            host = new NancyHost(new Uri(URL));
            host.Start();
        }

我们使用一个构造函数启动 Nancy,该构造函数为其提供了一个简单的域“localhost”和端口“8080”。您应该选择一个系统中尚未运行的端口。例如,如果您已经运行了 IIS 或其他 Web 服务,则端口 80 很可能已被占用。我们将在文章后面讨论多重绑定和端口。一旦我们告诉了 Nancy 要绑定到哪里,我们就通过调用“start”让她“翩翩起舞”……如果我们打开浏览器访问她给我们的 URL,我们会看到……

Nancy 的作者有一个他们称之为“超级无敌快乐路径”的理念……我认为我们可以安全地说我们很开心。微笑 | :)

好的,接下来我们要把 Nancy 裙子里的那个小绿怪弄出来,自己进去<嗯哼>...

下一步是创建一个类型为“NancyModule”的新类,并为其指定一个默认路径。

    public class MainMod : NancyModule    
{
    public MainMod()
    {
     Get["/"] = x =>
       {
        return "Wee hoo! - no more little green monster...";
       };

     }
}

这里有一个小陷阱——在 Windows 窗体中,如果您将此代码放在需要设计器的代码之前(例如,一个带有年轻南茜图片的图片容器),编译器会抱怨它需要排在第一位……要解决这个问题,只需将模块类放在窗体文件的末尾。

NancyModule”可以添加到应用程序中的任何位置,框架都会找到它。它通过在启动时遍历应用程序域来查找任何 NancyModules,并在找到时将它们挂接到应用程序中。在 NancyModule 中,我们添加“路由”或“路径”。您可以看到在本例中我添加了一个根路径“/”。为了开始,我只是向浏览器返回一个简单的字符串。当我们运行时,输出符合预期。

好的,文本输出很棒,但用处不大。接下来我们要添加一个 HTML 文件来处理。对于这个项目,我添加了一个名为“views”的文件夹,并在其中创建了一个名为“skirts.html”的新 HTML 文件(跟上节奏,主题必须继续!!)。

让我们向 HTML 文件添加一些简单的内容,并调整模块的 get 路由处理程序以返回文件。

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>Nancy's skirts</title>
</head>
<body>

Heylow der...
</body>
</html> 
         public MainMod()
          {
            Get["/"] = x =>
             {
                return View["views/skirts.html"];   //<-- sending back a view page
              };

          }

按 F5 运行,站远点……

哦,不好,那个小绿怪又回来了……Nancy 中发生了这种情况,我们需要告诉它比 IIS 更多信息。在这种情况下,我们需要告诉项目将其文件包含在其输出文件夹中。

现在当我们运行时,那个小绿怪就消失了……完美……

在了解了如何处理 GET 请求后,我们将处理 POST 请求。在本例中,让我们创建另外两个 HTML 文件“nice.html”和“very.html”——不要忘记将它们设置为“复制到输出目录(如果更新)”。在原始 HTML 文件中,我们将添加一个表单来提交……

<form method="post" action="NiceNancy">
    How nice is nancy? - please select...
<select name="HowNice"> 
 
<option value="1">Quite nice</option>

<option value="2">非常!</option> </select> <button type="submit">提交</button> </form>

我们的想法是,如果用户选择“相当不错”,则返回一个页面;如果他们选择“非常”,则返回另一个页面。这是添加到 Nancy 模块中的 POST 代码。

             Post["/NiceNancy"] = y =>
     {
     if (Request.Form["HowNice"].HasValue)
      {                      
      if (Request.Form["HowNice"] == "1")
       { return View["views/nice.html"]; }
      else if (Request.Form["HowNice"] == "2")
      { return View["views/very.html"]; }
      else return View["views/skirts.html"];
     }; 
      else return View["views/skirts.html"];
     }

请注意,与 MVC/ASP.NET 一样,表单数据可以通过“Request”对象访问。

 

告别 Nancy……

Nancy 非常轻量,不像 IIS 那么笨重,它没有您可能期望的内置功能。其中一些功能(如图像和路径)需要指导。Nancy 会自动提供文件(JS、CSS、图像……),但需要告诉它这些文件存储在哪里,相对于它自身的位置。这通过“Bootstrapper”类来处理。

我正在创建一个新的单元来存储这段代码,“Bootstrapper.cs”——您可以将其放在任何您想要的位置。在此文件中,我添加了以下 using 语句。

using Nancy;
using Nancy.Session;
using Nancy.Bootstrapper;
using Nancy.Conventions;
using System.Web.Routing;
using Nancy.TinyIoc; 

我还为“system.web”添加了一个项目引用。

我们需要让新类继承自“DefaultNancyBootstrapper”,然后才能开始做任何有用的事情。然后,我们添加一个覆盖方法“Configure conventions”来挂接存储图像等文件的文件夹的位置。

     

public class Bootstrapper : DefaultNancyBootstrapper
    {
        protected override void ConfigureConventions(NancyConventions nancyConventions)
        {
            base.ConfigureConventions(nancyConventions);
            nancyConventions.StaticContentsConventions.Clear();
            nancyConventions.StaticContentsConventions.Add
            (StaticContentConventionBuilder.AddDirectory("css", "/content/css"));
            nancyConventions.StaticContentsConventions.Add
            (StaticContentConventionBuilder.AddDirectory("js", "/content/js"));
            nancyConventions.StaticContentsConventions.Add
            (StaticContentConventionBuilder.AddDirectory("images", "/content/img"));
            nancyConventions.StaticContentsConventions.Add
            (StaticContentConventionBuilder.AddDirectory("fonts", "/content/fonts"));
        }

    }

在启动时,Nancy 现在知道了我们有一个本地路径“/content/img”,它通过虚拟路径“images”进行引用。让我们从主 HTML 文件中测试一下,添加一张图片并引用它。

 <form method="post" action="NiceNancy">
        <img src="images/SoWhat.jpg" />
        <br /> 

是的,成功了!

为了告诉 Nancy 我们的图像和其他资源存储在哪里,我们通过“bootstrapper”类进行了自定义。Bootstrapper 对各种事情都很有用,因为它允许我们挂接到 Nancy 的 IoC 容器并向我们的应用程序注入支持对象。我想实现的一个功能是能够拥有一个有效地存储全局变量的对象。为此,我创建了一个类来使用 XML 文件加载/保存数据,并根据需要提供对数据的访问。

以下是简单的“config-manager”类,我将其存储在单独的文件“shared”中(请注意添加了 XML using 语句,因为我正在使用序列化来快速保存/加载数据)。

using System.Xml;
using System.Xml.Serialization;
using System.IO;

namespace NancyWinForm
{
    public class ConfigInfo
    {
        public String TempFolder { get; set; }
        public String Username { get; set; }
        public String DateFormat { get; set; }
    }

    public class PracticeConfigManager
    {
        public ConfigInfo config;
        string _XMLFile = AppDomain.CurrentDomain.BaseDirectory + "MyConfig.xml";

        public void LoadConfig()
        {
            if (config == null)
            {
                config = new ConfigInfo();
            }

            if (!File.Exists(_XMLFile))
            {
                config.DateFormat = "dd/mmm/yyyy";
                SaveConfig();
            }

            XmlSerializer deserializer = new XmlSerializer(typeof(ConfigInfo));
            TextReader reader = new StreamReader(_XMLFile);
            object obj = deserializer.Deserialize(reader);
            config = (ConfigInfo)obj;
            reader.Close();
        }

        public void SaveConfig()
        {
            if (config != null)
            {
                XmlSerializer serializer = new XmlSerializer(typeof(ConfigInfo));
                using (TextWriter writer = new StreamWriter(_XMLFile))
                {
                    serializer.Serialize(writer, config);
                }
            }
        }
    }
}

在设置了保存/加载配置数据的类之后,我们现在需要将其注入到 Nancy 中。我们通过 bootstrapper 来实现这一点。在我们的 bootstrap 类中,我们覆盖“Application Startup”方法,并在其中将 ConfigManager 类注册到 IoC 容器中,如下所示:

    public class Bootstrapper : DefaultNancyBootstrapper
    {

        protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
        {
            base.ApplicationStartup(container, pipelines);
            ConfigManager mgr;
            mgr = new ConfigManager();
            mgr.LoadConfig();
            container.Register(mgr);
        }

在注册之后,我们现在需要能够在收到 GET/POST HTTP 请求时访问它。我们通过更改我们主要的 NancyModule 的签名来实现这一点。

public MainMod()

改为

public MainMod(ConfigManager mgr)

由于我们的 ConfigManagerNancy 启动时创建和初始化,因此我们现在可以在 NancyModule 中直接使用它。

            Get["/config"] = x =>
            {
               return mgr.config.DateFormat;
            };

结果……

好的,一切看起来都不错。bootstrapper 类是 NancyFX 中一个非常有用的部分,我发现自己经常会用到它。

在进行 Web 开发时,我如今倾向于避开 WebForms,几乎完全使用 MVC——我喜欢清晰的关注点分离,以及能够“接近底层”的能力。Nancy 允许我们以一种熟悉的 MVC 方式使用 Razor 语法。假设我们有一些持久化的值希望在网页上使用。我们在 Nancy 中使用 Razor 的方式与在 MVC 中相同……

  1. 创建一个名为“config.html”的页面,并将其属性设置为“添加(如果更新)”。
    <body>
    
        Model test page<br />
        <hr>
        <form method="post" action="SaveConfig">
    
            <input id="dateFormat" value="@Model.DateFormat" />
            <input id="username" value="@Model.Username" />
            <input id="tempFolder" value="@Model.TempFolder" />
    
            <button type="submit">Submit</button>
    
        </form>
    </body>
  2. 添加新的控制器代码,将 ConfigManager 作为模型传递。
                Get["/config"] = x =>
                {
                   //return mgr.config.DateFormat;
                    return View["views/config.html",mgr.config];
                };

因此,我们可以看到模型数据(日期格式)已正确渲染到 HTML 中。

绑定到 IP 堆栈

有时在设置 HTTP 服务器监听流量时,您可能想指定一个特定的 IP 地址,但有时您想绑定到所有可用的 IP,换句话说,是一个通配符绑定。我在这里找到了一些有用的代码,可以帮助实现这一点。您将端口发送给该方法,它会返回一个可用绑定的数组。

        private Uri[] GetUriParams(int port)
        {
            var uriParams = new List<uri>();
            string hostName = Dns.GetHostName();

            // Host name URI
            string hostNameUri = string.Format("http://{0}:{1}", Dns.GetHostName(), port);
            uriParams.Add(new Uri(hostNameUri));

            // Host address URI(s)
            var hostEntry = Dns.GetHostEntry(hostName);
            foreach (var ipAddress in hostEntry.AddressList)
            {
                if (ipAddress.AddressFamily == AddressFamily.InterNetwork)  // IPv4 addresses only
                {
                    var addrBytes = ipAddress.GetAddressBytes();
                    string hostAddressUri = string.Format("http://{0}.{1}.{2}.{3}:{4}", 
                    addrBytes[0], addrBytes[1], addrBytes[2], addrBytes[3], port);
                    uriParams.Add(new Uri(hostAddressUri));
                }
            }

            // Localhost URI
            uriParams.Add(new Uri(string.Format("https://:{0}", port)));
            return uriParams.ToArray();
        }

为了实现这一点,我们需要以略微不同的方式启动 Nancy……这样做的效果是强制 ACL 为新端口创建网络规则(如果它们尚不存在)。

 
        public Main()
        {
            InitializeComponent();
            int port = 8080;
            var hostConfiguration = new HostConfiguration
            {
                UrlReservations = new UrlReservations() { CreateAutomatically = true }
            };
           host = new NancyHost(hostConfiguration, GetUriParams(port));
            host.Start();
        }

有一个需要注意的“陷阱”……Windows 不允许除管理员以外的用户在非默认端口上运行任何东西——解决方案是手动打开端口——您也可以在安装过程中手动处理,如下所示:

netsh http add urlacl url=http://+:8888/app user=domain\user
+ 表示绑定到所有可用 IP。

Nancy 成为服务……

为了方便快速测试项目,我们将其集成在一个 Windows 窗体应用程序中。然而,目标是利用这个非常酷的小型微型 Web 服务器作为网关,以便更有意义地访问和与 Windows 服务交互。我们已经在文章前面完成了主要代码,所以这里我唯一要做的是展示服务的设置——如果您想在运行时查看它,或者希望自己使用这段代码,它是可以下载的!

服务的正常模板代码库开始如下:

    static class Program
    {       
        static void Main()
        {
            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[] 
            { 
                new Service1() 
            };
            ServiceBase.Run(ServicesToRun);
        }
    }

我们将进行两处修改——首先,我们将修改代码,以便如果您从 Visual Studio 开发环境启动它,它将启动并允许您调试,最后,我们将添加 Nancy 的启动代码本身。

    static class Program
    {       
        static void Main()
        {
            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[] 
            { 
                new Service1() 
            };
            ServiceBase.Run(ServicesToRun);
        }
    }

更改为

        static void Main()
        {
            NancyHost host;
            string URL = "https://:8080";
            host = new NancyHost(new Uri(URL));
            host.Start();

            //Debug code
            if (!Environment.UserInteractive)
            {
                ServiceBase[] ServicesToRun;
                ServicesToRun = new ServiceBase[] 
            { 
                new Service1() 
            };
                ServiceBase.Run(ServicesToRun);
            }
            else
            {
                Service1 service = new Service1();
                // forces debug to keep VS running while we debug the service  
                System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite);  
            }
        }

要安装服务,我们需要使用命令行。

"C:\Windows\Microsoft.NET\Framework\v4.0.30319\installutil.exe" "[Path]NancyService.exe"
(其中 [Path] 是您编译的服务可执行文件的位置)。

瞧,成品……

就是这样,这是一个有用的练习,下次编写 Windows 服务时可以考虑一下。

题外话……

使用 Nancy 对我来说特别有趣,因为很多年前,在一个遥远而古老的星球上,我曾深度参与 http://www.indyproject.org/index.en.aspx。Indy 是/是一个开源套接字库,在 Borland Delphi 社区被广泛使用,作为其中的一部分,我编写了许多演示,包括一个自托管的 HTTP 服务器……世事变迁。

历史

  • 13/12/10 - 提交第一个版本
  • 13/12/11 - 添加内容
  • 13/12/21 - 添加最终内容
© . All rights reserved.