将 Web 服务器嵌入 Windows 服务






4.90/5 (23投票s)
使用 NancyFX 为使用 VS 2013 的 Windows 服务提供 Web 界面。
- 下载 Windows 服务源代码 (无 EXE) - 1,018.4 KB
- 下载 Windows 服务源代码 - 2 MB
- 下载 Windows 窗体源代码 (无 EXE) - 1.7 MB
- 下载 Windows 窗体源代码 - 3 MB
引言
Windows 服务是一个长时间运行的进程,它在后台运行,并在需要时执行。服务不与桌面交互,这会引发许多问题,包括无法比在服务控制面板中简单地单击“启动服务”和“停止服务”更精细地控制服务。本文介绍如何使用 NancyFX 框架为您的 Windows 服务提供 Web 浏览器界面,从而让您能够更精细地控制服务内部的工作和管理服务本身。
(我 Windows 窗体上那张非常可爱的图片来自 NancyFX 网站,是的,使用它可能就像它所说的那样简单!)
基础知识
为了开始,并避免立即启动一个服务,我们将从一个标准的 Windows 窗体项目开始,然后在文章的最后进行转换。我将这个入门项目命名为“NancyWinForm
”。
创建 Windows 窗体项目后,第一件事是打开 NuGet 并添加以下包:
Nancy
, Nancy.ViewEngines.Razor
, Nancy.Hosting.Self
。Nancy
是核心包,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)
由于我们的 ConfigManager
在 Nancy
启动时创建和初始化,因此我们现在可以在 NancyModule
中直接使用它。
Get["/config"] = x =>
{
return mgr.config.DateFormat;
};
结果……
好的,一切看起来都不错。bootstrapper
类是 NancyFX
中一个非常有用的部分,我发现自己经常会用到它。
在进行 Web 开发时,我如今倾向于避开 WebForms,几乎完全使用 MVC——我喜欢清晰的关注点分离,以及能够“接近底层”的能力。Nancy
允许我们以一种熟悉的 MVC 方式使用 Razor 语法。假设我们有一些持久化的值希望在网页上使用。我们在 Nancy
中使用 Razor 的方式与在 MVC 中相同……
- 创建一个名为“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>
- 添加新的控制器代码,将
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 - 添加最终内容