上载文件 (C#)

作者 :Scott Mitchell

下载 PDF

了解如何允许用户将二进制文件 ((如Word或 PDF 文档)) 上传到网站,这些二进制文件可能存储在服务器的文件系统或数据库中。

简介

到目前为止,我们检查过的所有教程都专门处理文本数据。 但是,许多应用程序都有同时捕获文本和二进制数据的数据模型。 在线交友网站可能允许用户上传图片以与其个人资料相关联。 招聘网站可能允许用户将其简历作为 Microsoft Word 或 PDF 文档上传。

使用二进制数据会增加一组新的挑战。 我们必须确定二进制数据在应用程序中的存储方式。 必须更新用于插入新记录的接口,以允许用户从其计算机上传文件,并且必须采取额外的步骤来显示或提供下载记录的关联二进制数据的方法。 在本教程和接下来的三个教程中,我们将探讨如何应对这些挑战。 在这些教程结束时,我们将构建一个功能齐全的应用程序,它将图片和 PDF 小册子与每个类别相关联。 在此特定教程中,我们将介绍用于存储二进制数据的不同技术,并探讨如何使用户能够从其计算机上传文件并将其保存在 Web 服务器的文件系统上。

注意

属于应用程序数据模型的二进制数据有时称为 BLOB,这是二进制大型 OBject 的首字母缩略词。 在这些教程中,我选择使用术语二进制数据,尽管术语 BLOB 是同义词。

步骤 1:创建使用二进制数据网页

在开始探索与添加二进制数据支持相关的挑战之前,让我们先花点时间在网站项目中创建本教程和接下来三个页面所需的 ASP.NET 页面。 首先添加名为 BinaryData的新文件夹。 接下来,将以下 ASP.NET 页添加到该文件夹,确保将每个页面与 Site.master 母版页相关联:

  • Default.aspx
  • FileUpload.aspx
  • DisplayOrDownloadData.aspx
  • UploadInDetailsView.aspx
  • UpdatingAndDeleting.aspx

为二进制 Data-Related 教程添加 ASP.NET 页

图 1:为二进制 Data-Related 教程添加 ASP.NET 页

与其他文件夹中一样, Default.aspx 文件夹中 BinaryData 会列出其部分中的教程。 回想一下, SectionLevelTutorialListing.ascx 用户控件提供了此功能。 因此,通过将用户控件从解决方案资源管理器拖动到Default.aspx页面设计视图中,将此用户控件添加到 。

将 SectionLevelTutorialListing.ascx 用户控件添加到 Default.aspx

图 2:将 SectionLevelTutorialListing.ascx 用户控件添加到 Default.aspx (单击以查看全尺寸图像)

最后,将这些页作为条目添加到文件中 Web.sitemap 。 具体而言,在增强 GridView <siteMapNode>之后添加以下标记:

<siteMapNode 
    title="Working with Binary Data" 
    url="~/BinaryData/Default.aspx" 
    description="Extend the data model to include collecting binary data.">
    
    <siteMapNode 
        title="Uploading Files" 
        url="~/BinaryData/FileUpload.aspx" 
        description="Examine the different ways to store binary data on the 
                     web server and see how to accept uploaded files from users 
                     with the FileUpload control." />
    <siteMapNode 
        title="Display or Download Binary Data" 
        url="~/BinaryData/DisplayOrDownloadData.aspx" 
        description="Let users view or download the captured binary data." />
    <siteMapNode 
        title="Adding New Binary Data" 
        url="~/BinaryData/UploadInDetailsView.aspx" 
        description="Learn how to augment the inserting interface to 
                     include a FileUpload control." />
    <siteMapNode 
        title="Updating and Deleting Existing Binary Data" 
        url="~/BinaryData/UpdatingAndDeleting.aspx" 
        description="Learn how to update and delete existing binary data." />
</siteMapNode>

更新 Web.sitemap后,请花点时间通过浏览器查看教程网站。 左侧菜单现在包含“使用二进制数据”教程的项。

站点地图现在包括使用二进制数据教程的条目

图 3:站点地图现在包含使用二进制数据教程的条目

步骤 2:确定二进制数据的存储位置

与应用程序数据模型关联的二进制数据可以存储在以下两个位置之一:在 Web 服务器的文件系统上,引用存储在数据库中的文件;或直接在数据库本身 (请参阅图 4) 。 每种方法都有自己的一组利弊,值得进行更详细的讨论。

二进制数据可以存储在文件系统上,也可以直接存储在数据库中

图 4:二进制数据可以存储在文件系统上或直接存储在数据库中 (单击以查看全尺寸图像)

假设我们要扩展 Northwind 数据库,以便将图片与每个产品相关联。 一种选择是将这些图像文件存储在 Web 服务器的文件系统上,并在表中记录路径 Products 。 使用此方法,我们也许会向 类型varchar(200)Products 的表添加一列ImagePath。 当用户上传 Chai 的图片时,该图片可能存储在 Web 服务器的文件系统中 ~/Images/Tea.jpg,其中 ~ 表示应用程序的物理路径。 也就是说,如果网站位于物理路径 C:\Websites\Northwind\~/Images/Tea.jpg 上,则 等效于 C:\Websites\Northwind\Images\Tea.jpg。 上传图像文件后,我们会更新表中的 Chai 记录 Products ,使其 ImagePath 列引用新图像的路径。 我们可以使用 ~/Images/Tea.jpg ,或者只是 Tea.jpg 如果我们决定将所有产品图像放置在应用程序 文件夹中 Images

在文件系统上存储二进制数据main优势包括:

  • 我们 稍后将看到,与通过文件系统处理数据相比,存储和检索直接存储在数据库中的二进制数据涉及的代码要多一点。 此外,为了使用户能够查看或下载二进制数据,必须向用户显示该数据的 URL。 如果数据驻留在 Web 服务器的文件系统上,则 URL 非常简单。 但是,如果数据存储在数据库中,则需要创建一个网页来检索并返回数据库中的数据。
  • 其他服务或应用程序(无法从数据库拉取数据)可能需要对二进制数据进行更广泛的访问。 例如,与每个产品关联的图像可能还需要通过 FTP 提供给用户,在这种情况下,我们希望将二进制数据存储在文件系统上。
  • 如果 二进制数据存储在文件系统上,则数据库服务器和 Web 服务器之间的需求和网络拥塞将小于直接存储在数据库中的二进制数据时的性能。

在文件系统上存储二进制数据的main缺点是它将数据与数据库分离。 如果从 Products 表中删除了记录,则不会自动删除 Web 服务器文件系统上的关联文件。 我们必须编写额外的代码来删除该文件,否则文件系统将变得杂乱无章,并出现未使用的孤立文件。 此外,在备份数据库时,我们必须确保在文件系统上备份关联的二进制数据。 将数据库移动到另一个站点或服务器也带来了类似的挑战。

或者,通过创建 类型的varbinary列,可以将二进制数据直接存储在 Microsoft SQL Server 2005 数据库中。 与其他可变长度数据类型一样,可以指定可在此列中保存的二进制数据的最大长度。 例如,若要保留最多 5,000 个字节,请使用 varbinary(5000); varbinary(MAX) 允许最大存储大小(约 2 GB)。

直接在数据库中存储二进制数据main优点是二进制数据和数据库记录之间的紧密耦合。 这大大简化了数据库管理任务,例如备份或将数据库移动到其他站点或服务器。 此外,删除记录会自动删除相应的二进制数据。 在数据库中存储二进制数据还有更微妙的好处。 有关更深入的讨论 ,请参阅使用 ASP.NET 2.0 直接在数据库中存储二进制文件

注意

在 Microsoft SQL Server 2000 及更早版本中,varbinary数据类型的最大限制为 8,000 个字节。 若要存储最多 2 GB 的二进制数据,image需要改用数据类型。 但是,随着 SQL Server 2005 中的添加MAXimage数据类型已被弃用。 它仍受支持以实现向后兼容性,但 Microsoft 已image宣布,将来的 SQL Server 版本中将删除该数据类型。

如果使用的是较旧的数据模型, image 则可能会看到数据类型。 Northwind 数据库 表 Categories 有一个 Picture 列,可用于存储类别图像文件的二进制数据。 由于 Northwind 数据库根位于 Microsoft Access 和早期版本的 SQL Server,因此此列的类型image为 。

对于本教程和接下来的三种方法,我们将使用这两种方法。 该 Categories 表已有一列 Picture 用于存储类别图像的二进制内容。 我们将添加一个额外的列 BrochurePath,用于在 Web 服务器的文件系统上存储 PDF 的路径,该路径可用于提供类别的打印质量的完善概述。

步骤 3:将BrochurePath列添加到Categories

目前,“类别”表只有四列: CategoryIDCategoryNameDescriptionPicture。 除了这些字段,我们需要添加一个新字段,该字段将指向类别的小册子 (如果存在) 。 若要添加此列,请转到服务器资源管理器,向下钻取到表,右键单击表 Categories 并选择“打开表定义” (请参阅图 5) 。 如果看不到服务器资源管理器,请从“视图”菜单中选择“服务器资源管理器”选项,或按 Ctrl+Alt+S 将其打开。

将名为 BrochurePath 和 允许 NULL 的新varchar(200)列添加到Categories表中,然后单击“保存”图标 (或按 Ctrl+S) 。

将 BrochurePath 列添加到类别表

图 5:向表添加 BrochurePathCategories (单击以查看全尺寸图像)

步骤 4:更新体系结构以使用PictureBrochurePath

CategoriesDataTable数据访问层 (DAL) 当前定义了四DataColumn个 :CategoryIDCategoryNameDescription、 和 NumberOfProducts。 当我们最初在 创建数据访问层 教程中设计此 DataTable 时, CategoriesDataTable 只有前三列;该 NumberOfProducts 列已添加到 Master/Detail 中使用项目符号主记录列表和 Details DataList 教程中。

创建数据访问层中所述,类型化数据集中的数据表构成业务对象。 TableAdapter 负责与数据库通信,并使用查询结果填充业务对象。 CategoriesDataTableCategoriesTableAdapter填充,它具有三种数据检索方法:

  • GetCategories()执行 TableAdapter main查询,并返回CategoryID表中所有记录的 CategoriesCategoryNameDescription 字段。 main查询是自动生成Insert的 和 Update 方法使用的内容。
  • GetCategoryByCategoryID(categoryID)返回类别等于 CategoryIDcategoryIDCategoryIDCategoryNameDescription 字段。
  • GetCategoriesAndNumberOfProducts()- 返回CategoryID表中所有记录的 CategoriesCategoryNameDescription 字段。 此外,使用子查询返回与每个类别关联的产品数。

请注意,这些查询均不返回Categories表或PictureBrochurePath列;CategoriesDataTable也不为这些字段提供 DataColumn 。 若要使用 Picture 和 BrochurePath 属性,需要先将它们添加到 , CategoriesDataTable 然后更新 CategoriesTableAdapter 类以返回这些列。

Picture添加 和BrochurePath``DataColumn

首先将这两列添加到 。CategoriesDataTable 右键单击 CategoriesDataTable 标题,从上下文菜单中选择“添加”,然后选择“列”选项。 这会在 DataTable 中创建名为 Column1的新 DataColumn 。 将此列重命名为 Picture。 在属性窗口中,将 s DataType 属性设置为 DataColumnSystem.Byte[] (这不是下拉列表中的选项;需要在) 中键入它。

创建 DataColumn 命名图片,其数据类型为 System.Byte[]

图 6:创建DataColumnSystem.Byte[]PictureDataType命名 (单击以查看全尺寸图像)

将另一个DataColumn添加到 DataTable,使用默认值 DataType (System.String) 命名。BrochurePath

Picture从 TableAdapter 返回 和BrochurePath

将这两 DataColumn 个 添加到 后 CategoriesDataTable,我们已准备好更新 CategoriesTableAdapter。 我们可以在 main TableAdapter 查询中返回这两个列值,但这会在每次调用 方法时GetCategories()返回二进制数据。 相反,让我们更新 main TableAdapter 查询,以返回BrochurePath并创建返回特定类别列Picture的其他数据检索方法。

若要更新main TableAdapter 查询,请CategoriesTableAdapter右键单击 标头,然后从上下文菜单中选择“配置”选项。 此时会显示表适配器配置向导,我们在以前的许多教程中已看到该向导。 更新查询以恢复 , BrochurePath 然后单击“完成”。

将 SELECT 语句中的列列表更新为同时返回宣传册路径

图 7:将 语句中的 SELECT 列列表更新为同时返回 BrochurePath (单击以查看全尺寸图像)

对 TableAdapter 使用即席 SQL 语句时,更新main查询中的列列表会更新 TableAdapter 中所有查询方法的SELECT列列表。 这意味着 方法 GetCategoryByCategoryID(categoryID) 已更新以返回 BrochurePath 列,这可能是我们想要的。 但是,它还更新了 方法中的 GetCategoriesAndNumberOfProducts() 列列表,删除了返回每个类别的产品数的子查询! 因此,我们需要更新此方法查询 SELECT 。 右键单击 GetCategoriesAndNumberOfProducts() 方法,选择“配置”,然后将查询还原SELECT回其原始值:

SELECT CategoryID, CategoryName, Description, 
       (SELECT COUNT(*) 
            FROM Products p 
            WHERE p.CategoryID = c.CategoryID) 
       as NumberOfProducts
FROM Categories c

接下来,创建一个新的 TableAdapter 方法,该方法返回特定类别的 Picture 列值。 右键单击 CategoriesTableAdapter 标头,然后选择“添加查询”选项以启动 TableAdapter 查询配置向导。 此向导的第一步询问我们是否要使用即席 SQL 语句、新的存储过程或现有存储过程来查询数据。 选择“使用 SQL 语句”,然后单击“下一步”。 由于我们将返回行,因此请从第二步中选择 SELECT 以返回行选项。

选择“使用 SQL 语句”选项

图 8:选择“使用 SQL 语句”选项 (单击以查看全尺寸图像)

由于查询将从类别表返回记录,请选择返回行的 SELECT

图 9:由于查询将返回类别表中的记录,请选择返回行的 SELECT (单击 以查看全尺寸图像)

在第三步中,输入以下 SQL 查询并单击“下一步”:

SELECT     CategoryID, CategoryName, Description, BrochurePath, Picture
FROM       Categories
WHERE      CategoryID = @CategoryID

最后一步是选择新方法的名称。 分别将 FillCategoryWithBinaryDataByCategoryIDGetCategoryWithBinaryDataByCategoryID 用于填充 DataTable 和返回 DataTable 模式。 单击“完成”以完成向导。

选择 TableAdapter 方法的名称

图 10:选择 TableAdapter 方法的名称 (单击以查看全尺寸图像)

注意

完成表适配器查询配置向导后,可能会看到一个对话框,通知你新的命令文本返回具有不同于main查询架构的数据。 简言之,向导指出,TableAdapter main 查询GetCategories()返回的架构与刚创建的架构不同。 但这就是我们想要的,因此你可以忽略此消息。

此外,请记住,如果使用即席 SQL 语句并使用向导在以后某个时间点更改 TableAdapter main查询,它将修改GetCategoryWithBinaryDataByCategoryID方法 s SELECT 语句的列列表,使其仅包含main查询 (即,它将从查询) 中删除列Picture。 必须手动更新列列表才能返回列 Picture ,这类似于我们在此步骤前面对 GetCategoriesAndNumberOfProducts() 方法执行的操作。

将两DataColumnCategoriesDataTable 添加到 和 GetCategoryWithBinaryDataByCategoryID 方法CategoriesTableAdapter后,类型化数据集Designer中的这些类应类似于图 11 中的屏幕截图。

数据集Designer包括新列和方法

图 11:DataSet Designer包括新列和方法

更新业务逻辑层 (BLL)

更新 DAL 后,剩下的只是扩充业务逻辑层 (BLL) 以包含新 CategoriesTableAdapter 方法的方法。 将以下方法添加到 CategoriesBLL 类:

[System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Select, false)] 
public Northwind.CategoriesDataTable 
    GetCategoryWithBinaryDataByCategoryID(int categoryID)
{
    return Adapter.GetCategoryWithBinaryDataByCategoryID(categoryID);
}

