Android原生網(wǎng)絡(luò)庫HttpURLConnection分析——HTTP部分

一举户、前言

目前做Android開發(fā)在網(wǎng)絡(luò)框架上榄鉴,大公司一般都維護著有自己的網(wǎng)絡(luò)框架财著,而一般的應(yīng)用都在使用Volley或者OkHttp诵冒,或者封裝了OkHttp的Retrofit,那么為何還要來分析Android自帶的網(wǎng)絡(luò)框架呢境蜕。當(dāng)然蝙场,Android大部分是來自于Java本身的庫,只不過針對移動端做了更改和簡化粱年。自帶網(wǎng)絡(luò)框架雖然被大家所詬病售滤,但其實是復(fù)雜的,龐大的,很多情況下都是非常貼近 HTTP 協(xié)議以及TCP/IP協(xié)議的完箩。所謂越原始往往越接近真相赐俗。整個分析下來相信會有不錯的收獲。原生框架是一份不錯的學(xué)習(xí)資料弊知。

這里的Android原生網(wǎng)絡(luò)框架的分析是基于 4.3 的阻逮,想必了解的人都知道了。Android從4.4開始就引入OkHttp但還不是作為唯一底層框架秩彤,而在5.0之后完全以O(shè)kHttp為底層網(wǎng)絡(luò)框架了叔扼。

原生的網(wǎng)絡(luò)框架有好幾個包,關(guān)系上看起來有亂:
java.net:主要就是簡單的HTTP協(xié)議實現(xiàn)漫雷,包括URL瓜富,HttpUrlConnection等,也是最基礎(chǔ)的部分
java.security:安全相關(guān)降盹,Http協(xié)議的實現(xiàn)食呻,證書校驗等
javax.crypto:加解密相關(guān)
javax.net:主要有 ssl 相關(guān)
javax.security: 也是安全相關(guān)的。與 **java.security **有什么關(guān)系呢澎现?又有什么不同呢?各自負(fù)責(zé)的職責(zé)是什么每辟?
ibcore.net:算是網(wǎng)絡(luò)庫的中間層封裝
org.apache.harmony.xnet.provider.jsse: apache的包剑辫?有什么用?
這些是否有內(nèi)在聯(lián)系渠欺?帶著這些疑問妹蔽,開始Android原生網(wǎng)絡(luò)庫的分析之旅吧。

還是以網(wǎng)絡(luò)連接挠将、通信與斷開為線索進行分析胳岂。

二、Http連接

1.示例

先來看一段官方通信示例代碼舔稀。

    // 1.打開 URLConnection
    HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
    try {
         // 2.設(shè)置參數(shù)
      urlConnection.setDoOutput(true);
      urlConnection.setChunkedStreamingMode(0);
       // 3.獲取輸出流
      OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream());
      // 4.寫入?yún)?shù)
      writeStream(out);
      // 5.獲取讀入流
      InputStream in = new BufferedInputStream(urlConnection.getInputStream());
     // 6.讀取數(shù)據(jù)
     readStream(in);
   } finally {
    // 7.斷開連接
    urlConnection.disconnect();
   }

代碼很簡單乳丰,但如注釋時所述,共分了7個小步驟内贮。但代碼雖然短产园,可這里卻有不少的疑問:
(1)可以注意到有disconnect()調(diào)用,但卻沒有connect()調(diào)用夜郁。那么openConnection()發(fā)起了真正的網(wǎng)絡(luò)連接了嗎什燕?到底什么時候執(zhí)行的TCP三次握手呢?
(2)disconnect()調(diào)用后竞端,底層的TCP執(zhí)行了4次揮手協(xié)議了嗎屎即?

帶著這些疑問,先從 openConnection()看起吧。

2.openConnection()打開連接

OpenUrlConnection.jpg

URL的構(gòu)建函數(shù)重載的比較多技俐,這里只是簡單傳入一個url乘陪,最終會調(diào)用到如下這個版本的構(gòu)建函數(shù)。

public URL(URL context, String spec, URLStreamHandler handler) throws MalformedURLException {
153        if (spec == null) {
154            throw new MalformedURLException();
155        }
156        if (handler != null) {
157            streamHandler = handler;
158        }
159        spec = spec.trim();
160        // 1.先解析出協(xié)議頭虽另,確定協(xié)議
161        protocol = UrlUtils.getSchemePrefix(spec);
162        int schemeSpecificPartStart = protocol != null ? (protocol.length() + 1) : 0;
163
164        // If the context URL has a different protocol, discard it because we can't use it.
165        if (protocol != null && context != null && !protocol.equals(context.protocol)) {
166            context = null;
167        }
168
169        // Inherit from the context URL if it exists.
170        if (context != null) {
171            set(context.protocol, context.getHost(), context.getPort(), context.getAuthority(),
172                    context.getUserInfo(), context.getPath(), context.getQuery(),
173                    context.getRef());
174            if (streamHandler == null) {
175                streamHandler = context.streamHandler;
176            }
177        } else if (protocol == null) {
178            throw new MalformedURLException("Protocol not found: " + spec);
179        }
180
181        if (streamHandler == null) {
                 // 2.關(guān)鍵調(diào)用暂刘。根據(jù)協(xié)議設(shè)置用來干活的 Handler
182            setupStreamHandler();
183            if (streamHandler == null) {
184                throw new MalformedURLException("Unknown protocol: " + protocol);
185            }
186        }
187
188        // Parse the URL. If the handler throws any exception, throw MalformedURLException instead.
189        try { // 解析url的全部部分,handler解析完成后捂刺,最后會通過URL#set()方法將port,filepath,userinfo等都傳回來的
190            streamHandler.parseURL(this, spec, schemeSpecificPartStart, spec.length());
191        } catch (Exception e) {
192            throw new MalformedURLException(e.toString());
193        }
194    }

如代碼中的關(guān)鍵注釋谣拣,構(gòu)建函數(shù)的主要作用是確定協(xié)議,端口族展,文件路徑森缠,以用于處理實際事務(wù)的handler。這里就假設(shè)就是HTTP協(xié)議仪缸,那么其確定下來的Handler就是HttpHandler.
URLStreamHandler是一個抽象類贵涵,HttpHandler是其子類。parseURL這類公共操作在父類URLStreamHandler中完成恰画,openConnection()因為會發(fā)生不同的行為宾茂,所以其在子類HttpHandler中完成。

public URLConnection openConnection() throws IOException {
471        return streamHandler.openConnection(this);
472    }

URL#openConnection()非常簡單就是進一步調(diào)用相應(yīng)Handler的openConnection()拴还,這里調(diào)用的是HttpHandler#openConnection()

@Override protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
33        if (url == null || proxy == null) {
34            throw new IllegalArgumentException("url == null || proxy == null");
35        }
36        return new HttpURLConnectionImpl(url, getDefaultPort(), proxy);
37    }

這里最終構(gòu)建了一個具體的實現(xiàn)類HttpURLConnectionImpl跨晴,它繼承自HttpURLConnection,而HttpURLConnection又繼承自URLConnection片林。構(gòu)建函數(shù)并沒有做什么特殊的事情端盆,而是一級一級將url向父類傳遞直到URLConnection,并最終記錄在URLConnection费封。同時這里還有一個需要提一下的是焕妙,這里還確定了默認(rèn)端口為 80。

