首页 > 代码库 > 文件和流(使用流读写文件)

文件和流(使用流读写文件)

  .NET Framework 在框架的多个领域里使用了流模型。流是允许你用相似的方式(作为顺序字节流)对待不同数据源的一种抽象。所有 .NET 流类从 System.IO.Stream 类继承。

       流可以代表内存缓冲器中的数据、从网络连接获得的数据、从文件获得的或要写入文件的数据。

       下面这段代码演示了如何创建一个新文件并用 FileStream 写入一个字节数组:

FileStream fileStream = null;
try
{
    fileStream = new FileStream(filename, FileMode.Create);
    fileStream.Write(bytes, 0, bytes.Length - 1);
}
finally
{
    if (fileStream!=null)
    {
        fileStream.Close();
    }        
}

       这段代码演示了如何打开一个 FileStream 并把它的内容读入字节数组:

FileStream fileStream = null;
try
{
    fileStream = new FileStream(filename, FileMode.Open);
    byte[] dataArray = new byte[fileStream.Length];
    for (int i = 0; i < fileStream.Length; i++)
    {
        dataArray[i] = (byte)fileStream.ReadByte();
    }
}
finally
{
    if (fileStream!=null)
    {
        fileStream.Close();
    }        
}

       就其本身而言,流不太有用,因为它们完全以单个字节或字节数组的形式工作。

       一定要记得关闭流,它会释放文件句柄并允许其他人访问文件。此外,因为 FileStream 类使可释放的,所以建议在 using 语句块中使用,这就保证了块结束时 FileStream 立即被关闭。

 

FileMode 枚举值:

Append如果文件存在,就打开文件并找到文件尾,否则创建一个新文件
Create指定由操作系统创建一个新文件,如果文件存在,就覆盖它
CreateNew指定由操作系统创建一个新文件,如果文件存在,就抛出一个 IOException 异常
Open指定由操作系统打开一个现有的文件
OpenOrCreate如果文件已存在,就由操作系统打开它,否则,创建一个新文件
Truncate指定由操作系统打开一个现有的文件,打开后,文件被截断至 0 字节

 

文本文件

       你可以用 System.IO 命名空间中的 StreamWriter 和 StreamReader 类读写文件的内容。创建这些类时,只需要把底层的流作为构造函数的参数传入:

FileStream fileStream = new FileStream(@"c:\myfile.txt", FileMode.Create);
StreamWriter w = new StreamWriter(fileStream);

       你还可以使用 File 类和 FileInfo 类的静态方法,如 CreateText()或 OpenText()得到一个 StreamWriter 或 StreamReader 对象:

StreamWriter w = File.CreateText(@"c:\myfile.txt");

       这段代码和前面的示例等效。

 

       .NET 在 System.Text 命名空间里为每种编码方式提供了一个类。使用 StreamWriter 和 StreamReader 时,可以在构造函数参数中指定要使用的编码,或者直接使用默认的 UTF-8 编码:

FileStream fileStream = new FileStream(@"c:\myfile.txt", FileMode.Create);
StreamWriter w = new StreamWriter(fileStream, System.Text.Encoding.ASCII);

       结束文件处理时,必须保证把它关闭。否则,更新可能不会正确写到磁盘上,文件锁定不能被打开。在任意时刻都可以调用 Flush()确保所有的数据都写到了磁盘上,因为 StreamWriter 为了优化性能会在内存中缓存你的数据。

 

提示:

       还可以用 ReadToEnd()方法读取整个文件的内容,它返回一个字符串。File 类还有一些快捷方法,如静态方法 ReadAllText()和 ReadAllBytes(),但它们只适用于小型文件。大型文件不该一次读入内存,而是应该使用 FileStream 一次读取一部分内容来减轻内存负载。

 

二进制文件

       二进制数据更有效的利用了空间,但创建的文件不可读(基本读不懂)。要打开用二进制写的文件,需要创建一个新的 BinaryWriter :

// BinaryWriter 的构造函数接受一个流作为参数
// 可以手工创建,也可以用 File 类的静态方法获得
BinaryWriter w = new BinaryWriter(File.OpenWrite(@"c:\binaryfile.bin"));

       .NET 关注流对象,而不是数据源或数据目标。也就是说,你可以用相同的代码把二进制数据写入任意类型的流,无论他是一个文件还是其他存储介质

       遗憾的是,二进制流在读取数据时,必须知道要获取的数据类型:

BinaryReader r = new BinaryReader(File.OpenRead(@"c:\binaryfile.bin"));
string str = r.ReadString();
int integer = r.ReadInt32();

 

