流
处理文件 的 更强大的选项:流
流:是一个用于 传输数据 的对象。数据可以 向两个方向 传输:
- 从外部源 传输到 程序中:读取流
- 从程序 传输到 外部源中:写入流
使用 一个独立的对象 来 传输数据,比使用
FileInfo或DirectoryInfo类更好
- 把 传输数据的概念 与 特定数据源 分离开来,可以更容易地 交换数据源
外部源 可以是以下情况:
- 文件
- 使用一些 网络协议 读写 网络上的数据。目的是:选择数据或从另一个计算机上发送数据
- 把数据 读写到 命名管道 上
- 把数据 读写到 一个内存区域上
- ...
一些流 只允许写入,一些流 只允许读取
一些流 允许 随机存取。 随即存取:在流中 随机 定位游标
- 例如:从流的开头开始读取,之后移动到流的末尾,再从流的一个中间位置继续读取
System.IO.MemoryStream对象:读写内存System.Net.Sockets.NetworkStream对象:处理网络数据
System.IO.Stream类:
- 对 外部源 不做任何限定,外部源 可以是:
- 文件流
- 内存流
- 网络流
- 任意数据源
- 例如:外部源是代码中的一个变量。使用流 在变量之间 传输数据 很有用,可以 在数据类型之间 转换数据
- 包含 很多通用代码,可以在外部源和代码中的变量之间移动数据,把这些代码与特定数据源的概念 分离开来,更容易实现 不同环境下的 代码重用
- 直接读写流,不是那么容易。可以使用 阅读器 和 写入器。这也是一个关注点的分离。
StringReader和StringWriterStreamReader和StreamWriter
一些流 可以 链接 起来。例如,可以链接
DeflateStream和CryptoStream,再写入FileStream
DeflateStream:压缩数据- 这个流可以写入
FileStream、MemoryStream或NetworkStream
- 这个流可以写入
CryptoStream: 加密数据
文件读写 的常用类
FileStream: 在 二进制文件中 读写 二进制 数据StreamReader和StreamWriter:读写 文本格式的 流产品BinaryReader和BinaryWriter:读写 二进制格式的 流产品
使用这些类 和 直接使用 底层的流对象 之间的区别:基本流 是按照字节来工作的 例如:
BinaryReader.Write()一个重载的的参数是long类型,long占8个字节,它把8个字节写入流中BinaryReader.Read()就是,从流中提取8个字节,恢复long的值
FileStream
创建FileStream
有很多构造函数。其中第一个,有以下4个参数:
- 参数1:字符串 表示 文件的完整路径
- 参数2:打开文件的模式
- 新建一个文件
- 打开一个现有文件
- 写入操作 覆盖原来的内容
- 写入操作 追加到文件末尾
- 枚举
FileMode:- Append:文件不存在时,抛异常
- Create:不管文件是否存在,都不抛异常。删除任何现有文件,新建一个空文件
- CreateNew:文件存在时,抛异常
- Open:文件不存在时,抛异常
- OpenOrCreate:不管文件是否存在,都不抛异常。
- Truncate:文件不存在时,抛异常
FileMode与文件现有状态不一致:会抛异常,如上
- 参数3:访问文件的方式
- 只读,只写,读写
- 枚举
FileAccess:- Read
- ReadWrite
- Write
FileAccess是按位标志,可以和按位运算符 | 一起使用
- 参数4:是否独占文件
- 不允许 其他流 同时访问文件
- 允许 其他流 同时访问文件
- 其他流 只读
- 其他流 只写
- 其他流 读写
- 枚举
FileShare:- Delete
- Inheritable
- None
- Read
- ReadWrite
- Write
FileShare是按位标志,可以和按位运算符 | 一起使用
var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)
File.OpenFile()
File.OpenFile()也能返回FileStream
FileStream stream = File.OpenFile(file);
获取流的信息
Stream类定义了以下属性:
CanReadCanWriteCanSeekCanTimeout- 读写流,可以用
ReadTimeout和WriteTimeout来指定超时(毫秒)
- 读写流,可以用
Position- 返回 当前 光标 在流中的位置
- 每次 从流中读取 一些数据,光标的位置 就移动到 下一个 将读取的字节上
public static void ShowStreamInformation(Stream stream)
{
Console.WriteLine($"stream can read: {stream.CanRead}, can write: {stream.CanWrite}, can seek: {stream.CanSeek}, can timeout: {stream.CanTimeout}");
Console.WriteLine($"length: {stream.Length}, position: {stream.Position}");
if (stream.CanTimeout)
{
Console.WriteLine($"read timeout: {stream.ReadTimeout} write timeout: {stream.WriteTimeout} ");
}
}
输出:
stream can read: True, can write: False, can seek: True, can timeout: False
length: 431, position: 0
Read()和ReadByte()
读取一个流的时候:
ReadByte():从流中只读取一个字节Read(): 从流中读取一个字节 数组- 返回读取的字节数
- 如果没有更多可用于读取,就返回0
- 流可能小于缓冲区
利用Stream的Read() 分析文本文件的字符编码方式
序言:读取流中的前几个字节
- 提供了文本文件的字符如何编码的信息
- 也叫字节顺序标记 (Byte Order Mark, BOM)
文本文件的 字符编码 方式:
ASCII- 每一个字符使用7位来进行表示
- 基于字母表,提供了
- 小写字母
- 大写字母
- 控制字符
UTF-32- 前两个字节是 FF FE
- 第三个字节和第四个字节 都是0
- 前四个字节加起来一共32位
- 每一个字符使用32位来进行表示
UTF-16 小端序:- 是两字节的Unicode
- 前两个字节是 FF FE (FF在FE之前,表示小端序)
- 第三个字节和第四个字节 都不为0
- 前两个字节加起来一共16位
- 每一个字符使用16位来进行表示
UTF-16 大端序:- 是两字节的Unicode
- 前两个字节是 FE FF (FE在FF之前,表示大端序)
- 第三个字节和第四个字节 都不为0
- 前两个字节加起来一共16位
- 每一个字符使用16位来进行表示
UTF-8:- 目前最常用的 文本文件字符编码格式
- 第一个字节是EF
- 第二个字节是BB
- 第三个字节是BF
- 每个字符使用 1到6个字节 即8到48位来进行表示
public static Encoding GetEncoding(Stream stream)
{
if (!stream.CanSeek) throw new ArgumentException("require a stream that can seek");
Encoding encoding = Encoding.ASCII;
byte[] bom = new byte[5];
int nRead = stream.Read(bom, offset: 0, count: 5);
if (bom[0] == 0xff && bom[1] == 0xfe && bom[2] == 0 && bom[3] == 0)
{
Console.WriteLine("UTF-32");
stream.Seek(4, SeekOrigin.Begin);
return Encoding.UTF32;
}
else if (bom[0] == 0xff && bom[1] == 0xfe)
{
Console.WriteLine("UTF-16, little endian");
stream.Seek(2, SeekOrigin.Begin);
return Encoding.Unicode;
}
else if (bom[0] == 0xfe && bom[1] == 0xff)
{
Console.WriteLine("UTF-16, big endian");
stream.Seek(2, SeekOrigin.Begin);
return Encoding.BigEndianUnicode;
}
else if (bom[0] == 0xef && bom[1] == 0xbb && bom[2] == 0xbf)
{
Console.WriteLine("UTF-8");
stream.Seek(3, SeekOrigin.Begin);
return Encoding.UTF8;
}
stream.Seek(0, SeekOrigin.Begin);
return encoding;
}
读取流
File.OpenWrite()创建一个 可以写入 的流FileStream
示例
Path.GetTempFileName()创建 临时文件名Path.ChangeExtension()修改 文件扩展名- 写入UTF-8文件时,需要写入 序言:
- 要么,直接写入 字节
- 调用
WriteByte()给流 发送 三个字节的UTF-8 序言
- 调用
- 要么,使用
Encoding类- 无序记住UTF-8所对应的序言字节,
Encoding类有这些信息 GetPreamble()返回一个字节数组,包含序言- 调用
Write()写入此字节数组
- 无序记住UTF-8所对应的序言字节,
- 要么,直接写入 字节
- 写入 文件内容
Encoding.UTF8.GetBytes()得到 字节数组Write()写入 字节数组
- 用编辑器打开,它会使用 正确的编码 UTF-8
public static void WriteTextFile()
{
string tempTextFileName = Path.ChangeExtension(Path.GetTempFileName(), "txt");
using (FileStream stream = File.OpenWrite(tempTextFileName))
{
//// write BOM
//stream.WriteByte(0xef);
//stream.WriteByte(0xbb);
//stream.WriteByte(0xbf);
byte[] preamble = Encoding.UTF8.GetPreamble();
stream.Write(preamble, 0, preamble.Length);
string hello = "Hello, World!";
byte[] buffer = Encoding.UTF8.GetBytes(hello);
stream.Write(buffer, 0, buffer.Length);
Console.WriteLine($"file {stream.Name} written");
}
}
复制流
Stream.Read(): 读取缓冲区Stream.Write():写入缓冲区
public static void CopyUsingStreams(string inputFile, string outputFile)
{
const int BUFFERSIZE = 4096;
using (var inputStream = File.OpenRead(inputFile))
using (var outputStream = File.OpenWrite(outputFile))
{
byte[] buffer = new byte[BUFFERSIZE];
bool completed = false;
do
{
int nRead = inputStream.Read(buffer, 0, BUFFERSIZE);
if (nRead == 0) completed = true;
outputStream.Write(buffer, 0, nRead);
} while (!completed);
}
}
- 为了复制流,不需要编写上面的代码,可以直接使用
Stream类的CopyTo()方法
public static void CopyUsingStreams2(string inputFile, string outputFile)
{
using (var inputStream = File.OpenRead(inputFile))
using (var outputStream = File.OpenWrite(outputFile))
{
inputStream.CopyTo(outputStream);
}
}
随机访问 流
随即访问流(甚至可以访问大文件)的一个优势:可以快速访问文件中的特定位置
示例
- 创建一个大文件
public static async Task CreateSampleFileAsync(int nRecords)
{
using (FileStream stream = File.Create(SampleFileDataPath))
using (var writer = new StreamWriter(stream))
{
var r = new Random();
var records = Enumerable.Range(1, nRecords).Select(x => new
{
Number = x,
Text = $"Sample text {r.Next(200)}",
Date = new DateTime(Math.Abs((long)((r.NextDouble() * 2 - 1) * DateTime.MaxValue.Ticks)))
});
foreach (var rec in records)
{
string date = rec.Date.ToString("d", CultureInfo.InvariantCulture);
string s = $"#{rec.Number,8};{rec.Text,-20};{date}#{Environment.NewLine}";
await writer.WriteAsync(s);
}
}
}
- 用以上代码可以尝试创建一个包含150万条记录或更多的大文件
- 用记事本打开这个大文件会比较慢,但用随机存取会非常快
关于上面这一段code, 如果写成下面这样:
public static async Task CreateSampleFileAsync(int nRecords)
{
FileStream stream = File.Create(SampleFileDataPath)
using (var writer = new StreamWriter(stream))
{
}
}
- 每个实现
IDisposable的对象 都应该销毁。如FileStream StreamWriter销毁时,StreamWriter会控制所使用的资源,并销毁流- 为了使流打开得时间比
StreamWriter更长,可以使用StreamWriter的构造函数配置它 - 这种情况下,需要显式地销毁流
把流标定位到流中的一个随机位置,读取记录。
- 用户输入想访问的Record号
- 流中 要访问的字节 是基于Record号和Record大小
Stream的Seek()方法 - 第一个参数:定位流中的光标
- 第二个参数:表示 流的开头、流的末尾、当前位置
public static void RandomAccessSample()
{
try
{
using (FileStream stream = File.OpenRead(SampleFileDataPath))
{
byte[] buffer = new byte[RECORDSIZE];
do
{
try
{
Console.Write("record number (or 'bye' to end): ");
string line = Console.ReadLine();
if (line.ToUpper().CompareTo("BYE") == 0) break;
if (int.TryParse(line, out int record))
{
stream.Seek((record - 1) * RECORDSIZE, SeekOrigin.Begin);
stream.Read(buffer, 0, RECORDSIZE);
string s = Encoding.UTF8.GetString(buffer);
Console.WriteLine($"record: {s}");
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
} while (true);
Console.WriteLine("finished");
}
}
catch (FileNotFoundException)
{
Console.WriteLine("Create the sample file using the option -sample first");
}
}
使用缓存 的流
从性能原因上看,在读写文件时,输出结果会被缓存 例如:当程序要读取文件流中的下两个字节:
- 文件流 会把请求 传递给Windows
如果不使用缓存,则会:- Windows连接文件系统
- 定位文件
- 从磁盘中读取文件,仅读取两个字节
- 如果使用缓存,则会:
- 在一次读取过程中,检索文件中的一个大块
- 将该大块保存在一个内存区域,即缓冲区上
- 以后对流中数据的请求 就会从该缓冲区中读取,直到读完该缓冲区位置
- 这时,Windows会从文件中再获取另一个大块 例如:当程序要写入字节,与上面的方式相同。
对于文件,操作系统会自动完成缓冲区的填充和清空操作。
对于 从 没有缓存的地方 读取数据
- 首先需要创建一个流
- 再从
BufferedStream创建一个流,BufferedStream实现一个缓冲区BufferStream并不用于 程序 频繁切换 读数据和写数据 的情形
- 将第一步创建的需要缓存的流传递给
BufferStream的构造函数
使用 读取器 和 写入器
FileStream类 读写文本文件 需要使用 字节数组 和 处理编码
其实有 更简单的方法: 使用 读取器
StreamReader和 写入器StreamWriter来读写FileStream。不需要处理 字节数组 和 编码
StreamWriter和StreamReader的 工作级别 比较高
- 其实现方式 可以根据 流的内容,自动检测 停止读取文本比较方便的位置
- 一次读写一行文本
- 流 会自动确定 下一个回车符 的位置,并在该处 停止读取
- 写入文件时
- 流 会自动把回车符 和 换行符 追加到 文本的末尾
- 一次读写一行文本
- 不需要担心 文件中所使用的编码方式
- StreamReader 默认使用 UTF-8
- StreamReader 有重载的构造函数 可以指定使用文件中序言定义的编码
- StreamReader 有重载的构造函数 可以显式地指定一个编码
// 使用文件序言中定义的编码
var reader = new StreamReader(stream, detectEncodingFromByteOrderMarks: true);
// 显式地指定一个编码
var reader = new StreamReader(stream, Encoding.Unicode);
StreamReader
- 构造接收
FileStream EndOfStream检查是否到达文件的末尾- 读取方式:
ReadLine()读取文本行ReadToEnd()从光标处读取到末尾Read()把内容读入一个字符char数组(不是字节数组)。char类型使用两个字节。- 适合16位的
Unicode - 不适合
UTF-8(一个字符可以是1到6个字节)
- 适合16位的
- 有重载的构造函数 可以指定编码方式
- 默认是
UTF-8
- 默认是
- 有重载的构造函数 可以指定 要使用的缓冲区
- 默认为 1024字节
- 还可以指定 关闭
StreamReader时,不应该关闭底层流FileStream- 默认是 关闭
StreamReader(即调用Dispose方法),会关闭底层流
- 默认是 关闭
- 初始化方式:
new StreamReader()- 接收一个
FileStream
- 接收一个
- 或
File.OpenText()创建一个StreamReader()
public static void ReadFileUsingReader(string fileName)
{
var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read);
using (var reader = new StreamReader(stream))
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
Console.WriteLine(line);
}
}
}
StreamWriter
和
StreamReader工作方式相同。只不过 仅用于写入文件 (或写入另一个流)
- 构造函数有17个版本的重载。
- 默认以UTF-8格式写入文本内容
- 可以设置Encoding
- 可以指定缓冲区大小
- 可以指定关闭写入器时,是否关闭底层流
- 允许传递字符串和一些.NET数据类型
- 会使用指定的编码 变成字符串
public static void WriteFileUsingWriter(string fileName, string[] lines)
{
var outputStream = File.OpenWrite(fileName);
using (var writer = new StreamWriter(outputStream))
{
byte[] preamble = Encoding.UTF8.GetPreamble();
outputStream.Write(preamble, 0, preamble.Length);
writer.Write(lines);
}
}
BinaryWriter
读写二进制文件的另一种选择是 直接使用 流类型。这时,最好使用 字节数组 执行读写操作。
另一种选择是 使用专为这个场景定义的 读取器
BinaryReader和写入器BinaryWriter
BinaryWriter以二进制格式 写入 数据类型
- 不使用 任何编码
- 文件使用二进制格式 而不是 文本格式 写入
- 18个版本的
Write()。接收不同的类型double, int, long, string
public static void WriteFileUsingBinaryWriter(string binFile)
{
var outputStream = File.Create(binFile);
using (var writer = new BinaryWriter(outputStream))
{
double d = 47.47;
int i = 42;
long l = 987654321;
string s = "sample";
writer.Write(d);
writer.Write(i);
writer.Write(l);
writer.Write(s);
}
}
- 读取文件的顺序, 必须完全匹配 写入的顺序
- 创建自己的二进制格式时,需要知道存储的内容和方式,并用相应的方式读取
public static void ReadFileUsingBinaryReader(string binFile)
{
var inputStream = File.Open(binFile, FileMode.Open);
using (var reader = new BinaryReader(inputStream))
{
double d = reader.ReadDouble();
int i = reader.ReadInt32();
long l = reader.ReadInt64();
string s = reader.ReadString();
Console.WriteLine($"d: {d}, i: {i}, l: {l}, s: {s}");
}
}
压缩文件
.NET提供的类:包含不同的算法 压缩和解压 流。
DeflateStream- 压缩和解压 流
GZipStream- 压缩和解压 流
- 在后台使用
DeflateStream,算法一样。- Brotli算法:谷歌新开源压缩算法。
- 增加了循环冗余校验,来检测数据的损坏情况
ZipArchive- 创建和读取Zip文件
- Windows能直接打开zip文件
- Windows不能打开gzip文件, 需要第三方工具
- 创建和读取Zip文件
示例:
流的一个特性:它们可以链接起来
DeflateStream
CompressMode.Compress表示压缩- 使用
Write()方法 或 其他方法 写入DeflateStream。这里用CopyTo()方法写入这个DeflateStream
public static void CompressFile(string fileName, string compressedFileName)
{
using (FileStream inputStream = File.OpenRead(fileName))
{
FileStream outputStream = File.OpenWrite(compressedFileName);
using (var compressStream = new DeflateStream(outputStream, CompressionMode.Compress))
{
inputStream.CopyTo(compressStream);
}
}
}
CompressMode.Decompress表示解压缩
public static void DecompressFile(string fileName)
{
FileStream inputStream = File.OpenRead(fileName);
using (MemoryStream outputStream = new MemoryStream())
using (var compressStream = new DeflateStream(inputStream, CompressionMode.Decompress))
{
compressStream.CopyTo(outputStream);
outputStream.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(outputStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 4096, leaveOpen: true))
{
string result = reader.ReadToEnd();
Console.WriteLine(result);
}
}
}
BrotliStream
- 使用上,和
DeflateStream类似 - 添加Nuget包:`System.IO.Compression.Brotli
public static void CompressFileWithBrotli(string fileName, string compressedFileName)
{
using (FileStream inputStream = File.OpenRead(fileName))
{
FileStream outputStream = File.OpenWrite(compressedFileName);
using (var compressStream = new BrotliStream(outputStream, CompressionMode.Compress))
{
inputStream.CopyTo(compressStream);
}
}
}
public static void DecompressFileWithBrotli(string fileName)
{
FileStream inputStream = File.OpenRead(fileName);
using (MemoryStream outputStream = new MemoryStream())
using (var compressStream = new BrotliStream(inputStream, CompressionMode.Decompress))
{
compressStream.CopyTo(outputStream);
outputStream.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(outputStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 4096, leaveOpen: true))
{
string result = reader.ReadToEnd();
Console.WriteLine(result);
}
}
}
ZipArchive
- 包含多个
ZipArchiveEntry对象。为每个文件创建一个ZipArchiveEntry对象。 - 不是一个流
- 但使用流 进行读写
public static void CreateZipFile(string directory, string zipFile)
{
InitSampleFilesForZip(directory);
string destDirectory = Path.GetDirectoryName(zipFile);
if (!Directory.Exists(destDirectory))
{
Directory.CreateDirectory(destDirectory);
}
FileStream zipStream = File.Create(zipFile);
using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Create))
{
IEnumerable<string> files = Directory.EnumerateFiles(directory, "*", SearchOption.TopDirectoryOnly);
foreach (var file in files)
{
ZipArchiveEntry entry = archive.CreateEntry(Path.GetFileName(file));
using (FileStream inputStream = File.OpenRead(file))
using (Stream outputStream = entry.Open())
{
inputStream.CopyTo(outputStream);
}
}
}
}
private static void InitSampleFilesForZip(string directory)
{
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
for (int i = 0; i < 10; i++)
{
string destFileName = Path.Combine(directory, $"test{i}.txt");
File.Copy("Test.txt", destFileName);
}
} // else nothing to do, using existing files from the directory
}
FileSystemWatcher 观察文件的更改
- 事件触发,在
- 创建 文件:
FileSystemEventHandler - 删除 文件:
FileSystemEventHandler - 更改 文件:
FileSystemEventHandler - 重命名 文件:
RenamedEventHanlder派生自FileSystemEventHandler
- 创建 文件:
- 使用场景 举例:
- 服务器上传文件时
- 文件缓存在内存中,缓存需要在文件更新时失效
FileSystemWatchernew FileSystemWatcher(".", "*.txt")- 指定该观察的目录
- 指定过滤器
IncludeSubdirectories = true- 指定 是否观察子目录中的文件
EnableRaisingEvents = true- 需要 调用
Dispose()- 需要 调用
using System;
using System.IO;
namespace FileMonitor
{
public class Program
{
private static FileSystemWatcher s_watcher;
public static void Main(string[] args)
{
WatchFiles(".", "*.txt");
Console.ReadLine();
UnWatchFiles();
}
public static void WatchFiles(string path, string filter)
{
s_watcher = new FileSystemWatcher(path, filter)
{
IncludeSubdirectories = true
};
s_watcher.Created += OnFileChanged;
s_watcher.Changed += OnFileChanged;
s_watcher.Deleted += OnFileChanged;
s_watcher.Renamed += OnFileRenamed;
s_watcher.EnableRaisingEvents = true;
Console.WriteLine("watching file changes...");
}
public static void UnWatchFiles()
{
s_watcher.Created -= OnFileChanged;
s_watcher.Changed -= OnFileChanged;
s_watcher.Deleted -= OnFileChanged;
s_watcher.Renamed -= OnFileRenamed;
s_watcher.Dispose();
s_watcher = null;
}
private static void OnFileRenamed(object sender, RenamedEventArgs e) =>
Console.WriteLine($"file {e.OldName} {e.ChangeType} to {e.Name}");
private static void OnFileChanged(object sender, FileSystemEventArgs e) =>
Console.WriteLine($"file {e.Name} {e.ChangeType}");
}
}