3.參數(shù)設(shè)置

setDoOutput(),setChunkedStreamingMode(),setDoInput()等這些只是設(shè)置一些參數(shù)弓摘,也并沒有真正發(fā)起網(wǎng)絡(luò)請求焚鹊。稍微提一下setChunkedStreamingMode(),用來設(shè)置body的分塊大小韧献,系統(tǒng)建議默認(rèn)值寺旺,即為 1024。
除了這些基礎(chǔ)的參數(shù)可以進行設(shè)置势决,還一個關(guān)鍵的HTTP的request header參數(shù)的添加阻塑。

@Override public final void addRequestProperty(String field, String value) {
511        if (connected) {
512            throw new IllegalStateException("Cannot add request property after connection is made");
513        }
514        if (field == null) {
515            throw new NullPointerException("field == null");
516        }
517        rawRequestHeaders.add(field, value);
518    }

添加的field-value(通常描述是key-value)會被記錄到rawRequestHeaders中,它所屬的類是RawHeaders果复。另外一個要注意的是陈莽,它必須在連接之前就設(shè)置好,否則會引發(fā)異常。那么什么時候進行連接的呢走搁,進一步看getOutputStream吧独柑。

4.getOutputStream

getOutputStream.jpg

過程有點長峡捡,畫的也有點細(xì)贱田,分了20步,需要耐著點性子看内边,不要畏難也不要怕麻煩曲稼,其實挺簡單的索绪。

下面還是來分步驟更詳細(xì)的了解其是如何建立起連接的。

 @Override public final OutputStream getOutputStream() throws IOException {
197        connect();
198
199        OutputStream result = httpEngine.getRequestBody();
200        if (result == null) {
201            throw new ProtocolException("method does not support a request body: " + method);
202        } else if (httpEngine.hasResponse()) {
203            throw new ProtocolException("cannot write request body after response has been read");
204        }
205
206        return result;
207    }

根據(jù)前面的分析贫悄,getOutputStream調(diào)用的是HttpURLConnectionImpl的方法瑞驱。在這里調(diào)用了內(nèi)部方法 connect()

78    @Override public final void connect() throws IOException {
79        initHttpEngine();
80        try {
81            httpEngine.sendRequest();
82        } catch (IOException e) {
83            httpEngineFailure = e;
84            throw e;
85        }
86    }

這里僅有兩個關(guān)鍵調(diào)用,initHttpEngine()以及httpEngine.sendRequest()先來看initHttpEngine()方法窄坦。

private void initHttpEngine() throws IOException {
235        if (httpEngineFailure != null) {
236            throw httpEngineFailure;
237        } else if (httpEngine != null) {
238            return;
239        }
240        // 記錄 connected 狀態(tài)為 true唤反,記住不能再設(shè)置/添加Header參數(shù)了
241        connected = true;
242        try {
243            if (doOutput) { // 設(shè)置了 doOutput 且為 GET 方法的會被自動校正為 POST方法
244                if (method == HttpEngine.GET) {
245                    // they are requesting a stream to write to. This implies a POST method
246                    method = HttpEngine.POST;
247                } else if (method != HttpEngine.POST && method != HttpEngine.PUT) {
248                    // If the request method is neither POST nor PUT, then you're not writing
249                    throw new ProtocolException(method + " does not support writing");
250                }
251            }
                 // 該方法的關(guān)鍵,調(diào)用 newHttpEngine()
252            httpEngine = newHttpEngine(method, rawRequestHeaders, null, null);
253        } catch (IOException e) {
254            httpEngineFailure = e;
255            throw e;
256        }
257    }

這里的關(guān)鍵是記錄了connected狀態(tài)以及進一步調(diào)用 newHttpEngine()方法初始化HttpEngine實例鸭津。

259    /**
260     * Create a new HTTP engine. This hook method is non-final so it can be
261     * overridden by HttpsURLConnectionImpl.
262     */
263    protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
264            HttpConnection connection, RetryableOutputStream requestBody) throws IOException {
265        return new HttpEngine(this, method, requestHeaders, connection, requestBody);
266    }

newHttpEngine是一個可重載的方法彤侍,不同協(xié)議會有自己的 Engine。這里僅僅是構(gòu)建了一個HttpEngine的具體對象實例逆趋,另外需要注意的關(guān)鍵點是 connection 是傳入的 null拥刻。繼續(xù)看 HttpEngine的構(gòu)建函數(shù)。

187    public HttpEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
188            HttpConnection connection, RetryableOutputStream requestBodyOut) throws IOException {
189        this.policy = policy;
190        this.method = method;
191        this.connection = connection;
192        this.requestBodyOut = requestBodyOut;
193
194        try {
195            uri = policy.getURL().toURILenient();
196        } catch (URISyntaxException e) {
197            throw new IOException(e);
198        }
199
200        this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders));
201    }

構(gòu)建函數(shù)只是簡單的記錄相應(yīng)的參數(shù)父泳。但這里有幾個重要的類是需要關(guān)注一下的,下面說說幾個重要的類吴汪。
HttpEngine
它在這里是一個管理類惠窄,HTTP等協(xié)議相關(guān)的如發(fā)起連接,獲取結(jié)果漾橙,斷開連接等都通過該類來完成杆融。此外,還有一個最重要的任務(wù)也是在這里完成霜运,即生成請求行以及包裝請求頭脾歇,都在這里完成。
RequestHeaders淘捡、RawHeaders
RawHeaders記錄的是比較原始的數(shù)據(jù)藕各,如協(xié)議版本號,請求行以及請求Header參數(shù)的fieldname-value焦除〖た觯可以看成就是個暫時記錄數(shù)據(jù)的地方。
RequestHeaders是對RawHeaders的進一步解析,也是HTTP協(xié)議相關(guān)的乌逐。來看看其構(gòu)造函數(shù)竭讳。

