美文网首页
HTTP响应gzip+chunked分段压缩流的解压缩(java

HTTP响应gzip+chunked分段压缩流的解压缩(java

作者: 海盗的帽子 | 来源:发表于2019-02-21 20:29 被阅读1次

    一.问题阐述

    最近做项目的时候遇到这么一个问题:

    用 原生 Socket 进行 HTTP 请求的时候,添加了请求头

    Accept-Encoding: gzip
    

    这个请求头表示的含义就是:返回的数据中会对响应体进行压缩,响应头不进行压缩(HTTP/1.1版)

    如果服务器支持这种格式的压缩,那么返回的数据会如下这种格式

    // 响应头不会压缩 
    HTTP/1.1 200 OK
    Server: Apache-Coyote/1.1
    Content-Encoding: gzip
    Content-Type: text/html;charset=UTF-8
    Date: Wed, 20 Feb 2019 06:19:04 GMT
    
    // 响应体会进压缩
    xxxxxxxxxx
    

    服务器压缩的方式可能如下

        public static byte[] compress(String str, String encoding) {
            if (str == null || str.length() == 0) {
                return null;
            }
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            GZIPOutputStream gzip;
            try {
                gzip = new GZIPOutputStream(out);
                gzip.write(str.getBytes(encoding));//将字符串转为字节数组,对字节数组进行压缩
                gzip.close();
            } catch (IOException e) {
    
            }
            return out.toByteArray();//返回压缩后的字节流
        }
    

    正常情况下,如果请求头包含 gzip,响应时这种方式返回,那么在客户端接收到这种压缩的字节流,只有用同样的压缩流进行解压处理就可以得到数据,并且通常响应头都会包含如下的相应头

    Content-Encoding: gzip
    Content-Length: 13131
    

    这表示返回的响应体是 gzip 格式的,并且字节流长度为 13131

    一般情况是这样


    但是在这样一种情况,如果返回的数据很大,或者数据量不确定(如一些动态网页),这个时候服务器就会选择对数据进行分段,并用一个16进制的数进行划分,表示一段的长度,如

    HTTP/1.1 200 OK
    Server: Apache-Coyote/1.1
    Content-Encoding: gzip
    Content-Type: text/html;charset=UTF-8
    Transfer-Encoding: chunked // 分段的数据就会返回这个响应头
    Date: Wed, 20 Feb 2019 06:19:04 GMT
    
    
    a3 // 16进制 
    xxxxx
    5d9f
    xxxxx
    0  // 以 0 为结尾
    
    

    这就使得响应头包含 gzip 和 chunked 的数据是一段经过分段的压缩流,因此也就不能简单地使用 GZIPInputStream 对数据进行处理

    二.解决方法

    对返回的字节流进行一个代理处理

    public class SegmentInputStream extends InputStream {
        private InputStream mInputStream; //需要处理的字节流
        private HashMap<String, String> mHeaders; //响应头
        private boolean mChunked; //分段的标识
        private boolean mIsData; 
        private boolean mEnd; //读取到末尾的标志 即读取到长度为 0
        private long mReadLength = 0L;//当前读取到的长度
        private long mSegmentLength = -1L; //分段时,每一段的长度
        public final boolean DEBUG = true;
    
    
        public SegmentInputStream(InputStream inputStream) throws IOException {
            mInputStream = inputStream;
            mHeaders = new HashMap<>();
            mChunked = false;
            mIsData = false;
            mEnd = false;
            parseHeaders(); //在构造函数的时候就先将响应头解析,因为其没有压缩
        }
    
        public HashMap<String, String> getHeaders() {
            return mHeaders;
        }
    
       //重写read 方法,每次读的时候跳过分段的16 进制数字
        @Override
        public int read() throws IOException {
    
            return !mChunked ? mInputStream.read() : readChunked();
        }
    
        private int readChunked() throws IOException {
            if (mEnd) {
                return -1;
            }
            int byteCode;
            if (mIsData) {
                byteCode = mInputStream.read();
                mReadLength++;
    
                if (mReadLength == mSegmentLength) {
                    mIsData = false;
                    mReadLength = 0L;
                    mSegmentLength = -1L;
                }
            } // << 数据的部分读取完毕
            else {
                int endTag = 0;//回车字符标识  一个 /n/r 就是一个回车
                byte[] buffer = new byte[1];
                ArrayList<Byte> bytes = new ArrayList<>();
    
                while ((byteCode = mInputStream.read()) != -1) {
                    buffer[0] = (byte) byteCode;// 因为read(x,x,x)
                    // 最后会调用read 所以是一个递归,会栈溢出
                    if (buffer[0] != '\r' && buffer[0] != '\n') {
                        bytes.add(buffer[0]);
                        endTag = 0;
                    } else {/* (buffer[0] == '\n' || buffer[0] == '\r')*/
                        endTag++;
                        if (endTag == 2 && bytes.size() != 0) {//四个字符就是有两个回车符,响应头就终止
                            byte[] resultByte = new byte[bytes.size()];
                            for (int i = 0; i < resultByte.length; i++) {
                                resultByte[i] = bytes.get(i);
                            }
                            String resultStr = new String(resultByte);
                            mSegmentLength = Integer.parseInt(resultStr.trim(), 16);
                            mEnd = mSegmentLength == 0;
                            mIsData = true;
                            break;
                        }
    
                    }
                }//每次处理完成 长度数字后 都 要返回一个 data
                if (mIsData) {
                    if (mEnd) {
                        return -1;
                    }
                    byteCode = mInputStream.read();
                    mReadLength++;
    
                    if (mReadLength == mSegmentLength) {
                        mIsData = false;
                        mReadLength = 0L;
                        mSegmentLength = -1L;
                    }
                }
            }// << 分段的长度读取完毕
    
            return byteCode;
        }
    
        private void parseHeaders() throws IOException {
            if (mInputStream == null) {
                return;
            }
            int enterCount = 0;//回车字符标识  一个 /n/r 就是一个回车
            byte[] buffer = new byte[1];
            ArrayList<Byte> bytes = new ArrayList<>();
            while (read(buffer, 0, 1) != -1) { //
                bytes.add(buffer[0]);
                if (buffer[0] == '\n' || buffer[0] == '\r') {
                    enterCount++;
                    if (enterCount == 4) { //四个字符就是有两个回车符,响应头就终止
                        break;
                    }
                } else {
                    enterCount = 0;
                }
            }
    
            byte[] resultByte = new byte[bytes.size()];
            for (int i = 0; i < resultByte.length; i++) {
                resultByte[i] = bytes.get(i);
            }
            String resultStr = new String(resultByte);
    
    
            String[] headerLines = resultStr.split("\r\n");
            for (String headerLine : headerLines) {
                String[] header = headerLine.split(": ");
                if (header.length == 1) { //HTTP/1.1 200 OK 响应行只有一句
                    mHeaders.put("", header[0].trim());
                } else {
                    mHeaders.put(header[0].trim(), header[1].trim());
    
                }
            }
            mChunked = mHeaders.containsValue("chunked") || mHeaders.containsValue("CHUNKED");
    
            if (DEBUG) {
                System.out.println(mHeaders);
            }
        }
    
    }
    
    

    详细的处理可以看 AppStore

    相关文章

      网友评论

          本文标题:HTTP响应gzip+chunked分段压缩流的解压缩(java

          本文链接:https://www.haomeiwen.com/subject/vqadyqtx.html