zoukankan      html  css  js  c++  java
  • ASP.NET Core 问题排查:Request.EnableRewind 后第一次读取不到 Request.Body

    实际应用场景是将用户上传的文件依次保存到阿里云 OSS 与腾讯云 COS ,实现方式是在启用 Request.EnableRewind() 的情况下通过 Request.Body 读取流,并依次通过 2 个 StreamContent 分别上传到阿里云 OSS 与 腾讯云 COS ,在集成测试中可以正常上传(用的是 TestServer 启动站点),而部署到服务器上通过浏览器上传却出现了奇怪的问题 —— 第一个 StreamContent 上传后的文件大小总是0,而第二个 StreamContent 上传正常。上传文件大小为 0 时,对应的 Request.Body.Length 也为 0 。(注:如果不使用 Request.EnableRewind ,Request.Body 只能被读取一次)

    而如果在第一个 StreamContent 读取 Request.Body 之前先通过 MemoryStream 进行一次流的 Copy 操作,就能正常读取。

    using (var ms = new MemoryStream())
    {
        await Request.Body.CopyToAsync(ms);
    }

    好奇怪的问题!要牺牲第一个流,才能让后面的 StreamContent 从 Request.Body 中读到数据。 为什么会这样?

    先从 Request.EnableRewind() 下手,通过它的实现源码知道了 EnableRewind 之后 Request.Body 被替换为 FileBufferingReadStream ,所以 StreamContent 实际读取的是 FileBufferingReadStream ,问题可能与 FileBufferingReadStream 有关。

    public static HttpRequest EnableRewind(this HttpRequest request, int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null)
    {
        //..
        var body = request.Body;
        if (!body.CanSeek)
        {
            var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, _getTempDirectory);
            request.Body = fileStream;
            request.HttpContext.Response.RegisterForDispose(fileStream);
        }
        return request;
    }

    向前进,查看 FileBufferingReadStream 的实现源码。

    在构造函数中 _buffer 的长度被设置为 0 :

    if (memoryThreshold < _maxRentedBufferSize)
    {
        _rentedBuffer = bytePool.Rent(memoryThreshold);
        _buffer = new MemoryStream(_rentedBuffer);
        _buffer.SetLength(0);
    }
    else
    {
        _buffer = new MemoryStream();
    }

    FileBufferingReadStream 的长度实际就是 _buffer 的长度:

    public override long Length
    {
        get { return _buffer.Length; }
    }

    ReadAsync 读取流的代码(已移除不相关的代码):

    public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {
        ThrowIfDisposed();
        if (_buffer.Position < _buffer.Length || _completelyBuffered)
        {
            // Just read from the buffer
            return await _buffer.ReadAsync(buffer, offset, (int)Math.Min(count, _buffer.Length - _buffer.Position), cancellationToken);
        }
    
        int read = await _inner.ReadAsync(buffer, offset, count, cancellationToken);
        //...
        if (read > 0)
        {
            await _buffer.WriteAsync(buffer, offset, read, cancellationToken);
        }
        //...
        return read;
    }

    从 FileBufferingReadStream 的实现代码中没有发现问题。

    只有 StreamContent 才会出现这个问题吗?写了个简单的 ASP.NET Core 程序验证了一下:

    public async Task<IActionResult> Index()
    {
        Request.EnableRewind();
    
        Request.Body.Seek(0, 0);
        Console.WriteLine("First Read Request.Body");
        await Request.Body.CopyToAsync(Console.OpenStandardOutput());
        Console.WriteLine();
    
        Request.Body.Seek(0, 0);
        Console.WriteLine("Second Read Request.Body");
        await Request.Body.CopyToAsync(Console.OpenStandardOutput());
        Console.WriteLine();
    
        Request.Body.Seek(0, 0);
        using (var sr = new StreamReader(Request.Body))
        {
            return Ok(await sr.ReadToEndAsync());
        }
    }

    控制台输出流(System.ConsolePal+WindowsConsoleStream)没这个问题。

    是 StreamContent 的问题吗?用下面的代码验证一下 

    public async Task<IActionResult> Index()
    {
        Request.EnableRewind();
        var streamContent = new StreamContent(Request.Body);
        return Ok(await streamContent.ReadAsStringAsync());
    }

    奇怪了,StreamContnent 也没问题,只是剩下唯一的嫌疑对象 —— HttpClient 。

    写测试代码进行验证,站点A的代码(监听于5000端口)

    public async Task<IActionResult> Index()
    {
        Request.EnableRewind();
        var streamContent = new StreamContent(Request.Body);
        var httpClient = new HttpClient();
        var response = await httpClient.PostAsync("http://localhost:5002", streamContent);            
        return Ok(await response.Content.ReadAsStringAsync());
    }

    站点B的代码(监听于5002端口)

    public async Task<IActionResult> Index()
    {
        using (var ms = new MemoryStream())
        {
            await Request.Body.CopyToAsync(ms);
            return Ok(ms.Length);
        }
    }

    站点 A 启用 EnableRewind 并直接将 Request.Body 流 POST 到站点 B ,模拟实际应用场景。

    测试得到的返回值是 0 ,问题重现了。

    为了进一步验证是否是 HttpClient 的问题,将 HttpClient 改为 WebRequest 。

    public async Task<IActionResult> Index()
    {
        Request.EnableRewind();
        var request = WebRequest.CreateHttp("http://localhost:5002");
        request.Method = "POST";
        using (var requestStream = await request.GetRequestStreamAsync())
        {
            await Request.Body.CopyToAsync(requestStream);
        }
        using (var response = await request.GetResponseAsync())
        {
            using (var sr = new StreamReader(response.GetResponseStream()))
            {
                return Ok(await sr.ReadToEndAsync());
            }
        }
    }

    测试结果显示 WebRequest 没这个问题,果然与 HttpClient 有关。

    向 HttpClient 的源代码进军。。。

    从 HttpClient.SendAsync 到 HttpMessageInvoker.SendAsync 再到 HttpMessageHandler.SendAsync ,默认用的是 SocketsHttpHandler ,从 SocketsHttpHandler.SendAsync 到 HttpConnectionHandler.SendAsync 到 HttpConnectionPoolManager.SendAsync 。。。翻山越岭,长途跋涉,来到了 HttpConnection 的 SendAsyncCore 方法。

    public async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        //..
        await SendRequestContentAsync(request, CreateRequestContentStream(request), cancellationToken).ConfigureAwait(false);
        //...
    }

    原来是调用 SendRequestContentAsync 方法发送请求内容的

    private async Task SendRequestContentAsync(HttpRequestMessage request, HttpContentWriteStream stream, CancellationToken cancellationToken)
    {
        // Now that we're sending content, prohibit retries on this connection.
        _canRetry = false;
    
        // Copy all of the data to the server.
        await request.Content.CopyToAsync(stream, _transportContext, cancellationToken).ConfigureAwait(false);
    
        // Finish the content; with a chunked upload, this includes writing the terminating chunk.
        await stream.FinishAsync().ConfigureAwait(false);
    
        // Flush any content that might still be buffered.
        await FlushAsync().ConfigureAwait(false);
    }

    从 request.Content.CopyToAsync 追踪到 HttpConnection 的  WriteAsync 方法

    private async Task WriteAsync(ReadOnlyMemory<byte> source)
    {
        int remaining = _writeBuffer.Length - _writeOffset;
    
        if (source.Length <= remaining)
        {
            // Fits in current write buffer.  Just copy and return.
            WriteToBuffer(source);
            return;
        }
    
        if (_writeOffset != 0)
        {
            // Fit what we can in the current write buffer and flush it.
            WriteToBuffer(source.Slice(0, remaining));
            source = source.Slice(remaining);
            await FlushAsync().ConfigureAwait(false);
        }
    
        if (source.Length >= _writeBuffer.Length)
        {
            // Large write.  No sense buffering this.  Write directly to stream.
            await WriteToStreamAsync(source).ConfigureAwait(false);
        }
        else
        {
            // Copy remainder into buffer
            WriteToBuffer(source);
        }
    }

    看这部分代码实在毫无头绪,于是采用笨方法——手工打点在控制台显示信息,在 WriteToStreamAsync 进行打点

    private ValueTask WriteToStreamAsync(ReadOnlyMemory<byte> source)
    {
        if (NetEventSource.IsEnabled) Trace($"Writing {source.Length} bytes.");
        Console.WriteLine($"{_stream} Writing {source.Length} bytes.");
        Console.WriteLine("source text: " + System.Text.Encoding.Default.GetString(source.ToArray()));
        return _stream.WriteAsync(source);
    }

    编译 System.Net.Http 解决方案,将编译输出的 corefxinWindows_NT.AnyCPU.DebugSystem.Net.Http etcoreappSystem.Net.Http.dll 复制到 C:Program FilesdotnetsharedMicrosoft.NETCore.App2.1.2 文件夹中,然后就可以使用自己编译的 System.Net.Http.dll 运行 ASP.NET Core 程序。

    运行测试站点(见之前的站点A与站点B的代码,站点 A 将 Request.Body 流中的内容通过 HttpClient POST 到站点 B ),站点 A 的控制台显示了下面的打点信息:

    System.Net.Sockets.NetworkStream Writing 10 bytes.
    source text: POST / HTT
    System.Net.Sockets.NetworkStream Writing 10 bytes.
    source text: P/1.1
    Con
    System.Net.Sockets.NetworkStream Writing 10 bytes.
    source text: tent-Lengt
    System.Net.Sockets.NetworkStream Writing 10 bytes.
    source text: h: 0
    Host
    System.Net.Sockets.NetworkStream Writing 10 bytes.
    source text: : localhos
    _writeBuffer.Length: 10
    _writeOffset: 10
    remaining: 0
    source.Length: 4
    System.Net.Sockets.NetworkStream Writing 10 bytes.
    source text: t:5002
    
    
    System.Net.Sockets.NetworkStream Writing 4 bytes.
    source text: test

    将上面的 source text 内容连接起来,到下面的 http 请求内容:

    POST / HTTP/1.1
    Content-Length: 0
    Host: localhost:5002
    
    
    test

    立马就发现了问题:Content-Length: 0 ,原来是 Content-Length 惹的祸,怎么会是 0 ?

    继续打点。。。

    找到了 "Content-Length: 0" 是  StreamContent 中的 TryComputeLength 方法引起的 

    protected internal override bool TryComputeLength(out long length)
    {
        if (_content.CanSeek)
        {
            length = _content.Length - _start;
    return true; } else { length = 0; return false; } }

    上面的代码中 _content.Length 的值为 0 (在博文的开头我们提到过 FileBufferingReadStream 在未被读取时 Length 的值为 0 ),于是 length 为 0 并返回 true ,所以生成了 "Content-Length: 0" 请求头。

    如果当 length 为 0 时,让 TryComputeLength 返回 false ,这样就不会生成 "Content-Length: 0" 请求头,是不是可以解决问题呢?

    protected internal override bool TryComputeLength(out long length)
    {
        if (_content.CanSeek)
        {
            length = _content.Length - _start;
            return length > 0;
        }
        else
        {
            length = 0;
            return false;
        }
    }

    这样会产生下面的请求内容:

    POST / HTTP/1.1
    Transfer-Encoding: chunked
    Host: localhost:5002
    
    4
    test
    0



    这样的请求内容在示例程序服务端就可以正常读取到 Request.Body ,但是无法将文件上传到阿里云 OSS 与腾讯云 COS ,应该是 "Transfer-Encoding: chunked" 请求头的原因。

    后来改为从 HttpAbstractions 下手,修改了 BufferingHelper.cs 与 FileBufferingReadStream.cs 的代码,终于解决了这个问题。

    给 FileBufferingReadStream.cs 添加一个私有字段 _innerLength ,在 Request.EnableRewind 时通过构造函数将 Request.ContentLength 的值传给 _innerLength 。

    var fileStream = new FileBufferingReadStream(body, request.ContentLength, bufferThreshold, bufferLimit, _getTempDirectory);

    在 FileBufferingReadStream 的 Length 属性中,如果流还没被读取过,就返回 _innerLength 的值。

    public override long Length
    {
        get
        {
            var useInnerLength = _innerLength.HasValue && _innerLength > 0 
                && !_completelyBuffered && _buffer.Position == 0;
            return useInnerLength ?_innerLength.Value : _buffer.Length;
        }
    }

    修改 HttpAbstractions 的源代码后,需要将编译生成的下面5个文件都复制到 C:Program FilesdotnetsharedMicrosoft.NETCore.App2.1.2 文件夹中。

    Microsoft.AspNetCore.Http.Abstractions.dll
    Microsoft.AspNetCore.Http.dll
    Microsoft.AspNetCore.Http.Features.dll
    Microsoft.Net.Http.Headers.dll
    Microsoft.AspNetCore.WebUtilities.dll

    如果用的是 Linux ,需要复制到 /usr/share/dotnet/shared/Microsoft.AspNetCore.App/2.1.2/ 目录中。 

    后来发现基于 request.ContentLength 的解决方法不适用于 chunked requests ,将 CanSeek 属性改为下面的实现(原先是直接返回true)

    public override bool CanSeek
    {
        get { return Length > 0; }
    }

    这样第一读 Request.Body 正常,但之后继续读会出现下面的错误:

    System.ObjectDisposedException: Cannot access a disposed object.
    Object name: 'FileBufferingReadStream'.
       at WebsiteA.FileBufferingReadStream.ThrowIfDisposed()
  • 相关阅读:
    jQuery使用手册之Ajax支持(8)
    jQuery使用手册之动态效果(6)
    jquery插件 操作select
    提高jQuery的性能
    jQuery起点教程之使用AJAX(4)
    jQuery起点教程之插件制作(7)
    IE开发工具栏
    信息安全之DNS欺骗详解
    颜色取色器
    使DIV不被select等控件遮挡的解决办法
  • 原文地址:https://www.cnblogs.com/dudu/p/9505292.html
Copyright © 2011-2022 走看看