public RequestHeaders(URI uri, RawHeaders headers) {
65        this.uri = uri;
66        this.headers = headers;
67
68        HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {
69            @Override public void handle(String directive, String parameter) {
70                if (directive.equalsIgnoreCase("no-cache")) {
71                    noCache = true;
72                } else if (directive.equalsIgnoreCase("max-age")) {
73                    maxAgeSeconds = HeaderParser.parseSeconds(parameter);
74                } else if (directive.equalsIgnoreCase("max-stale")) {
75                    maxStaleSeconds = HeaderParser.parseSeconds(parameter);
76                } else if (directive.equalsIgnoreCase("min-fresh")) {
77                    minFreshSeconds = HeaderParser.parseSeconds(parameter);
78                } else if (directive.equalsIgnoreCase("only-if-cached")) {
79                    onlyIfCached = true;
80                }
81            }
82        };
83
84        for (int i = 0; i < headers.length(); i++) {
85            String fieldName = headers.getFieldName(i);
86            String value = headers.getValue(i);
87            if ("Cache-Control".equalsIgnoreCase(fieldName)) {
88                HeaderParser.parseCacheControl(value, handler);
89            } else if ("Pragma".equalsIgnoreCase(fieldName)) {
90                if (value.equalsIgnoreCase("no-cache")) {
91                    noCache = true;
92                }
93            } else if ("If-None-Match".equalsIgnoreCase(fieldName)) {
94                ifNoneMatch = value;
95            } else if ("If-Modified-Since".equalsIgnoreCase(fieldName)) {
96                ifModifiedSince = value;
97            } else if ("Authorization".equalsIgnoreCase(fieldName)) {
98                hasAuthorization = true;
99            } else if ("Content-Length".equalsIgnoreCase(fieldName)) {
100                try {
101                    contentLength = Integer.parseInt(value);
102                } catch (NumberFormatException ignored) {
103                }
104            } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) {
105                transferEncoding = value;
106            } else if ("User-Agent".equalsIgnoreCase(fieldName)) {
107                userAgent = value;
108            } else if ("Host".equalsIgnoreCase(fieldName)) {
109                host = value;
110            } else if ("Connection".equalsIgnoreCase(fieldName)) {
111                connection = value;
112            } else if ("Accept-Encoding".equalsIgnoreCase(fieldName)) {
113                acceptEncoding = value;
114            } else if ("Content-Type".equalsIgnoreCase(fieldName)) {
115                contentType = value;
116            } else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) {
117                proxyAuthorization = value;
118            }
119        }
120    }

非常的長,但不要畏難或者被嚇到浙踢。其實就是解析RawHeader中的Header字段绢慢,說白了就是處理數(shù)據(jù),只不過處理的協(xié)議頭的參數(shù)罷了洛波。
到這里胰舆,終于構(gòu)建好了 HttpEngine 類了,Header的參數(shù)也解析好了奋岁。下面就可以發(fā)請求了思瘟。好,來看 HttpEngine#sendRequest()闻伶。

207    /**
208     * Figures out what the response source will be, and opens a socket to that
209     * source if necessary. Prepares the request headers and gets ready to start
210     * writing the request body if it exists.
211     */
212    public final void sendRequest() throws IOException {
213        if (responseSource != null) {
214            return;
215        }
216        //1. 準(zhǔn)備請求頭的參數(shù)
217        prepareRawRequestHeaders();
              // 2.初始化響應(yīng)資源
218        initResponseSource();
219        if (responseCache instanceof ExtendedResponseCache) {
220            ((ExtendedResponseCache) responseCache).trackResponse(responseSource);
221        }
222
223        /*
224         * The raw response source may require the network, but the request
225         * headers may forbid network use. In that case, dispose of the network
226         * response and use a GATEWAY_TIMEOUT response instead, as specified
227         * by http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4.
228         */
229        if (requestHeaders.isOnlyIfCached() && responseSource.requiresConnection()) {
230            if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
231                IoUtils.closeQuietly(cachedResponseBody);
232            }
233            this.responseSource = ResponseSource.CACHE;
234            this.cacheResponse = GATEWAY_TIMEOUT_RESPONSE;
235            RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(cacheResponse.getHeaders());
236            setResponse(new ResponseHeaders(uri, rawResponseHeaders), cacheResponse.getBody());
237        }
238
239        if (responseSource.requiresConnection()) {
                  // 3.發(fā)起 socket 請求
240            sendSocketRequest();
241        } else if (connection != null) {
242            HttpConnectionPool.INSTANCE.recycle(connection);
243            connection = null;
244        }
245    }

這里有三個的關(guān)鍵調(diào)用滨攻,在注釋中有描述。下面一個一個來看吧蓝翰。先看 prepareRawRequestHeaders()方法光绕。

prepareRawRequestHeaders()

693    /**
694     * Populates requestHeaders with defaults and cookies.
695     *
696     * <p>This client doesn't specify a default {@code Accept} header because it
697     * doesn't know what content types the application is interested in.
698     */
699    private void prepareRawRequestHeaders() throws IOException {
700        requestHeaders.getHeaders().setStatusLine(getRequestLine());
701
702        if (requestHeaders.getUserAgent() == null) {
703            requestHeaders.setUserAgent(getDefaultUserAgent());
704        }
705
706        if (requestHeaders.getHost() == null) {
707            requestHeaders.setHost(getOriginAddress(policy.getURL()));
708        }
709
710        if (httpMinorVersion > 0 && requestHeaders.getConnection() == null) {
711            requestHeaders.setConnection("Keep-Alive");
712        }
713
714        if (requestHeaders.getAcceptEncoding() == null) {
715            transparentGzip = true;
716            requestHeaders.setAcceptEncoding("gzip");
717        }
718
719        if (hasRequestBody() && requestHeaders.getContentType() == null) {
720            requestHeaders.setContentType("application/x-www-form-urlencoded");
721        }
722
723        long ifModifiedSince = policy.getIfModifiedSince();
724        if (ifModifiedSince != 0) {
725            requestHeaders.setIfModifiedSince(new Date(ifModifiedSince));
726        }
727
728        CookieHandler cookieHandler = CookieHandler.getDefault();
729        if (cookieHandler != null) {
730            requestHeaders.addCookies(
731                    cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap()));
732        }
733    }

這里設(shè)置了請求行:如 GET url HTTP/1.1,設(shè)置UA畜份,設(shè)置Host诞帐,設(shè)置 Connection 為 keep-alive,設(shè)置 accept-encoding 為gzip爆雹。設(shè)置 content-type 為 "application/x-www-form-urlencoded",設(shè)置 if-Modified-Since停蕉,添加發(fā)送給服務(wù)器的 cookie。
準(zhǔn)備好了請求參數(shù)钙态,下面來看一看initResponseSource()方法慧起。

initResponseSource

private void initResponseSource() throws IOException {
              // 默認(rèn)為是從網(wǎng)絡(luò)獲取
252        responseSource = ResponseSource.NETWORK;
253        if (!policy.getUseCaches() || responseCache == null) {
254            return;
255        }
256        // 查看是否有緩存
257        CacheResponse candidate = responseCache.get(uri, method,
258                requestHeaders.getHeaders().toMultimap());
259        if (candidate == null) {
260            return;
261        }
262        // 有緩存的情況下取出響Header與body
263        Map<String, List<String>> responseHeadersMap = candidate.getHeaders();
264        cachedResponseBody = candidate.getBody();
              // 對于不接受的緩存類型直接關(guān)掉被返回
265        if (!acceptCacheResponseType(candidate)
266                || responseHeadersMap == null
267                || cachedResponseBody == null) {
268            IoUtils.closeQuietly(cachedResponseBody);
269            return;
270        }
271        // 緩存可接受,從responseHeadersMap解析出RawHeaders册倒,這個類在上面的分析中已經(jīng)講過了◎炯罚現(xiàn)在看來它也同時用于記錄 Response 的 Header
272        RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap);
              // 構(gòu)建 ResponseHeaders
273        cachedResponseHeaders = new ResponseHeaders(uri, rawResponseHeaders);
274        long now = System.currentTimeMillis();
              // 最后的 response 的實際來源
