背景
有时客户端给服务器传输的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个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
| 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的测试命令
1 2 3
| 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来统一增加压缩。实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| 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);
@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){ 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; } } }
|