转载http://blog.csdn.net/yanghua_kobe/article/details/6937816

我们怎样才能在服务器上使用asp.net定时执行任务而不需要安装windows service?我们经常需要运行一些维护性的任务或者像发送提醒邮件给用户这样的定时任务。这些仅仅通过使用Windows Service就可以完成。Asp.net通常是一个无状态的提供程序,不支持持续运行代码或者定时执行某段代码。所以,我们不得不构建自己的windows service来运行那些定时任务。但是在一个共享的托管环境下,我们并不总是有机会部署我们自己的windwos service到我们托管服务提供商的web服务器上。我们要么买一个专用的服务器,当然这是非常昂贵的,要么就牺牲我们网站的一些功能。然而,运行一个定期执行的任务是一个非常有用的功能,特别是对那些需要发送提醒邮件的用户、需要维护报表以及运行清理操作的的管理员而言。我将给你展示一种无须使用任何windows service,仅仅采用asp.net来运行定期任务的方式。

它怎样工作

首先,我们需要asp.net中的某些“场景”,能够持续不断地运行并且给我们一个回调。而IIS上的web服务器就是一个很不错的选择。所以,我们需要从它那里很“频繁”地获得回调,这样我们可以查看一个任务队列,并且能够看到是否有任务需要执行。现在,这里有一些方式可以为我们获得对web服务器的“操作权”:

(1)     当一个页面被请求

(2)     当一个应用程序被启动

(3)     当一个应用程序被停止

(4)     当一个会话开启、结束或者超时

(5)     当一个缓存项失效

一个页面被请求是随机的。如果几个小时内没有人访问你的站点,那么几个小时内你都无法完成任何“任务”。另外,一个请求的执行时间是非常短的,并且它本身也需要越快越好。如果你计划在页面请求的时候执行“计划任务”,这样页面将会被迫执行很长时间,这将导致一个很糟糕的用户体验。所以,选择在页面请求的时机做这样的操作不是一个好的选择。

一个页面被请求是随机的。如果几个小时内没有人访问你的站点,那么几个小时内你都无法完成任何“任务”。另外,一个请求的执行时间是非常短的,并且它本身也需要越快越好。如果你计划在页面请求的时候执行“计划任务”,这样页面将会被迫执行很长时间,这将导致一个很糟糕的用户体验。所以,选择在页面请求的时机做这样的操作不是一个好的选择。

当一个应该程序启动时,Global.asax内的Application_Start方法给我们提供了一个回调。所以这是一个开启后台线程的好地方,后台线程可以永久运行以执行“计划任务”。然而,当该线程在web服务器由于零负载而“休息”一会儿的时候,却可能被随时“杀死”。

当一个应用程序停止的时候,我们同样可以从Application_End方法获得一个回调。但是我们在这里却不能做任何事情,因为整个应该程序都已经快要结束运行了。Global.asax里的Session_Start会在当一个用户访问一个需要被实例化为新会话的页面时被触发。所以这也是一个随机事件。而我们需要一个能持久且定期运行的“场景”。

一个缓存项的失效可以提供一个时间点或持续时间。在asp.net中你可以在Cache对象中增加一个实体,并且可以设置一个绝对失效时间,或者设置当其被从缓存中移除后失效。你可以利用下面的Cache类中的方法来做这些:

  1. public void Insert ( System.String key , System.Object value ,
  2. System.Web.Caching.CacheDependency dependencies ,
  3. System.DateTime absoluteExpiration ,
  4. System.TimeSpan slidingExpiration ,
  5. System.Web.Caching.CacheItemPriority priority ,
  6. System.Web.Caching.CacheItemRemovedCallback onRemoveCallback )

onRemoveCallback是一个方法的委托,该方法在一个缓存项失效时被调用。在该方法中,我们可以做任何我们想做的事情。所以,这是一个定期、持续运行代码而不需要任何页面请求的很好的候选。

这意味着,我们可以在一个缓存项失效时模拟一个简单的windows service。

创建缓存项的回调

首先,在Application_Start中,我们需要注册一个缓存项,并让它在两分钟后失效。请注意,你设置回调的失效时间的最小值是两分钟。尽管你可以设置一个更小的值,但它似乎不会工作。出现该问题最大的可能是,asp.net工作进程每两分钟才查看一次缓存项。

  1. private const string DummyCacheItemKey = "GagaGuguGigi";
  2. protected void Application_Start(Object sender, EventArgs e)
  3. {
  4. RegisterCacheEntry();
  5. }
  6. private bool RegisterCacheEntry()
  7. {
  8. if( null != HttpContext.Current.Cache[ DummyCacheItemKey ] ) return false;
  9. HttpContext.Current.Cache.Add( DummyCacheItemKey, "Test", null,
  10. DateTime.MaxValue, TimeSpan.FromMinutes(1),
  11. CacheItemPriority.Normal,
  12. new CacheItemRemovedCallback( CacheItemRemovedCallback ) );
  13. return true;
  14. }

