昨天下午,突然线上用户反馈,不能正常登陆,吓尿了。本人和后台距离上次发包已经1个多星期过去了,且测试正常BUG反馈全部结束完成的情况下发的包,突然出现问题。
马上开始解决问题,DEBUG开到生产环境下,两段都没有发现任何的错误。由于对产品的安全性做了相应的措施,产品的每个端口都有签名要效验。就在这个时候发现,乱码了。
能发生乱码,必然就是原始字节流发生了什么问题,但是从后台看到的日志,响应的文字都是正常的UTF-8可解读的,且请求头的charset=UTF-8,第一次挠头,后台没问题。
这时前端的请求日志为
HTTP/1.1 200
Date: Mon, 04 Dec 2017 10:34:24 GMT
Content-Type: application/json;charset=UTF-8
Vary: Accept-Encoding
Set-Cookie: yd_cookie=4f620b07-a120-49968ba3d253d11d1b785f3bf8e4f2ed5db0; Expires=1512390864; Path=/; HttpOnly
X-Application-Context: im-gateway:prod:8000
sdkSign: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Server: WAF/2.4-12.1
Content-Encoding: gzip
Transfer-Encoding: chunked
Proxy-Connection: Keep-alive
其实,大佬可能已经看到了端倪。我们待会再说。经过一个小时的排查,决定尝试用裸IP跑一下,结果突然发现是response返回正常字节,在逻辑不动的情况下,可正常接收解析。
此时请求日志为:
Server: nginx/1.12.0
Date: Mon, 04 Dec 2017 11:37:07 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Application-Context: im-gateway:prod:8000
sdkSign:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
没错,此时本文的主角就出现了,Content-Encoding: gzip,在上下两个请求文本的对比下看到了。
HTTP协议上的GZIP编码是一种用来改进WEB应用程序性能的技术。大流量的WEB站点常常使用GZIP压缩技术来让用户感受更快的速度。这一般是指WWW服务器中安装的一个功能,当有人来访问这个服务器中的网站时,服务器中的这个功能就将网页内容压缩后传输到来访的电脑浏览器中显示出来.一般对纯文本内容可压缩到原大小的40%.这样传输就快了,效果就是你点击网址后会很快的显示出来.当然这也会增加服务器的负载. 一般服务器中都安装有这个功能模块的。
举个简单的例子,我们知道数据的最原始形态都是二进制的,计算机看到的就是0100010100110000000001这样的东西,那压缩就是把之间的0变成了8/0类似这样的,减少了实际的字节长度,从来达到压缩的目的。那很明显,看到了gzip,那就要给response做解压。
if (GZIPUtils.isGzip(response.headers())) {
//请求头显示有gzip,需要解压
data = GZIPUtils.uncompress(data);
}
本人近期网络请求都是习惯于用retrofit2,所以解释一下这个问题。强大的网络请求框架能被很多人拥护,自然这样的问题肯定会很好的规避。又因为解析工厂在交给Gosn之后,选择了在网络拦截器Interceptor上过滤响应,辨别是否为服务器正规响应。
public class NetworkInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
boolean checked = true;
if (response.code() == 200) {
//这里是网络拦截器,可以做错误处理
MediaType mediaType = response.body().contentType();
//当调用此方法java.lang.IllegalStateException: closed,原因为OkHttp请求回调中response.body().string()只能有效调用一次
//String content = response.body().string();
byte[] data = response.body().bytes();
if (GZIPUtils.isGzip(response.headers())) {
//请求头显示有gzip,需要解压
data = GZIPUtils.uncompress(data);
}
//获取签名
String sdkSign = response.header("sdkSign");
try {
//效验签名
checked = RSAUtils.verify(data, GlobalField.APP_SERVICE_KEY(), sdkSign);
} catch (Exception e) {
e.printStackTrace();
}
if (!checked) {
return null;
} else {
//创建一个新的responseBody,返回进行处理
return response.newBuilder()
.body(ResponseBody.create(mediaType, data))
.build();
}
} else {
return response;
}
}
}
这就是一切罪恶的根源,如果不处理,retrofit2是默认可以接受带有gzip的字符集的。且在这里有一个注意的点,response.body().string()这个方法一旦使用,只允许使用一次,response中body的content会直接为空,需要在这里做处理的同学主意这个问题。
附上一工具类,便于解决解压和压缩用
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import okhttp3.Headers;
/**
* Created by gaozhen on 2017/12/4.
*/
public class GZIPUtils {
public static final String ENCODE_UTF_8 = "UTF-8";
public static final String ENCODE_ISO_8859_1 = "ISO-8859-1";
/**
* String 压缩至gzip 字节数据
*/
public static byte[] compress(String str) {
return compress(str, ENCODE_UTF_8);
}
/**
* String 压缩至gzip 字节数组,可选择encoding配置
*/
public static byte[] compress(String str, String encoding) {
if (str == null || str.length() == 0) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPOutputStream gzipInputStream;
try {
gzipInputStream = new GZIPOutputStream(out);
gzipInputStream.write(str.getBytes(encoding));
gzipInputStream.close();
} catch (IOException e) {
System.out.println("gzip compress error");
}
return out.toByteArray();
}
/**
* 字节数组解压
*/
public static byte[] uncompress(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
try {
GZIPInputStream gzipInputStream = new GZIPInputStream(in);
byte[] buffer = new byte[256];
int n;
while ((n = gzipInputStream.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
} catch (IOException e) {
System.out.println("gzip uncompress error.");
}
return out.toByteArray();
}
/**
* 字节数组解压至string
*/
public static String uncompressToString(byte[] bytes) {
return uncompressToString(bytes, ENCODE_UTF_8);
}
/**
* 字节数组解压至string,可选择encoding配置
*/
public static String uncompressToString(byte[] bytes, String encoding) {
if (bytes == null || bytes.length == 0) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
try {
GZIPInputStream ungzip = new GZIPInputStream(in);
byte[] buffer = new byte[256];
int n;
while ((n = ungzip.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
return out.toString(encoding);
} catch (IOException e) {
System.out.println("gzip uncompress to string error");
}
return null;
}
/**
* 判断请求头是否存在gzip
*/
public static boolean isGzip(Headers headers) {
boolean gzip = false;
for (String key : headers.names()) {
if (key.equalsIgnoreCase("Accept-Encoding") && headers.get(key).contains("gzip") || key.equalsIgnoreCase("Content-Encoding") && headers.get(key).contains("gzip")) {
gzip = true;
break;
}
}
return gzip;
}
}