1. 程式人生 > 實用技巧 >C# Stream篇(四) -- FileStream

C# Stream篇(四) -- FileStream

FileStream

目錄:

如何去理解FileStream?

通過前3章的學習相信大家對於Stream已經有一定的瞭解,但是又如何去理解FileStream呢?請看下圖  

我們磁碟的中任何檔案都是通過2進位制組成,最為直觀的便是記事本了,當我們新建一個記事本時,它的大小是0KB, 我們每次輸入一個數字或

字母時檔案便會自動增大4kb,可見隨著我們輸入的內容越來越多,檔案也會相應增大,同理當我們刪除檔案內容時,檔案也會相應減小,對了,

聰明的你肯定會問:誰將內容以怎麼樣的形式放到檔案中去了?好問題,還記得第一篇流的概念麼?對了,真實世界的一群魚可以通過河流來

往於各個地方,FileStream也是一樣,byte可以通過FileStream進行傳輸,這樣我們便能在計算機上對任何檔案進行一系列的操作了。

FileStream 的重要性

FileStream 顧名思義檔案流,我們電腦上的檔案都可以通過檔案流進行操作,例如檔案的複製,剪下,貼上,刪除, 本地檔案上傳,下載,等許

多重要的功能都離不開檔案流,所以檔案流不僅在本機上非常重要,在如今的網路世界也是萬萬不能缺少的,想象一下我們開啟虛機後,直接從本

地複製一個檔案到虛機上,是多麼方便,如果沒有檔案流,這個將難以想象。(大家別誤解,檔案流無法直接通過網路進行傳輸,而是

通過網路流將客戶端上傳的檔案傳到伺服器端接收,然後通過檔案流進行處理,下載正好相反)

FileStream 常用建構函式介紹(可能理解上有點複雜,請大家務必深刻理解)

*1: FileStream(SafeFileHandle, FileAccess)

非託管引數SafeFileHandle簡單介紹

SafeFileHandle :是一個檔案安全控制代碼,這樣的解釋可能大家一頭霧水,

別急,大家先不要去理睬這深邃的含義,只要知道這個型別是c#非託管資源,

也就是說它能夠呼叫非託管資源的方法,而且不屬於c#回收機制,所以我們必須

使用GC手動或其他方式(Finalize 或Dispose方法)進行非託管資源的回收,所以

SafeFileHandle是個默默無聞的保鏢 ,一直暗中保護FileStream和檔案的安全

為了能讓大家更好的理解這個保鏢,請看第一段程式碼:

會什麼會報錯呢?其實程式被卡在 Console.ReadLine()這裡,FileStream並沒有

被釋放,系統不知道這個檔案是否還有用﹐所以幫我們保護這個檔案

(那個非託管資源SafeFileHandle所使用的記憶體還被程式佔用著)

所以SafeFileHandled 在內部保護了這個檔案從而報出了這個異常

如果我們將流關閉後,這個問題也就不存在了

可以看見stream.SafeFileHandle的IsClose屬性變成true了,也就是說這時候可以安全的刪除檔案了

所以又回到了一個老問題上面,我們每次使用完FileStream後都必須將他關閉並釋放資源

*2: FileStream(String, FileMode)

String 引數表示檔案所在的地址,FIleMode是個列舉,表示確定如何開啟或建立檔案。

FileMode列舉引數包含以下內容:

成員名稱

說明

Append

開啟現有檔案並查詢到檔案尾,或建立新檔案。FileMode.Append 只能同 FileAccess.Write 一起使用。

Create

指定作業系統應建立新檔案。如果檔案已存在,它將被改寫。這要求 FileIOPermissionAccess.Write。

System.IO.FileMode.Create 等效於這樣的請求:如果檔案不存在,則使用 CreateNew;否則使用 Truncate。

CreateNew

指定作業系統應建立新檔案。此操作需要 FileIOPermissionAccess.Write。如果檔案已存在,則將引發 IOException。

Open

