使用ASP.NET实现定时计划任务,不依靠windows服务

我们怎样才能在服务器上使用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 = "[email protected]";
  7. msg.To = "[email protected]";
  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是多线程的。并且页面会在不同的线程上执行,所以同时往任务队列中写是很有可能的。

时间: 12-24

使用ASP.NET实现定时计划任务,不依靠windows服务的相关文章

Oracle定时计划快速使用

Oracle定时计划快速使用 前言: SQL Server中有相关的定时计划,可以直接打开sql server 的任务管理器进行配置,可以方便.快速实现定时执行相应任务.相应的Oracle也有对应的定时计划,只不过没有一个很好的图形界面供用户去操作.本文主要是为了方便用户在Oracle中快速创建定时计划,定期执行相应的sql或者存储过程. 1.   创建定时计划 说明:以下所有的示例代码,都需要用户先连上sql/plus之后,在sql/plus中执行. 1.1      创建存储过程 存储过程不

ASP.NET MVC 中应用Windows服务以及Webservice服务开发分布式定时器

ASP.NET MVC 中应用Windows服务以及Webservice服务开发分布式定时器一:闲谈一下:1.现在任务跟踪管理系统已经开发快要结束了,抽一点时间来写一下,想一想自己就有成就感啊!!  2.关于任务跟踪管理系统项目中遇到的Windows服务以及Webservice的综合应用的问题. 大家好这是我第二次写博客 ,写的不好请大家多多谅解, 希望大家可以多多指正. 二:我稍微的整理了一下关于这个分布式定时器需求:1.根据任务跟踪管理系统中的数据库的AnswerSheet 表格中找到客户编

windows 服务实现定时任务调度(Quartz.Net)

我们通常在一些情况下需要软件具有一个自动执行某些任务的功能,但是又不希望直接启动软件,或者每次都要手动的来启动软件,这时我们可可以考虑到windows服务了. 首先创建一个windows服务项目(详细信息请参阅:C#创建Windows Service(Windows 服务)基础教程) 在创建好的项目中点击“单击此处切换到代码视图”切换到代码 我们主要关注一下两个方法: • OnStart – 控制服务启动 • OnStop – 控制服务停止 例: 1 public partial class S

centos定时计划任务

任务调度的crond常驻命令crond 是linux用来定期执行程序的命令.当安装完成操作系统之后,默认便会启动此任务调度命令.crond命令每分锺会定期检查是否有要执行的工作,如果有要执行的工作便会自动执行该工作.而linux任务调度的工作主要分为以下两类:1.系统执行的工作:系统周期性所要执行的工作,如备份系统数据.清理缓存2.个人执行的工作:某个用户定期要做的工作,例如每隔10分钟检查邮件服务器是否有新信,这些工作可由每个用户自行设置. 一.任务调度设置文件的写法其格式如下:Minute

ASP.NET Core官方计划路线及需要废除的一些Framework技术

概述 下面是 ASP.NET Core的时间表和路线图. 注意日期和特性都可能更改. 作为.NET Core这么大的一个项目,很难准确预测每一个计划的是否有变动. 即便如此,我们还是计划公开和透明的实施,以便我们的用户可以有正确的期望值, 并为我们的用户自己在技术实施时有更好的打算和安排 发布时间表 Release 发布日志 1.1 Q4 2016 / Q1 2017 1.2 Q1 2017 / Q2 2017 Release 版本特性 1.1 URL 重写中间件 Response 缓存中间件

使用ASP.NET Web Api构建基于REST风格的服务实战系列教程【开篇】【持续更新中。。。】

小分享:我有几张阿里云优惠券,用券购买或者升级阿里云相应产品最多可以优惠五折!领券地址:https://promotion.aliyun.com/ntms/act/ambassador/sharetouser.html?userCode=ohmepe03 最近发现web api很火,园内也有各种大神已经在研究,本人在asp.net官网上看到一个系列教程,原文地址:http://bitoftech.net/2013/11/25/detailed-tutorial-building-asp-net-

在ASP.NET Core中使用Apworks快速开发数据服务

不少关注我博客的朋友都知道我在2009年左右开发过一个名为Apworks的企业级应用程序开发框架,旨在为分布式企业系统软件开发提供面向领域驱动(DDD)的框架级别的解决方案,并对多种系统架构风格提供支持.这个框架的开发和维护我坚持了很久,一直到2015年,我都一直在不停地重构这个项目.目前这个项目在Github上也得到了将近260的推荐数,很多对技术感兴趣的朋友也一直与我保持着联系和交流,甚至还有爱好者自发组成了技术讨论群,专门讨论分享Apworks框架. 然而,随着软件开发技术和.NET的发展

ASP.NET C# 如何在程序中控制IIS服务或应用程序池重启?

停止IIS服务ServiceController sc = new ServiceController("iisadmin");if(sc.Status==ServiceControllerStatus.Running) { sc.Stop();} 重启IIS服务ServiceController sc = new ServiceController("iisadmin"); sc.Start(); 重启进程using System.Diagnostics;Proc

ASP.NET实现微信功能(2)(服务号高级群发)

前面写了一篇文章,关于微信的:http://www.cnblogs.com/kmsfan/p/4047097.html 今天打算来写本系列的第二批文章,服务号后台群发. 在写本篇文章之前,我们先来看看腾讯的后台群发是怎么实现的,因为我们无论做什么事情都要知道原理.开始吧. 由于本人的奉献精神,等下回把实现源码的框架全部公开,里面什么东西都有,由于自己才识浅薄,只弄了图文消息的群发,那么今天只会介绍图文消息的群发. 框架的源码文章最后会提到,这本来是一个开源的框架,经过一些大神的二次修改而更加的完