【问题标题】:Compress HTTP GET Response压缩 HTTP GET 响应
【发布时间】:2012-05-04 06:12:50
【问题描述】:

我目前正在将我的一些 MVC3 控制器迁移到 MVC4 Api 控制器。 我通过继承ActionFilterAttribute 并覆盖OnActionExecutiong 方法为MVC3 控制器获取方法响应实现了压缩机制。经过一番研究,我发现我需要使用来自System.Web.HttpFiltersActionFilterMethod。如果有人可以分享一段示例代码让我开始使用 GZip 压缩 HTTP 响应,那就太好了

【问题讨论】:

  • 我遇到了同样的问题,尽管在我的情况下我已经启用了 IIS 压缩。在您的情况下,是 IIS 压缩,还是您创建了自定义处理程序?
  • 是的,我已经为此使用了自定义处理程序,就像 Darin 在这里提到的方式一样。

标签: c# asp.net-web-api http-compression


【解决方案1】:

最简单的方法是直接在 IIS 级别enable compression

如果您想在应用程序级别执行此操作,您可以编写一个自定义委托消息处理程序,如 following post 所示:

public class CompressHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken).ContinueWith<HttpResponseMessage>((responseToCompleteTask) =>
        {
            HttpResponseMessage response = responseToCompleteTask.Result;

            if (response.RequestMessage.Headers.AcceptEncoding != null)
            {
                string encodingType = response.RequestMessage.Headers.AcceptEncoding.First().Value;

                response.Content = new CompressedContent(response.Content, encodingType);
            }

            return response;
        },
        TaskContinuationOptions.OnlyOnRanToCompletion);
    }
}

public class CompressedContent : HttpContent
{
    private HttpContent originalContent;
    private string encodingType;

    public CompressedContent(HttpContent content, string encodingType)
    {
        if (content == null)
        {
            throw new ArgumentNullException("content");
        }

        if (encodingType == null)
        {
            throw new ArgumentNullException("encodingType");
        }

        originalContent = content;
        this.encodingType = encodingType.ToLowerInvariant();

        if (this.encodingType != "gzip" && this.encodingType != "deflate")
        {
            throw new InvalidOperationException(string.Format("Encoding '{0}' is not supported. Only supports gzip or deflate encoding.", this.encodingType));
        }

        // copy the headers from the original content
        foreach (KeyValuePair<string, IEnumerable<string>> header in originalContent.Headers)
        {
            this.Headers.AddWithoutValidation(header.Key, header.Value);
        }

        this.Headers.ContentEncoding.Add(encodingType);
    }

    protected override bool TryComputeLength(out long length)
    {
        length = -1;

        return false;
    }

    protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
    {
        Stream compressedStream = null;

        if (encodingType == "gzip")
        {
            compressedStream = new GZipStream(stream, CompressionMode.Compress, leaveOpen: true);
        }
        else if (encodingType == "deflate")
        {
            compressedStream = new DeflateStream(stream, CompressionMode.Compress, leaveOpen: true);
        }

        return originalContent.CopyToAsync(compressedStream).ContinueWith(tsk =>
        {
            if (compressedStream != null)
            {
                compressedStream.Dispose();
            }
        });
    }
}

现在剩下的就是在Application_Start 中注册处理程序:

GlobalConfiguration.Configuration.MessageHandlers.Add(new CompressHandler());

【讨论】:

  • 我认为这段代码中存在一个错误(以及在网络上找到的类似示例): Content-Length Header 设置不正确,因为 Content-Length Header 是从 gzip 压缩的内容中复制的。这可以通过将 StringContent 传递给 Compression Handler 来轻松复制。要解决这个问题,originalContent.Headers 的行需要像这样修复:originalContent.Headers.Where(x =&gt; x.Key != "Content-Length")
  • 如果没有提供 Accept-Encoding,代码将会失败。 if (response.RequestMessage.Headers.AcceptEncoding != null) 应该是 if (response.RequestMessage.Headers.AcceptEncoding.Any())
  • 我建议在 SendAsync 的 encodingType 分配和 response.Content 分配之间添加以下内容,以允许错误响应在不压缩的情况下返回 if (response.StatusCode != HttpStatusCode.OK || response.Content == null || string.IsNullOrWhiteSpace(encodingType)) return response;
  • 我需要用以下代码替换 AcceptEncoding 检查: if (response.RequestMessage.Headers.AcceptEncoding.Any()) { string encodingType = response.RequestMessage.Headers.AcceptEncoding.First().价值; if (response.Content != null) { response.Content = new CompressedContent(response.Content, encodingType); } }
  • 你将如何合并 response.Content.LoadIntoBufferAsync() 以获取响应内容的长度(response.Content.Headers.ContentLength),然后如果它小于某个阈值,则从压缩中排除结果?在设置response.Content之前添加上一行时,调用以超时/死锁结束
