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

2019-02-21  本文已影响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

上一篇下一篇

猜你喜欢

热点阅读