步骤 5:将文件从客户端上传到 Web 服务器

收集二进制数据时,通常此数据由最终用户提供。 若要捕获此信息,用户需要能够将文件从其计算机上传到 Web 服务器。 然后,上传的数据需要与数据模型集成,这可能意味着将文件保存到 Web 服务器的文件系统,并在数据库中添加文件路径,或者将二进制内容直接写入数据库中。 在此步骤中,我们将了解如何允许用户将文件从其计算机上传到服务器。 在下一教程中,我们将把注意力转向将上传的文件与数据模型集成。

ASP.NET 2.0 s 新的 FileUpload Web 控件 为用户提供了一种机制,使用户能够将文件从其计算机发送到 Web 服务器。 FileUpload 控件呈现为一个 <input> 元素,其 type 属性设置为 文件,浏览器将显示为带有“浏览”按钮的文本框。 单击“浏览”按钮会显示一个对话框,用户可以从中选择文件。 当表单发回时,所选文件的内容随回发一起发送。 在服务器端,可以通过 FileUpload 控件属性访问上传的文件的相关信息。

若要演示如何上传文件,请FileUpload.aspx打开 文件夹中的页面BinaryData,将“工具箱”中的 FileUpload 控件拖到Designer,然后将控件的 ID 属性设置为 UploadTest。 接下来,分别将按钮 Web 控件和Text属性添加到 IDUploadButton 并上传所选文件。 最后,将标签 Web 控件放在 Button 下面,清除其 Text 属性并将其属性设置为 IDUploadDetails

