Skip to content

流 - csharp

C#中的流

1.方法中stream返回的时机

csharp
public async Task<Stream> GetStreamAsync(){
	var client = _httpClientFactory.CreateClient();
	var response = await client.GetAsync("");
	var fileStream = await response.Content.ReadAsStreamAsync();
	return fileStream;
}

public static (MemoryStream, MemoryStream) DuplicateStream(Stream inputStream)
{
    var stream1 = new MemoryStream();
    var stream2 = new MemoryStream();

    inputStream.Position = 0;
    byte[] buffer = new byte[8192];
    int bytesRead;
    while ((bytesRead = inputStream.Read(buffer, 0, buffer.Length)) > 0)
    {
        stream1.Write(buffer, 0, bytesRead);
        stream2.Write(buffer, 0, bytesRead);
    }
  
    stream1.Position = 0;
    stream2.Position = 0;
    return (stream1, stream2);
}

public async Task<Stream> GetCopyStreamAsync(){
	var fileStream = await GetStreamAsync();
  var (copyStream1, copyStream2) = DuplicateStream(fileStream);
	return copyStream1;
}

这是一段从网络下载文件的代码,现在假设网络文件有1G的大小。

1. return fileStream 是什么时候返回的?

在代码中,response.Content.ReadAsStreamAsync() 会返回一个可以用于读取 HTTP 响应内容的 Stream。这个流实际上是一个 网络流 (Network Stream),它并不是一次性将整个文件加载到内存中,而是按需从网络中逐步读取数据。

也就是说,当 return fileStream 被调用时,文件的内容并没有完全加载到内存中。此时,返回的 Stream 是一个懒加载的流,只有在调用方读取 Stream 的数据时,才会通过网络逐步下载文件的内容。

因此,return fileStream 是立即返回的,不会等待整个文件下载完成。

2. 如果内存只有 200M,怎么能确保 Stream 完整返回?

由于 Stream 是一个按需读取的流(懒加载),它不会一次性将整个 1GB 文件加载到内存中,而是通过网络分块读取文件的数据。具体来说:

  1. 当调用者读取返回的 Stream 时,底层会向 HTTP 响应的内容流请求数据。
  2. 数据会以一定的块大小(通常是几 KB 到几 MB,具体取决于实现)从网络中读取到内存中。
  3. 读取完一个块后,数据会被处理或传递给调用方,然后释放内存,再去读取下一块。
  4. 因此,即使内存只有 200MB,也可以通过流式处理完整读取 1GB 的文件。

这种机制依赖于 流式处理,它避免了将整个文件加载到内存中的问题。

3.return fileStream 是立即返回,返回后方法就执行完成了,stream怎么能够继续从网络获取数据的?

核心点:Stream 的生命周期和懒加载

response.Content.ReadAsStreamAsync() 被调用时,它返回的是一个 Stream 对象,这个对象并不是一次性加载所有数据,而是通过 懒加载 的方式按需从网络中读取数据。

即使方法 GetStreamAsync 已经执行完成并返回了 Stream,这个 Stream 本身仍然是一个活跃的对象,并且它背后与 HTTP 响应的底层网络连接(HttpClient 的 HTTP 请求)保持着联系。只要调用者继续读取 Stream,它就会通过底层的 HTTP 连接去获取更多的数据。

为什么方法执行完成后 Stream 还能工作?

这是因为:

  1. Stream 是一个对象
    • 在 C# 中,返回的 Stream 对象是一个引用类型,返回时并不会销毁它。调用者拿到的 Stream 引用仍然指向这个对象。
    • 这个 Stream 对象的生命周期由调用者控制,调用者可以继续操作它,直到显式释放它(例如调用 Dispose 或使用 using 语句)。
  2. 底层的网络连接没有关闭
    • HttpClient 发起请求后,HttpResponseMessage 中的 Content 会持有对底层 HTTP 连接的引用。
    • 返回的 Stream 实际上包装了 HTTP 响应的网络流,Stream 的读取操作会通过这个连接来获取数据。
    • 只要调用者没有关闭 Stream,底层的 HTTP 连接会保持打开状态,允许数据继续传输。
  3. 异步按需读取
    • 当调用者通过 Stream.ReadAsync 或类似的方法读取数据时,底层的 HttpContent 会从网络中按需读取数据块,填充到流中。
    • 这是一种流式处理方式,数据会在调用者读取时才逐步下载,而不是一次性加载到内存中。

为什么网络连接不会被提前关闭?

关键在于 HttpResponseMessage 和它的 Content 对象的生命周期:

  • HttpResponseMessage 和它的 Content 会在返回的 Stream 存在期间保持有效,直到调用者显式关闭或释放 Stream
  • 只要 Stream 没有被关闭,底层的网络连接会保持打开状态,允许数据继续传输。

如果调用者没有正确地关闭 Stream(例如忘记调用 Dispose),就可能导致资源泄漏,比如底层的 HTTP 连接未能及时释放。

4.执行Copy后会为什么不会影响 HttpResponseMessage 和它的 Content 对象的生命周期

在方法 DuplicateStream 中,inputStream 是一个参数,你并没有直接修改 HttpResponseMessage 或它的 Content 对象的状态,而只是对传入的 Stream(可能是 HttpResponseMessage.Content.ReadAsStreamAsync() 返回的流)进行了读取操作,并将数据复制到了两个新的 MemoryStream 中。

新的两个 MemoryStream 是完全独立的,它们存储了从 inputStream 读取的数据的副本,不再依赖于原始的 inputStream 或它的底层资源(例如网络连接)。因此,这两个 MemoryStream 的生命周期与 HttpResponseMessage 和它的 Content 对象没有任何直接关系。

csharp
public async Task<Stream> GetStreamAsync()
{
    var client = _httpClientFactory.CreateClient();
    var response = await client.GetAsync("", HttpCompletionOption.ResponseHeadersRead);
    response.EnsureSuccessStatusCode();

    var memoryStream = new MemoryStream();
    await using (var responseStream = await response.Content.ReadAsStreamAsync())
    {
        await responseStream.CopyToAsync(memoryStream);
    }

    memoryStream.Position = 0;
    return memoryStream;
}

方法返回stream时,using 的 responseStream不会被释放吗?

1. 异步方法的执行顺序

在 C# 中,async 方法的执行依赖于 await 的行为。await 关键字会暂停方法的执行,直到其右侧的异步操作完成。只有在异步操作完成后,方法才会继续执行后续代码。因此,await 是保证异步方法按顺序执行的关键。

在你的代码中:

csharpawait using (var responseStream = await response.Content.ReadAsStreamAsync())
{
    await responseStream.CopyToAsync(memoryStream);
}

memoryStream.Position = 0;
return memoryStream;
  • 第一步await response.Content.ReadAsStreamAsync() 会异步获取 HTTP 响应流。方法会在这里暂停,直到流被完全初始化,然后继续执行。
  • 第二步await responseStream.CopyToAsync(memoryStream) 会异步将数据从 responseStream 复制到 memoryStream。方法会在这里再次暂停,直到数据复制完成,然后继续执行。
  • 第三步memoryStream.Position = 0memoryStream 的位置重置为开头。
  • 第四步return memoryStream 返回 memoryStream

因为 await 会暂停方法的执行,所以在 CopyToAsync 完成之前,return memoryStream 不会被执行。

上次更新时间:

最近更新