.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);