275        this.responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders);
276        if (responseSource == ResponseSource.CACHE) {//如果是cache,把剛才的 request 以及 response 包裝成Response驻子,這也是要返回給上層用戶的結(jié)果灿意。
277            this.cacheResponse = candidate;
278            setResponse(cachedResponseHeaders, cachedResponseBody);
279        } else if (responseSource == ResponseSource.CONDITIONAL_CACHE) {//條件緩存
280            this.cacheResponse = candidate;
281        } else if (responseSource == ResponseSource.NETWORK) {//網(wǎng)絡(luò)則直接關(guān)閉緩存
282            IoUtils.closeQuietly(cachedResponseBody);
283        } else {
284            throw new AssertionError();
285        }
286    }
287

initResponseSource的主要作用決定請求的結(jié)果是由哪里來的。共有三種情況:網(wǎng)絡(luò)崇呵,緩存缤剧,條件緩存。網(wǎng)絡(luò)沒什么好解釋的域慷,緩存被禁用或者沒有自然就是要發(fā)起網(wǎng)絡(luò)請了鞭执。
緩存司顿,相應(yīng)的類是ResponseCache,這是一個接口兄纺,其具體的子類為HttpResponseCache大溜。關(guān)于這個類的細(xì)節(jié)實現(xiàn)沒有必要再進一步分析,因為其和Volley還算比較深入的分析中的緩存差不太多估脆,緩存算法也是 DiskLruCache钦奋,但這里的key有點差別,用的是 url 的md5編碼疙赠,既安全又節(jié)省資源大小付材。
條件緩存,與Http協(xié)議的緩存密切相關(guān)圃阳。在chooseResponseSource()中最后會根據(jù)request的請求頭參數(shù)來確定是否為條件緩存厌衔。

280    public boolean hasConditions() {
281        return ifModifiedSince != null || ifNoneMatch != null;
282    }

而它們所對應(yīng)的字段就是"If-None-Match"和"If-Modified-Since"。
假設(shè)緩存里沒有吧捍岳,那就要發(fā)起真正的網(wǎng)絡(luò)請了富寿。來看看 sendSocketRequest()方法。

sendSocketRequest

private void sendSocketRequest() throws IOException {
289        if (connection == null) {
290            connect();
291        }
292
293        if (socketOut != null || requestOut != null || socketIn != null) {
294            throw new IllegalStateException();
295        }
296
297        socketOut = connection.getOutputStream();
298        requestOut = socketOut;
299        socketIn = connection.getInputStream();
300
301        if (hasRequestBody()) {
302            initRequestBodyOut();
303        }
304    }

這里以 connection 來決定是否要發(fā)起內(nèi)部的 connect()锣夹。connection是 HttpConnection的實例页徐。進一步看看connect()的代碼。

309    protected void connect() throws IOException {
310        if (connection == null) {
311            connection = openSocketConnection();
312        }
313    }

比較簡單银萍,進一步調(diào)用了 openSocketConnection()來構(gòu)建一個 connection变勇。

315    protected final HttpConnection openSocketConnection() throws IOException {
316        HttpConnection result = HttpConnection.connect(uri, getSslSocketFactory(),
317                policy.getProxy(), requiresTunnel(), policy.getConnectTimeout());
318        Proxy proxy = result.getAddress().getProxy();
319        if (proxy != null) {
320            policy.setProxy(proxy);
321        }
322        result.setSoTimeout(policy.getReadTimeout());
323        return result;
324    }

進一步調(diào)用HttpConnection的靜態(tài)方法connect()來創(chuàng)建 HttpConnection。

 public static HttpConnection connect(URI uri, SSLSocketFactory sslSocketFactory,
90            Proxy proxy, boolean requiresTunnel, int connectTimeout) throws IOException {
91        /*
92         * Try an explicitly-specified proxy.
93         */
94        if (proxy != null) {
95            Address address = (proxy.type() == Proxy.Type.DIRECT)
96                    ? new Address(uri, sslSocketFactory)
97                    : new Address(uri, sslSocketFactory, proxy, requiresTunnel);
98            return HttpConnectionPool.INSTANCE.get(address, connectTimeout);
99        }
100
101        /*
102         * Try connecting to each of the proxies provided by the ProxySelector
103         * until a connection succeeds.
104         */
105        ProxySelector selector = ProxySelector.getDefault();
106        List<Proxy> proxyList = selector.select(uri);
107        if (proxyList != null) {
108            for (Proxy selectedProxy : proxyList) {
109                if (selectedProxy.type() == Proxy.Type.DIRECT) {
110                    // the same as NO_PROXY
111                    // TODO: if the selector recommends a direct connection, attempt that?
112                    continue;
113                }
114                try {
115                    Address address = new Address(uri, sslSocketFactory,
116                            selectedProxy, requiresTunnel);
117                    return HttpConnectionPool.INSTANCE.get(address, connectTimeout);
118                } catch (IOException e) {
119                    // failed to connect, tell it to the selector
120                    selector.connectFailed(uri, selectedProxy.address(), e);
121                }
122            }
123        }
124
125        /*
126         * Try a direct connection. If this fails, this method will throw.
127         */
128        return HttpConnectionPool.INSTANCE.get(new Address(uri, sslSocketFactory), connectTimeout);
129    }

這一堆的代碼贴唇,前面都是設(shè)置代理的搀绣,不管它。真正管用的是最后一句HttpConnectionPool.INSTANCE#get()戳气。即從HttpConnectionPool里返回一個HttpConnection链患。補充一點,INSTANCE代表著HttpConnectionPool對象本身物咳,其實現(xiàn)為一個單例類。進一步看看是如何 Get 的吧蹄皱。

public HttpConnection get(HttpConnection.Address address, int connectTimeout)
64            throws IOException {
65        // First try to reuse an existing HTTP connection.
66        synchronized (connectionPool) {
67            List<HttpConnection> connections = connectionPool.get(address);
68            while (connections != null) {
69                HttpConnection connection = connections.remove(connections.size() - 1);
70                if (connections.isEmpty()) {
71                    connectionPool.remove(address);
72                    connections = null;
73                }
74                if (connection.isEligibleForRecycling()) {
75                    // Since Socket is recycled, re-tag before using
76                    Socket socket = connection.getSocket();
77                    SocketTagger.get().tag(socket);
78                    return connection;
79                }
80            }
81        }
82
83        /*
84         * We couldn't find a reusable connection, so we need to create a new
85         * connection. We're careful not to do so while holding a lock!
86         */
87        return address.connect(connectTimeout);
88    }

Get()是一個常見的緩存池的套路览闰。先判斷當(dāng)前連接池里是否包含有同一Address的且沒有被回收的連接,如果能找到就重新re-tag一下socket巷折,再返回當(dāng)前找到的HttpCoonection压鉴。如果沒有找到就通過 address#connect()方法來創(chuàng)建一個。先不進行下一步來關(guān)注兩個比較重要的點锻拘。

isEligibleForRecycling()方法的實現(xiàn)

248    /**
249     * Returns true if this connection is eligible to be reused for another
250     * request/response pair.
251     */
252    protected boolean isEligibleForRecycling() {
253        return !socket.isClosed()
254                && !socket.isInputShutdown()
255                && !socket.isOutputShutdown();
256    }

從代碼中知道油吭,未被回收的連接的判定條件是:socket未關(guān)閉击蹲,輸入以及輸出流未被關(guān)閉。