将 FileUpload 控件添加到 ASP.NET 页

图 12:将 FileUpload 控件添加到 ASP.NET 页 (单击以查看全尺寸图像)

图 13 显示了通过浏览器查看时的此页面。 请注意,单击“浏览”按钮会显示文件选择对话框,允许用户从其计算机中选取文件。 选择文件后,单击“上传所选文件”按钮会导致回发,将所选文件的二进制内容发送到 Web 服务器。

用户可以选择要从其计算机上传到服务器的文件

图 13:用户可以选择要从其计算机上传到服务器的文件 (单击以查看全尺寸图像)

回发时,上传的文件可以保存到文件系统,也可以通过Stream直接处理其二进制数据。 对于此示例,让我们创建一个 ~/Brochures 文件夹,并将上传的文件保存到该文件夹。 首先,将 Brochures 文件夹作为根目录的子文件夹添加到站点。 接下来,为 UploadButton 事件 Click 创建事件处理程序,并添加以下代码:

protected void UploadButton_Click(object sender, EventArgs e)
{
    if (UploadTest.HasFile == false)
    {
        // No file uploaded!
        UploadDetails.Text = "Please first select a file to upload...";            
    }
    else
    {
        // Display the uploaded file's details
        UploadDetails.Text = string.Format(
                @"Uploaded file: {0}<br />
                  File size (in bytes): {1:N0}<br />
                  Content-type: {2}", 
                  UploadTest.FileName, 
                  UploadTest.FileBytes.Length,
                  UploadTest.PostedFile.ContentType);
        // Save the file
        string filePath = 
            Server.MapPath("~/Brochures/" + UploadTest.FileName);
        UploadTest.SaveAs(filePath);
    }
}