指定作業系統應開啟現有檔案。開啟檔案的能力取決於 FileAccess 所指定的值。如果該檔案不存在,

則引發 System.IO.FileNotFoundException。

OpenOrCreate

指定作業系統應開啟檔案(如果檔案存在);否則,應建立新檔案。如果用 FileAccess.Read 開啟檔案,則需要

FileIOPermissionAccess.Read。如果檔案訪問為 FileAccess.Write 或 FileAccess.ReadWrite,則需要

FileIOPermissionAccess.Write。如果檔案訪問為 FileAccess.Append,則需要 FileIOPermissionAccess.Append。

Truncate

指定作業系統應開啟現有檔案。檔案一旦開啟,就將被截斷為零位元組大小。此操作需要 FileIOPermissionAccess.Write。

試圖從使用 Truncate 開啟的檔案中進行讀取將導致異常。

*3: FileStream(IntPtr, FileAccess, Boolean ownsHandle)

FileAccess 引數也是一個列舉, 表示對於該檔案的操作許可權

ReadWrite

對檔案的讀訪問和寫訪問。可從檔案讀取資料和將資料寫入檔案

Write

檔案的寫訪問。可將資料寫入檔案。同 Read組合即構成讀/寫訪問權

Read

對檔案的讀訪問。可從檔案中讀取資料。同 Write組合即構成讀寫訪問權

引數ownsHandle:也就是類似於前面和大家介紹的SafeFileHandler,有2點必須注意:

1對於指定的檔案控制代碼,作業系統不允許所請求的 access,例如,當 access 為 Write 或 ReadWrite 而檔案控制代碼設定為只讀訪問時,會報出異常。

所以 ownsHandle才是老大,FileAccess的許可權應該在ownsHandle的範圍之內

2. FileStream 假定它對控制代碼有獨佔控制權。當 FileStream 也持有控制代碼時,讀取、寫入或查詢可能會導致資料破壞。為了資料的安全,請使用

控制代碼前呼叫 Flush,並避免在使用完控制代碼後呼叫 Close 以外的任何方法。

*4: FileStream(String, FileMode, FileAccess, FileShare)

FileShare:同樣是個列舉型別:確定檔案如何由程序共享。  

Delete

允許隨後刪除檔案。

Inheritable

使檔案控制代碼可由子程序繼承。Win32 不直接支援此功能。

None

謝絕共享當前檔案。檔案關閉前,開啟該檔案的任何請求(由此程序或另一程序發出的請求)都將失敗。

Read

允許隨後開啟檔案讀取。如果未指定此標誌,則檔案關閉前,任何開啟該檔案以進行讀取的請求(由此程序或另一程序發出的請求)都將失敗。但是,即使指定了此標誌,仍可能需要附加許可權才能夠訪問該檔案。

ReadWrite

允許隨後開啟檔案讀取或寫入。如果未指定此標誌,則檔案關閉前,任何開啟該檔案以進行讀取或寫入的請求(由此程序或另一程序發出)都將失敗。但是,即使指定了此標誌,仍可能需要附加許可權才能夠訪問該檔案。

Write

允許隨後開啟檔案寫入。如果未指定此標誌,則檔案關閉前,任何開啟該檔案以進行寫入的請求(由此程序或另一進過程發出的請求)都將失敗。但是,即使指定了此標誌,仍可能需要附加許可權才能夠訪問該檔案。

*5: FileStream(String, FileMode, FileAccess, FileShare, Int32, Boolean async )

Int32:這是一個緩衝區的大小,大家可以按照自己的需要定製,

Boolean async:是否非同步讀寫,告訴FileStream示例,是否採用非同步讀寫

*6: FileStream(String, FileMode, FileAccess, FileShare, Int32, FileOptions)

FileOptions:這是類似於FileStream對於檔案操作的高階選項

FileStream 常用屬性介紹

*1:CanRead :指示FileStream是否可以讀操作

*2:CanSeek:指示FileStream是否可以跟蹤查詢流操作

*3:IsAsync:FileStream是否同步工作還是非同步工作