連接池的Key——Address
Address是HttpConnection的內(nèi)部類婉宰。我們知道任何一個xx池之類的歌豺,都要有一個唯一的key用來判斷是否為同一資源。這里使用Address對象心包,看起來有點奇怪类咧。既然這么用了,那么它一定是重寫了 equal()方法和hashcode()方法了蟹腾。來看一看痕惋。

@Override public boolean equals(Object other) {
318            if (other instanceof Address) {
319                Address that = (Address) other;
320                return Objects.equal(this.proxy, that.proxy)
321                        && this.uriHost.equals(that.uriHost)
322                        && this.uriPort == that.uriPort
323                        && Objects.equal(this.sslSocketFactory, that.sslSocketFactory)
324                        && this.requiresTunnel == that.requiresTunnel;
325            }
326            return false;
327        }
328
329        @Override public int hashCode() {
330            int result = 17;
331            result = 31 * result + uriHost.hashCode();
332            result = 31 * result + uriPort;
333            result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0);
334            result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
335            result = 31 * result + (requiresTunnel ? 1 : 0);
336            return result;
337        }

果然是被重寫了。從上面兩個方法也可以看出娃殖,判斷為同一Key值戳,需要 host,port,sslfactory,proxy,tunel都是相等的才能算是同一key。
下面繼續(xù)看HttpConnection是如何創(chuàng)建的以及創(chuàng)建完成后做了什么炉爆。前面有分析到堕虹,如果連接池里沒有就會創(chuàng)建一個新的。而創(chuàng)建一個新的HttpConnection是通過HttpConnection的子類Address#connect()方法進行的叶洞。

public HttpConnection connect(int connectTimeout) throws IOException {
340            return new HttpConnection(this, connectTimeout);
341        }

挺簡單的鲫凶,直接new一個實例,那來看看HttpConnection的構(gòu)建函數(shù)衩辟。

private HttpConnection(Address config, int connectTimeout) throws IOException {
62        this.address = config;
63
64        /*
65         * Try each of the host's addresses for best behavior in mixed IPv4/IPv6
66         * environments. See http://b/2876927
67         * TODO: add a hidden method so that Socket.tryAllAddresses can does this for us
68         */
69        Socket socketCandidate = null;
            // 1.尋址螟炫,即通過 hostname查找 IP 地址,可能會查到多個艺晴。
70        InetAddress[] addresses = InetAddress.getAllByName(config.socketHost);
71        for (int i = 0; i < addresses.length; i++) {
                //4. 創(chuàng)建用于執(zhí)行TCP連接的套接字 Socket
72            socketCandidate = (config.proxy != null && config.proxy.type() != Proxy.Type.HTTP)
73                    ? new Socket(config.proxy)
74                    : new Socket();
75            try {
                    // 3. 發(fā)起套接字Socket的TCP請求連接
76                socketCandidate.connect(
77                        new InetSocketAddress(addresses[i], config.socketPort), connectTimeout);
78                break;
79            } catch (IOException e) {
                    // 4.所有地址都試過了昼钻,彈盡糧絕了,報異常封寞。
80                if (i == addresses.length - 1) {
81                    throw e;
82                }
83            }
84        }
85
86        this.socket = socketCandidate;
87    }

雖然只是一個構(gòu)造函數(shù)然评,但復(fù)雜度確不低,按照注釋的步驟來詳細(xì)的看一下狈究。

尋址
調(diào)用的是InetAddress#getAllByName()碗淌,這是一個靜態(tài)方法。

public static InetAddress[] getAllByName(String host) throws UnknownHostException {
214        return getAllByNameImpl(host).clone();
215    }
216
217    /**
218     * Returns the InetAddresses for {@code host}. The returned array is shared
219     * and must be cloned before it is returned to application code.
220     */
221    private static InetAddress[] getAllByNameImpl(String host) throws UnknownHostException {
              // 為空則判定為回路地址抖锥,即 127.0.0.1
222        if (host == null || host.isEmpty()) {
223            return loopbackAddresses();
224        }
225
226        // Is it a numeric address?本身就是IP地址嗎亿眠?是的話就不用繼續(xù)找了。
227        InetAddress result = parseNumericAddressNoThrow(host);
228        if (result != null) {
229            result = disallowDeprecatedFormats(host, result);
230            if (result == null) {
231                throw new UnknownHostException("Deprecated IPv4 address format: " + host);
232            }
233            return new InetAddress[] { result };
234        }
235        // 前面的都不是磅废,那就要進一步查找了纳像。
236        return lookupHostByName(host).clone();
237    }

這里需要關(guān)注的是對內(nèi)部方法lookupHostByName()的調(diào)用。

378    /**
379     * Resolves a hostname to its IP addresses using a cache.
380     *
381     * @param host the hostname to resolve.
382     * @return the IP addresses of the host.
383     */
384    private static InetAddress[] lookupHostByName(String host) throws UnknownHostException {
385        BlockGuard.getThreadPolicy().onNetwork();
386        // Do we have a result cached? 
              // 1.先從緩存里面找
387        Object cachedResult = addressCache.get(host);
388        if (cachedResult != null) {
389            if (cachedResult instanceof InetAddress[]) {
390                // A cached positive result.
391                return (InetAddress[]) cachedResult;
392            } else {
393                // A cached negative result.
394                throw new UnknownHostException((String) cachedResult);
395            }
396        }
397        try {
398            StructAddrinfo hints = new StructAddrinfo();
399            hints.ai_flags = AI_ADDRCONFIG;
400            hints.ai_family = AF_UNSPEC;
401            // If we don't specify a socket type, every address will appear twice, once
402            // for SOCK_STREAM and one for SOCK_DGRAM. Since we do not return the family
403            // anyway, just pick one.
                  // 2.緩存里面沒有拯勉,調(diào)用native方法進一步查找竟趾。Libcore.os.getaddrinfo底層會發(fā)起DNS域名解析請求
404            hints.ai_socktype = SOCK_STREAM;
405            InetAddress[] addresses = Libcore.os.getaddrinfo(host, hints);
406            // TODO: should getaddrinfo set the hostname of the InetAddresses it returns?
407            for (InetAddress address : addresses) {
408                address.hostName = host;
409            }
410            addressCache.put(host, addresses);
411            return addresses;
412        } catch (GaiException gaiException) {
413            // If the failure appears to have been a lack of INTERNET permission, throw a clear
414            // SecurityException to aid in debugging this common mistake.
415            // http://code.google.com/p/android/issues/detail?id=15722
416            if (gaiException.getCause() instanceof ErrnoException) {
417                if (((ErrnoException) gaiException.getCause()).errno == EACCES) {
418                    throw new SecurityException("Permission denied (missing INTERNET permission?)", gaiException);
419                }
420            }
421            // Otherwise, throw an UnknownHostException.
422            String detailMessage = "Unable to resolve host \"" + host + "\": " + Libcore.os.gai_strerror(gaiException.error);
423            addressCache.putUnknownHost(host, detailMessage);
424            throw gaiException.rethrowAsUnknownHostException(detailMessage);
425        }
426    }

尋址就是DNS域名解析憔购,會先從緩存中查看是存在,如果沒有則會由底層發(fā)起請求到DNS服務(wù)器來解析域名映射到實際用于通信的IP地址岔帽。

