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​ 能够读写:

  1. 基本的数据类型
  2. string
  3. 基础数据类型的数组

如下代码演示了二进制数据的读写:

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

    1. 会在文件开头和结尾处写入额外的协议信息,其中包括检测错误的 CRC。
    2. 遵循公认标准 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​ 流用于读写文件。它的实例化方式有两种:

  1. 通过 File​ 类型的静态方法
  2. 通过 FileStream​ 的构造器

文件在读写时的情况有多种:只读、读写、文件不存在、要覆盖写入文件等等。这些模式可以通过 FileMode​ 枚举传入 FileStream​ 的构造器来修改,也可以通过调用 File​ 静态类的相应方法。我们可以根据下图选择需要的模式:

FileStrem​ 还有若干高级特性。以下是创建 FileStream​ 时可选的其他参数:

  • FileShare​ 枚举:占用文件后,若其他进程访问该文件,通过该枚举可以给予一定的访问权限(None​、Read​、ReadWrite​ 或者 Write​,其中 Read​ 为默认权限)

  • 内部缓冲区的大小(字节为单位,默认大小为 4KB)。

  • FileSecurity​ 对象:描述给新文件分配的用户角色和权限。

  • FileOptions​ 标记枚举,其中包括:

    1. Encrypted​:请求操作系统加密。
    2. DeleteOnClose​:在文件关闭时自动删除临时文件。
    3. RandomAccess​ 和 SequentialScan​:优化提示。
    4. 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​ 中的数据方式有二:

  1. ToArray()​ 方法

    返回数据对应的 byte​ 数组。

  2. GetBuffer()

    返回底层存储数组的引用,比流的实际长度要长。

Tips

MemoryStream​ 的关闭(Close()​)和刷新(Flush()​)不是必须的。MemoryStream​ 关闭后将无法再次读写,但是我们仍然可以调用 ToArray()​ 方法来获得底层的数据。而刷新操作则不会对内存流执行任何操作。

PipeStream

PipeStream​ 是管道流,适用于在同一台计算机进行进程间通信(IPC):它不依赖于任何网络传输(因此没有网络协议开销),性能更好,也不会有防火墙问题。

管道类型有两种:

  1. 匿名管道(速度更快):支持同一台计算机中的父进程和子进程之间进行单向通信。
  2. 命名管道(更加灵活):支持同一台不同计算机(使用 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​ 数组中,并返回接收的字节数。返回值分两种情况:

  1. 返回值小于传入的 count​ 参数:读取位置已达到流的末尾,或流本身是以小块方式提供数据(通常是网络流)。
  2. 返回值等于传入的 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​ 才能进行查找。若流可查找,则:

  1. 流的长度(Length)可修改

    通过 SetLength()​ 方法设置

  2. Position​ 属性可修改

    可以改变读写位置

  3. Seek()​ 方法可以参照当前位置(SeekOrigin.Current​)

Tips

SetLength()​ 的使用场景如下:

  1. 文件截断或扩展:当使用文件流(FileStream​)时,SetLength()​ 方法可以用来截断或扩展文件。如果指定的长度小于当前文件大小,文件将被截断,超出的数据会被丢弃。如果指定的长度大于当前文件大小,文件将被扩展,新增的部分通常会用零字节填充。
  2. 调整内存流的大小:对于 MemoryStream​,SetLength()​ 方法可以用来调整内存中存储的数据量。这可以在你需要更大或更小的缓冲区时非常有用。

如果流不支持查找功能(例如加密流),则只能通过遍历整个流获取长度。重新读取先前的位置也必须关闭整个流,再从头读取。

关闭和刷新

通常,流对象的标准销毁语义为:

  • Dispose​ 和 Close​ 方法的功能是一样的。
  • 重复销毁或者关闭流对象不会产生任何错误。

Flush()​ 方法可以强制将缓冲区数据写入后台存储中。当流关闭的时候,也会自动调用 Flush​ 方法。因此关闭前无需再调用 Flush​ 方法:

// 没有必要调用 s.Flush()
s.Flush();
s.Close();

超时

相关的属性有:

  • CanTimeout
  • ReadTimeout
  • WriteTimeout

网络流NetworkStream​)支持该特性,文件流内存流不支持。设置的超时时间以毫秒为单位,0 代表不进行超时设置。

线程安全

通常情况下流并不是线程安全的。Stream​ 类提供了一个静态的 Synchronized()​ 方法,该方法可以接受任何类型的流,并返回一个线程安全的包装器,这个包装器会使用一个排它锁保证每一次读、写或者查找操作只能有一个线程执行。

posted @ 2025-05-17 16:41  hihaojie  阅读(176)  评论(0)    收藏  举报