首页 > 代码库 > 文件和流(使用流读写文件)
文件和流(使用流读写文件)
.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;
}
}
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);
文件和流(使用流读写文件)