该缓存实体是一个虚设的实体。我们不需要在这里存储任何有价值的信息,因为无论我们在这里存储什么,他们都有可能在应用程序重启时丢失。另外,我们所需要的只是使该项的频繁回调。

在回调的内部,我们就可以完成“计划任务”:

  1. public void CacheItemRemovedCallback( string key,
  2. object value, CacheItemRemovedReason reason)
  3. {
  4. Debug.WriteLine("Cache item callback: " + DateTime.Now.ToString() );
  5. DoWork();
  6. }

在缓存项失效时再次存储缓存项

无论何时缓存项失效,我们都能够获得一个回调同时该项将永久地从缓存中消失。所以,我们将不能再次获得回调了。为了能提供一个持续的回调,我们需要在下次失效之前重新存储一个缓存项。这看起来似乎相当容易:我们可以在回调函数中调用我们上面展示的RegisterCacheEntry方法,可以这么做吗?它不会工作!当回调发生,HttpContext已经无法访问。HttpContext仅仅在一个请求正在被处理的时候才可以被访问。因为回调发生在web服务器的幕后,所以这里没有请求需要被处理,因而HttpContext对象无法获得。因此,你也无法从回调中访问Cache对象。

方案是,我们需要一个简单的请求。我们可以利用.netFramework中的WebClient类来实现一个对虚拟页面的“虚拟”访问。当虚拟页面被执行,我们可以Hold住HttpContext对象,然后再次注册一个缓存项的回调。

所以,回调方法作一点修改来发出一个虚拟调用。

  1. public void CacheItemRemovedCallback( string key,
  2. object value, CacheItemRemovedReason reason)
  3. {
  4. Debug.WriteLine("Cache item callback: " + DateTime.Now.ToString() );
  5. HitPage();
  6. // Do the service works
  7. DoWork();
  8. }

HitPage方法对一个虚拟页面发出调用:

  1. private const string DummyPageUrl =
  2. "http://localhost/TestCacheTimeout/WebForm1.aspx";
  3. private void HitPage()
  4. {
  5. WebClient client = new WebClient();
  6. client.DownloadData(DummyPageUrl);
  7. }

无论虚拟页面在什么时候被调用,Application_BeginRequest方法都将被调用。在那里,我们可以核查是否它是一个“虚拟”页面。

  1. protected void Application_BeginRequest(Object sender, EventArgs e)
  2. {
  3. // If the dummy page is hit, then it means we want to add another item
  4. // in cache
  5. if( HttpContext.Current.Request.Url.ToString() == DummyPageUrl )
  6. {
  7. // Add the item in cache and when succesful, do the work.
  8. RegisterCacheEntry();
  9. }
  10. }

我们仅仅截获虚拟页面的请求,并且让其他的页面以他们原来的方式继续执行。

Web进程重启时重启缓存项回调

这里有很多情况,可能导致web服务器重启。例如,如果系统管理员重启IIS,或者重启电脑,或者web进程陷入死循环(在windows 2003下)。在这样的情况下,服务将停止运行,直到一个页面被请求和Application_Start被调用。Application_Start仅仅在当一个页面第一次被访问时才会被调用。所以,当web进程被重启时为了让“服务”运行起来,我们只能手动调用“虚拟”页面,或者某人需要访问你站点的主页。

一个“滑头”的方案是:可以把搜索引擎加入你的站点中。搜索引擎时常会爬行页面。因此,它们将访问你站点的一个网页,这就可以触发Application_Start的执行,因此服务将被再次启动运行。

另一个方案是向某些通信或可用性监控服务注册你的站点。有许多关注你站点以及可以检查你的站点是否正常并且性能是否良好的Web 服务。所有这些服务都需要访问你站点的页面然后收集统计信息。所以,通过注册这样的服务,你可以保证你的站点一直“存活”着。

测试可执行任务的类型

