部署了网站之后,就可以通过 IIS 的功能来管理网站宿主和执行的方式。
创建新站点
IIS 7 能够在单台服务器上支持多个站点。要创建新站点,展开 IIS 管理器的树控件,右击“网站”,选择“添加网站”,会显示如下对话框:
网站名称中可以填写有意义的内容,不过,它只用于在 IIS 里标识网站,但并不影响网站的内容。物理路径指定了 IIS 7 到哪里寻找服务新网站请求的内容。我们创建了一个新目录 C:\FileCopySite。“连接为”和“测试设置”按钮可以指定用于访问网站内容的不同用户身份。
“绑定”一节可以指定 IIS 7 如何监听来自客户端的请求。IIS 7 支持一系列的网络协议,“类型”下拉列表有众多选项,但我们只关注 HTTP,因为它是最广泛被使用的。IP 地址我们采用默认的“全部未分配”,也就是说 IIS 监听处理其他网站在同一 TCP 端口上已经被服务的网络接口之外的全部网络接口。“端口”值指定 IIS 7 要监听客户端请求的 TCP 端口号,一般而言每个网站必须仅由唯一的端口提供服务,因此我们选择了 81,这样不会和默认网站的 80 端口相冲突。
勾选了“立即启动网站”,它表示在我们单击“确定”之后,IIS 就创建网站并开始监听请求。现在单击确定。
创建虚拟目录
在之前的部署网站系列文章里,我们把内容放到了 IIS 7 寻找内容的默认目录里。但我们也可以不这样做,而是把内容放到其他地方,然后通过虚拟目录来引用它。为了演示,我们在服务器上创建一个新目录,并把网站的内容复制到了那里,新目录路径如下:
C:\WebSiteDeployment\VirtualDirectory
为了把新目录关联到 IIS ,右击“默认网站”,选择“添加虚拟目录”,会打开如下对话框:
我们希望网站的 URL 是这样的:/virtual">/virtual">/virtual">http://<servername>/virtual,因此在别名里输入了 virtual。单击“确定”创建虚拟目录。为了测试它,浏览器现在访问 URL http://localhost/virtual,你会看到自己的简单网站,但这一回内容是以新目录作为源的,并且通过自定义的 URL 来进行访问。
甚至可以有多个虚拟目录指向一个实际物理路径,访问同一资源时,显示的 URL 是不同的。
使用 VirtualPathProvider
VirtualPathProvider 类提供了虚拟目录之外的另一个选择,不再由文件系统提供网站内容,内容可以通过编程生成或者取自数据库。
下面我将通过一个从 SQL Server 表读取 ASPX 文件的简单示例来理解 VirtualPathProvider 类。我们在本地 SQL Server 上有一个如下图所示的数据库表,里面保存了 3 个页面:
你可以看到表里有一个文件名(同时还是主键)和真是内容。内容可以是 ASP.NET 能够理解的任意类型的代码。
VirtualPathProvider 类被定义在 System.Web.Hosting 命名空间里。在 App_Code 目录里新增一个类并继承 VirtualPathProvider,至少要实现以下方法:
using System;
using System.Data.SqlClient;
using System.IO;
using System.Text;
using System.Web.Hosting;
// VirtualPathProvider: 使 Web 应用程序可以从虚拟文件系统中检索资源
public class DBPathProvider : VirtualPathProvider
{
public static void AppInitialize()
{
// HostingEnvironment: 在托管应用程序的应用程序域内向托管应用程
// 序提供应用程序管理功能和应用程序服务
// RegisterVirtualPathProvider(): 在 ASP.NET 编译系统中注册新
// 的 VirtualPathProvider 实例
HostingEnvironment.RegisterVirtualPathProvider(new DBPathProvider());
}
// VirtualPathProvider.FileExists(): 获取一个值,该值指示文件是否
// 存在于虚拟文件系统中
public override bool FileExists(string virtualPath)
{
throw new Exception("The method or operation is not implemented.");
}
// VirtualPathProvider.GetFile(): 从虚拟文件系统中获取一个虚拟文件
public override VirtualFile GetFile(string virtualPath)
{
throw new Exception("The method or operation is not implemented.");
}
}
提供程序必须实现一个静态的 AppInitialize 方法,这个方法应创建自定义类的实例并把它注册为 Framework 的提供程序;FileExists 方法用于检查某个路径是否可以由提供程序提供;GetFile 方法用于获取某个路径的内容,返回抽象类 VirtualFile 的实例。
Framework 没有提供 VirtualFile 类的具体实现,我们必须自己扩展 VirtualFile 类来支持自己的提供程序。下面是我们的提供程序的 VirtualFile 类的实现,它被放在同一个代码文件中:
public class DBVirtualFile : VirtualFile
{
private string _FileContent;
public DBVirtualFile(string virtualPath, string fileContent)
: base(virtualPath)
{
_FileContent = fileContent;
}
public override Stream Open()
{
Stream stream = new MemoryStream();
StreamWriter write = new StreamWriter(stream,Encoding.Unicode);
write.Write(_FileContent);
write.Flush();
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
}
这个类的构造函数获取虚拟路径以及文件的内容。在 Open 方法里,真实内容的字符串被保存到了 MemoryStream,然后返回这个流。ASP.NET 使用这个流读取内容,就好像它在读取文件系统一样(感谢基于 Stream 类的字节抽象)。
下一步是继续完成 VirtualPathProvider 类。它需要读取来自数据库文件的真实数据。如果文件在数据库里不存在,提供程序就把请求转送给它的前一个提供程序(由框架在静态方法 AppInitialize 里注册时选定)。给 DBPathProvider 类增加一个从数据库获取内容的方法:
private string GetFileFromDB(string virtualPath)
{
string contents;
string fileName = virtualPath.Substring(virtualPath.IndexOf('/', 1), +1);
// Read the file from the database.
SqlConnection conn = new SqlConnection();
conn.ConnectionString = @"Data Source=.\SqlExpress;Initial Catalog=AspNetContents;
Integrated Security=true";
conn.Open();
try
{
SqlCommand cmd = new SqlCommand("select FileContents from AspContent "
+ "where FileName=@fn", conn);
cmd.Parameters.AddWithValue("@fn", fileName);
contents = cmd.ExecuteScalar() as string;
if (contents == null)
{
contents = string.Empty;
}
}
catch
{
contents = string.Empty;
}
finally
{
conn.Close();
}
return contents;
}
GetFileFromDB 函数从虚拟路径获取文件名并从数据库读取该文件名对应的内容。此方法将同时被 FileExists() 和 GetFile() 使用:
// VirtualPathProvider.FileExists(): 获取一个值,该值指示文件是否
// 存在于虚拟文件系统中
public override bool FileExists(string virtualPath)
{
string contents = this.GetFileFromDB(virtualPath);
if (contents.Equals(string.Empty))
{
return false;
}
else
{
return true;
}
}
// VirtualPathProvider.GetFile(): 从虚拟文件系统中获取一个虚拟文件
public override VirtualFile GetFile(string virtualPath)
{
string contents = this.GetFileFromDB(virtualPath);
if (contents.Equals(string.Empty))
{
// VirtualPathProvider.Previous: 获取对编译系统中以前注册的
// System.Web.Hosting.VirtualPathProvider 对象的引用
return Previous.GetFile(virtualPath);
}
else
{
return new DBVirtualFile(virtualPath, contents);
}
}
你还可以在自己的提供程序里实现一些额外的方法,它们对更复杂的模型会很有用。比如,验证目录是否存在(DirectoryExists)、计算文件散列值(GetFileHash),以及执行缓存验证(GetCacheDependency)等等。
现在输入以下3个请求URL可以看见数据库中这 3 个简单网页的效果:
现在你不使用网站的文件系统,也可也动态的生成网页了,将网站部署到服务器即可。
示例完整代码如下:
using System;
using System.Data.SqlClient;
using System.IO;
using System.Text;
using System.Web.Hosting;
// VirtualPathProvider: 使 Web 应用程序可以从虚拟文件系统中检索资源
public class DBPathProvider : VirtualPathProvider
{
public static void AppInitialize()
{
// HostingEnvironment: 在托管应用程序的应用程序域内向托管应用程
// 序提供应用程序管理功能和应用程序服务
// RegisterVirtualPathProvider(): 在 ASP.NET 编译系统中注册新
// 的 VirtualPathProvider 实例
HostingEnvironment.RegisterVirtualPathProvider(new DBPathProvider());
}
// VirtualPathProvider.FileExists(): 获取一个值,该值指示文件是否
// 存在于虚拟文件系统中
public override bool FileExists(string virtualPath)
{
string contents = this.GetFileFromDB(virtualPath);
if (contents.Equals(string.Empty))
{
return false;
}
else
{
return true;
}
}
// VirtualPathProvider.GetFile(): 从虚拟文件系统中获取一个虚拟文件
public override VirtualFile GetFile(string virtualPath)
{
string contents = this.GetFileFromDB(virtualPath);
if (contents.Equals(string.Empty))
{
// VirtualPathProvider.Previous: 获取对编译系统中以前注册的
// System.Web.Hosting.VirtualPathProvider 对象的引用
return Previous.GetFile(virtualPath);
}
else
{
return new DBVirtualFile(virtualPath, contents);
}
}
private string GetFileFromDB(string virtualPath)
{
string contents;
string fileName = virtualPath.Substring(virtualPath.IndexOf('/', 1) + 1);
// Read the file from the database.
SqlConnection conn = new SqlConnection();
conn.ConnectionString = @"Data Source=.\SqlExpress;Initial Catalog=AspNetContents;
Integrated Security=true";
conn.Open();
try
{
SqlCommand cmd = new SqlCommand("select FileContents from AspContent "
+ "where FileName=@fn", conn);
cmd.Parameters.AddWithValue("@fn", fileName);
contents = cmd.ExecuteScalar() as string;
if (contents == null)
{
contents = string.Empty;
}
}
catch
{
contents = string.Empty;
}
finally
{
conn.Close();
}
return contents;
}
}
public class DBVirtualFile : VirtualFile
{
private string _FileContent;
public DBVirtualFile(string virtualPath, string fileContent)
: base(virtualPath)
{
_FileContent = fileContent;
}
public override Stream Open()
{
Stream stream = new MemoryStream();
StreamWriter write = new StreamWriter(stream, Encoding.Unicode);
write.Write(_FileContent);
write.Flush();
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
}
总结一下整篇代码的工作顺序与原理。
- AppInitialize() 被 Framework 调用。我们向 ASP.NET 编译系统注册了自定义的 DBPathProvider 类,使得这个 Web 应用程序可以从虚拟文件系统中检索资源。
- FileExists(string virtualPath) 被调用。它告诉系统是否存在请求路径,如果存在,则通知程序继续查找内容(事件激发系统继续调用 GetFile 方法)
- GetFile(string virtualPath) 被调用。如果获取不到文件内容,则将请求交由编译系统以前注册的提供程序处理(回复到 ASP.NET 网页请求的默认处理流程)。如果成功获取数据库中对应文件名的网页内容,则创建 VirtualFile 类的实例(我们的扩展类 DBVirtualFile 实现了它),同时传入路径和网页内容至 DBVirtualFile 的构造函数
- Open() 被调用。它将字符串表现形式的网页内容读取到内存流中并返回这个流。
- ASP.NET 使用这个流读取内容,就好像它在读取文件一样!
上述方法的调用均由 .NET 内部事件机制做驱动,并无显式的调用逻辑和顺序。