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

一.问题阐述

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

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

1
Accept-Encoding: gzip

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

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

1
2
3
4
5
6
7
8
9
// 响应头不会压缩 
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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,响应时这种方式返回,那么在客户端接收到这种压缩的字节流,只有用同样的压缩流进行解压处理就可以得到数据,并且通常响应头都会包含如下的相应头

1
2
Content-Encoding: gzip
Content-Length: 13131

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

一般情况是这样


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

1
2
3
4
5
6
7
8
9
10
11
12
13
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 对数据进行处理

二.解决方法

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

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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

0%