让我们来测试一下,是否我们能够做一个windowsservice能够做的一切任务。首先,第一个问题是,我们不能做一个windows service能够做的所有事情,因为windowsservice运行在一个本地系统账户的权限下。这是一个具有非常高权限的账户,使用这个账户你可以在你的系统中做任何事情。然而,asp.net web线程运行在ASPNET账户下(windows xp)或者NETWORKSERVICE账户下(windows 2003)。这是一个低权限的账户,并且没有权限访问硬盘。为了允许服务向硬盘写东西,web进程需要被授予对文件夹的写权限。我们都知道关于此的安全问题,所以我将不再详述细节。

现在,我们将开始测试我们通常利用windowsservice完成的事情:

(1)    向文件写东西

(2)    数据库操作

(3)    Web Service调用

(4)    MSMQ 操作

(5)    Email 发送

让我们来写一些测试代码:

  1. private void DoWork()
  2. {
  3. Debug.WriteLine("Begin DoWork...");
  4. Debug.WriteLine("Running as: " +
  5. WindowsIdentity.GetCurrent().Name );
  6. DoSomeFileWritingStuff();
  7. DoSomeDatabaseOperation();
  8. DoSomeWebserviceCall();
  9. DoSomeMSMQStuff();
  10. DoSomeEmailSendStuff();
  11. Debug.WriteLine("End DoWork...");
  12. }

测试文件“写”操作

让我们来测试一下是否我们真的能够向文件内写东西。在C盘创建一个文件夹,将其命名为“temp”(如果磁盘的格式是NTFS,允许ASPNET/NETWORKSERVICE账户向该文件夹的写权限)。

  1. private void DoSomeFileWritingStuff()
  2. {
  3. Debug.WriteLine("Writing to file...");
  4. try
  5. {
  6. using( StreamWriter writer =
  7. new StreamWriter(@"c:\temp\Cachecallback.txt", true) )
  8. {
  9. writer.WriteLine("Cache Callback: {0}", DateTime.Now);
  10. writer.Close();
  11. }
  12. }
  13. catch( Exception x )
  14. {
  15. Debug.WriteLine( x );
  16. }
  17. Debug.WriteLine("File write successful");
  18. }

打开该文件,然后你应该看到这样的信息:

  1. Cache Callback: 10/17/2005 2:50:00 PM
  2. Cache Callback: 10/17/2005 2:52:00 PM
  3. Cache Callback: 10/17/2005 2:54:00 PM
  4. Cache Callback: 10/17/2005 2:56:00 PM
  5. Cache Callback: 10/17/2005 2:58:00 PM
  6. Cache Callback: 10/17/2005 3:00:00 PM

测试数据库的可连接性

在你的“tempdb”数据库中运行下面的代码(也可以自己建数据库测试)

  1. IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id =
  2. object_id(N'[dbo].[ASPNETServiceLog]') AND
  3. OBJECTPROPERTY(id, N'IsUserTable') = 1)
  4. DROP TABLE [dbo].[ASPNETServiceLog]
  5. GO
  6. CREATE TABLE [dbo].[ASPNETServiceLog] (
  7. [Mesage] [varchar] (1000)
  8. COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
  9. [DateTime] [datetime] NOT NULL
  10. ) ON [PRIMARY]
  11. GO

上面的代码将创建一个名为ASPNETServiceLog的表。记住,因为该表创建于tempdb中,所以该表在SQL Server重启的时候将消失。

接下来,为ASPNET/NETWORKSERVICE账户授予tempdb数据库的db_datawriter权限。另外,你可以定义更多特殊的权限,并且只允许往表中写权限。

现在,写下测试方法:

  1. private void DoSomeDatabaseOperation()
  2. {
  3. Debug.WriteLine("Connecting to database...");
  4. using( SqlConnection con = new SqlConnection("Data Source" +
  5. "=(local);Initial Catalog=tempdb;Integrated Security=SSPI;") )
  6. {
  7. con.Open();
  8. using( SqlCommand cmd = new SqlCommand( "INSERT" +
  9. " INTO ASPNETServiceLog VALUES" +
  10. " (@Message, @DateTime)", con ) )
  11. {
  12. cmd.Parameters.Add("@Message", SqlDbType.VarChar, 1024).Value =
  13. "Hi I'm the ASP NET Service";
  14. cmd.Parameters.Add("@DateTime", SqlDbType.DateTime).Value =
  15. DateTime.Now;
  16. cmd.ExecuteNonQuery();
  17. }
  18. con.Close();
  19. }
  20. Debug.WriteLine("Database connection successful");
  21. }

这将在log表中产生一些记录,你可以测试来确保“服务”的执行是否有延迟。你应该会再每两分钟获得一行数据。

