我们怎样才能在服务器上使用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会在当一个用户访问一个需要被实例化为新会话的页面时被触发。所以这也是一个随机事件。而我们需要一个能持久且定期运行的“场景”。

一个缓存项的失效可以提供一个时间点或持续时间。参考:雪维网 http://xuevi.com在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服务的更多相关文章

  1. 通过asp.net程序来控制自己开发的windows服务

    public ActionResult ListService() { //获取已经保存好的windows服务名称 IList<Model.ReportServicesInfoEnt> L ...

  2. Asp.net(C#) windows 服务{用于实现计划任务,事件监控等}

    什么是windows服务?      一个Windows服务程序是在Windows操作系统下能完成特定功能的可执行的应用程序.Windows服务程序虽然是可执行的,但是它不像一般的可执行文件通过双击就 ...

  3. Asp.net Global 定时执行

    在复杂的业务应用程序中,有时候会要求一个或者多个任务在一定的时间或者一定的时间间隔内计划进行,比如定时备份或同步数据库,定时发送电子邮件,定期处理用户状态信息,支付系统中定期同步异常账单等等,我们称之 ...

  4. Oracle定时计划快速使用

    Oracle定时计划快速使用 前言: SQL Server中有相关的定时计划,可以直接打开sql server 的任务管理器进行配置,可以方便.快速实现定时执行相应任务.相应的Oracle也有对应的定 ...

  5. Quartz.net 定时计划使用

    新建解决方案和工程Quartz.net 使用Power Shell 命令 Install-Package Quartz 导入Quartz.net程序集 新建一个计划TestJob using Syst ...

  6. MySQL架构优化:定时计划任务与表分区

    转自: MySQL架构优化实战系列3:定时计划任务与表分区 - 今日头条(TouTiao.com)http://toutiao.com/a6304736482361049345/?tt_from=mo ...

  7. Windows定时计划执行Python

    先看下定时执行的py文件 #coding:gbk from selenium import webdriver import time,os driver = webdriver.PhantomJS( ...

  8. Linux定时计划(crontab)使用说明

    一.设置定时计划步骤 第一步,编缉计划文件:crontab -e 第二步,在文件中写入计划,格式如:minute hour day month week command.如0 8 * * * sh / ...

  9. asp.net基于windows服务实现定时发送邮件的方法

    本文实例讲述了asp.net基于windows服务实现定时发送邮件的方法.分享给大家供大家参考,具体如下: //定义组件 private System.Timers.Timer time; publi ...

随机推荐

  1. java方法中开启一个线程

    很多业务场景下需要你在一个方法中去开启一个线程,去跑一些处理时间较长的代码,这样调用方就不必经过长时间的等待了.好了 话不多说  先上代码: package test; public class Th ...

  2. 《JavaScript高级程序设计》(第二版)

    这本书的作者是 Nicholas C.Zakas ,博客地址是 http://www.nczonline.net/ ,大家可以去多关注,雅虎的前端工程师,是YUI的代码贡献者,可想而知这本书得含金量, ...

  3. 问题 C: 最短路径

    问题 C: 最短路径 在洛谷上刷最短路的题然后被老师拉回去做算法笔记上面的题... 拿到这道题,先确定所有路径唯一,然后是无向边,那么对于边权处理,直接赋值为2的k次方就可以了,然后直接跑最短路. 这 ...

  4. 利用Chrome浏览器调试线上代码

    前言 之前调试前端bug都是在开发环境中做完并多次测试没有问题之后发布测试环境,验收合格之后发布生产.但生产环境偏偏会有和开发和测试环境不一致的情况,例如测试环境需要加密,而开发环境先不加密,测试环境 ...

  5. Python3笔记012 - 3.3 条件表达式

    第3章 流程控制语句 3.3 条件表达式 在程序开发中,经常会根据表达式的结果,有条件地进行赋值. # 返回两个数中较大的数 a = 10 b = 6 if a>b: r = a else: r ...

  6. 浅谈JVM和垃圾回收

    写在前面 简单的介绍一下JVM(Java Virtual Machine)吧,它也叫Java虚拟机.虽然它叫虚拟机,但是实际上不是我们所理解的虚拟机,它更像操作系统中的一个进程.JVM屏蔽了各个操作系 ...

  7. css定位方式有哪几种?

    复杂的网页布局都是通过各种网页元素灵活定位实现的,网页中的各种元素定位都有自己的特点.下面我们来看一下css的几种定位方式. float定位(即浮动定位): 这种定位方式很简单,只需规定一个浮动的方向 ...

  8. ZJOI2008 骑士(树型DP)

    ZJOI2008 骑士 题目大意 给出n个人的战斗力和每个人讨厌的人,然后问最大能有多大的战斗力 solution 简单粗暴的题意,有一丢丢背包的感觉 那敢情就是DP了 有点像没有上司的舞会,,, 根 ...

  9. MySQL分库分表的原则

    一.分表 当一个表的数据达到几千万条的时候,每一次查询都会花费更长的时间,如果这时候在使用链表查询,那么我想应该会实在那里,那么我们应该如何解决这个问题呢? 1.为什么要分表: 分表的目的就是为了解决 ...

  10. JavaScript学习 Ⅲ

    六. 面向对象 对象属于一种复合的数据类型,在对象中可以保存多个不同的数据类型的属性. 对象分类 内建对象 由ES标准种定义的对象.比如:Math String Number 宿主对象 由JS的运行环 ...