完全解读 spring gateway 中 content-length 与 chunked 的用法及注意事项
一、http中 chunked 与 content-length的区别
熟悉HTTP协议的朋友都知道,在HTTP的回复报文中一般使用content-length定义body的长度,当浏览器读取报文的时候会根据content-length来开启buffer接收数据,这个content-length必须如实准确反应HTTP body报文的长度,否则将会出现报文不完整或者pendding的情况。
现在使用content-length
将会产生两个问题:
- 1、不能边读取一边渲染,导致整个画面需要在数据加载后才能呈现,速度慢不说,加载突兀,如果在数据包传输不完整,将会导致整个画面无法展示。
- 2、服务器必须计算完content-length的值才能开始传输。
为了使用web2.0的需求,在HTTP 1.1版本加入了Transfer-Encoding = chunked 的方式解决这个问题。Transfer-Encoding = chunked 告诉浏览器这是以 chunked 编码方式来传输的报文,使得浏览器边加载内容边渲染,完全不担心HTTP数据包的大小。由于边加载边渲染,在渲染上不用等待,也不需要校验数据是否完整,在体验效果上会优于content-length,而且加载和传输的速度更快,还有小避免pending(数据等待)的问题。
参考文章说明:HTTP 协议中的 Transfer-Encoding
二、spring boot 为何没有返回content-length ?
要回答这个问题就要搞清楚spring boot在回复报文的时候发生那些流程,下面我们来简单回顾下从return语句到HTTP报文发生了什么。本文并不打算打开源码研读,只是列举简单的步骤进行说明。
- 1、首先API在return 一个Object交给框架,框架接收到后将会将Object序列化成Json String。
- 2、Json String将会扔给Servlet框架,Servlet收到Json String后将会写到Response中,然后数据流到传输层传输到客户端中。
那么Servlet将Json String写到Response中,使用content-length还是Transfer-Encoding = chunked来定义Header头呢?经过测试发现,Spring Boot会根据具体的情况来决定使用content-length 或者 Transfer-Encoding = chunked,如果API 函数直接return String类型的数据,那么Spring Boot默认使用 content-length 来定义 Header头,如果return的是类型是一个Object,那么默认使用Transfer-Encoding = chunked 进行分片传输。
三、spring boot返回 content-length 两种方法
最近对接下位机的时候发现,下位机由于功能比较弱,不能开启大buffer来接收数据而且不支持分片接收。因此,只能通过定义content-length来告诉接收方这个包多大,需要开启多大的buffer。
那么,spring boot 如果返回携带 content-length 头部的Response?其实在上面的分析过程中已经给出其中的一种方式,那就是在API中直接返回String类型,spring boot框架检测到返回类型是String时,将自动使用 content-length 作为Response Http Header。
第二种方式,使用HttpResponseWrapper来重写回复包。相关的代码如下:
/**
* (废弃原因:因为发现api返回类型换成string,默认也会在response的 header中添加 content-length 字段)
* java servlet 方向的过滤器,区别于gateway过滤器。
* 1、servlet类的操作可以执行一些比较底层的操作。
* 2、spring boot 就是基于servlet
*/
//@Order(2)
//@WebFilter(urlPatterns = "/*", filterName = "responseFilter")
@Deprecated
public class HttpStreamFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(HttpStreamFilter.class);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
logger.info("HttpStreamFilter start");
CustomizeResponseWrapper responseWrapper = new CustomizeResponseWrapper((HttpServletResponse) servletResponse);
filterChain.doFilter(servletRequest, responseWrapper);
byte[] bytes = responseWrapper.getBytes();
servletResponse.setContentLength(bytes.length);
servletResponse.setContentType("text/plain");
((HttpServletResponse) servletResponse).setStatus(200);
servletResponse.getOutputStream().write(bytes);
servletResponse.flushBuffer();
servletResponse.getOutputStream().close();
logger.info("HttpStreamFilter end");
}
public static class CustomizeResponseWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
private final PrintWriter printWriter = new PrintWriter(outputStream);
public CustomizeResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public PrintWriter getWriter() throws IOException {
return printWriter;
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new ServletOutputStream() {
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener listener) {
}
@Override
public void write(int b) throws IOException {
//写入缓存
outputStream.write(b);
}
};
}
public void flush(){
try {
printWriter.flush();
printWriter.close();
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public ByteArrayOutputStream getByteArrayOutputStream(){
return outputStream;
}
public byte[] getBytes() {
flush();
return outputStream.toByteArray();
}
}
}
代码解释:
- 1、通过Wrapper的方式截获Response报文,将报文copy 到本地buffer中。
- 2、计算buffer的长度a,重新打开response的通道写入 content-length = a,接着将buffer写入到Response的body当中。
注意:这里计算buffer的时候,不同的编码可能长度结果不一样,所以计算的时候要额外小心。
虽然这两个方法能够成功将content-length暴露给客户端,但是经过测试发现,讲过网关后的程序,content-length将会有可能被改写成 Transfer-Encoding = chunked 。为何发生这种情况呢?
其实很容易理解,因为系统是一个微服务架构,微服务是进程与进程之间的通信。spring boot api携带content-legnth返回网关的时候,此时可以把网关作为客户端看待,那么上一个spring boot已经完成自己的传输。结论:api其实携带有content-legnth有,只是网关没有完全copy上一个进程中的header头到本进程,所以实际客户端接收到的header可能有偏差。如图所示:
四、其他不返回content-length 的情况
根据第三点我们知道,即使业务进程返回 content-length 信息,但是网关依然可能会改写,那么什么情况下将不会返回content-length呢?
回答:当前端的Request Header传入"Accept-Encoding" = "gzip,deflate"时,假如Http response的回复包比较小则使用content-length;假如回复包比较大,则网关会使用Transfer-Encoding = chunked 进行声明,进而强制分包分片进行传输。
如果前端不支持Transfer-Encoding = chunked 或者支持起来很麻烦,则要求前端在请求的时候无需传入"Accept-Encoding" = "gzip",网关就不会强制分包分片进行传输。
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。