解決OkHttp接收gzip壓縮數(shù)據(jù)返回亂碼問題
問題
Retrofit 是現(xiàn)在最流行的網(wǎng)絡(luò)開發(fā)框架之一,功能十分強(qiáng)大,但是最近確遇到一個(gè)十分坑的問題,現(xiàn)在記錄下來,希望看到的人能注意下。
眾所周知,在 HTTP 傳輸時(shí)是支持 gzip 壓縮的,客戶端發(fā)起請(qǐng)求時(shí)在請(qǐng)求頭里增加 Accept-Encoding: gzip,服務(wù)端響應(yīng)時(shí)在返回的頭信息里增加 Content-Encoding: gzip,這表示傳輸?shù)臄?shù)據(jù)是采用 gzip 壓縮的。默認(rèn)情況下,傳輸內(nèi)容是不壓縮的,采用 gzip 壓縮后可以大幅減少傳輸內(nèi)容大小,這樣可以提高傳輸速度,減少流量的使用。
請(qǐng)求頭信息
本來 OkHttp 是默認(rèn)支持 gzip 解壓縮的,不需要額外配置的。但是我在攔截器里統(tǒng)一添加了很多請(qǐng)求頭信息,大概代碼如下:
public class RequestInterceptor implements Interceptor { public RequestInterceptor() { } @Override public Response intercept(Chain chain) throws IOException { Request.Builder builder = chain.request() .newBuilder() .addHeader("Accept", "application/json") .addHeader("Accept-Encoding", "gzip"); Request request = builder.build(); return chain.proceed(request); } }
以前服務(wù)端沒有開啟 gzip 壓縮,一直都沒有問題,某天突然運(yùn)維加了 gzip 壓縮,說是為了要省流量帶寬,結(jié)果就悲劇了,我們 Android APP 里所有的接口都報(bào)錯(cuò)了,明明前一秒都是OK的,后一秒就都不能訪問了,但是 iOS 里卻能正常訪問,這是最令人崩潰的事情。
立即進(jìn)行代碼調(diào)試,發(fā)現(xiàn) Android 里的 http 請(qǐng)求返回的都是亂碼字符串了,其實(shí)這些都是 gzip 壓縮的數(shù)據(jù),不是說 OkHttp 是自動(dòng)支持 gzip 解壓縮的嗎?為什么我們的返回?cái)?shù)據(jù)沒有進(jìn)行 gzip 解壓?還有一個(gè)奇怪的現(xiàn)象是,當(dāng)我把這段代碼 addHeader("Accept-Encoding", "gzip") 去掉之后,一切又恢復(fù)正常了。
BridgeInterceptor攔截器
這是一個(gè)很費(fèi)解的問題,當(dāng)我手動(dòng)加上這個(gè)頭信息時(shí),OkHttp 不會(huì)自動(dòng)解壓 gzip 流,當(dāng)我去掉時(shí) OkHttp 又會(huì)自動(dòng)解壓 gzip 流了,秉著刨根究底的精神我翻看了源碼,終于找到了原因。原來 OkHttp 在最終構(gòu)建請(qǐng)求信息以及處理返回信息時(shí),內(nèi)部使用了一個(gè)叫做 BridgeInterceptor 的攔截器,該類的代碼如下:
public final class BridgeInterceptor implements Interceptor { private final CookieJar cookieJar; public BridgeInterceptor(CookieJar cookieJar) { this.cookieJar = cookieJar; } @Override public Response intercept(Chain chain) throws IOException { Request userRequest = chain.request(); Request.Builder requestBuilder = userRequest.newBuilder(); RequestBody body = userRequest.body(); if (body != null) { MediaType contentType = body.contentType(); //自動(dòng)增添加請(qǐng)求頭 Content-Type if (contentType != null) { requestBuilder.header("Content-Type", contentType.toString()); } long contentLength = body.contentLength(); //如果傳輸長度不為-1,則表示完整傳輸 if (contentLength != -1) { //設(shè)置頭信息 Content-Length requestBuilder.header("Content-Length", Long.toString(contentLength)); requestBuilder.removeHeader("Transfer-Encoding"); } else { //如果傳輸長度為-1,則表示分塊傳輸,自動(dòng)設(shè)置頭信息 requestBuilder.header("Transfer-Encoding", "chunked"); requestBuilder.removeHeader("Content-Length"); } } if (userRequest.header("Host") == null) { requestBuilder.header("Host", hostHeader(userRequest.url(), false)); } //如果沒有設(shè)置頭信息 Connection,則自動(dòng)設(shè)置為 Keep-Alive if (userRequest.header("Connection") == null) { requestBuilder.header("Connection", "Keep-Alive"); } // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing // the transfer stream. boolean transparentGzip = false; if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) { //如果我們沒有在請(qǐng)求頭信息里增加Accept-Encoding,在這里會(huì)自動(dòng)設(shè)置頭信息 Accept-Encoding = gzip transparentGzip = true; requestBuilder.header("Accept-Encoding", "gzip"); } List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url()); if (!cookies.isEmpty()) { requestBuilder.header("Cookie", cookieHeader(cookies)); } if (userRequest.header("User-Agent") == null) { requestBuilder.header("User-Agent", Version.userAgent()); } Response networkResponse = chain.proceed(requestBuilder.build()); HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers()); Response.Builder responseBuilder = networkResponse.newBuilder() .request(userRequest); //如果返回的頭信息里Content-Encoding = gzip,并且我們沒有手動(dòng)在請(qǐng)求頭信息里設(shè)置 Accept-Encoding = gzip,則會(huì)進(jìn)行 gzip 解壓數(shù)據(jù)流 if (transparentGzip && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding")) && HttpHeaders.hasBody(networkResponse)) { GzipSource responseBody = new GzipSource(networkResponse.body().source()); Headers strippedHeaders = networkResponse.headers().newBuilder() .removeAll("Content-Encoding") .removeAll("Content-Length") .build(); responseBuilder.headers(strippedHeaders); String contentType = networkResponse.header("Content-Type"); responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody))); } return responseBuilder.build(); } }
上面代碼關(guān)鍵地方我做了注釋,OkHttp會(huì)額外的增加很多請(qǐng)求頭信息,如果我們?cè)诖a里沒有手動(dòng)設(shè)置Accept-Encoding = gzip,那么OkHttp會(huì)自動(dòng)處理gzip的解壓縮;反之,你需要手動(dòng)對(duì)返回的數(shù)據(jù)流進(jìn)行g(shù)zip解壓縮。
以上就是我的代碼里 gzip 處理失敗的根本原因了,更多關(guān)于OkHttp接收gzip壓縮數(shù)據(jù)返回亂碼的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Android OKHttp源碼解析Https安全處理
- Minio與SpringBoot使用okhttp3問題解決
- Android開發(fā)OkHttp執(zhí)行流程源碼分析
- Feign 使用HttpClient和OkHttp方式
- Android使用OKhttp3實(shí)現(xiàn)登錄注冊(cè)功能+springboot搭建后端的詳細(xì)過程
- 淺談HttpClient、okhttp和RestTemplate的區(qū)別
- Android的簡單前后端交互(okHttp+springboot+mysql)
- OKhttp攔截器實(shí)現(xiàn)實(shí)踐環(huán)節(jié)源碼解析
相關(guān)文章
SpringBoot同一接口多個(gè)實(shí)現(xiàn)類配置的實(shí)例詳解
這篇文章主要介紹了SpringBoot同一接口多個(gè)實(shí)現(xiàn)類配置的實(shí)例詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11Springboot+ElementUi實(shí)現(xiàn)評(píng)論、回復(fù)、點(diǎn)贊功能
這篇文章主要介紹了通過Springboot ElementUi實(shí)現(xiàn)評(píng)論、回復(fù)、點(diǎn)贊功能。如果是自己評(píng)論的還可以刪除,刪除的規(guī)則是如果該評(píng)論下還有回復(fù),也一并刪除。需要的可以參考一下2022-01-01springboot連接neo4j報(bào)錯(cuò)的解決方案
這篇文章主要介紹了springboot連接neo4j報(bào)錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-02-02深入淺析Netty 在 Dubbo 中是如何應(yīng)用的
國內(nèi)知名框架 Dubbo 底層使用的是 Netty 作為網(wǎng)絡(luò)通信,那么內(nèi)部到底是如何使用的呢?今天通過本文給大家詳細(xì)講解,對(duì)Netty 在 Dubbo中應(yīng)用相關(guān)知識(shí)感興趣的朋友跟隨小編一起看看吧2020-05-05Java設(shè)計(jì)模式之代理模式與@Async異步注解失效的解決
代理模式是Java常見的設(shè)計(jì)模式之一。所謂代理模式是指客戶端并不直接調(diào)用實(shí)際的對(duì)象,而是通過調(diào)用代理,來間接的調(diào)用實(shí)際的對(duì)象2022-07-07