测试邮件的分发

对运行一个windows service最基本的需求是定期发送邮件提醒,状态报告等等。所以,测试是否可以像windows service一样发送email很重要:

  1. private void DoSomeEmailSendStuff()
  2. {
  3. try
  4. {
  5. MailMessage msg = new MailMessage();
  6. msg.From = "abc@cde.fgh";
  7. msg.To = "ijk@lmn.opq";
  8. msg.Subject = "Reminder: " + DateTime.Now.ToString();
  9. msg.Body = "This is a server generated message";
  10. SmtpMail.Send( msg );
  11. }
  12. catch( Exception x )
  13. {
  14. Debug.WriteLine( x );
  15. }
  16. }

请将From和To 修改为某些有效的地址,并且你应该每两分钟就可以收到一次邮件提醒。

测试MSMQ

让我们写一个简单的方法来测试是否我们可以从asp.net直接访问MSMQ:

  1. private void DoSomeMSMQStuff()
  2. {
  3. using( MessageQueue queue = new MessageQueue(MSMQ_NAME) )
  4. {
  5. queue.Send(DateTime.Now);
  6. queue.Close();
  7. }
  8. }

另外,你可以调用队列的Receive方法来解析队列中需要被处理的消息。

这里,有一个你必须记住的问题是,不要订阅队列的Receive事件。因为线程可能随时会被杀死,并且web服务器可能随时会被重启,一个持续阻塞的Receive将不能正常地工作。另外,如果你调用BeginReceive方法同时阻塞代码的执行直到一个消息到达,服务将被卡住然后其他的代码将不会再运行。所以,在这种情况下,你将不得不调用Receive方法来解析消息。

扩展系统功能

Asp.net服务可以被用来扩展那些可插拔的任务。你可以从web页面中引入作业排队,让这种服务定期执行。例如,你可以将作业队列放入一个缓存项,让“服务”来选择任务然后执行它。采用这种方式,你可以在你的asp.net项目中实现一个简单的任务处理系统。

让我们实现一个简单的Job类,它包含了一个任务执行的信息。

  1. public class Job
  2. {
  3. public string Title;
  4. public DateTime ExecutionTime;
  5. public Job( string title, DateTime executionTime )
  6. {
  7. this.Title = title;
  8. this.ExecutionTime = executionTime;
  9. }
  10. public void Execute()
  11. {
  12. Debug.WriteLine("Executing job at: " + DateTime.Now );
  13. Debug.WriteLine(this.Title);
  14. Debug.WriteLine(this.ExecutionTime);
  15. }
  16. }

在一个简单的aspx页面上,我们将一个任务排入一个定义在Global.Asax中的名为_JobQueue的ArrayList中。

  1. Job newJob = new Job( "A job queued at: " + DateTime.Now,
  2. DateTime.Now.AddMinutes(4) );
  3. lock( Global._JobQueue )
  4. {
  5. Global._JobQueue.Add( newJob );
  6. }

所以,被排入队列中的任务将在4分钟之后被执行。该服务的代码每两分钟执行一次,它会检查任务队列,是否有任何逾期且需要被执行的任务。如果有任何的任务在等待,它将被从队列中移除并执行。服务代码有一个额外的方法,叫做ExecuteQueuedJobs。该方法做定期任务的执行:

  1. private void ExecuteQueuedJobs()
  2. {
  3. ArrayList jobs = new ArrayList();
  4. // Collect which jobs are overdue
  5. foreach( Job job in _JobQueue )
  6. {
  7. if( job.ExecutionTime <= DateTime.Now )
  8. jobs.Add( job );
  9. }
  10. // Execute the jobs that are overdue
  11. foreach( Job job in jobs )
  12. {
  13. lock( _JobQueue )
  14. {
  15. _JobQueue.Remove( job );
  16. }
  17. job.Execute();
  18. }
  19. }

不要忘记锁住静态的“任务集合”,因为asp.net是多线程的。并且页面会在不同的线程上执行,所以同时往任务队列中写是很有可能的。