創(chuàng)建socket并發(fā)起TCP連接
Socket本身還只是一個包裝類玫鸟,它的connect()會繼續(xù)調(diào)具體的實現(xiàn)類。由Socket的構(gòu)造函數(shù)可知山卦,它默認(rèn)是PlainSocketImpl鞋邑,也就是最后調(diào)用了它的connect()方法進行TCP連接。

56        this.impl = factory != null ? factory.createSocketImpl() : new PlainSocketImpl();
57        this.proxy = null;
58    }

到這里終于看到連接發(fā)出去了账蓉。原來構(gòu)建一個HttpConnection就會發(fā)起TCP連接了枚碗。創(chuàng)建好了HttpConnection,方法就會原路返回铸本,并最終返回輸出流給到示例處調(diào)用 getOutputStream()的地方肮雨。

5.getInputStream()

getInputStream()與getOutputStream()的整個執(zhí)行流程是大同小異的,有很多的共同部分箱玷,比如也可能發(fā)起網(wǎng)絡(luò)連接怨规,建立TCP的三次握手。


getInputStream.jpg

有了前面getOutputStream的基礎(chǔ)锡足,getInputStream就簡單多了波丰。

@Override public final InputStream getInputStream() throws IOException {
173        if (!doInput) {
174            throw new ProtocolException("This protocol does not support input");
175        }
176        // 獲取 HttpEngine
177        HttpEngine response = getResponse();
178
179        /*
180         * if the requested file does not exist, throw an exception formerly the
181         * Error page from the server was returned if the requested file was
182         * text/html this has changed to return FileNotFoundException for all
183         * file types
184         */
185        if (getResponseCode() >= HTTP_BAD_REQUEST) {
186            throw new FileNotFoundException(url.toString());
187        }
188         // 從 HttpEngin 那里獲取 InputStream
189        InputStream result = response.getResponseBody();
190        if (result == null) {
191            throw new IOException("No response body exists; responseCode=" + getResponseCode());
192        }
193        return result;
194    }

如注釋第一步是獲取HttpEngine,因為在getOutputStream階段已經(jīng)創(chuàng)建好了舶得,所以不會再重新創(chuàng)建了掰烟,用現(xiàn)成的就好。第二步就是從HttpEngine那里拿到 response body沐批。當(dāng)然這里需要進一步分析的是 getResponse纫骑。

/**
269     * Aggressively tries to get the final HTTP response, potentially making
270     * many HTTP requests in the process in order to cope with redirects and
271     * authentication.
272     */
273    private HttpEngine getResponse() throws IOException {
274        initHttpEngine();
275
276        if (httpEngine.hasResponse()) {
277            return httpEngine;
278        }
279
280        while (true) {
281            try {
282                httpEngine.sendRequest();
283                httpEngine.readResponse();
284            } catch (IOException e) {
285                /*
286                 * If the connection was recycled, its staleness may have caused
287                 * the failure. Silently retry with a different connection.
288                 */
289                OutputStream requestBody = httpEngine.getRequestBody();
290                if (httpEngine.hasRecycledConnection()
291                        && (requestBody == null || requestBody instanceof RetryableOutputStream)) {
292                    httpEngine.release(false);
293                    httpEngine = newHttpEngine(method, rawRequestHeaders, null,
294                            (RetryableOutputStream) requestBody);
295                    continue;
296                }
297                httpEngineFailure = e;
298                throw e;
299            }
300            // 處理響應(yīng)頭,確定重試的策略九孩。
301            Retry retry = processResponseHeaders();
302            if (retry == Retry.NONE) {
                      // 正常情況下不需要重試先馆,因此這里也標(biāo)記HttpConnection由HttpConnectionPool來自動釋放。
303                httpEngine.automaticallyReleaseConnectionToPool();
304                return httpEngine;
305            }
306
307            /*
308             * The first request was insufficient. Prepare for another...
309             */
310            String retryMethod = method;
311            OutputStream requestBody = httpEngine.getRequestBody();
312
313            /*
314             * Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM
315             * redirect should keep the same method, Chrome, Firefox and the
316             * RI all issue GETs when following any redirect.
317             */
318            int responseCode = getResponseCode();
319            if (responseCode == HTTP_MULT_CHOICE || responseCode == HTTP_MOVED_PERM
320                    || responseCode == HTTP_MOVED_TEMP || responseCode == HTTP_SEE_OTHER) {
321                retryMethod = HttpEngine.GET;
322                requestBody = null;
323            }
324
325            if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
326                throw new HttpRetryException("Cannot retry streamed HTTP body",
327                        httpEngine.getResponseCode());
328            }
329
330            if (retry == Retry.DIFFERENT_CONNECTION) {
331                httpEngine.automaticallyReleaseConnectionToPool();
332            } else {
333                httpEngine.markConnectionAsRecycled();
334            }
335            // 需要重試情況下躺彬,主動斷開連接
336            httpEngine.release(true);
337            // 生成一個新的 HttpEngine
338            httpEngine = newHttpEngine(retryMethod, rawRequestHeaders,
339                    httpEngine.getConnection(), (RetryableOutputStream) requestBody);
340        }
341    }

如前所述initHttpEngine()以及httpEngine.sendRequest在getOutputStream那里已經(jīng)做了詳細(xì)分析了煤墙,這里也不用再贅述了。
這里有HttpEngine中的兩個方法需要關(guān)注宪拥,它與如何緩存響應(yīng)數(shù)據(jù)以及如何重用連接有關(guān)仿野。先來看readResponse()方法。

/**
794     * Flushes the remaining request header and body, parses the HTTP response
795     * headers and starts reading the HTTP response body if it exists.
796     */
797    public final void readResponse() throws IOException {
798        if (hasResponse()) {
799            return;
800        }
801
802        if (responseSource == null) {
803            throw new IllegalStateException("readResponse() without sendRequest()");
804        }
805
806        if (!responseSource.requiresConnection()) {
807            return;
808        }
809
810        if (sentRequestMillis == -1) {
811            int contentLength = requestBodyOut instanceof RetryableOutputStream
812                    ? ((RetryableOutputStream) requestBodyOut).contentLength()
813                    : -1;
814            writeRequestHeaders(contentLength);
815        }
816
817        if (requestBodyOut != null) {
818            requestBodyOut.close();
819            if (requestBodyOut instanceof RetryableOutputStream) {
820                ((RetryableOutputStream) requestBodyOut).writeToSocket(requestOut);
821            }
822        }
823
824        requestOut.flush();
825        requestOut = socketOut;
826        // 讀取響應(yīng)頭
827        readResponseHeaders();
828        responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis());
829
830        if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
831            if (cachedResponseHeaders.validate(responseHeaders)) {
832                release(true);//如果是條件緩存且緩存有效則釋放連接并標(biāo)記為重用連接
833                ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
834                setResponse(combinedHeaders, cachedResponseBody);
835                if (responseCache instanceof ExtendedResponseCache) {
836                    ExtendedResponseCache httpResponseCache = (ExtendedResponseCache) responseCache;
837                    httpResponseCache.trackConditionalCacheHit();
838                    httpResponseCache.update(cacheResponse, getHttpConnectionToCache());
839                }
840                return;
841            } else {
842                IoUtils.closeQuietly(cachedResponseBody);
843            }
844        }
845
846        if (hasResponseBody()) {
                  // 將響應(yīng)數(shù)據(jù)寫入緩存
847            maybeCache(); // reentrant. this calls into user code which may call back into this!
848        }
849        // 獲取響應(yīng)體的輸入流江解。
850        initContentStream(getTransferStream());
851    }

