Skip to content

传输压缩的JSON

GeXiangDong edited this page Dec 15, 2018 · 3 revisions

背景

有时客户端给服务器传输的JSON比较大,几兆甚至几十兆,网络传输占用时间较多,而json都是文本格式,压缩率较高。因此考虑传输压缩后的格式。

HTTP协议在相应部分支持Content-Encoding:gzip,压缩response body,而没有压缩request body的设计,这也很合理,因为在客户端发起请求时并不知道服务器是否支持压缩。因此没法通过http自身解决,只能增加些程序。

压缩和解压都消耗CPU时间,需要根据实际情况决定是否使用压缩方案,一般内网(微服务)调用启用压缩后有可能得不偿失。在网速较慢的情况下才会有意义。

方案

考虑到通用性,仿效response的header Content-Encoding:gzip方式。

客户端把压缩过的json作为post-body传输,然后增加一个request header: Content-Encoding: gzip来告诉服务器端是压缩的格式。

服务端增加一个Filter,对request头进行检查,如果有Content-Encoding则解压缩后继续。这样不影响现有程序。

服务端还有另外一个解决方案,在NGINX反向代理处增加一个插件(需要去找实现这种功能的插件,官方未提供)把解压缩后的request body传给应用服务器(tomcat)。

参照 http://www.pataliebre.net/howto-make-nginx-decompress-a-gzipped-request.html#.XBToGi276i5

实现

服务器端(Spring)

增加2个类:

package cn.devmgr.springcloud;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Service
public class ContentEncodingFilter extends OncePerRequestFilter {
    Logger logger = LoggerFactory.getLogger(ContentEncodingFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {

        String conentEncoding = request.getHeader("Content-Encoding");
        if(conentEncoding != null && ("gzip".equalsIgnoreCase(conentEncoding) || "deflate".equalsIgnoreCase(conentEncoding))){
            logger.trace("Content-Encoding: {}", conentEncoding);
            chain.doFilter(new GZIPRequestWrapper(request), response);
            return;
        }

        chain.doFilter(request, response);
    }
}
package cn.devmgr.springcloud;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.zip.DeflaterInputStream;
import java.util.zip.GZIPInputStream;

public class GZIPRequestWrapper extends HttpServletRequestWrapper {
    private final static Logger logger = LoggerFactory.getLogger(GZIPRequestWrapper.class);

    protected HttpServletRequest request;

    public GZIPRequestWrapper(HttpServletRequest request){
        super(request);
        this.request = request;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        ServletInputStream sis = request.getInputStream();
        InputStream is = null;
        String conentEncoding = request.getHeader("Content-Encoding");
        if("gzip".equalsIgnoreCase(conentEncoding)){
            is = new GZIPInputStream(sis);
        }else if("deflate".equalsIgnoreCase(conentEncoding)){
            is = new DeflaterInputStream(sis);
        }else{
            throw new UnsupportedEncodingException(conentEncoding + " is not supported.");
        }
        final InputStream compressInputStream = is;
        return new ServletInputStream(){
            ReadListener readListener;

            @Override
            public int read() throws IOException {
                int b = compressInputStream.read();
                if(b == -1 && readListener != null) {
                    readListener.onAllDataRead();
                }
                return b;
            }

            @Override
            public boolean isFinished(){
                try {
                    return compressInputStream.available() == 0;
                } catch (IOException e) {
                    logger.error("error", e);
                    if(readListener != null) {
                        readListener.onError(e);
                    }
                    return false;
                }
            }

            @Override
            public boolean isReady() {
                try {
                    return compressInputStream.available() > 0;
                } catch (IOException e) {
                    logger.error("error", e);
                    if(readListener != null) {
                        readListener.onError(e);
                    }
                    return false;
                }
            }

            @Override
            public void setReadListener(final ReadListener readListener) {
                this.readListener = readListener;
                sis.setReadListener(new ReadListener() {
                    @Override
                    public void onDataAvailable() throws IOException {
                        logger.trace("onDataAvailable");
                        if(readListener != null){
                            readListener.onDataAvailable();
                        }
                    }

                    @Override
                    public void onAllDataRead() throws IOException {
                        logger.trace("onAllDataRead");
                    }

                    @Override
                    public void onError(Throwable throwable) {
                        logger.error("onError", throwable);
                        if(readListener != null){
                            readListener.onError(throwable);
                        }
                    }
                });
            }
        };
    }
}

客户端

CURL的测试命令

echo '{"type": "json", "length": 2222}' | gzip > body.gz
curl -v -i http://127.0.0.1:8080/ss -H 'Content-Encoding: gzip' -H 'Content-Type:application/json' --data-binary @body.gz

RestTemplate

使用RestTemplate可以通过配置interceptor来统一增加压缩。实现代码如下:

package cn.devmgr.springcloud;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestTemplate;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPOutputStream;

@Configuration
public class RestClientConfig {
    private final static Logger logger = LoggerFactory.getLogger(RestClientConfig.class);

    /**
     * request body超过1M时,自动开启压缩模式;(需要服务端支持)
     * 压缩和解压会增加程序处理时间,一般内网调用不压缩速度会更快;压缩只是在网络环境较差的情况下有使用意义。
     * @return
     */
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();

        List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
        if (CollectionUtils.isEmpty(interceptors)) {
            interceptors = new ArrayList<>();
        }
        interceptors.add(new RestTemplateCompressInterceptor());

        restTemplate.setInterceptors(interceptors);
        return restTemplate;
    }


    class RestTemplateCompressInterceptor implements ClientHttpRequestInterceptor {

        @Override
        public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                                            ClientHttpRequestExecution execution) throws IOException {
            logger.trace("RestTemplateCompressInterceptor:intercept() body-length: {}", body == null ? 0 : body.length);
            byte[] newBody = null;
            if(body == null || body.length < 1024 * 1024){
                // 小于1M不开启压缩模式
                logger.trace("{}不需要压缩", body == null ? 0 : body.length);
                newBody = body;
            }else{
                logger.trace("{}开启压缩模式", body.length);
                request.getHeaders().add("Content-Encoding", "gzip");
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                GZIPOutputStream gzip;
                try {
                    gzip = new GZIPOutputStream(baos);
                    gzip.write(body);
                    gzip.close();
                } catch (IOException e) {
                    logger.error("压缩request body出错.", e);
                    throw(e);
                }
                newBody = baos.toByteArray();
                logger.trace("压缩后大小 {}", newBody.length);
            }
            ClientHttpResponse response = execution.execute(request, newBody);
            return response;
        }
    }
}