C# 的流(Stream)
C# 的流(Stream)
流适配器
流适配器提供了处理高级类型(如文本和 XML)的方法。
可以看到,下面这些类均以“Reader”、“Writer”为后缀,而非 Stream。
TextReader
、TextWriter
Stream
仅支持处理字节,为此 .NET 提供了高级类 TextReader
和 TextWriter
用于处理字符串。二者是抽象类,因此无法直接使用。通常我们使用它的两组子类:
-
StreamReader
/StreamWriter
使用
Stream
存储其原始数据,将流的字节转换为字符或者字符串。 -
StringReader
/StringWriter
使用内存字符串(实际是
StringBuilder
)实现了TextReader
/TextWriter
.
StringReader
、StringWriter
StringReader
和 StringWriter
用于将字符串包装为流,便于一些仅接受流的方法使用。例如:
XmlReader r = XmlReader.Create(new StringReader(myString))
StreamReader
、StreamWriter
StreamReader
、StreamWriter
用于从其他流读取字符串。File 类提供了一些静态方法返回此类型,如:
-
返回
StreamWriter
-
File.CreateText
-
File.AppendText
-
-
返回
StreamReader
-
File.OpenText
-
用法如下:
const string Path = "test.txt";
using (TextWriter writer = File.CreateText(Path))
{
writer.WriteLine("Line1");
writer.WriteLine("Line2");
}
using (TextWriter writer = File.AppendText(Path))
writer.WriteLine("Line3");
using (TextReader reader = File.OpenText(Path))
while(reader.Peek() > -1)
Console.WriteLine(reader.ReadLine());
// or
using (TextReader reader = File.OpenText(Path))
{
string content;
while ((content = reader.ReadLine()) != null)
{
Console.WriteLine(content);
}
}
此外还可以通过构造器创建实例,其构造器接受 Stream
实例或文件。
Warn
对于
NetWorkStream
,切勿通过StreamReader
读取数据。网络流(NetworkStream
)并不包含结尾(EOF),因此StreamReader
的ReadToEnd()
方法可能会无限期阻塞。只要连接是打开状态,网络流就无法确定客户端何时停止发送数据。
StreamReader
使用内部缓冲区从基础流中读取数据,即使调用ReadLine()
方法,也会预先读取多于一行的数据以提高效率。对于网络流来说,如果流中没有更多数据且没有遇到行结束符,ReadLine()
方法会阻塞等待数据到达,直到从流中读取到完整的一行或者连接超时。这可能会导致在网络连接缓慢或不稳定时出现长时间阻塞的情况。
XmlReader
、XmlWriter
Info
关于二者更详细的用法,见第11章 其他 XML 技术 - hihaojie - 博客园
XmlReader
XmlReader
是一个高性能的类,它能够以低层次、前向的方式读取 XML 流。XmlReader
可能会从一些较慢的数据源(例如 Stream
和 URI)读取数据,因此它的大多数方法都提供了异步版本。
XmlReader
是一个抽象类(abstract),它通过工厂方法 Create()
创建实例,用法如下:
using XmlReader reader = XmlReader.Create (new System.IO.StringReader (myString));
此外,XmlReader
还有一个因子类型 XmlReaderSettings
,用于设置读取的一些参数:
XmlReaderSettings
XmlReaderSettings
有若干属性。
跳过无关内容的有:
-
IgnoreComments
属性:是否忽略注释节点 -
IgnoreProcessingInstructions
属性:是否忽略处理指令 -
IgnoreWhitespace
属性:是否忽略空白字符
读取片段的有:
-
ConformanceLevel
属性:ConformanceLevel
枚举类型,用于指示所读取的内容是部分节点还是完整文档
关于流的有:
-
CloseInput
属性:用于指示关闭XmlReader
时是否关闭底层流。默认为 true。
XmlWriterSettings
有类似属性CloseOutput
,默认为 true
XmlWriter
XmlWriter
是一个 XML 流的前向写入器。XmlWriter
的设计和 XmlReader
是对称的。它也通过 Create()
方法创建实例,创建时可以传入 XmlWriterSettings
对象,配置写入方式:
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
using XmlWriter writer = XmlWriter.Create("foo.xml", settings);
writer.WriteStartElement("customer");
writer.WriteElementString("firstname", "Jim");
writer.WriteElementString("lastname", "Bo");
writer.WriteEndElement();
<!--写入的内容如下-->
<?xml version="1.0" encoding="utf-8"?>
<customer>
<firstname>Jim</firstname>
<lastname>Bo</lastname>
</customer>
XmlWriterSettings
XmlWriterSettings
有若干属性设置写入 XML 的方式:
-
OmitXmlDeclaration
属性:是否忽略写入 XML 的声明 -
ConfirmanceLevel
属性:ConfirmanceLevel
枚举类型,用于指示所写入的内容是部分节点还是完整文档该属性也可以用于忽略写入 XML 声明
-
Indent
属性:输出时是否添加缩进
BinaryReader
、BinaryWriter
BinaryReader
和 BinaryWriter
能够读写:
- 基本的数据类型
-
string
- 基础数据类型的数组
如下代码演示了二进制数据的读写:
public class Person
{
public string Name;
public int Age;
public double Height;
public void SaveData(Stream s)
{
var w = new BinaryWriter(s);
w.Write(Name);
w.Write(Age);
w.Write(Height);
w.Flush();
}
public void LoadData(Stream s)
{
var r = new BinaryReader(s);
Name = r.ReadString();
Age = r.ReadInt32();
Height = r.ReadDouble();
}
}
BinaryReader
也可以读取 byte 流:
byte[] data = new BinaryReader(stream).ReadBytes((int)stream.Length);
装饰器流
BufferedStream
BufferedStream
用于提供缓冲/扩充缓冲区。
如下代码对 FileStream
进行包装,将缓冲区扩充至 20KB:
const string Filename = "MyFile.bin";
File.WriteAllBytes(Filename, new byte[100_000]);
using(FileStream fs = File.OpenRead(Filename))
using(BufferedStream bs = new BufferedStream(fs, 20_000)) // 20K 缓冲
{
bs.ReadByte();
Console.WriteLine(fs.Position); // 20000
}
这段代码虽然只读了一个字节,但底层流已经读了 20k 字节,剩余的 19999 次 ReadByte
调用将不再访问 FileStream
。
压缩流
System.IO.Compression
命名空间有两个通用的压缩流,支持 ZIP 压缩算法:
-
DeflateStream
-
GZipStream
- 会在文件开头和结尾处写入额外的协议信息,其中包括检测错误的 CRC。
- 遵循公认标准 RFC 1952。
使用方式如下:
using (Stream s = File.Create("compressed.bin"))
using (Stream ds = new DeflateStream(s, CompressionMode.Compress)){
for(byte i = 0; i < 100; i++)
ds.WriteByte(i);
}
using (Stream s = File.OpenRead("compressed.bin"))
using (Stream ds = new DeflateStream(s, CompressionMode.Decompress)){
for (byte i = 0; i < 100; i++)
Console.WriteLine(ds.ReadByte());
}
如下代码在内存中压缩数据:
byte[] data = new byte[1000];
var ms = new MemoryStream();
using (Stream ds = new DeflateStream(ms, CompressionMode.Compress))
ds.Write(data, 0, data.Length);
byte[] compressed = ms.ToArray();
Console.WriteLine(compressed.Length); // 压缩后数据仅 11 byte
ms = new MemoryStream(compressed);
using (Stream ds = new DeflateStream(ms, CompressionMode.Decompress))
for (int i = 0; i < 1000; i += ds.Read(data, i, 1000 - i));
DeflateStream
构造器支持 Close 时不关闭底层流,用法如下:
byte[] data = new byte[1000];
MemoryStream ms = new MemoryStream();
using (Stream ds = new DeflateStream(ms, CompressionMode.Compress, true))
await ds.WriteAsync(data, 0, data.Length);
Console.WriteLine(ms.Length);
// 因流未关闭,可以继续使用
ms.Position = 0;
using (Stream ds = new DeflateStream(ms, CompressionMode.Decompress))
for(int i = 0; i < 1000; i += await ds.ReadAsync(data, i, 1000 - i));
后台存储流
FileStream
FileStream
流用于读写文件。它的实例化方式有两种:
- 通过
File
类型的静态方法 - 通过
FileStream
的构造器
文件在读写时的情况有多种:只读、读写、文件不存在、要覆盖写入文件等等。这些模式可以通过 FileMode
枚举传入 FileStream
的构造器来修改,也可以通过调用 File
静态类的相应方法。我们可以根据下图选择需要的模式:
FileStrem
还有若干高级特性。以下是创建 FileStream
时可选的其他参数:
-
FileShare
枚举:占用文件后,若其他进程访问该文件,通过该枚举可以给予一定的访问权限(None
、Read
、ReadWrite
或者Write
,其中Read
为默认权限) -
内部缓冲区的大小(字节为单位,默认大小为 4KB)。
-
FileSecurity
对象:描述给新文件分配的用户角色和权限。 -
FileOptions
标记枚举,其中包括:-
Encrypted
:请求操作系统加密。 -
DeleteOnClose
:在文件关闭时自动删除临时文件。 -
RandomAccess
和SequentialScan
:优化提示。 -
WriteThrough
:要求操作系统禁用写后缓存,适用于事物文件或日志文件。
-
使用 FileShare.ReadWrite
打开一个文件可以允许其他进程或用户读写同一个文件。为了避免混乱,我们可以使用以下方法在读或者写之前锁定文件的特定部分。
// Defined on the FileStream class:
public virtual void Lock (long position, long length);
public virtual void Unlock (long position, long length);
如果所请求的文件段的部分或者全部已经被锁定,Lock 操作会抛出异常。
MemoryStream
MemoryStream
使用数组作为后台存储,可以通过 CopyTo()
方法将数据复制到 MemoryStream
中:
var ms = new MemoryStream();
sourceStream.CopyTo(ms);
获取 MemoryStream
中的数据方式有二:
-
ToArray()
方法返回数据对应的
byte
数组。 -
GetBuffer()
返回底层存储数组的引用,比流的实际长度要长。
Tips
MemoryStream
的关闭(Close()
)和刷新(Flush()
)不是必须的。MemoryStream
关闭后将无法再次读写,但是我们仍然可以调用ToArray()
方法来获得底层的数据。而刷新操作则不会对内存流执行任何操作。
PipeStream
PipeStream
是管道流,适用于在同一台计算机进行进程间通信(IPC):它不依赖于任何网络传输(因此没有网络协议开销),性能更好,也不会有防火墙问题。
管道类型有两种:
- 匿名管道(速度更快):支持同一台计算机中的父进程和子进程之间进行单向通信。
- 命名管道(更加灵活):支持同一台或不同计算机(使用 Windows 网络)的任意两个进程间进行双向通信。
PipeStream
是抽象类,有 4 个子类:
- 匿名管道:
AnonymousPipeServerStream
和AnonymousPipeClientStream
- 命名管道:
NamedPipeServerStream
和NamedPipeClientStream
Info
管道的使用较少,此处不做过多讲解。想详细了解请参考 15.2.9
PipeStream
IsolatedStorageFileStream
每一个.NET 应用程序都可以访问其独有的本地存储区域,称为独立存储区(isolated storage)。如果应用程序无法访问标准文件系统(因此也无法在 ApplicationData、LocalApplicationData、CommonApplicationData、MyDocuments 中写入数据)那么则更适合使用独立存储区。使用受限 Internet 权限部署的 Silverlight 应用程序和 ClickOnce 应用程序就属于这种情况。
其他流
NetworkStream
NetworkStream
主要用于 TCP/IP 通讯。它有如下特点
- 当连接的服务器/客户端发生断开,
NetworkStream.Read()
方法回返回 0,我们可以根据该返回值判断连接是否发生断开。 -
NetworkStream
不包含 EOF,使用StreamReader
的ReadToEnd()
方法可能会无限阻塞;也不应该使用StreamReader
的ReadLine()
方法,它会预先读取多于一行的数据以提高效率。 -
Timeout
属性仅对同步读写方法有效,异步读写方法的超时需要通过CancellationToken
实现。
Warn
需要注意的是,通过
CancellationToken
取消NetwrokStream.ReadAsync()
的执行,在 .NET Framework 中无效。以如下代码为例,在 .NET 中可以正常取消,在 .NET Framework 中不会取消:TcpClient client = new TcpClient("127.0.0.1", 3000); NetworkStream stream = client.GetStream(); byte[] buffer = Encoding.ASCII.GetBytes("123456"); await stream.WriteAsync(buffer, 0, buffer.Length); buffer = new byte[1024]; CancellationTokenSource source = new CancellationTokenSource(1000); int count = await stream.ReadAsync(buffer, 0, buffer.Length, source.Token);
流的使用
读写流
Stream.Read()
Stream.Read()
方法用于将数据块读取到 byte
数组中,并返回接收的字节数。返回值分两种情况:
- 返回值小于传入的
count
参数:读取位置已达到流的末尾,或流本身是以小块方式提供数据(通常是网络流)。 - 返回值等于传入的
count
参数:数据块可能未读完。
流的正确读取方式如下,该代码每次读取都判断读到的数据数量:
int bytesRead = 0;
int chunkSize = 1;
while (bytesRead < data.Length && chunkSize > 0)
{
bytesRead += chunkSize = s.Read(data, bytesRead, data.Length - bytesRead);
}
我们也可以使用 BinaryReader
读取流,省去自己判断的麻烦,前提是需要知道流中的数据是多少:
byte[] data = new BinaryReader(stream).ReadBytes(1000);
Stream.ReadByte()
ReadByte
方法:它每次读取一个字节,通过返回值返回,并在流结束时返回-1。我们需要将得到的数据按照 byte
而非 int
进行处理。
Stream.Write()
和 Stream.WriteByte()
Write()
方法和 WriteByte()
方法将数据发送到流中。如果无法发送指定的字节,则抛出异常。
Notice
Read()
和Write()
方法中的offset
参数指的是buffer
数组中开始读写的索引位置,而不是流中的位置。
查找
流的 CanSeek
属性为 true
才能进行查找。若流可查找,则:
-
流的长度(Length)可修改
通过
SetLength()
方法设置 -
Position
属性可修改可以改变读写位置
-
Seek()
方法可以参照当前位置(SeekOrigin.Current
)
Tips
SetLength()
的使用场景如下:
- 文件截断或扩展:当使用文件流(
FileStream
)时,SetLength()
方法可以用来截断或扩展文件。如果指定的长度小于当前文件大小,文件将被截断,超出的数据会被丢弃。如果指定的长度大于当前文件大小,文件将被扩展,新增的部分通常会用零字节填充。- 调整内存流的大小:对于
MemoryStream
,SetLength()
方法可以用来调整内存中存储的数据量。这可以在你需要更大或更小的缓冲区时非常有用。
如果流不支持查找功能(例如加密流),则只能通过遍历整个流获取长度。重新读取先前的位置也必须关闭整个流,再从头读取。
关闭和刷新
通常,流对象的标准销毁语义为:
-
Dispose
和Close
方法的功能是一样的。 - 重复销毁或者关闭流对象不会产生任何错误。
Flush()
方法可以强制将缓冲区数据写入后台存储中。当流关闭的时候,也会自动调用 Flush
方法。因此关闭前无需再调用 Flush
方法:
// 没有必要调用 s.Flush()
s.Flush();
s.Close();
超时
相关的属性有:
-
CanTimeout
-
ReadTimeout
-
WriteTimeout
网络流(NetworkStream
)支持该特性,文件流和内存流不支持。设置的超时时间以毫秒为单位,0 代表不进行超时设置。
线程安全
通常情况下流并不是线程安全的。Stream
类提供了一个静态的 Synchronized()
方法,该方法可以接受任何类型的流,并返回一个线程安全的包装器,这个包装器会使用一个排它锁保证每一次读、写或者查找操作只能有一个线程执行。