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);
}

1月29日

XML Web Service学习笔记 1 .NET框架基础

一、了解.NET

通过XML Web服务,.NET解决了软件业的一个最大难题:在以不同语言、为不同环境编写的应用程序之间交换数据。.NET能够创建多种平台下的应用程序,并通过标准协议(HTTP,XML,SOAP等)允许它们交换数据。

.NET提供的工具及操作系统包括:

  • 智能客户端软件:允许客户端、PC或移动设备利用XML Web服务从任何设备和位置访问数据。
  • .NET Server基础结构:Windows 2000 Server、Windows .NET Server 以及 .NET Enterprise Server
  • XML Web服务:.NET环境下应用程序集成的核心。
  • Visual Studio .NET和.NET框架:创建工具。

二、自动内存管理

自动内存管理运作方式:初始化一个进程时,运行库会预留一个连续的地址空间,但不为其分配任何存储空间。这一段预留的地址称为托管堆。当使用new运算符初始化对象时,才使用托管堆中的地址分配存储空间。分配和垃圾回收机制保证了分配的地址空间总是连续的,这样的话,在托管堆中为对象分配内存所需的时间比分配非托管内存的时间短,非托管内存指针是以链表方式存储的,每次分配都需要遍历链表找到较大的内存块以容纳对象。

释放内存:每个应用程序都有一组根,指向托管堆上的对象,这些跟包括了所有应用程序当前使用的对象,JIT编译器和运行库维护应用程序根的列表。垃圾回收器使用这一根的列表来创建托管堆上对象的图表,所有从根开始直接或间接被引用到的对象标记为可访问的,而其他对象被视为垃圾应当回收。这些垃圾对象所占用的内存被回收以后,托管堆上的地址将不再连续,这时垃圾回收器执行内存复制功能来压缩托管堆中的对象(只是改变对象的地址,所以并不太耗时间),并更新根的列表使应用程序的根正确的指向先前所指的对象。

为了更优化垃圾回收的效率,垃圾回收器将托管堆上的对象分为0、1、2三代,0代包含最近创建的对象,每次垃圾回收,优先回收第0代对象,并将幸存的对象提升一代,只有当第0代对象回收所释放的空间不足时,才回收更高代的对象。

垃圾回收器回收对象之前,都会调用对象的Finalize方法以期释放对象使用的非托管资源,如文件指针或网络连接等。另一种确保非托管资源被正确回收的方法是在Dispose方法中提供清除代码,并在完成对象后显式的调用它。

实现终结程序:Dispose和Finalize方法叫做终结程序。Finalize方法会在垃圾回收时被自动调用,Dispose方法不会。如果实现了Dispose方法,那么可以在Finalize方法中调用Dispose方法,这时,Dispose方法应当只释放非托管资源,这是由于在终结过程中,垃圾回收器可能已经删除了Dispose方法欲处理的那个对象,这将导致Dispose方法失败。而如果Dispose方法是被显式调用的,那么Dispose方法应当释放所有托管及非托管资源。最后,Dispose方法应当调用GC.SuppressFinalize方法,阻止对Finalize方法的调用,并阻止重复调用Dispose方法。

.NET框架提供了IDisposable接口来为那些实现了Dispose方法的类提供统一的类型。

下面是一个Dispose和Finalize方法的例子:

public class Parent:IDisposable
{
    private IntPtr ptr;                              //非托管资源
    private System.Timers.Timer timer;       //托管资源
    private bool disposed;                        //这个变量用于防止Dispose方法被多次调用

    public void Dispose()                          //这个方法被用户显式调用
    {
         Dispose(true);
         GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool called_by_user)     //这个保护(protected)方法会被Finalize方法调用
    {
        if(!this.disposed)
        {
             if(called_by_user)
             {
                  //清除托管资源
                  timer.Dispose();
             }
             //清除非托管资源
             ptr=IntPtr.Zero;
             disposed=true;
        }
    }

    ~Parent()                    //Finalize方法
    {
         Dispose(false);
    }
}

