Flikas 的个人资料Flikas' Space照片日志列表更多 工具 帮助

日志


    2月7日

    XML Web Service学习笔记 2 创建和管理Windows服务

    一、为什么使用Windows服务

    Windows服务是作为后台进程运行的。这些应用程序没有用户界面,适于处理不要求与用户交互的任务。Windows 服务应用程序在各自的安全上下文中运行,并且在用户登录到安装有该程序的 Windows 计算机之前启动。就是说,Windows服务可以在不同于当前登录用户的其他用户帐户安全上下文中运行,在系统帐户下运行的服务比在用户帐户下运行的服务具有更多的权限和特权。

    综上所述,如果你的应用程序:

    • 不需要与用户交互
    • 需要在用户登录之前运行,或需要在特定的帐户(如系统帐户)安全上下文中运行

    那么请选择Windows服务,否则,编写一个在用户控制下的Windows应用程序可能更为妥当,因为:

    • 服务应用程序不易调试:必须将服务应用程序项目创建的已编译可执行文件安装在服务器上,此项目才能以有意义的方式运行。不能通过按 F5 键或 F11 键来调试或运行服务应用程序;不能立即运行服务或进入并单步执行其代码。相反,必须安装和启动服务,然后将一个调试器附加到服务的进程中。
    • 服务应用程序几乎无法与用户交互:Windows 服务应用程序在不同于登录用户的交互区域的窗口区域中运行。由于 Windows 服务的区域不是交互区域,因此 Windows 服务应用程序中引发的对话框将是不可见的,并且可能导致程序停止响应。同样,错误信息应记录在 Windows 事件日志中,而不是在用户界面中引发。.NET Framework 支持的 Windows 服务类不支持与交互区域(即登录用户)进行交互。同时,.NET Framework 不包含表示区域和桌面的类。如果 Windows 服务必须与其他区域进行交互,则需要访问非托管的 Windows API。
    • 由于没有运行在当前登录用户帐户下,一些与当前登录用户相关的设置及权限就不易获取。

    二、如果仍旧要编写Windows服务

    Windows服务体系结构:

    • 服务应用程序
    • 服务控制器应用程序
    • 服务管理器:Service Control Manager,SCM,就是控制面板->管理工具->服务,或者Services.msc

    再看一下.NET Framework提供的编程模型,所有相关的类都位于System.ServiceProcess命名空间:

    • ServiceBase类:用于创建Windows服务,重写该类的方法来自定义你自己的服务应用程序。
    • ServiceInstaller,ServiceProcessInstaller:这两个类用于安装服务应用程序,与普通应用程序不同,服务应用程序必须先安装到系统中才能有意义的运行。ServiceProcessInstaller用于设置服务应用程序进程,包括服务运行的帐户等;ServiceInstaller用于安装服务,一个服务应用程序进程可以包含多个服务,需要为每个服务都提供一个ServiceInstaller。
    • ServiceController类:用于启动、停止、暂停服务,与SCM不同的是,还可以在服务上执行自定义命令。

    三、编写Windows服务

    使用Visual Studio.NET编写Windows服务十分方便,虽然服务及其使用的事件日志组件和性能计数器组件都需要提供安装程序,但是只要单击属性窗口下方的“添加安装程序”选项,就可以自动完成安装程序的编写,相关设置都会被复制到安装程序中,但是,如果修改了原设置,安装程序中的设置不会跟着变化。

    下面我要编写一个自己的服务应用程序,它的目的是监视硬盘上的临时文件夹的大小,并定期清理以节省磁盘空间。

    Visutal Studio.NET提供了服务应用程序模板,使用这个模板生成服务应用程序,在Main()方法中,已经添加了类似如下的代码:

    System.ServiceProcess.ServiceBase[] ServicesToRun;
    ServicesToRun = new System.ServiceProcess.ServiceBase[] { new TempdirMonitorService() }; System.ServiceProcess.ServiceBase.Run(ServicesToRun);

    ServiceBase.Run()方法用于把服务载入到内存,以便可以启动服务。

    并且已经重写了OnStart和OnStop这两个方法,要编写最简单的服务应用程序,只要重写这两个方法就可以了。另外,ServiceBase类还有OnPause、OnContinue、OnShutDown、OnPowerEvent、OnCustomCommand这几个方法可供重写,在使用这些方法前,需要将类的CanPauseAndContinue、CanHandlePowerEvent、CanShutdown这几个属性设为True(默认为False),否则即使重写了方法也不会被执行。另外还有CanStop属性,如果将它设为false的话,这个服务甚至不能被停止。OnCustomCommand用于处理自定义命令,它的原型是:

    void OnCustomCommand(int command);

    这就意味着除了指令本身不能传递任何参数,要传递其他信息就必须通过其他方式,如磁盘文件或注册表。

    可以在应用程序中定义或在 OnCustomCommand 中使用的自定义命令的值只有 128 和 256 之间的值。128 以下的整数对应于系统保留的值。

    如果Autolog属性为true,自定义命令就会像其他所有命令一样将项写入事件日志以报告方法执行是否成功。

    1)使用EventLog组件输出信息

    因为服务应用程序无法与用户交互,我又添加了一个EventLog组件,可以在系统日志中写入一些调试信息等。EventLog组件有以下几个属性需要设置:

    • Log:用于设置日志的类别,Application、Security和System分别对应应用程序、安全性和系统三个默认类别,另外还可以创建自定义的类别。
    • MachineName:用于设置日志所在的计算机名,设置为小数点(.)可以表示当前计算机。
    • Source:用于设置日志源名称。
    • AutoLog:这个属性默认为True,这样程序会自动的在Application类别中添加服务启动或关闭等信息记录。

    我的EventLog组件名称为eventLog,在程序中,就可以用eventLog.WriteEntry()方法在日志中写入信息了。

    2)使用动态属性配置应用程序

    再拖动一个timer组件到程序集中,用于定时。在这里,注意到timer组件的定时时长应当是可以设置的,而服务应用程序无法与用户交互,那么如何使这个属性成为可配置的呢?动态属性很方便的解决了这个问题,只要在timer组件的属性窗口中,点击(DynamicPoperties)左边的加号,就可以看到timer控件默认可配置的属性Interval,点击将其映射到配置文件中就可以了。

    动态属性

    然后,Visual Studio.NET将会生成app.config文件,这是一个XML类型的文件,在编译后他会重新被命名为程序集的名字,里面有类似如下的内容:

    <add key="timer.Interval" value="600000" />

    同时在设计器生成的InitializeComponents()方法中,初始化timer.Interval属性的语句也变成了:

    this.timer.Interval = ((System.Double)(configurationAppSettings.GetValue("timer.Interval", typeof(System.Double))));

    这样,每次启动程序的时候,都会从配置文件中载入这个属性的值。

    这里的configurationAppSettings是一个System.Configuration.AppSettingsReader类型的对象,也可以使用System.Configuration.ConfigurationSettings类中的静态成员来获取配置文件中的值。

    注意:配置文件是作为应用程序的一部分存在的,它只会在应用程序启动时被读取一次,也就是说,如果应用程序运行中,配置文件被修改,再使用System.Configuration命名空间中的类来读取值,也不能够读取到新的值。

    3)编写核心部分

    核心部分的内容很少,只要在OnStart方法中启动timer,并在timer的Elapsed事件处理程序中检查临时文件夹的大小并删除没用的文件就可以了。

    在编写中只遇到了一个问题,由于服务应用程序没有运行在当前登录的用户帐户下,所以无法用System.IO.Path.GetTempPath()获取当前登录用户的临时文件夹。我的解决方案仍然是使用app.config文件,只要在文件中添加一个新项TempDir,然后在程序载入这个值就可以了。配置文件可以由在用户帐户下运行的服务管理程序来修改。

    我也重载了OnCustomCommand方法,在方法中设置了立即清理临时文件夹的命令。

    4)添加安装程序

    服务必须先安装才能运行,前面已经介绍了添加安装程序的方法,由设计环境自动添加的安装程序就可以正确运行。需要修改的地方是:

    • ServiceProcessInstaller.Account属性:默认是User,即以用户帐户身份运行,这个选项将在安装时要求提供用户名和密码,一般使用的选项是LocalSystem,这是一个本地非特权帐户,并在远程中使用匿名凭据。
    • ServiceInstaller.StartType属性:默认是手动启动,可以根据需要修改为自动启动。

    在为服务以及服务使用的EventLog组件和PerformanceCounter组件都添加好安装程序后,就可以编译并安装应用程序了。

    5)安装服务

    要安装服务,可以使用.NET安装实用程序Installutil,安装命令是:

    Installutil <.exe file>

    同时还可以卸载应用程序,命令是:

    Installutil /u <.exe file>

    下面是我安装服务时的输出:

    安装

    安装后测试运行成功,下面两幅图中可以看到服务已经被添加进了SCM中,并在性能日志中建立了自己的日志类别并记录了信息。

    服务 日志

    四、调试Windows服务

    要调试Windows服务,需要启动服务,然后将调试器附加到服务所处的进程中,然后就可以像调试普通程序一样调试服务了。但是这样不能调试OnStart和Main方法,因为服务是在调试器被附加到服务进程以前启动的,要调试这两个方法,就需要创建测试套服务来帮助调试。

    五、使用ServiceController管理Windows服务

    可以使用ServiceController类来管理和控制服务,它可以:

    • 查看计算机上服务的列表
    • 启动、停止、暂停和恢复服务
    • 查询和检索服务的属性
    • 在服务上运行自定义命令

    要查看计算机上服务的列表,可以使用GetServices方法;要运行自定义命令,可以使用ExecuteCommand方法。

    注意:应当始终创建单独的应用程序,然后该程序包含ServiceController组件来控制应用程序。

    下面是我编写的服务控制程序:

    控制器

    在这个程序中,我使用System.XML命名空间下的方法来读取和修改服务应用程序的配置文件,下面是读取设置部分的代码:

    XmlDocument configFile = new XmlDocument();
    XmlTextReader reader = new XmlTextReader("TempdirMon.exe.config");
    reader.WhitespaceHandling = WhitespaceHandling.None;
    configFile.Load(reader);
    XmlElement settings = configFile["configuration"]["appSettings"];
    foreach(XmlNode n in settings.ChildNodes)
    {
        if(!n.LocalName.StartsWith("#"))
        {
            switch(n.Attributes["key"].Value)
            {
                case "MaxSize":
                    this.nudMaxSize.Value = decimal.Parse(n.Attributes["value"].Value);
                    break;
                case "TempDir":
                    string dir = n.Attributes["value"].Value;
                    if(!Directory.Exists(dir)) Directory.CreateDirectory(dir);
                    txtFolder.Text = dir;
                    break;
                case "timer.Interval":
                    this.nudInterval.Value = long.Parse(n.Attributes["value"].Value)/60000;
                    break;
            }
        }
    }
    reader.Close();

    类XmlDocument是W3C文档对象模型(DOM)的.NET实现,它将XML文档映射到一个树形结构上,它扩展自XmlNode这个递归定义的树节点,使用它的索引器可以方便的访问XML文档的各个元素。

    我使用ServiceController的Start和Stop方法来启动和停止服务,下面是启动服务的代码:

    serviceController.Start();
    serviceController.WaitForStatus(ServiceControllerStatus.Running);
    this.txtStatus.Text = serviceController.Status.ToString();

    使用WaitForStatus方法可以阻塞当前线程等待服务达到某一个状态,还可以使用它的另一个重载方法来设置一个timeout。

    “立即清理”按钮用于发送一个自定义命令至服务,只要使用ExecuteCommand方法并加上一个代表命令的int型参数即可,当然在发送指令前不要忘了检查服务的状态,向不在运行中的服务发出指令会引起InvalidOperationException:

    if (serviceController.Status == ServiceControllerStatus.Running)
    {
        serviceController.ExecuteCommand(128);
    }
    else
    {
        MessageBox.Show("服务没有启动。", "错误", MessageBoxButtons.OK, MessageBoxIcon.Warning);
    }

    评论

    请稍候...
    很抱歉,您输入的评论太长。请缩短您的评论。
    您没有输入任何内容,请重试。
    很抱歉,我们当前无法添加您的评论。请稍后重试。
    若要添加评论,需要您的家长授予您相应权限。请求权限
    您的家长禁用了评论功能。
    很抱歉,我们当前无法删除您的评论。请稍后重试。
    您已超过了一天之内允许提供的评论数上限。请在 24 小时后重试。
    因为我们的系统表明您可能在向其他用户提供垃圾评论,您的帐户已禁用了评论功能。如果您认为我们错误地禁用了您的帐户,请联系 Windows Live 支持部门
    完成下面的安全检查,您提供评论的过程才能完成。
    您在安全检查中键入的字符必须与图片或音频中的字符一致。

    若要添加评论,请使用您的 Windows Live ID 登录(如果您使用过 Hotmail、Messenger 或 Xbox LIVE,您就拥有 Windows Live ID)。登录


    还没有 Windows Live ID 吗?请注册

    引用通告

    此日志的引用通告 URL 是:
    http://flikas.spaces.live.com/blog/cns!FD1E577523FE041C!172.trak
    引用此项的网络日志