压缩HTTP GET响应

17

我目前正在将我的MVC3控制器迁移到MVC4 Api控制器。

我已经通过继承ActionFilterAttribute并重写OnActionExecutiong方法为MVC3控制器Get方法响应实现了压缩机制。 经过一些调查,我发现需要使用来自System.Web.HttpFiltersActionFilterMethod。如果有人能分享一个示例代码以便我开始使用GZip压缩HTTP响应,那就太好了。


我也遇到了同样的问题,尽管在我的情况下我已经启用了IIS压缩。在你的情况下,是IIS压缩引起的问题,还是你创建了自定义处理程序? - Carvellis
是的,我已经像Darin在这里提到的那样使用了自定义处理程序。 - Pavan Josyula
3个回答

40
最简单的方法是直接在IIS级别启用压缩
如果你想在应用程序级别进行操作,你可以编写一个自定义委托消息处理程序,如下面的帖子所示:
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头设置不正确,因为Content-Length头从gzipped内容中复制而来。可以通过将StringContent通过Compression Handler传递来轻松重现此问题。要解决此问题,需要像这样修复带有“originalContent.Headers”的行:originalContent.Headers.Where(x => x.Key != "Content-Length") - Johannes Rudolph
如果没有提供Accept-Encoding,则代码将失败。if (response.RequestMessage.Headers.AcceptEncoding != null) 应该改为 if (response.RequestMessage.Headers.AcceptEncoding.Any()) - Jan Sommer
我建议在SendAsync中的encodingType分配和response.Content分配之间添加以下内容,以允许错误响应在没有压缩的情况下返回:if (response.StatusCode != HttpStatusCode.OK || response.Content == null || string.IsNullOrWhiteSpace(encodingType)) return response; - Paul
我需要用以下代码替换AcceptEncoding检查: if (response.RequestMessage.Headers.AcceptEncoding.Any()) { string encodingType = response.RequestMessage.Headers.AcceptEncoding.First().Value; if (response.Content != null) { response.Content = new CompressedContent(response.Content, encodingType); } } - mike gold
你如何将response.Content.LoadIntoBufferAsync()整合进代码中,以获取响应内容的长度(response.Content.Headers.ContentLength),并在其小于某个阈值时从压缩结果中排除?当在设置response.Content之前添加上述代码行时,调用会因超时/死锁而结束。 - Philipp

6
如果您正在使用IIS 7+,我建议将压缩交给IIS,因为它支持GZIP压缩。只需打开即可。
另一方面,压缩对控制器来说太过接近底层。理想情况下,控制器应该在比字节和流更高的级别上工作。

1
总的来说,我同意,但是IIS级别的压缩需要配置使用它的任何服务器。 - samus

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();
            }
        });
    }
}

现在,在控制器或任何API操作方法中使用以下属性,例如:
[Route("GetData")]
<b>[CompressFilter]</b>         
public HttpResponseMessage GetData()
{
}

我在我的Web API上配置了OWIN中间件,这是唯一对我起作用的解决方案。此外,您可以真正针对您想要压缩的内容进行定制。好的解决方案! - Elferone
如果您在控制器方法中执行"return (Ok());",则此操作将失败,因为_originalContent将为空,并且您将收到有关"The async operation did not return a System.Threading.Tasks.Task object."的异常...我该如何解决? - CodeOrElse
啊,只需在OnActionExecuted()中添加以下内容: "if (context.Response.Content == null) return;" - CodeOrElse
如果请求中没有Accept-Encoding头,则它也会失败(崩溃)。那个First()方法。请改用以下代码: string acceptedEncoding = string.Empty; var acceptedEncodingHeaders = context.Response.RequestMessage.Headers.AcceptEncoding; if (acceptedEncodingHeaders.Any()) acceptedEncoding = acceptedEncodingHeaders.First().Value; - CodeOrElse

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接