*4:Name:FileStream的名字 只讀屬性

*5:ReadTimeout :設定讀取超時時間

*6:SafeFileHandle : 檔案安全控制代碼 只讀屬性

*7:position:當前FileStream所在的流位置

FileStream 常用方法介紹

以下方法重寫了Stream的一些虛方法(**這裡大家點選這裡可以參考第一篇來溫故下,這裡不再敘述)

1:IAsyncResult BeginRead 非同步讀取

2:IAsyncResult BeginWrite 非同步寫

3:void Close 關閉當前FileStream

4:void EndRead 非同步讀結束

5:void EndWrite 非同步寫結束

6:void Flush 立刻釋放緩衝區,將資料全部匯出到基礎流(檔案中)

7:int Read 一般讀取

8:int ReadByte 讀取單個位元組

9:long Seek 跟蹤查詢流所在的位置

10:void SetLength 設定FileStream的長度

11:void Write 一般寫

12:void WriteByte寫入單個位元組

屬於FileStream獨有的方法

*1:FileSecurity GetAccessControl()

這個不是很常用,FileSecurity 是檔案安全類,直接表達當前檔案的訪問控制列表(ACL)的符合當前檔案許可權的專案,ACL大家有個瞭解就行,以後會單獨和大家討論下ACL方面的知識

*2: void Lock(long position,long length)

這個Lock方法和執行緒中的Look關鍵字很不一樣,它能夠鎖住檔案中的某一部分,非常的強悍!用了這個方法我們能夠精確鎖定住我們需要鎖住的檔案的部分內容

*3: void SetAccessControl(FileSecurity fileSecurity)

和GetAccessControl很相似,ACL技術會在以後單獨介紹

*4: void Unlock (long position,long length)

正好和lock方法相反,對於檔案部分的解鎖

檔案的新建和拷貝(主要演示檔案同步和非同步操作)

首先我們嘗試DIY一個IFileConfig

    /// <summary>
/// 檔案配置介面
/// </summary>
public interface IFileConfig
{
string FileName { get; set; }
bool IsAsync { get; set; }
}

建立檔案配置類CreateFileConfig,用於新增檔案一些配置設定,實現新增檔案的操作

    /// <summary>
/// 建立檔案配置類
/// </summary>
public class CreateFileConfig : IFileConfig
{
// 檔名
public string FileName { get; set; }
//是否非同步操作
public bool IsAsync { get; set; }
//建立檔案所在url
public string CreateUrl { get; set; }
}

讓我們定義一個檔案流測試類:FileStreamTest 來實現檔案的操作

    /// <summary>
/// FileStreamTest 類
/// </summary>
public class FileStreamTest

在該類中實現一個簡單的Create方法用來同步或非同步的實現新增檔案,FileStream會根據配置類去選擇相應的建構函式,實現非同步或同步的新增方式

       /// <summary>
/// 新增檔案方法
/// </summary>
/// <param name="config"> 建立檔案配置類</param>
public void Create(IFileConfig config)
{
lock (_lockObject)
{
//得到建立檔案配置類物件
var createFileConfig = config as CreateFileConfig;
//檢查建立檔案配置類是否為空
if (this.CheckConfigIsError(config)) return;
//假設建立完檔案後寫入一段話,實際專案中無需這麼做,這裡只是一個演示
char[] insertContent = "HellowWorld".ToCharArray();
//轉化成 byte[]
byte[] byteArrayContent = Encoding.Default.GetBytes(insertContent, 0, insertContent.Length);
//根據傳入的配置檔案中來決定是否同步或非同步例項化stream物件
FileStream stream = createFileConfig.IsAsync ?
new FileStream(createFileConfig.CreateUrl, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, true)
: new FileStream(createFileConfig.CreateUrl, FileMode.Create);
using (stream)
{
// 如果不註釋下面程式碼會丟擲異常,google上提示是WriteTimeout只支援網路流
// stream.WriteTimeout = READ_OR_WRITE_TIMEOUT;
//如果該流是同步流並且可寫
if (!stream.IsAsync && stream.CanWrite)
stream.Write(byteArrayContent, 0, byteArrayContent.Length);
else if (stream.CanWrite)//非同步流並且可寫
stream.BeginWrite(byteArrayContent, 0, byteArrayContent.Length, this.End_CreateFileCallBack, stream);

stream.Close();
}
}
}

