(轉)C# 溫故而知新:Stream篇(四)

(本文轉自)http://www.cnblogs.com/JimmyZheng/archive/2012/04/03/2430585.html

C# 溫故而知新:Stream篇(四)

FileStream

目錄:

  • 如何去理解FileStream?
  • FileStream的重要性
  • FileStream常用構造函數(重要)
  •               非託管參數SafeFileHandle簡單介紹
  • FileStream常用屬性介紹
  • FileStream常用方法介紹
  • FileStream示例1:*文件的新建和拷貝(主要演示文件同步和異步操作)
  • FileStream示例2:*實現文件本地分段上傳
  • 本章總結

 

 

 

 

      

如何去理解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(StringFileMode)

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(IntPtrFileAccessBoolean 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(StringFileModeFileAccessFileShare)

FileShare:同樣是個枚舉類型:確定文件如何由進程共。   

Delete

允許隨後刪除文件。

Inheritable

使文件句柄可由子進程繼承。Win32 不直接支持此功能。

None

謝絕共享當前文件。文件關閉前,打開該文件的任何請求(由此進程或另一進程發出的請求)都將失敗。

Read

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

ReadWrite

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

Write

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

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

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

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

 

*6: FileStream(StringFileModeFileAccessFileShareInt32FileOptions)

      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,敬請期待!

 

發佈了4 篇原創文章 · 獲贊 14 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章