public class Child:Parent
{
     .
     .
     .
     protected override void Dispose(bool called_by_user)
     {
           //Child对象的清除代码:
           .
           .
           .
           //调用父对象的清除代码:
           base.Dispose(called_by_user);
     }
}

三、程序集

通常使用Visual Studio.NET创建的可移植的可执行文件(.exe或.dll)都是静态程序集。另外,借助System.Reflection.Emit命名空间下的反射API,可以创建动态程序集。反射API允许程序在运行时通过发送元数据和MSIL代码来实时创建程序集,并根据情况在磁盘上将其保存为可移植的可执行文件。

程序集既可以是单文件的,也可以是多文件的。单文件程序集包含程序集清单,类型元数据,MSIL代码和资源。多文件程序集将单文件程序集的内容拆成几个部分,并由单独的程序集清单将它们连接在一起,这样可以将很少使用的类型存储在单独的文件中,当需要时再下载。程序集链接器工具——AL.exe,用于通过组合一个或多个模块和资源文件来创建单独的清单文件。

程序集还可以分为私有程序集和共享程序集。私有程序集只可为该应用程序访问。共享程序集具有强名称,安装在GAC中,可由多个程序共享。强名称包含程序集名称、版本、区域信息、数字签名以及公钥信息。

要使应用程序具有强名称,就必须为它提供一个密钥对,并在程序集的AssemblyKeyFile属性中指定密钥对文件名称。如下:

using System.Reflection;
[assembly:AssemblyKeyFile("MyKey.dat")]

可以使用强名称工具SN.exe创建密钥对,另外,sn.exe还可以实现验证,创建密钥对和为程序集签名。

为程序集分配强名称以后,就可以使用GAC工具——GACUtil.exe将程序集安装到GAC中了。

四、一些实验

1.使用Visual Basic.NET 创建一个模块:

namespace Util
     public class HelloUtil
          public shared sub Hello()
               System.Console.WriteLine("Hello, how are you doing!")
          end sub
     end class
end namespace

将以上代码写入文本文件,命名为HelloUtil.vb,然后编译该模块:

vbs /t:module HelloUtil.vb

编译成功,将生成HelloUtil.netmodule文件。

2.使用JScript创建一个模块:

import System;
package Util
{
     public class GoodByeUtil
     {
           public static function GoodBye()
           {
                  Console.WriteLine("Goodbye, see you next time!");
           }
     }
}

将以上的代码写入文本文件,命名为GoodbyeUtil.js,编译:

jsc /t:library GoodbyeUtil.js

注意:编译成功后,将生成GoodbyeUtil.dll文件,不能将JScript代码编译至模块中。

3.使用C#创建Main模块:

using System;
using Util;           //前两个模块使用的命名空间,这两个模块使用的是同一个命名空间

class MyClass
{
     [STAThread]
     public static void Main(string[] args)
     {
           Console.WriteLine("Hello from Main.");
           HelloUtil.Hello();
           GoodByeUtil.GoodBye();
     }
}

将以上代码保存为文本文件,命名为Main.cs,使用csc.exe编译以上代码。由于引用了其他模块,这次的命令比较复杂:

csc /addmodule:HelloUtil.netmodule /r:GoodByeUtil.dll, Microsoft.Jscript.dll /target:module Main.cs

编译生成Main.netmodule文件,如果把上面的/target:module改成/target:exe,就可以编译生成可执行文件Main.exe,但是这个可执行文件需要HelloUtil.netmodule以及GoodByeUtil.dll这两个文件才能执行,如果想要生成单一文件,就需要使用程序集链接器——Al.exe:

al /t:exe /out:MyApp.exe /main:MyClass.Main HelloUtil.netmodule GoodByeUtil.dll Main.netmodule

生成的MyApp.exe就是将以上文件组合而成的单独的程序集。

链接时会出现警告AL1020,原因是从GoodByeUtil.dll创建了程序集,而GoodbyeUtil.dll本身也是一个程序集,程序集链接器将忽略它。