FileUpload 控件提供了用于处理上传数据的各种属性。 例如, HasFile 属性 指示文件是否由用户上传,而 FileBytes 属性 提供对上传的二进制数据作为字节数组的访问。 事件处理程序 Click 首先确保已上传文件。 如果文件已上载,则标签会显示已上传文件的名称、其大小(以字节为单位)及其内容类型。

注意

若要确保用户上传文件,可以检查 HasFile 属性并显示警告(false如果是 ),也可以改用 RequiredFieldValidator 控件

FileUpload 将 SaveAs(filePath) 上传的文件保存到指定的 filePathfilePath 必须是 () C:\Websites\Brochures\SomeFile.pdf的物理路径,而不是 () /Brochures/SomeFile.pdf的虚拟路径。 方法Server.MapPath(virtPath)采用虚拟路径并返回其相应的物理路径。 此处,虚拟路径为 ~/Brochures/fileName,其中 fileName 是已上传文件的名称。 有关虚拟路径和物理路径和使用 Server.MapPath的详细信息,请参阅 Server.MapPath 方法

完成 Click 事件处理程序后,请花点时间在浏览器中测试页面。 单击“浏览”按钮,从硬盘驱动器中选择一个文件,然后单击“上传所选文件”按钮。 回发会将所选文件的内容发送到 Web 服务器,然后 Web 服务器会在将文件保存到 ~/Brochures 文件夹之前显示有关该文件的信息。 上传文件后,返回到 Visual Studio,然后单击解决方案资源管理器中的“刷新”按钮。 你应该会在 ~/Brochures 文件夹中看到刚刚上传的文件!