如果採用非同步的方式則最後會進入End_CreateFileCallBack回撥方法,result.AsyncState物件就是上圖stream.BeginWrite()方法的最後一個引數

還有一點必須注意的是每一次使用BeginWrite()方法事都要帶上EndWrite()方法,Read方法也一樣

        /// <summary>
/// 非同步寫檔案callBack方法
/// </summary>
/// <param name="result">IAsyncResult</param>
private void End_CreateFileCallBack(IAsyncResult result)
{
//從IAsyncResult物件中得到原來的FileStream
var stream = result.AsyncState as FileStream;
//結束非同步寫

Console.WriteLine("非同步建立檔案地址:{0}", stream.Name);
stream.EndWrite(result);
Console.ReadLine();
}

檔案複製的方式思路比較相似,首先定義複製檔案配置類,由於在非同步回撥中用到該配置類的屬性,所以新增了檔案流物件和相應的位元組陣列

    /// <summary>
/// 檔案複製
/// </summary>
public class CopyFileConfig : IFileConfig
{
// 檔名
public string FileName { get; set; }
//是否非同步操作
public bool IsAsync { get; set; }
//原檔案地址
public string OrginalFileUrl { get; set; }
//拷貝目的地址
public string DestinationFileUrl { get; set; }
//檔案流,非同步讀取後在回撥方法內使用
public FileStream OriginalFileStream { get; set; }
//原檔案位元組陣列,非同步讀取後在回撥方法內使用
public byte[] OriginalFileBytes { get; set; }
}

然後在FileStreamTest 類中新增一個Copy方法實現檔案的複製功能

        /// <summary>
/// 複製方法
/// </summary>
/// <param name="config">拷貝檔案複製</param>
public void Copy(IFileConfig config)
{
lock (_lockObject)
{
//得到CopyFileConfig物件
var copyFileConfig = config as CopyFileConfig;
// 檢查CopyFileConfig類物件是否為空或者OrginalFileUrl是否為空
if (CheckConfigIsError(copyFileConfig) || !File.Exists(copyFileConfig.OrginalFileUrl)) return;
//建立同步或非同步流
FileStream stream = copyFileConfig.IsAsync ?
new FileStream(copyFileConfig.OrginalFileUrl, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true)
: new FileStream(copyFileConfig.OrginalFileUrl, FileMode.Open);
//定義一個byte陣列接受從原檔案讀出的byte資料
byte[] orignalFileBytes = new byte[stream.Length];
using (stream)
{
// stream.ReadTimeout = READ_OR_WRITE_TIMEOUT;
//如果非同步流
if (stream.IsAsync)
{
//將該流和讀出的byte[]資料放入配置類,在callBack中可以使用
copyFileConfig.OriginalFileStream = stream;
copyFileConfig.OriginalFileBytes = orignalFileBytes;
if (stream.CanRead)
//非同步開始讀取,讀完後進入End_ReadFileCallBack方法,該方法接受copyFileConfig引數
stream.BeginRead(orignalFileBytes, 0, orignalFileBytes.Length, End_ReadFileCallBack, copyFileConfig);
}
else//否則同步讀取
{
if (stream.CanRead)
{
//一般讀取原檔案
stream.Read(orignalFileBytes, 0, orignalFileBytes.Length);
}
//定義一個寫流,在新位置中建立一個檔案
FileStream copyStream = new FileStream(copyFileConfig.DestinationFileUrl, FileMode.CreateNew);
using (copyStream)
{
// copyStream.WriteTimeout = READ_OR_WRITE_TIMEOUT;
//將原始檔的內容寫進新檔案
copyStream.Write(orignalFileBytes, 0, orignalFileBytes.Length);
copyStream.Close();
}
}
stream.Close();
Console.ReadLine();
}
}


}