上传文件

       ASP.NET 有两个控件可以让用户把文件上传到 Web 服务器。服务器接收到上传文件的数据后,你的应用程序就可以确定是查看、忽略还是保存到后端数据库或者 Web 服务器的文件系统中。

       允许上传的控件是 HtmlInputFile(HMTL 服务器控件)和 FileUpload(ASP.NET Web 控件)。两者都代表 <input type=‘file‘> HTML 标签。唯一真正的差别是 FileUpload 控件自动设置表单的编码,把它设置为 multipart/form 数据。如果你使用 HtmlInputFile 控件就必须手动设置 <form> 标签的这个特性,如果未设置,HtmlInputFile 控件就不能工作。

       通常会在页面上添加一个 Button 控件来回送页面,看下面的示例:

protected void btnUpload_Click(object sender, EventArgs e)
{
    if (Uploader.PostedFile.ContentLength != 0)
    {
        try
        {
            if (Uploader.PostedFile.ContentLength > 1048576)
            {
                lblStatus.Text = "Too large. This file is not allowed.";
            }
            else
            {
                string destDir = Server.MapPath("~/Upload");
                string fileName = Path.GetFileName(Uploader.PostedFile.FileName);
                string destPath = Path.Combine(destDir, fileName);
                Uploader.PostedFile.SaveAs(destPath);
                lblStatus.Text = "Thank you for submitting your file.";
            }
        }
        catch (Exception err)
        {
            lblStatus.Text = err.Message;
        }
    }
}

 

       除了把直接上传的文件保存到磁盘外,还可以通过流模型与其交互。需要借助 FileUpload.PostedFile.InputStream 属性获得对数据的访问:

// 假设这个文件是基于文本的
StreamReader r = new StreamReader(Uploader.PostedFile.InputStream);
lblStatus.Text = r.ReadToEnd();
r.Close();

 

提示:

       默认情况下,允许上传的最大文件是 4MB。如果试图上传一个更大的文件,会得到一个运行时错误。可以修改 web.config 文件中 <httpRuntime> 设置的 maxRequestLength 特性。这个设置以字节为单位:<httpRuntime maxRequestLength="8192" > 即 8MB。

 

使文件对多用户安全

       虽然很容易就可以创建一个唯一的文件名,但如果不得不在多个不同的请求间访问同一个文件,会发生什么呢?

       一个办法是用共享方式打开文件,这样将会允许多个进程同时访问同一个文件。要使用这一技术,你必须使用一个接收 4 个参数的 FileStream 构造函数,它允许你选择 FileMode:

FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read);

       这条语句允许多个用户同时打开文件来读。不过,没有人能更新该文件。可以指定不同的 FileAccess 值让多个用户以读-写模式打开文件。此时,当你写文件时,Windows 会动态锁定文件的一小部分(或者你可以用 FileStream.Lock()方法锁定文件某一字节范围内的部分),如果两个用户试图同时写锁定的部分,会产生一个异常。Web 应用程序有高度并发性的需求,所有不推荐使用这项技术,而且它的实现非常困难,它还迫使你使用低层次的字节偏移计算,这很容易产生细小而扰人的错误。

 

提示:

       另一项技术在多用户需要访问同一数据时非常有效,尤其是数据被频繁使用且不是特别大的时候,就是把数据加载到缓存。这样,多个用户可以毫无顾忌的同时访问数据,如果另一个进程负责创建或定期更新文件,在文件变更的时候可以使用文件依赖来使缓存失效。

 

       那么多个用户必须同时更新文件,解决方案是什么呢?

  • 办法一:为每个请求创建一个单独的用户特定的文件
  • 办法二:把文件绑定到另一个对象并使用锁定。

1. 创建唯一的文件名      

       为避免冲突,可以为每个用户创建一个目录或者给文件名添加一些信息,如时间戳、GUID(全球唯一标识符)或者随机数。

private string GetFileName()
{
    string fileName = "user." + Guid.NewGuid().ToString();
 
    // 获取当前正在执行的服务器应用程序的根目录的物理文件系统路径。
    return Path.Combine(Request.PhysicalApplicationPath, fileName);
}

注解:

       GUID 是一个 128 位整数。GUID 对程序非常有用,因为它们从统计学的角度来说是唯一的,因此广泛运用于唯一标识的队列任务、用户会话及其他动态信息。相对数字序列,它们还有不易猜测的优点。GUID 通常用一组小写的十六进制数字字符串表示。

 

       使用 GetFileName()就可以创建一个更为安全的日志程序,在本示例中,所有日志通过调用 Log()方法来记录:

private void Log(string message)
{
    FileMode mode;
    if (ViewState["LogFile"] == null)
    {
        ViewState["LogFile"] = GetFileName();
        mode = FileMode.Create;
    }
    else
    {
        mode = FileMode.Append;
    }
 
    string fileName = ViewState["LogFile"].ToString();
    using (FileStream fs = new FileStream(fileName, mode))
    {
        StreamWriter w = new StreamWriter(fs);
        w.WriteLine(DateTime.Now);
        w.WriteLine(message);
        w.WriteLine();
        w.Close();
    }
}

       每次加载页面时都会记录一条日志信息:

protected void Page_Load(object sender, EventArgs e)
{
    if (Page.IsPostBack)
    {
        Log("Page posted back.");
    }
    else
    {
        Log("Page loaded for the first time.");
    }
}

       最后是两个按钮事件,允许删除日志文件或者显示它的内容:

protected void btnRead_Click(object sender, EventArgs e)
{
    if (ViewState["LogFile"] != null)
    {
        StringBuilder log = new StringBuilder();
        string fileName = ViewState["LogFile"].ToString();
        using (FileStream fs = new FileStream(fileName, FileMode.Open))
        {
            StreamReader r = new StreamReader(fs);
            string line;
            do
            {
                line = r.ReadLine();
                if (line != null)
                {
                    log.Append(line + "<br />");
                }
            } while (line != null);
            r.Close();
        }
        lblInfo.Text = log.ToString();
    }
    else
    {
        lblInfo.Text = "There is no log file";
    }
}
 
protected void btnDelete_Click(object sender, EventArgs e)
{
    if (ViewState["LogFile"] != null)
    {
        File.Delete(ViewState["LogFile"].ToString());
        ViewState["LogFile"] = null;
    }
}

image

 

2. 锁定文件访问对象

       有些情况你却是需要响应多个用户活动而更新同一个文件。一个办法是使用锁。基本的技术就是为所有获取数据的任务创建一个单独的类。一旦定义了这个类,就可以为该类创建一个全局的实例并把它加入到 Application 集合。现在,可以用 C# 的 lock 语句来确保每次只有一个线程可以访问这个对象。

       例如,假设你设了如下的 Logger 类:

public class Logger
{
    public void LogMessage()
    {
        lock (this)
        {
            // Open file and update it.
        }       
    }
}

       Logger 对象在访问日志文件,创建临界区之前将自身锁定,这就保证了每次只能有一个线程可以执行 LogMessage()代码,从而消除了文件的冲突。

       不过,要让这一方式起效,你必须保证所有的类都使用 Logger 对象的同一个实例。有好几个选择:

  • 响应 global.asax 的 HttpApplication.Start 事件创建一个 Logger 类实例并保存到 Application 集合中。
  • 在 global.asax 中添加下述代码来通过一个静态变量公开一个 Logger 实例。
private static Logger log = new Logger();
public Logger Log
{
    get { return log; }
}

       现在,任何使用 Logger 调用 LogMessage()的页面都会得到一个排它的访问:

Application.Log.LogMessage(myMessage);

       要记住的是,这种方式只是对文件系统先天局限性的一种拙劣补偿,它不会允许你管理更加复杂的任务。如让每个用户同时读写同一文件的片段,此外文件被某个客户端锁住时,其他请求不得不等待。这肯定会降低应用程序的性能。这项技术仅适用于小型 Web 应用程序。也正是基于这样的原因,ASP.NET 应用程序几乎从不使用基于文件的日志,相反,它们把日志写在 Windows 事件日志或数据库里。

 

压缩

       .NET 支持在任何流中压缩数据,这一技巧允许你压缩写入任意文件的数据。这一支持来自 System.IO.Compression 命名空间的 GZipStream 和 DeflateStream 类。这两个类都提供相似的高效无损压缩算法。

       要使用压缩,必须把真实的流包装到某个压缩流中。例如可以包装一个 FileStream(写入磁盘时将其压缩)或 MemoryStream(为了压缩内存中的数据)。使用 MemoryStream 时,可以在数据存入数据库的某个二进制字段前或者在把数据传送给 Web 服务前对其进行压缩。

       假设你希望压缩保存到文件的数据:

FileStream fs = new FileStream(fileName, FileMode.Create);
 
// CompressionMode.Compress 枚举指定是压缩还是解压
GZipStream compressStream = new GZipStream(fs, CompressionMode.Compress);
 
// 写入真是的数据时,要使用压缩流的 Write(),而不是 FileStream 的 Write()
// 如果要使用更高层次的写入器,可以提供一个压缩流代替 FileStream
StreamWriter w = new StreamWriter(compressStream);
w.WriteLine();
w.Flush();
fs.Close();

       读文件很简单。差别在于枚举值的选择:

FileStream fs = new FileStream(fileName, FileMode.Open);
GZipStream decompressStream = new GZipStream(fs, CompressionMode.Decompress);
StreamReader r = new StreamReader(decompressStream);

文件和流(使用流读写文件)