文件 EvolutionValley.jpg 已上传到 Web 服务器

图 14:文件 EvolutionValley.jpg 已上传到 Web 服务器 (单击以查看全尺寸图像)

EvolutionValley.jpg 已保存到 ~/宣传册文件夹

图 15EvolutionValley.jpg 已保存到 ~/Brochures 文件夹

将上传的文件保存到文件系统的细微之处

将文件保存到 Web 服务器的文件系统时,必须解决几个微妙的问题。 首先,存在安全问题。 若要将文件保存到文件系统,执行 ASP.NET 页的安全上下文必须具有写入权限。 ASP.NET 开发 Web 服务器在当前用户帐户的上下文中运行。 如果使用 Microsoft Internet Information Services (IIS) 作为 Web 服务器,则安全上下文取决于 IIS 的版本及其配置。

将文件保存到文件系统的另一个难题是命名文件。 目前,我们的页面将使用与客户端计算机上的文件相同的名称将所有 ~/Brochures 上传的文件保存到目录中。 如果用户 A 上传了名为 Brochure.pdf的小册子,则该文件将保存为 ~/Brochure/Brochure.pdf。 但是,如果稍后用户 B 上传了另一个宣传册文件,该文件的文件名恰好 (Brochure.pdf) ,该怎么办? 使用我们现在拥有的代码,用户 A 文件将被用户 B 上传覆盖。

