序:
最近由于需要一个转移文件的程序,当时想到的有几种做法:
1、用批处理文件;
2、写一个控制台程序,然后使用用户计划定期进行调用;
3、写一个windows服务,然后自己存在一个Timer控件进行调用。
第一种方式我不是很熟悉命令,所以处理程序也不强(比如:判断转移目标路径所在盘符是否有足够的空间等,如果空间不足那么就不进行转移)
第二种方式实际从代码主题上和第三种方式基本一致,但是由于我还没有做过Windows服务所以决定正好乘这个机会来看看在VS.NET情况下是如何进行开发的。所以选择了第三种方法。
开发环境:
1、Windows XP Pro SP3;
2、.NET Framework 2.0;
3、VS.NET 2005 Professional;
4、C#。
开发步骤:
1、打开VS.NET 2005,并建立一个“Windows 服务”项目。
2、在右侧的Service1.cs文件按下F7,进入代码编辑界面。其中OnStart()方法是启动服务时触发的方法,而OnStop()方法则是在停止服务时触发的方法。在Windows服务的生命周期中还存在其他的事件,这两个事件触发方法是最常用的两个,其他方法可以查看相关资料。同样在建立Windows服务的项目中还会同时生成一个Program.cs,其中包含了一个Main方法,是程序启动的入口,这里就调用了OnStart()方法;
3、在Service1.cs的类文件下添加如下代码,定义一些系统中使用到的常量及一些信息字段。
//计时器时间间隔;
private const string AS_TIMERINTERVAL = "TimerInterval";
//监视文件夹;
private const string AS_SPYDIR = "SpyDir";
//目标文件夹;
private const string AS_AIMDIR = "AimDir";
//日志文件名称;
private const string LOGFILENAME = "LogFile.txt";
//日志文件内的说明文字(将显示在日志文件的最上部分)
private const string LOGFILEREMARK = "说明:日志文件的文件内容结构为 【记录时间】 | 【信息内容】,每条信息占一行。(fujun 2008-01-17)";
//目标盘在移动了文件以后还需要保留的最少空间数,当前设置为100MB;(这里是保守的做法,不使用极端填充的方法);
private const long RESERVEDSPACE = 1024 * 1024 * 100;
//从单位Byte转换到MB需要换算的除数;
private const long DIVBYTETOMB = 1048576;
4、然后在OnStart()代码中输入如下代码:
//启动服务时触发的响应函数;
protected override void OnStart(string[] args)
{
try
{
Timer timerCheck = new Timer();
//设置计时器触发的时间间隔,这里需要将时间单位转化为秒(s);
timerCheck.Interval = Convert.ToInt32(ConfigurationManager.AppSettings[AS_TIMERINTERVAL]) * 1000;
//将计时器激活;
timerCheck.Enabled = true;
timerCheck.AutoReset = true;
timerCheck.Elapsed += new ElapsedEventHandler(timerCheck_Elapsed);
}
catch (Exception ex)
{
//将出错信息记录到日志文件中;
LogFile(ex.Message);
return;
}
}
5、在OnStop()代码中输入如下代码:
//当服务停止时触发的响应函数;
protected override void OnStop()
{
//在日志文件中记录服务被停止的信息;
LogFile("FileMover服务被停止。");
}
6、这个是计时器触发的响应函数:
/// <summary>
/// 当计时器的间隔到达时触发的响应函数;
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks>
/// 1、检查指定文件夹是否存在文件;
/// 2、如果存在文件则将文件转移到对应的目标文件夹中。
/// 2.1、检查对应的文件夹中是否存在同名的文件,如果存在同名的文件则放弃转移;(需要有日志记录)
/// 2.1、检查对应文件夹所在盘符是否存在足够的空间,如果没有足够的空间那么也放弃转移。(需要有日期记录)
/// </remarks>
private void timerCheck_Elapsed(object sender, ElapsedEventArgs e)
{
//1、获取服务配置参数;
//2、检查“监视文件夹”中是否存在文件;
// 2.1、如果存在文件则依次进行转移,如果已经存在同名的文件则放弃转移,并将对应文件记录到日志文件中;
// 2.2、如果存在文件则依次进行转移,(这里要根据是否在同一个盘符来分开进行对待)如果发现目标文件所在硬盘
// 空间不够则放弃转移,并将对应文件记录到日志文件中。
// 2.3、如果均不满足以上条件,则进行文件转移。
string spyDirPath = ConfigurationManager.AppSettings[AS_SPYDIR];
string aimDirPath = ConfigurationManager.AppSettings[AS_AIMDIR];
if (!string.IsNullOrEmpty(spyDirPath) && !string.IsNullOrEmpty(aimDirPath))
{
try
{
string[] spyfileList = Directory.GetFiles(spyDirPath);
string[] aimfileList = Directory.GetFiles(aimDirPath);
//由于直接通过GetPathRoot获取的盘符名称带有"/",所以要去掉;
string spyDiskName = Path.GetPathRoot(spyDirPath).Substring(0, spyDirPath.IndexOf(Path.VolumeSeparatorChar) + 1);
string aimDiskName = Path.GetPathRoot(aimDirPath).Substring(0, aimDirPath.IndexOf(Path.VolumeSeparatorChar) + 1);
bool isSameDisk = (spyDiskName.ToLower() == aimDiskName.ToLower());
if (spyfileList != null && spyfileList.Length > 0)
{
//如果发现监视文件夹已经存在文件;
foreach (string spyFile in spyfileList)
{
if (!isSameDisk)
{
//如果监视文件和目标文件不在一个盘符,那么就需要检查目标文件是否存在足够的空间进行文件的转移;
//获取文件大小;
FileInfo fi = new FileInfo(spyFile);
long spyFileSize = fi.Length;
long aimDiskFreeSpace = GetDiskFreeSpace(aimDiskName);
if (aimDiskFreeSpace < (spyFileSize + RESERVEDSPACE))
{
LogFile(string.Format("目标盘符剩余空间不足。(文件大小:{0}MB, 目标盘符剩余空间大小:{1}MB, 保留空间大小, {3}MB",
(Convert.ToDouble(spyFileSize) / DIVBYTETOMB).ToString(),
(Convert.ToDouble(aimDiskFreeSpace / DIVBYTETOMB).ToString()),
(Convert.ToDouble(RESERVEDSPACE / DIVBYTETOMB).ToString())));
//不再执行后续操作,对下一个文件尝试进行移动操作;
continue;
}
}
//仅仅包含了文件名(而不包含文件路径名称)的信息;
string spyFileName = Path.GetFileName(spyFile);
foreach (string aimFile in aimfileList)
{
string aimFileName = Path.GetFileName(aimFile);
if (spyFileName == aimFileName)
{
//如果存在同名的文件,则不进行文件移动,但是在日志中进行记录;
LogFile(string.Format("存在同名文件:{0},无法完成文件转移工作。", spyFileName));
//不再执行后续操作,对下一个文件尝试进行移动操作;
continue;
}
}
//如果空间也足够,并且没有发现同名的文件,那么进行文件转移;
File.Move(spyFile, Path.Combine(aimDirPath, spyFileName));
}
}
}
catch (Exception ex)
{
LogFile(ex.Message);
return;
}
}
else
{
LogFile(string.Format("监视文件夹和目标文件夹路径存在空值,监视文件夹路径(SpyDirPath) = {0}, 目标文件夹(AimDirPath) = {1}", spyDirPath, aimDirPath));
return;
}
}
7、记录日志的文件:
/// <summary>
/// 将信息记录到日志文件中;
/// </summary>
/// <param name="message">需要记录到日志文件中的信息(如果信息为空则不会将信息记录到日志文件中)</param>
private void LogFile(string message)
{
if (!string.IsNullOrEmpty(message))
{
bool isCreateFile = !File.Exists(LOGFILENAME);
FileStream fs = File.Open(LOGFILENAME, FileMode.OpenOrCreate | FileMode.Append);
try
{
if (isCreateFile)
{
byte[] logFileRemark = System.Text.Encoding.Default.GetBytes(LOGFILEREMARK + Environment.NewLine);
fs.Write(logFileRemark, 0, logFileRemark.Length);
}
string logDateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
string logFullContent = string.Format("{0} | {1}", logDateTime, message + Environment.NewLine);
byte[] logContent = System.Text.Encoding.Default.GetBytes(logFullContent);
fs.Write(logContent, 0, logContent.Length);
}
finally
{
fs.Close();
}
}
}
8、根据得到对应盘符的剩余空间:(由于这里使用到了System.Management.dll中WMI相关功能,所以在项目需要增加对System.Management.dll的引用)
/// <summary>
/// 根据输入的盘符(如:C:, D:等)返回对应盘符剩余空间信息;
/// </summary>
/// <param name="driver">盘符名称</param>
/// <returns>
/// 返回对应盘符剩余空间的值。(bytes)
/// </returns>
private long GetDiskFreeSpace(string driver)
{
//验证盘符是否合法的正则表达式;(正确的盘符如:c:或者C:)
string driverPattern = "^[a-zA-Z]:$";
if (!string.IsNullOrEmpty(driverPattern))
{
if (Regex.IsMatch(driver, driverPattern))
{
//生成一个针对对应硬盘盘符的WMI实例;
ManagementObject disk = new ManagementObject(string.Format("win32_logicaldisk.deviceid=/"{0}/"", driver));
//获取信息;
disk.Get();
//将对应盘符剩余空间信息进行返回;
return (Convert.ToInt64(disk["FreeSpace"]));
}
else
{
ArgumentException ex = new ArgumentException(string.Format("输入的盘符不合法,当前输入盘符为: {0}", driver));
throw (ex);
}
}
else
{
ArgumentException ex = new ArgumentException("用于查找空间大小的盘符为空(null or Empty)。");
throw (ex);
}
}
}
9、然后在项目中新增一个App.config,用于配置如:计时器触发间隔、源目标文件夹、移动目标文件夹等信息,如:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!--检查指定文件夹时间间隔(单位: 秒[s])-->
<add key="TimerInterval" value="1800"/>
<!--监视文件夹(请填写绝对路径)-->
<add key="SpyDir" value="G:/FromDir"/>
<!--目标文件夹(文件将被转移到的文件夹,请填写绝对路径)-->
<add key="AimDir" value="G:/ToDir"/>
</appSettings>
</configuration>
10、通过快捷键(Shift + F7)从代码视图切换到设计视图,然后再空白处右击,从弹出的上下文菜单中选择“添加安装程序”。
在Windows服务项目中将增加文件“ProjectInstaller.cs”,在文件中存在两个组件,一个是servicesInstaller1, 选择后按F4,可以在弹出的属性窗口中设置一些服务的相关参数,如:
Discription : 对服务的描述,当服务安装以后对应信息将显示在服务的描述中;
ServiceName : 在服务安装后显示的服务名称;
然后选择serviceProcessInstaller1组件,同样通过F4弹出属性菜单,然后进行设置:
Account :这里需要设置为LocalService,这个是运行服务的windows账户,如果账户权限不够将可能导致服务不能正常运行;
11、OK,到此为止这个用于将一个文件夹中的文件转移到另外一个文件夹的windows服务就写完了,编译解决方案,然后进入安装过程。
安装过程有两种,一种是直接通过InstallUtil.exe进行安装。首先通过"开始" -> "所有程序" -> "Microsoft Visual Studio 2005" -> "Visual Studio Tools" -> "Visual Studio 2005 命令提示"
12、然后敲入如下命令: InstallUtil.exe "【您生成.exe文件】" (对应的.exe文件可以在您建立工程的目录下 bin /Debug | Release/下可以找到)
13、当提示安装成功以后,通过右击“我的电脑”-> "管理" -> "服务" 从服务列表中就可以查看到刚刚安装的服务了。
然后将对应的服务进行启动就可以了。
14、当然在一个服务能够正常运行之前,调试是不可避免的,一种方式是通过附加进程的方式,在服务运行以后,从VS中点击“调试”-> “附加到进程”从进程列表中选择服务的进程,如对于我的例子就是“FileMover.vshost.exe”进程。然后在代码中定时处理代码中设置断点,然后就静候断点被命中就可以了。
但是个人认为这种方式并不太好,因为服务的卸载和安装实际上还是很繁琐的过程,所以个人建议比较好的调试方式还是将其看成控制台程序进行调试。具体做法就是在Program.cs中的Main函数中注释掉对应的启动服务的代码,而是直接调用Service1中对应的函数代码,然后从功能上测试完成以后再将代码修改回来,在此期间由于一些访问性的限制可能需要稍稍修改一下原先的函数签名。
15、至此,一个移动文件的Windows服务已经完成了。如果你想分发你的Windows服务,并且让人们觉得专业一点,那么还是通过VS.NET2005自带的”安装项目“进行打包吧。由于这个过程和Windows服务的内容不是关联非常大,所以这里就不详细的描述了(网络上有很多文章,这个不错:http://www.cnblogs.com/mindotnet/archive/2005/07/07/52078.html),对于我自己在制作过程中的一些经验我将另外通过blog来描述。
我在做这个Windows服务的时候参考的主要文章是:http://hi.baidu.com/lieyu063/blog/item/bdebcd19fae16e7bdbb4bdd1.html。
第一次正式的做Windows服务,无论是从理解还是代码如果有不足和错误,欢迎交流。:)