【解决方案2】:

如果您使用的是 IIS 7+,我会说将压缩留给 IIS,因为它支持 GZIP 压缩。只需turn it on

另一方面,对于控制器来说,压缩太靠近金属了。理想情况下,控制器应该工作在比字节和流更高的级别。

【讨论】:

  • 总的来说我同意,但是 IIS 级压缩需要配置任何使用它的服务器。
【解决方案3】:

使用一个类并编写如下代码

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CompressFilter : ActionFilterAttribute
{
    public override void OnActionExecuted(HttpActionExecutedContext context)
    {
        var acceptedEncoding = context.Response.RequestMessage.Headers.AcceptEncoding.First().Value;
        if (!acceptedEncoding.Equals("gzip", StringComparison.InvariantCultureIgnoreCase)
        && !acceptedEncoding.Equals("deflate", StringComparison.InvariantCultureIgnoreCase))
        {
            return;
        }
        context.Response.Content = new CompressedContent(context.Response.Content, acceptedEncoding);
    }
}

现在创建另一个类并编写以下代码。

public class CompressedContent : HttpContent
{
    private readonly string _encodingType;
    private readonly HttpContent _originalContent;
    public CompressedContent(HttpContent content, string encodingType = "gzip")
    {
        if (content == null)
        {
            throw new ArgumentNullException("content");
        }
        _originalContent = content;
        _encodingType = encodingType.ToLowerInvariant();
        foreach (var header in _originalContent.Headers)
        {
            Headers.TryAddWithoutValidation(header.Key, header.Value);
        }
        Headers.ContentEncoding.Add(encodingType);
    }
    protected override bool TryComputeLength(out long length)
    {
        length = -1;
        return false;
    }
    protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
    {
        Stream compressedStream = null;
        switch (_encodingType)
        {
            case "gzip":
                compressedStream = new GZipStream(stream, CompressionMode.Compress, true);
                break;
            case "deflate":
                compressedStream = new DeflateStream(stream, CompressionMode.Compress, true);
                break;
            default:
                compressedStream = stream;
                break;
        }
        return _originalContent.CopyToAsync(compressedStream).ContinueWith(tsk =>
        {
            if (compressedStream != null)
            {
                compressedStream.Dispose();
            }
        });
    }
}

现在在 Controller 或类似这样的任何 api 操作方法中使用以下属性

[Route("GetData")]
[CompressFilter]         
public HttpResponseMessage GetData()
{
}

【讨论】:

  • 我在我的 Web API 上配置了 OWIN 中间件,这是唯一适合我的解决方案。另外,您可以真正定位您想要压缩的内容。很好的解决方案!
  • 如果你执行“return (Ok());”这将失败在您的控制器方法中,因为 _originalContent 将为 null 并且您将收到有关“异步操作未返回 System.Threading.Tasks.Task 对象。”的异常...我该如何解决?
  • 啊,在 OnActionExecuted() 中添加这个:“if (context.Response.Content == null) return;”
  • 如果请求中没有 Accept-Encoding 标头,它也会失败(崩溃)。那个 First() 方法。改为这样做: string acceptedEncoding = string.Empty; var acceptedEncodingHeaders = context.Response.RequestMessage.Headers.AcceptEncoding; if (acceptedEncodingHeaders.Any()) acceptedEncoding = acceptedEncodingHeaders.First().Value;
猜你喜欢
  • 2012-02-12
  • 2012-06-12
  • 1970-01-01
  • 1970-01-01
  • 2019-04-13
  • 1970-01-01
  • 2012-12-02
  • 1970-01-01
相关资源
最近更新 更多