大约在去年的12月份,我们开始着手设计和开发这项功能,而该项功能主要是解决类似于一些
帖子附件(图片或文件)访问比较频繁,同时附件的体积又比较大,从而造成对主站服务器访问压
力过大的问题。而实现了该项功能之后,在一些合作伙伴的站点上使用了一段时间,发现该功能明
显的降低了主站服务器的负载,使其可以节省更多的资源(cpu,内存等) 用于处理用户的其它访问
请求。
下面就简要介绍一下该功能的一些实现细节, 该项功能所实现的流程如下图所示:
而主要的核心就是采用FTP协议上传附件到远程的服务器上,这样当用户点开网页或进行附件下
载时,就会将链接指向远程的FTP服务上(该服务器要支持HTTP协议访问其资源)。而这个类的原
型链接如下:
http://www.csharphelp.com/archives/archive9.html
本人在其基础上修改了该类在DEBUG模式下上传文件过程中的BUG,同时翻译了其注释内容。大
家可在dicuz.common.dll(discuz!nt 2.1以后的版块)的中找到该类(使用Reflector)。
下面是其核心代码(您可在下个开源版本中获取该类的全部代码):
有了核心代码,下面就是相关的FTP信息(如服务器站点,端口号,密码等)是如何在我们产品代
码中进行设置并保存呢?
请用Reflectort工具反射文件:discuz.config.dll,其配置类如下(其也采用序列化方式进行保
存):
FTPConfigInfo [FTP配置信息类]
FTPConfigInfoCollection [FTP配置信息类集合]
其中的FTPConfigInfo类的代码如下(我已将注释补充):
/// FTP配置信息类
/// </summary>
[Serializable]
public class FTPConfigInfo : IConfigInfo
{
#region FTP私有字段
private string m_name; // 名称,如forumattach:论坛附件,spaceattach:空间附件,album:相册等
private string m_serveraddress; // 服务器地址
private int m_serverport = 25 ; // 服务器端口号
private string m_username; // 登陆帐号
private string m_password; // 登陆密码
private int m_mode = 1 ; // 链接模式 1:被动 2:主动
private int m_allowupload; // 允许FTP上传附件 0:不允许 1:允许
private string m_uploadpath; // 上传路径
private int m_timeout; // 无响应时间(FTP在指定时间内无响应),单位:秒
private string m_remoteurl; // 远程访问 URL
private int m_reservelocalattach = 0 ; // 是否保留本地附件. 0:不保留 1:保留
#endregion
public FTPConfigInfo()
{
}
#region 属性
/// <summary>
/// 名称
/// </summary>
public string Name
{
get { return m_name; }
set { m_name = value; }
}
/// <summary>
/// FTP服务器名称
/// </summary>
public string Serveraddress
{
get { return m_serveraddress; }
set { m_serveraddress = value; }
}
/// <summary>
/// FTP端口号
/// </summary>
public int Serverport
{
get { return m_serverport; }
set { m_serverport = value; }
}
/// <summary>
/// 登陆帐号
/// </summary>
public string Username
{
get { return m_username; }
set { m_username = value; }
}
/// <summary>
/// 登陆密码
/// </summary>
public string Password
{
get { return m_password; }
set { m_password = value; }
}
/// <summary>
/// 链接模式 1:被动 2:主动
/// </summary>
public int Mode
{
get { return m_mode <= 0 ? 1 : m_mode; }
set { m_mode = value <= 0 ? 1 : m_mode; }
}
/// <summary>
/// 允许FTP上传附件
/// </summary>
public int Allowupload
{
get { return m_allowupload; }
set { m_allowupload = value; }
}
/// <summary>
/// 上传路径
/// </summary>
public string Uploadpath
{
get { return m_uploadpath; }
set { m_uploadpath = value; }
}
/// <summary>
/// 无响应时间(FTP在指定时间内无响应),单位:秒
/// </summary>
public int Timeout
{
get { return m_timeout <= 0 ? 10 : m_timeout; }
set { m_timeout = value <= 0 ? 10 : m_timeout; }
}
/// <summary>
/// 远程访问 URL
/// </summary>
public string Remoteurl
{
get { return m_remoteurl; }
set { m_remoteurl = value; }
}
/// <summary>
/// 保留本地附件:0为不保留 1为保留
/// </summary>
public int Reservelocalattach
{
get { return m_reservelocalattach; }
set { m_reservelocalattach = value; }
}
#endregion
}
而上面的FTPConfigInfoCollection类是一个可序列化的集合类(为FTPConfigInfo类实例集合)。
这样做是因为如果当论坛,空间,相册等功能需要远程附件支持时,都需要各自的配置信息,而通过序列
化FTPConfigInfoCollection 便可获取或保存各个功能的相应配置信息(也便于日后扩展),其生成的序
列化信息格式如下(相应的节点信息对应上面的FTPConfigInfo类的属性字段):
<ArrayOfFTPConfigInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
< FTPConfigInfo >
< Name > ForumAttach </ Name >
< Serveraddress > ftpserver </ Serveraddress >
< Serverport > 21 </ Serverport >
< Username > username </ Username >
< Password > password </ Password >
< Mode > 1 </ Mode >
< Allowupload > 0 </ Allowupload >
< Uploadpath > test </ Uploadpath >
< Timeout > 10 </ Timeout >
< Remoteurl > http://localhost/forumattach </ Remoteurl >
< Reservelocalattach > 0 </ Reservelocalattach >
</ FTPConfigInfo >
< FTPConfigInfo >
< Name > SpaceAttach </ Name >
< Serveraddress > ftpserver </ Serveraddress >
< Serverport > 21 </ Serverport >
< Username > username </ Username >
< Password > password </ Password >
< Mode > 1 </ Mode >
< Allowupload > 0 </ Allowupload >
< Uploadpath > test </ Uploadpath >
< Timeout > 10 </ Timeout >
< Remoteurl > http://localhost/spaceattach </ Remoteurl >
< Reservelocalattach > 0 </ Reservelocalattach >
</ FTPConfigInfo >
< FTPConfigInfo >
< Name > AlbumAttach </ Name >
< Serveraddress > ftpserver </ Serveraddress >
< Serverport > 21 </ Serverport >
< Username > username </ Username >
< Password > password </ Password >
< Mode > 1 </ Mode >
< Allowupload > 0 </ Allowupload >
< Uploadpath > test </ Uploadpath >
< Timeout > 10 </ Timeout >
< Remoteurl > http://localhost/albumattach </ Remoteurl >
< Reservelocalattach > 0 </ Reservelocalattach >
</ FTPConfigInfo >
</ ArrayOfFTPConfigInfo >
而该序列化配置文件位于discuz.web项目的config/ftp.config下。
当然上面所说的只是相应的配置类,而为了便于前台开发相应功能,我对上面所说的FTP和配置类又进行了一次
类封装,并将类命名为FTPs,放在了discuz.forum下,大家可以用Reflector获得其代码,我在这里将注释补充如下:
/// FTP操作类
/// </summary>
public class FTPs
{
#region 声明上传信息(静态)对象
private static FTPConfigInfo m_forumattach;
private static FTPConfigInfo m_spaceattach;
private static FTPConfigInfo m_albumattach;
private static FTPConfigInfo m_mallattach;
#endregion
private static string m_configfilepath = Utils.GetMapPath(BaseConfigs.GetForumPath + " config/ftp.config " );
/// <summary>
/// 程序刚加载时ftp.config文件修改时间
/// </summary>
private static DateTime m_fileoldchange;
/// <summary>
/// 最近ftp.config文件修改时间
/// </summary>
private static DateTime m_filenewchange;
private static object lockhelper = new object ();
/// <summary>
/// FTP信息枚举类型
/// </summary>
public enum FTPUploadEnum
{
ForumAttach = 1 , // 论坛附件
SpaceAttach = 2 , // 空间附件
AlbumAttach = 3 , // 相册附件
MallAttach = 4 // 商场附件
}
/// <summary>
/// 静态构造函数(用于初始化对象和变量)
/// </summary>
static FTPs()
{
SetFtpConfigInfo();
m_fileoldchange = System.IO.File.GetLastWriteTime(m_configfilepath);
}
/// <summary>
/// FTP配置文件监视方法
/// </summary>
private static void FtpFileMonitor()
{
// 获取文件最近修改时间
m_filenewchange = System.IO.File.GetLastWriteTime(m_configfilepath);
// 当ftp.config修改时间发生变化时
if (m_fileoldchange != m_filenewchange)
{
lock (lockhelper)
{
if (m_fileoldchange != m_filenewchange)
{
// 当文件发生修改(时间变化)则重新设置相关FTP信息对象
SetFtpConfigInfo();
m_fileoldchange = m_filenewchange;
}
}
}
}
/// <summary>
/// 设置FTP对象信息
/// </summary>
private static void SetFtpConfigInfo()
{
FTPConfigInfoCollection ftpconfiginfocollection =
(FTPConfigInfoCollection)SerializationHelper.Load(typeof(FTPConfigInfoCollection), m_configfilepath);
FTPConfigInfoCollection.FTPConfigInfoCollectionEnumerator fcice = ftpconfiginfocollection.GetEnumerator();
// 遍历集合并设置相应的FTP信息(静态)对象
while (fcice.MoveNext())
{
if (fcice.Current.Name == " ForumAttach " )
{
m_forumattach = fcice.Current;
continue ;
}
if (fcice.Current.Name == " SpaceAttach " )
{
m_spaceattach = fcice.Current;
continue ;
}
if (fcice.Current.Name == " AlbumAttach " )
{
m_albumattach = fcice.Current;
continue ;
}
if (fcice.Current.Name == " MallAttach " )
{
m_mallattach = fcice.Current;
continue ;
}
}
}
/// <summary>
/// 论坛附件FTP信息
/// </summary>
public static FTPConfigInfo GetForumAttachInfo
{
get
{
FtpFileMonitor();
return m_forumattach;
}
}
/// <summary>
/// 空间附件FTP信息
/// </summary>
public static FTPConfigInfo GetSpaceAttachInfo
{
get
{
FtpFileMonitor();
return m_spaceattach;
}
}
/// <summary>
/// 相册附件FTP信息
/// </summary>
public static FTPConfigInfo GetAlbumAttachInfo
{
get
{
FtpFileMonitor();
return m_albumattach;
}
}
/// <summary>
/// 相册附件FTP信息
/// </summary>
public static FTPConfigInfo GetMallAttachInfo
{
get
{
FtpFileMonitor();
return m_mallattach;
}
}
#region 异步FTP上传文件
private delegate bool delegateUpLoadFile( string path, string file, FTPUploadEnum ftpuploadname);
// 异步FTP上传文件代理
private delegateUpLoadFile upload_aysncallback;
public void AsyncUpLoadFile( string path, string file, FTPUploadEnum ftpuploadname)
{
upload_aysncallback = new delegateUpLoadFile(UpLoadFile);
upload_aysncallback.BeginInvoke(path, file, ftpuploadname, null , null );
}
#endregion
/// <summary>
/// 普通FTP上传文件
/// </summary>
/// <param name="file"> 要FTP上传的文件 </param>
/// <returns> 上传是否成功 </returns>
public bool UpLoadFile( string path, string file, FTPUploadEnum ftpuploadname)
{
FTP ftpupload = new FTP();
// 转换路径分割符为"/"
path = path.Replace( """" , " / " );
path = path.StartsWith( " / " ) ? path : " / " + path ;
// 删除file参数文件
bool delfile = true ;
// 根据上传名称确定上传的FTP服务器
switch (ftpuploadname)
{
// 论坛附件
case FTPUploadEnum.ForumAttach:
{
ftpupload = new FTP(m_forumattach.Serveraddress, m_forumattach.Serverport,
m_forumattach.Username, m_forumattach.Password, m_forumattach.Timeout);
path = m_forumattach.Uploadpath + path;
delfile = (m_forumattach.Reservelocalattach == 1 ) ? false : true ;
break ;
}
// 空间附件
case FTPUploadEnum.SpaceAttach:
{
ftpupload = new FTP(m_spaceattach.Serveraddress, m_spaceattach.Serverport,
m_spaceattach.Username, m_spaceattach.Password, m_spaceattach.Timeout);
path = m_spaceattach.Uploadpath + path;
delfile = (m_spaceattach.Reservelocalattach == 1 ) ? false : true ;
break ;
}
// 相册附件
case FTPUploadEnum.AlbumAttach:
{
ftpupload = new FTP(m_albumattach.Serveraddress, m_albumattach.Serverport,
m_albumattach.Username, m_albumattach.Password, m_albumattach.Timeout);
path = m_albumattach.Uploadpath + path;
delfile = (m_albumattach.Reservelocalattach == 1 ) ? false : true ;
break ;
}
// 商城附件
case FTPUploadEnum.MallAttach:
{
ftpupload = new FTP(m_mallattach.Serveraddress, m_mallattach.Serverport,
m_mallattach.Username, m_mallattach.Password, m_mallattach.Timeout);
path = m_mallattach.Uploadpath + path;
delfile = (m_mallattach.Reservelocalattach == 1 ) ? false : true ;
break ;
}
}
// 切换到指定路径下,如果目录不存在,将创建
if ( ! ftpupload.ChangeDir(path))
{
// ftpupload.MakeDir(path);
foreach ( string pathstr in path.Split( ' / ' ))
{
if (pathstr.Trim() != "" )
{
ftpupload.MakeDir(pathstr);
ftpupload.ChangeDir(pathstr);
}
}
}
ftpupload.Connect();
if ( ! ftpupload.IsConnected)
{
return false ;
}
int perc = 0 ;
// 绑定要上传的文件
if ( ! ftpupload.OpenUpload(file, System.IO.Path.GetFileName(file)))
{
ftpupload.Disconnect();
return false ;
}
// 开始进行上传
while (ftpupload.DoUpload() > 0 )
{
perc = ( int )(((ftpupload.BytesTotal) * 100 ) / ftpupload.FileSize);
}
ftpupload.Disconnect();
// (如存在)删除指定目录下的文件
if (delfile && Utils.FileExists(file))
{
System.IO.File.Delete(file);
}
if (perc >= 100 )
{
return true ;
}
else
{
return false ;
}
}
/// <summary>
/// FTP连接测试
/// </summary>
/// <param name="Serveraddress"> FTP服务器地址 </param>
/// <param name="Serverport"> FTP端口 </param>
/// <param name="Username"> 用户名 </param>
/// <param name="Password"> 密码 </param>
/// <param name="Timeout"> 超时时间(秒) </param>
/// <param name="uploadpath"> 附件保存路径 </param>
/// <param name="message"> 返回信息 </param>
/// <returns> 是否可用 </returns>
public bool TestConnect(string Serveraddress, int Serverport, string Username, string Password,
int Timeout, string uploadpath, ref string message)
{
FTP ftpupload = new FTP(Serveraddress, Serverport, Username, Password, Timeout);
bool isvalid = ftpupload.Connect();
if ( ! isvalid)
{
message = ftpupload.errormessage;
return isvalid;
}
// 切换到指定路径下,如果目录不存在,将创建
if ( ! ftpupload.ChangeDir(uploadpath))
{
ftpupload.MakeDir(uploadpath);
if ( ! ftpupload.ChangeDir(uploadpath))
{
message += ftpupload.errormessage;
isvalid = false ;
}
}
return isvalid;
}
}
现在万事俱备,我们要将新增的的远程附件功能的调用代码放到原有的附件上传代码中了,这里以 " ForumUtils.cs "
文件(位于discuz.forum)为例, 通过Reflector, 查看该类下的SaveRequestFiles方法中的如下代码段(已添加注释):
public static AttachmentInfo[] SaveRequestFiles(int forumid, int MaxAllowFileCount, int MaxSizePerDay,
int MaxFileSize, int TodayUploadedSize, string AllowFileType, int watermarkstatus,
GeneralConfigInfo config, string filekey)
{
// 当支持FTP上传附件时,使用FTP上传远程附件
if (FTPs.GetForumAttachInfo.Allowupload == 1 )
{
FTPs ftps = new FTPs();
// 当不保留本地附件模式时,在上传完成之后删除本地tempfilename文件
if (FTPs.GetForumAttachInfo.Reservelocalattach == 0 )
{
ftps.UpLoadFile(newfilename.Substring(0, newfilename.LastIndexOf("""")),
UploadDir + tempfilename, FTPs.FTPUploadEnum.ForumAttach);
}
else
{
ftps.UpLoadFile(newfilename.Substring(0, newfilename.LastIndexOf("""")),
UploadDir + newfilename, FTPs.FTPUploadEnum.ForumAttach);
}
}
}
当然这里只是列举了一处改动,其实还有几处不小的变化也是相似的代码实现,比如空间或相册中进行图片等
附件上传时。
而这些功能的设置可以到"后台管理"中找到,如下图所示:
好了,今天的内容就先到这里了,感兴趣的朋友可以在回复中或用EMAIL的方法与我联系。
老外的FTP类源码下载链接,请点击这里:)
作者:代震军 (daizhj)
博客:http://daizhj.cnblogs.com