有多种方法可用于解决文件名冲突。 一种选择是禁止上传文件(如果已经存在同名的文件)。 使用此方法时,当用户 B 尝试上传名为 Brochure.pdf的文件时,系统不会保存其文件,而是显示一条消息,通知用户 B 重命名该文件并重试。 另一种方法是使用唯一文件名保存文件,该名称可以是 全局唯一标识符 (GUID) ,也可以是相应数据库记录主键列的值, () (假设上传与数据模型中的特定行相关联) 。 在下一教程中,我们将更详细地探讨这些选项。

涉及大量二进制数据的挑战

这些教程假定捕获的二进制数据大小适中。 使用数兆字节或更大的大量二进制数据文件会带来超出这些教程范围的新挑战。 例如,默认情况下,ASP.NET 将拒绝超过 4 MB 的上传,不过这可以通过 中的 Web.config元素进行配置<httpRuntime>。 IIS 也施加了自己的文件上传大小限制。 此外,上传大型文件所需的时间可能超过默认的 110 秒,ASP.NET 将等待请求。 使用大型文件时,还会出现内存和性能问题。

FileUpload 控件对于大型文件上传不切实际。 当文件内容发布到服务器时,最终用户必须耐心等待,而不会确认其上传正在进行。 在处理可在几秒钟内上传的较小文件时,与其说是一个问题,不如说是处理可能需要几分钟上传的大型文件时出现问题。 有各种第三方文件上传控件更适合处理大型上传,其中许多供应商提供进度指示器和 ActiveX 上传管理器,以提供更完善的用户体验。

如果应用程序需要处理大型文件,则需要仔细调查挑战,并找到适合特定需求的解决方案。

总结

生成需要捕获二进制数据的应用程序会带来许多挑战。 在本教程中,我们探讨了前两个因素:确定存储二进制数据的位置,以及允许用户通过网页上传二进制内容。 在接下来的三个教程中,我们将了解如何将上传的数据与数据库中的记录相关联,以及如何在文本数据字段旁边显示二进制数据。

编程快乐!

深入阅读

有关本教程中讨论的主题的详细信息,请参阅以下资源:

关于作者

斯科特·米切尔是七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自 1998 年以来一直在使用 Microsoft Web 技术。 Scott 担任独立顾问、培训师和作家。 他的最新一本书是 山姆斯在 24 小时内 ASP.NET 2.0。 可以在 上mitchell@4GuysFromRolla.com联系他,也可以通过他的博客(可在 中找到http://ScottOnWriting.NET)。

特别感谢

本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者是 Teresa Murphy 和 Bernadette Leigh。 有兴趣查看我即将发布的 MSDN 文章? 如果是,请在 处mitchell@4GuysFromRolla.com放置一行。