注釋里面有幾個關(guān)鍵的內(nèi)部方法調(diào)用设预,一個一個來看吧徙歼。
readResponseHeaders讀取響應(yīng)行與響應(yīng)頭

5    private void readResponseHeaders() throws IOException {
576        RawHeaders headers;
577        do {
578            headers = new RawHeaders();
                  // 讀取響應(yīng)行
579            headers.setStatusLine(Streams.readAsciiLine(socketIn));
                  // 讀取其他響應(yīng)頭參數(shù)犁河,下面直接貼出來了
580            readHeaders(headers);
581        } while (headers.getResponseCode() == HTTP_CONTINUE);
582        setResponse(new ResponseHeaders(uri, headers), null);
583    }



624    private void readHeaders(RawHeaders headers) throws IOException {
625        // parse the result headers until the first blank line
626        String line;
             // 直到讀到空行為止鳖枕,頭與數(shù)據(jù)以空行分開
627        while (!(line = Streams.readAsciiLine(socketIn)).isEmpty()) {
628            headers.addLine(line);
629        }
630        // 解析 cookie
631        CookieHandler cookieHandler = CookieHandler.getDefault();
632        if (cookieHandler != null) {
633            cookieHandler.put(uri, headers.toMultimap());
634        }
635    }

關(guān)鍵的東西在代碼里有注釋了,主要是讀響應(yīng)行桨螺,讀響應(yīng)頭并設(shè)置cookie宾符。而響應(yīng)頭與響應(yīng)數(shù)據(jù)之間的空格就是空行。其實請求的也是灭翔,如果包含有請求體時魏烫,請求頭與請求體之間也是空行分開。
release肝箱,這里先不貼代碼哄褒,后面會再繼續(xù)分析,只要記住在條件緩存的情況下會釋放connection煌张,且?guī)У膮?shù)為 true.
maybeCache呐赡,將響應(yīng)數(shù)據(jù)寫入HttpResponseCache,比較簡單骏融,不展開了链嘀。
getTransferStream,構(gòu)建返回給調(diào)用者的 InputStream档玻。終于拿到InputStream了怀泊。

readResponse()已經(jīng)解析完了,然而在getResponse中還有工作需要做误趴。其接下來會通過處理響應(yīng)頭來確認(rèn)是否需要重試或者重試的策略霹琼,是否需要授權(quán)(401,可能需要用戶名或者密碼),是否需要重定向次數(shù)已達到最大(5次)冤留。不需要重試的情況下標(biāo)記請求由請求池自動回收碧囊。需要重試的情況下,釋放并重用連接纤怒,因為這里調(diào)用了HttpEngine#release()方法時傳入的參數(shù)為 true糯而。這里同樣記住傳入的參數(shù)為true

6.小結(jié)

連接所涉及的東西有點多,也可能有些亂泊窘,也記不住了熄驼,那先總結(jié)一波吧。

6.1openConnection()方法

該方法主要是根據(jù)協(xié)議確定對應(yīng)的幫助類HttpHandler以及HttpURLConnectionImpl烘豹。之后的操作都基于HttpURLConnectionImpl來進行瓜贾。

6.2參數(shù)設(shè)置

在getOutpuStream()以及getInputStream()或者顯式直接調(diào)用connect()方法之前設(shè)置好參數(shù),否則會報異常携悯。這三個方法都會觸發(fā)狀態(tài) connected 為 true祭芦。

6.3Http協(xié)議包裝

在發(fā)出請求前,會構(gòu)造好請求行憔鬼,請求頭參數(shù)一起發(fā)送給服務(wù)器龟劲。并且注意到在HTTP的版本大于 0 的情況下胃夏,默認(rèn)開啟了 keep-alive

6.4緩存

根據(jù)HTTP的緩存規(guī)則,在getInputStream階段會對響應(yīng)回來的數(shù)據(jù)即response進行緩存昌跌。緩存的內(nèi)部算法采用的是DiskLRUCache實現(xiàn)仰禀,key 為對應(yīng) url 的MD5編碼。

6.5連接池

對當(dāng)前連接好的HTTP會通過連接池緩存起來蚕愤,連接池里已經(jīng)有相等的Address了就會進行重用答恶。其中Address相等需要host,port,sslfactory,proxy,tunel都相等。連接池的全部內(nèi)容這里其實還沒有講完萍诱,比如連接put以及回收悬嗓。

6.6TCP連接

getOutpuStream()以及getInputStream()或者顯式直接調(diào)用connect()方法最終都會觸發(fā)TCP的連接,也就是我們需要的HTTP連接裕坊。

三烫扼、HTTP通信

所謂的通信,就是利用getOutputStream()獲取輸出流碍庵,從而將數(shù)據(jù)寫到服務(wù)端映企。再利用getInputStream()獲取輸入流,讀取從服務(wù)端返回的數(shù)據(jù)静浴。

四堰氓、斷開連接

disconnect.jpg

先來看disconnect()方法

@Override public final void disconnect() {
89        // Calling disconnect() before a connection exists should have no effect.
90        if (httpEngine != null) {
91            // We close the response body here instead of in
92            // HttpEngine.release because that is called when input
93            // has been completely read from the underlying socket.
94            // However the response body can be a GZIPInputStream that
95            // still has unread data.
96            if (httpEngine.hasResponse()) {
97                IoUtils.closeQuietly(httpEngine.getResponseBody());
98            }
99            httpEngine.release(false);
100        }
101    }

主要是進一步調(diào)用 HttpEngine#release()方法進行釋放。另外需要注意的是關(guān)閉了responseBody苹享,那這個又是什么呢双絮?后面會詳情講解。

/**
483     * Releases this engine so that its resources may be either reused or
484     * closed.
485     */
486    public final void release(boolean reusable) {
487        // If the response body comes from the cache, close it.
              // 從緩存取的結(jié)果直接關(guān)掉
488        if (responseBodyIn == cachedResponseBody) {
489            IoUtils.closeQuietly(responseBodyIn);
490        }
491       
492        if (!connectionReleased && connection != null) {
493            connectionReleased = true;
494
495            // We cannot reuse sockets that have incomplete output.
                  // 輸出數(shù)據(jù)流還沒有關(guān)閉不能重用
496            if (requestBodyOut != null && !requestBodyOut.closed) {
497                reusable = false;
498            }
499
500            // If the request specified that the connection shouldn't be reused,
501            // don't reuse it. This advice doesn't apply to CONNECT requests because
502            // the "Connection: close" header goes the origin server, not the proxy.
                  // 請求協(xié)議頭描述了connection為close的不能重用
503            if (requestHeaders.hasConnectionClose() && method != CONNECT) {
504                reusable = false;
505            }
506
507            // If the response specified that the connection shouldn't be reused, don't reuse it.
                 // 響應(yīng)協(xié)議頭描述了connection為close的不能重用
508            if (responseHeaders != null && responseHeaders.hasConnectionClose()) {
509                reusable = false;
510            }
511            // 不知道長度的輸入流不能重用
512            if (responseBodyIn instanceof UnknownLengthHttpInputStream) {
513                reusable = false;
514            }
515
516            if (reusable && responseBodyIn != null) {
517                // We must discard the response body before the connection can be reused.
518                try {
519                    Streams.skipAll(responseBodyIn);
520                } catch (IOException e) {
521                    reusable = false;
522                }
523            }
524
525            if (!reusable) {
526                connection.closeSocketAndStreams();
527                connection = null;
528            } else if (automaticallyReleaseConnectionToPool) {
529                HttpConnectionPool.INSTANCE.recycle(connection);
530                connection = null;
531            }
532        }
533    }

