gzip+chunked页面分段输出,resin gzip trunked无效,页面数据写入自定义buffer
伸手黨請繞路!
首先普及知識:
gzip:一種web常見的壓縮格式,壓縮率大致50%,能極大降低帶寬占用量,gzip 格式: 10字節首部+數據部分+8字節尾部。
chrunked:http1.1功能,支持內容分段傳送給browser,對于后端邏輯比較耗時的頁面,可以先給用戶展示一部分內容,減少白屏時間。
使用nginx作為反向代理的話請確保nginx使用的是http1.1 長連接。
一下是tomcat與resin的實現方式:
tomcat:
tomcat比較簡單,默認是支持trunked模式的,gzip開啟方式如下:
找到tomcat安裝目錄的conf/server.xml,加上如下信息:
對應的jsp位置加上out.flush()將數據刷給browser:
就能見效。
對于resin,或者是需要對內容進行過濾再輸出的情況,就需要先將數據flush到自定義的緩沖區中,以便于我們對內容進行過濾。
這里我們需要使用自定義的response(繼承自HttpServletResponseWrapper)替換原來的HttpServletResponse,見代碼:
public class ResponseWrapper extends HttpServletResponseWrapper implements AutoCloseable {//數據存儲區private AsyByteArrayOutputStream bufferOutStream;private ServletOutputStream servletOutputStream;private PrintWriter printWriter;private HttpServletResponse response;private GzipWrapper gzipWrapper;public ResponseWrapper(HttpServletResponse response) throws IOException {super(response);this.bufferOutStream = new AsyByteArrayOutputStream(1000);this.printWriter = new PrintWriter(new OutputStreamWriter(bufferOutStream, "utf-8"), false);this.response = response;this.servletOutputStream = new WrapperServletOutStream(this.bufferOutStream);this.gzipWrapper = new GzipWrapper(1024);}@Overridepublic PrintWriter getWriter() {return this.printWriter;}@Overridepublic ServletOutputStream getOutputStream() throws IOException {return super.getOutputStream();}@Overridepublic void reset() {this.bufferOutStream.reset();}public void flushBuffer() throws IOException{this.printWriter.flush();this.servletOutputStream.flush();}public byte[] getResponseData() throws IOException{this.flushBuffer();byte[] dataArr = this.bufferOutStream.toByteArray();this.reset();return dataArr;}/*** @description: send datas to browser* @date 16:26 2020/3/1* @param jspWriter* @param lastTrunked* @return void*/public void writerToClient(JspWriter jspWriter, boolean lastTrunked) throws IOException{if (jspWriter != null){jspWriter.flush();}byte[] rowData = this.getResponseData(); // System.out.println("rowData:" + new String(rowData));byte[] gzipData = this.gzipWrapper.writer(rowData, lastTrunked);// System.out.println("row data size:" + rowData.length); // System.out.println("gzip Data size:" + gzipData.length);ServletOutputStream rsOutStream = this.response.getOutputStream();rsOutStream.write(gzipData);rsOutStream.flush();}@Overridepublic void close() throws Exception {this.gzipWrapper.close();}/*** @description:將自定義的buffer關聯servletOutputStream,以便于通過stream能寫入新的buffer* @exception * @return */private class WrapperServletOutStream extends ServletOutputStream{private AsyByteArrayOutputStream outputStream;public WrapperServletOutStream(AsyByteArrayOutputStream outputStream) {this.outputStream = outputStream;}@Overridepublic boolean isReady() {return true; }@Overridepublic void setWriteListener(WriteListener writeListener) {}@Overridepublic void write(int b) throws IOException {outputStream.write(b);}}/*** @description: copy from java.io.ByteArrayOutputStream, remove synchronize* @see ByteArrayOutputStream*/public class AsyByteArrayOutputStream extends OutputStream {protected byte buf[];protected int count;public AsyByteArrayOutputStream() {this(32);}public AsyByteArrayOutputStream(int size) {if (size < 0) {throw new IllegalArgumentException("Negative initial size: "+ size);}buf = new byte[size];}private void ensureCapacity(int minCapacity) {// overflow-conscious codeif (minCapacity - buf.length > 0)grow(minCapacity);}private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = buf.length;int newCapacity = oldCapacity << 1;if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);buf = Arrays.copyOf(buf, newCapacity);}private int hugeCapacity(int minCapacity) {if (minCapacity < 0) // overflowthrow new OutOfMemoryError();return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;}public void write(int b) {ensureCapacity(count + 1);buf[count] = (byte) b;count += 1;}public void write(byte b[], int off, int len) {if ((off < 0) || (off > b.length) || (len < 0) ||((off + len) - b.length > 0)) {throw new IndexOutOfBoundsException();}ensureCapacity(count + len);System.arraycopy(b, off, buf, count, len);count += len;}public void writeTo(OutputStream out) throws IOException {out.write(buf, 0, count);}public void reset() {count = 0;}public byte toByteArray()[] {return Arrays.copyOf(buf, count);}public int size() {return count;}public String toString() {return new String(buf, 0, count);}public String toString(String charsetName)throws UnsupportedEncodingException{return new String(buf, 0, count, charsetName);}@Deprecatedpublic String toString(int hibyte) {return new String(buf, hibyte, 0, count);}public void close() throws IOException {}} }通過以上代碼我們能實現將頁面數據存儲到自定義的bufferOutStream中。
接著在需要分段輸出的位置調用封裝好的方法進行向browser寫數據的操作,注意,由于頁面數據被存儲到我們自定義的buffer中,需要在filter中對最后一段數據進行flush操作,否則瀏覽器得到的頁面數據就是不完整的了。
上述代碼還使用了自定義的gzip實現,因為瀏覽器不支持一個頁面分成多個gzip壓縮包的形式,要是壓縮成多個gzip發送給browser,browser只會顯示第一個包的數據。這里參考了tomcat的實現思路,實際上tomcat的實現復雜得多,功能也更加強大(關于tomcat的實現有興趣可以去查看tomcat的源碼),以及借助jdk1.7在GZIPOutputStream新增的方法,對頁面的數據進行分段壓縮進同一個gzip包,每壓縮進一段數據就通過該段數據在gzip格式buffer中的起止位置將數據拷貝過來(要是最后一塊數據,則調用GZIPOutputStream的finish方法以完成壓縮,將gzip的尾部也發送給browser。),再將該段數據再發送給broswer,這樣在不損害gzip包格式的情況下,瀏覽器接受的就是同一個gzip包,就能正常顯示出頁面了。具體代碼如下:
public class GzipWrapper implements AutoCloseable{//buffer,存儲gzip數據 gzip 格式: 10字節首部,8字節尾部private ByteArrayOutputStream byteArrOutStream;private GZIPOutputStream gzipOutputStream;//下一次讀取的起始位置private int nextReadStart = 0;public GzipWrapper(int size) throws IOException {this( new ByteArrayOutputStream(size) );}public GzipWrapper(ByteArrayOutputStream byteArrOutStream) throws IOException {this.byteArrOutStream = byteArrOutStream;//同步刷新很重要,jdk min version 1.7this.gzipOutputStream = new GZIPOutputStream(this.byteArrOutStream, true);}/*** @Description:壓縮成gzip,再分段返回壓縮的數據* @date 13:50 2020/3/2 0002* @param data* @param lastTrunked* @return byte[]*/public byte[] writer(byte data[], boolean lastTrunked) throws IOException{this.gzipOutputStream.write(data, 0, data.length);this.gzipOutputStream.flush();//是最后一段數據則完成壓縮,返回剩下的數據,包括gzip 壓縮包的尾部格式數據if (lastTrunked){this.gzipOutputStream.finish();}byte[] allGzipData = this.byteArrOutStream.toByteArray();int dataLen = allGzipData.length - this.nextReadStart;byte[] gzipArr = new byte[dataLen];System.arraycopy(allGzipData, this.nextReadStart, gzipArr, 0, dataLen);this.nextReadStart += dataLen;return gzipArr;}@Overridepublic void close() throws Exception {this.gzipOutputStream.close();} }值得注意的是,要是jsp頁面有重定向或者cookie操作的話,需要將對應的操作邏輯寫到第一個trunked輸出的前面,不然瀏覽器收到第一個包的時候,header等信息已經收到了,后續對其修改將無效。
git? demo地址:https://gitee.com/tandatda/base.git
總結
以上是生活随笔為你收集整理的gzip+chunked页面分段输出,resin gzip trunked无效,页面数据写入自定义buffer的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第三方分享接口api
- 下一篇: springboot配置template