最後,如果採用非同步的方式,則會進入End_ReadFileCallBack回撥函式進行非同步讀取和非同步寫操作

  /// <summary>
/// 非同步讀寫檔案方法
/// </summary>
/// <param name="result"></param>
private void End_ReadFileCallBack(IAsyncResult result)
{
//得到先前的配置檔案
var config = result.AsyncState as CopyFileConfig;
//結束非同步讀
config.OriginalFileStream.EndRead(result);
//非同步讀後立即寫入新檔案地址
if (File.Exists(config.DestinationFileUrl)) File.Delete(config.DestinationFileUrl);
FileStream copyStream = new FileStream(config.DestinationFileUrl, FileMode.CreateNew);
using (copyStream)
{
Console.WriteLine("非同步複製原檔案地址:{0}", config.OriginalFileStream.Name);
Console.WriteLine("複製後的新檔案地址:{0}", config.DestinationFileUrl);
//呼叫非同步寫方法CallBack方法為End_CreateFileCallBack,引數是copyStream
copyStream.BeginWrite(config.OriginalFileBytes, 0, config.OriginalFileBytes.Length, this.End_CreateFileCallBack,copyStream);
copyStream.Close();

}

}

最後讓我們在main函式呼叫下:

  static void Main(string[] args)
{
FileStreamTest test = new FileStreamTest();
//建立檔案配置類
CreateFileConfig createFileConfig = new CreateFileConfig { CreateUrl = @"d:\MyFile.txt", IsAsync = true };
//複製檔案配置類
CopyFileConfig copyFileConfig = new CopyFileConfig
{
OrginalFileUrl = @"d:\8.jpg",
DestinationFileUrl = @"d:\9.jpg",
IsAsync = true
};
test.Create(createFileConfig);
test.Copy(copyFileConfig);
}

輸出結果:

實現檔案本地分段上傳

上面的例子是將一個檔案作為整體進行操作,這樣會帶來一個問題,當檔案很大或者網路不是很穩定的時候會發生意想不到的錯誤

那我們該怎麼解決這一問題呢?其實有種思路還是不錯的,那就是分段傳輸:

  

那就DIY一個簡單的分段傳輸的例子,我們先將處理每一段的邏輯先整理好