中間一大段都可以先不用太關(guān)注得问,看看注釋就行囤攀。看最后幾句宫纬,注意這里的 reuseable 傳遞進來的是false焚挠,那么最后是一定會關(guān)閉連接的了。也就是說一旦執(zhí)行disconnect()連接就會被關(guān)閉漓骚。

131    public void closeSocketAndStreams() {
132        IoUtils.closeQuietly(sslOutputStream);
133        IoUtils.closeQuietly(sslInputStream);
134        IoUtils.closeQuietly(sslSocket);
135        IoUtils.closeQuietly(outputStream);
136        IoUtils.closeQuietly(inputStream);
137        IoUtils.closeQuietly(socket);
138    }

最后把socket與輸入輸出流都關(guān)閉了蝌衔。也就是說整個連接都斷開了。那如果要保持連接一直是重用的蝌蹂,那就不能disconnect()了噩斟。這樣又是否會有內(nèi)存泄漏?
還記得前面提到過的嗎孤个?在兩種情況下框架會自動發(fā)生重用剃允,一個是重試情況下,一個條件緩存情況下。所以說這個連接重用還真的是弱呢斥废。
關(guān)于HttpConnectionPool#recycle()方法來繼續(xù)看代碼覆享。

public void recycle(HttpConnection connection) {
91        Socket socket = connection.getSocket();
92        try {
93            SocketTagger.get().untag(socket);
94        } catch (SocketException e) {
95            // When unable to remove tagging, skip recycling and close
96            System.logW("Unable to untagSocket(): " + e);
97            connection.closeSocketAndStreams();
98            return;
99        }
100        // 
101        if (maxConnections > 0 && connection.isEligibleForRecycling()) {
102            HttpConnection.Address address = connection.getAddress();
103            synchronized (connectionPool) {
104                List<HttpConnection> connections = connectionPool.get(address);
105                if (connections == null) {
106                    connections = new ArrayList<HttpConnection>();
        
107                    connectionPool.put(address, connections);
108                }
                      // 未達到最大連接數(shù)
109                if (connections.size() < maxConnections) {
110                    connection.setRecycled();
                         // 把連接加入到連接池
111                    connections.add(connection);
112                    return; // keep the connection open
113                }
114            }
115        }
116
117        // don't close streams while holding a lock!
             // 不能重用就關(guān)閉
118        connection.closeSocketAndStreams();
119    }

在recycle()時判斷是否已經(jīng)達到最大連接數(shù),如果沒有就可以重用营袜,否則就會關(guān)閉連接。這里還需要關(guān)注一下maxConnections丑罪,默認(rèn)情況下每個URI最大的連接數(shù)為 5荚板。如何得來的?需要關(guān)注一下HttpConnectionPool的構(gòu)造函數(shù)吩屹。

private HttpConnectionPool() {
51        String keepAlive = System.getProperty("http.keepAlive");
52        if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) {
53            maxConnections = 0;
54            return;
55        }
56
57        String maxConnectionsString = System.getProperty("http.maxConnections");
58        this.maxConnections = maxConnectionsString != null
59                ? Integer.parseInt(maxConnectionsString)
60                : 5;
61    }

也就是說需要系統(tǒng)設(shè)置了 http.keepAlive 這個參數(shù)為true跪另,那么就可以進一步設(shè)置maxConnections的大小了∶核眩可以自己定義免绿,也可以使用默認(rèn)值為5.

五、總結(jié)

(1)至此擦盾,Android原生網(wǎng)絡(luò)庫嘲驾,僅HTTP部分終于分析完了,還是學(xué)習(xí)不到少東西的迹卢。
(2)Android原生網(wǎng)絡(luò)庫只是HTTP協(xié)議在網(wǎng)絡(luò)應(yīng)用層的實現(xiàn)辽故,有一定的封裝,但本身并沒有進行過多的擴展腐碱。
(3)這里面我們能看到最基本的一些Http協(xié)議相關(guān)的內(nèi)容誊垢,如請求行,請求頭以及請求體的封裝症见,解析喂走,默認(rèn)參數(shù)設(shè)置等。當(dāng)然也有對響應(yīng)行谋作,響應(yīng)頭以及響應(yīng)體的解析芋肠,封裝,默認(rèn)處理等遵蚜。對了业栅,還有一個重要的,那就是cookie谬晕,也有相應(yīng)的解析以及處理碘裕,包括請求和響應(yīng)時。
(4)Android原生網(wǎng)絡(luò)庫同樣也實現(xiàn)了響應(yīng)內(nèi)容的緩存攒钳,所用的算法也是DiskLRUCache
(5)關(guān)于重用帮孔,除了依賴Http的請求參數(shù) keep-alive通知服務(wù)器希望連接之外,還依賴于系統(tǒng)對 http.keepalive的設(shè)定。而同一個url默認(rèn)最多只能有5個連接文兢。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末晤斩,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子姆坚,更是在濱河造成了極大的恐慌澳泵,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兼呵,死亡現(xiàn)場離奇詭異兔辅,居然都是意外死亡,警方通過查閱死者的電腦和手機击喂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門维苔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人懂昂,你說我怎么就攤上這事介时。” “怎么了凌彬?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵沸柔,是天一觀的道長。 經(jīng)常有香客問我铲敛,道長勉失,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任原探,我火速辦了婚禮乱凿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘咽弦。我一直安慰自己徒蟆,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布型型。 她就那樣靜靜地躺著段审,像睡著了一般。 火紅的嫁衣襯著肌膚如雪闹蒜。 梳的紋絲不亂的頭發(fā)上寺枉,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天,我揣著相機與錄音绷落,去河邊找鬼姥闪。 笑死,一個胖子當(dāng)著我的面吹牛砌烁,可吹牛的內(nèi)容都是我干的筐喳。 我是一名探鬼主播催式,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼避归!你這毒婦竟也來了荣月?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤梳毙,失蹤者是張志新(化名)和其女友劉穎哺窄,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體账锹,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡萌业,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了牌废。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡啤握,死狀恐怖鸟缕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情排抬,我是刑警寧澤懂从,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站蹲蒲,受9級特大地震影響番甩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜届搁,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一缘薛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧卡睦,春花似錦宴胧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至瞬逊,卻和暖如春显歧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背确镊。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工士骤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蕾域。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓敦间,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子廓块,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容