Skip to content

传输压缩的JSON

GeXiangDong edited this page Dec 15, 2018 · 3 revisions

背景

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

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

方案

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

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

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

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

实现

服务器端(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