/// <summary>
/// 分段上傳例子
/// </summary>
public class UpFileSingleTest
{
//我們定義Buffer為1000
public const int BUFFER_COUNT = 1000;

/// <summary>
/// 將檔案上傳至伺服器(本地),由於採取分段傳輸所以,
/// 每段必須有一個起始位置和相對應該資料段的資料
/// </summary>
/// <param name="filePath">伺服器上檔案地址</param>
/// <param name="startPositon">分段起始位置</param>
/// <param name="btArray">每段的資料</param>
private void WriteToServer(string filePath,int startPositon,byte[] btArray)
{
FileStream fileStream = new FileStream(filePath, FileMode.OpenOrCreate);
using (fileStream)
{
//將流的位置設定在該段起始位置
fileStream.Position = startPositon;
//將該段資料通過FileStream寫入檔案中,每次寫一段的資料,就好比是個水池,分段蓄水一樣,直到蓄滿為止
fileStream.Write(btArray, 0, btArray.Length);
}
}


/// <summary>
/// 處理單獨一段本地資料上傳至伺服器的邏輯,根據客戶端傳入的startPostion
/// 和totalCount來處理相應段的資料上傳至伺服器(本地)
/// </summary>
/// <param name="localFilePath">本地需要上傳的檔案地址</param>
/// <param name="uploadFilePath">伺服器(本地)目標地址</param>
/// <param name="startPostion">該段起始位置</param>
/// <param name="totalCount">該段最大資料量</param>
public void UpLoadFileFromLocal(string localFilePath,string uploadFilePath,int startPostion,int totalCount)
{
//if(!File.Exists(localFilePath)){return;}
//每次臨時讀取資料數
int tempReadCount = 0;
int tempBuffer = 0;
//定義一個緩衝區陣列
byte[] bufferByteArray = new byte[BUFFER_COUNT];
//定義一個FileStream物件
FileStream fileStream = new FileStream(localFilePath,FileMode.Open);
//將流的位置設定在每段資料的初始位置
fileStream.Position = startPostion;
using (fileStream)
{
//迴圈將該段資料讀出在寫入伺服器中
while (tempReadCount < totalCount)
{

tempBuffer = BUFFER_COUNT;
//每段起始位置+每次迴圈讀取資料的長度
var writeStartPosition = startPostion + tempReadCount;
//當緩衝區的資料加上臨時讀取數大於該段資料量時,
//則設定緩衝區的資料為totalCount-tempReadCount 這一段的資料
if (tempBuffer + tempReadCount > totalCount)
{
//緩衝區的資料為totalCount-tempReadCount
tempBuffer = totalCount-tempReadCount;
//讀取該段資料放入bufferByteArray陣列中
fileStream.Read(bufferByteArray, 0, tempBuffer);
if (tempBuffer > 0)
{
byte[] newTempBtArray = new byte[tempBuffer];
Array.Copy(bufferByteArray, 0, newTempBtArray, 0, tempBuffer);
//將緩衝區的資料上傳至伺服器
this.WriteToServer(uploadFilePath, writeStartPosition, newTempBtArray);
}

}
//如果緩衝區的資料量小於該段資料量,並且tempBuffer=設定BUFFER_COUNT時,通過
//while 迴圈每次讀取一樣的buffer值的資料寫入伺服器中,直到將該段資料全部處理完畢
else if (tempBuffer == BUFFER_COUNT)
{
fileStream.Read(bufferByteArray, 0, tempBuffer);
this.WriteToServer(uploadFilePath, writeStartPosition, bufferByteArray);
}

//通過每次的緩衝區資料,累計增加臨時讀取數
tempReadCount += tempBuffer;
}
}
}

}

一切準備就緒,我們剩下的就是將檔案切成幾段進行上傳了

 static void Main(string[] args)
{
UpFileSingleTest test=new UpFileSingleTest();
FileInfo info = new FileInfo(@"G:\\Skyrim\20080204173728108.torrent");
//取得檔案總長度
var fileLegth = info.Length;
//假設將檔案切成5段
var divide = 5;
//取到每個檔案段的長度
var perFileLengh = (int)fileLegth / divide;
//表示最後剩下的檔案段長度比perFileLengh小
var restCount = (int)fileLegth % divide;
//迴圈上傳資料
for (int i = 0; i < divide+1; i++)
{
//每次定義不同的資料段,假設資料長度是500,那麼每段的開始位置都是i*perFileLength
var startPosition = i * perFileLengh;
//取得每次資料段的資料量
var totalCount = fileLegth - perFileLengh * i > perFileLengh ? perFileLengh : (int)(fileLegth - perFileLengh * i);
//上傳該段資料
test.UpLoadFileFromLocal(@"G:\\Skyrim\\20080204173728108.torrent", @"G:\\Skyrim\\20080204173728109.torrent", startPosition, i == divide ? divide : totalCount);
}

}

上傳結果:

總的來說,分段傳輸比直接傳輸複雜許多,我會在今後的例子中加入多執行緒,這樣的話每段資料的傳輸都能通過一個執行緒單獨處理,能夠提升上傳效能和速度

本章總結

本章介紹了Stream中最關鍵的派生類FileStream的概念,屬性,方法,建構函式等重要的概念,包括一些難點和重要點都一一列舉出來,最後2個例子讓大家在溫故下

FileStream的使用方法,包括FileStream非同步同步操作和分段傳輸操作。

如果大家喜歡我的文章,請大家多多關注下,下一章將會介紹MemoryStream,敬請期待!