使用ASP.NET实现Windows Service定时执行任务的更多相关文章

  1. windows每天定时执行脚本

     windows每天定时执行脚本 这里说的定时器就是Windows下的任务计划,当时遇到的坑正好总结一下,因为Windows10的定时器去执行脚本当时试了好多遍,都是没有成功,后来通过自己的观察发现是 ...

  2. C#中级-通过注册表读取Windows Service程序执行路径

    一.前言        假设我们的C#解决方案中有多个程序应用,如:Web应用.控制台程序.WPF程序应用和Windows服务应用. 那么这些非Windows Service应用程序怎么在代码中找到W ...

  3. asp.net web 服务器端全局定时执行任务

    web网站里面,需要每隔1分钟,执行一个任务,并且一直保持这个定时执行状态,可以用如下一个方法:    1,Global.asax里面的 Application_Start ,发生在第一次请求网站的时 ...

  4. C# Windows Service中执行死循环轮询

    用C#编写Windows Service时,执行轮询一般有两种方式,一种是用Timer,System.Timers或者是System.Thread下的,这种执行是按时间循环执行,缺点是也许上个执行还没 ...

  5. windows上定时执行php文件

    <?php $fp = fopen("E:/wwwroot/test/plan.txt", "w+"); fwrite($fp, date("Y ...

  6. windows关于定时执行的php脚本

    根据业务需求,需要服务器每天定时执行一些脚本,如后台提交数据,定时处理数据库等. 最初的思路是在某个控制器里写好方法,加入code验证,定期的用计划任务去访问.由于window计划任务这方面比较low ...

  7. 转:windows下定时执行备份数据库

    上一篇写了linux下定时任务,这一篇转发一个windows下定时备份数据库. 第一种:新建批处理文件 backup.dat,里面输入以下 net stop mysql xcopy "C:\ ...

  8. 怎样在windows上定时执行python脚本

    作为一个需要在电脑上工作和学习的人,一件十分困扰我的事情就是怎样不受互联网中其他内容的干扰而专注于自己想要做的事情,有的时候真的是沉浸于微博上的消息,忘了自己本来想要做的事.不过我有一件神器,自己爱豆 ...

  9. 在Windows里定时执行一个Python文件

    一.系统环境 操作系统:Win7 64位 二.说明 1.建立一个dos批处理文件 例: @echo off C: cd C:\work\python python aaa.py exit 2.利用Wi ...

随机推荐

  1. NEsper事件处理 z

    http://esper.codehaus.org/nesper/documentation/documentation.html 环境配置 NEsper库下载:下载网址 Vs2010环境集成 在项目 ...

  2. 【原】Spark Standalone模式

    Spark Standalone模式 安装Spark Standalone集群 手动启动集群 集群创建脚本 提交应用到集群 创建Spark应用 资源调度及分配 监控与日志 与Hadoop共存 配置网络 ...

  3. vs2010常见错误

    安装vs2013以后,链接数据库总是报内存损坏,无法写入的错误 安装vs2013以后,链接数据库总是报内存损坏,无法写入的错误 用VS2012调试时发现在调用数据集时提示“尝试读取或写入受保护的内存. ...

  4. linux进程的几种状态

    Linux是一个多用户,多任务的系统,可以同时运行多个用户的多个程序,就必然会产生很多的进程,而每个进程会有不同的状态. Linux进程状态:R (TASK_RUNNING),可执行状态. 只有在该状 ...

  5. hadoop-1.2.0安装记录

    一.添加用户(各机器均一致)     添加组: sudo addgroup hadoop     添加用户并到组:sudo adduser -ingroup hadoop hadoop 二.ssh无验 ...

  6. POJ-3468 A Simple Problem with Integers Splay Tree区间练习

    题目链接:http://poj.org/problem?id=3468 以前用线段树做过,现在用Splay Tree A了,向HH.kuangbin.cxlove大牛学习了各种Splay各种操作,,, ...

  7. 局域网内Linux服务器时间同步

    局域网内Linux服务器时间同步   1.将一台能够上网的服务器作为时间服务器:  # /usr/bin/rdate -s time-b.timefreq.bldrdoc.gov //将时间服务器与互 ...

  8. Laravle Introduction

    Where To Start Learning a new framework can be daunting, but it's also exciting. To smooth your tran ...

  9. iPad开发(相对于iPhone开发时专有的API)

    iPad开发 一.iPad开发简介 1.什么是iPad 一款苹果公司于2010年发布的平板电脑 定价介于苹果的智能手机iPhone和笔记本电脑产品之间 跟iPhone一样,搭载的是iOS操作系统 2. ...

  10. 【JAVA - SSM】之MyBatis查询缓存

    为了减轻数据压力,提高数据库的性能,我们往往会需要使用缓存.MyBatis为我们提供了一级缓存和二级缓存. (1)一级缓存是SqlSession级别的缓存,在操作数据库的时候需要创建一个SqlSess ...