HttpUrlConnection 源碼分析

先介紹一下我研究HttpUrlConnection的背景貌矿,公司對外提供的SDK是使用HttpUrlConnection(歷史原因)寫的,有開發(fā)者反饋調(diào)用量很大傲须,短連接太耗資源赦肋。然后我們后臺給他開了長連接白名單,但是他們還是反饋我們提供的不是長連接哮塞,因為他們看了我們sdk的源碼,說我們調(diào)用了HttpURLConnection.disconnect()方法凳谦,所以不是長連接忆畅。為了確認這個問題,開始了我的驗證和研究之路尸执。

驗證過程

  • 測試代碼
package com;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpRetryException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.locks.LockSupport;

/**
 * create by liuyj on 2020/6/30
 *
 * @author yuanjian.e@foxmail.com
 */
public class ConnTest {

    public static void main(String[] args) throws Exception {
        final int code = 1;
        get(conn(code));
        get(conn(code));
        get(conn(code));
        LockSupport.park();
        System.out.println("============");
    }

    public static HttpURLConnection conn(int code) throws IOException {
        URL url = new URL("http://127.0.0.1/test/checkStatus?code=" + code);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();

        return conn;
    }

    public static boolean get(HttpURLConnection connection) throws IOException, InterruptedException {
        connection.setRequestMethod("GET");
        connection.setUseCaches(false);
        connection.setRequestProperty("Content-Type", "text/html;charset=UTF-8");
        connection.setDoOutput(false);
        connection.setDoInput(true);
        try {
            connection.connect();
            int code = connection.getResponseCode();
            if (code == HttpURLConnection.HTTP_OK) {
                return true;
            } else {
                throw new HttpRetryException("Response Code Error", code);
            }
        } finally {
            InputStream inputStream = connection.getInputStream();
            if (inputStream != null) {
                inputStream.close();
            }
            if (connection != null) {
                connection.disconnect();
            }
            System.out.println("closed");
        }
    }
}

  • 本地通過抓包工具 wireshark 確認是否使用長連接家凯,為模擬線上環(huán)境,本地安裝了nginx如失,端口為80
    3次Http請求的包

    從圖中可以看出三次HTTP請求端口號沒有改變绊诲,且只進行了三次握手和四次揮手,所以說是長連接(客戶端和nginx之間)褪贵。但是問題來了驯镊,為什么調(diào)用了HttpURLConnection.disconnect()了還是長連接了?后面讓我們一起來分析一下源碼竭鞍。

源碼分析

源碼分析按照何時連接何時緩存橄镜,何時關(guān)閉三個過程分析源碼偎快。其實看源碼的過程中,因為用戶反饋我們調(diào)用了disconnect()方法洽胶,所以先看了該方法并斷點晒夹,然后一步一步下去的。排查過程中發(fā)現(xiàn)一個很關(guān)鍵的類KeepAliveCache姊氓,是用來緩存連接的類丐怯,后面的斷點調(diào)試會主要用到這個類,所以我們先簡單看一下這個類翔横。

public synchronized void put(URL var1, Object var2, HttpClient var3);
public synchronized HttpClient get(URL var1, Object var2);

這個類有兩個核心方法读跷,put()get(),看名字基本可以聯(lián)想到是用來做什么的禾唁,put()方法是用來緩存連接使用的效览,get()方法是用來獲取緩存中的連接无切。

何時連接

首先我們看一下HttpURLConnection conn = (HttpURLConnection) url.openConnection();做了什么,下圖是方法注釋丐枉。

openConnection()

從上圖注釋中可以看出openConnection()方法會創(chuàng)建URLConnection實例哆键,但是URLConnection實例并不代表真正的TCP連接,只有當(dāng)調(diào)用URLConnection.connect()方法才會創(chuàng)建TCP連接瘦锹,接下來我們看一下這個方法的注釋籍嘹。
下圖是connect()方法的注釋,可以看出調(diào)用此方法便會建立連接
connect()

此時建立連接弯院,那么如果是長連接那是不是在這里就會獲取緩存里的連接呢辱士?抱著疑問,開始斷點抽兆。
長連接

圖中可以看出识补,確實是去緩存中獲取了連接,不過這個連接不是URLConnection辫红,而是HttpClient凭涂。那么問題來了,這個緩存是在什么時候存儲的呢贴妻?

何時緩存

斷點put()方法

put()

發(fā)現(xiàn)調(diào)用inputStream.close();時緩存了HttpClient切油。我們看一下這個方法HttpInputStream.close()的源碼。

public void close() throws IOException {
    if (!this.closed) {
        try {
            if (this.outputStream != null) {
                if (this.read() != -1) {
                    this.cacheRequest.abort();
                } else {
                    this.outputStream.close();
                }
            }

            super.close();
        } catch (IOException var5) {
            if (this.cacheRequest != null) {
                this.cacheRequest.abort();
            }

            throw var5;
        } finally {
            this.closed = true;
            HttpURLConnection.this.http = null;
            HttpURLConnection.this.checkResponseCredentials(true);
        }

    }
}

HttpInputStreamHttpURLConnection的內(nèi)部類名惩,可以看到finally中將HttpURLConnection的成員變量http置為了null澎胡,可能有同學(xué)會好奇為什么這么做呢?是因為前面說的娩鹉,http對象被緩存了攻谁,所以這里不能再有這個對象的引用了。那么它的連接到底什么時候斷開呢弯予?調(diào)用disconnect()方法會斷開這個長連接嗎戚宦?

何時斷開

我們先看一下HttpURLConnection.disconnect()的源碼

public void disconnect() {
    this.responseCode = -1;
    if (this.pi != null) {
        this.pi.finishTracking();
        this.pi = null;
    }

    if (this.http != null) {
        if (this.inputStream != null) {
            HttpClient var1 = this.http;
            boolean var2 = var1.isKeepingAlive();

            try {
                this.inputStream.close();
            } catch (IOException var4) {
            }

            if (var2) {
                var1.closeIdleConnection();
            }
        } else {
            this.http.setDoNotRetry(true);
            this.http.closeServer();
        }

        this.http = null;
        this.connected = false;
    }

    this.cachedInputStream = null;
    if (this.cachedHeaders != null) {
        this.cachedHeaders.reset();
    }

}
disconnect()

通過斷點可以看到,disconnect()方法中的三個判斷都會返回false锈嫩,相當(dāng)于這個方法只做了一件事受楼,this.responseCode = -1;,所以這個方法并不會斷開TCP連接呼寸。另外上面分析了HttpURLConnection.http對象是在inputStream.close()方法被調(diào)用時置為null的艳汽,另外連個對象我并沒有深入去了解,有興趣的同學(xué)可以自己研究一下对雪。那么長連接到底何時會被關(guān)閉呢河狐?會根據(jù)nginx端設(shè)置的超時時間自動過期,同時若nginx本身不支持長連接,HttpClient對象也不會被緩存甚牲,具體細節(jié)义郑,大家可以自行研究。

總結(jié)

如果要使用長連接丈钙,首先服務(wù)端需要支持非驮,其次必須調(diào)用HttpURLConnection.getInputStream().close()方法,跟是否調(diào)用HttpURLConnection.disconnect()無關(guān)雏赦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末劫笙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子星岗,更是在濱河造成了極大的恐慌填大,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件俏橘,死亡現(xiàn)場離奇詭異允华,居然都是意外死亡,警方通過查閱死者的電腦和手機寥掐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門靴寂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人召耘,你說我怎么就攤上這事百炬。” “怎么了污它?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵剖踊,是天一觀的道長。 經(jīng)常有香客問我衫贬,道長德澈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任固惯,我火速辦了婚禮圃验,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘缝呕。我一直安慰自己,他們只是感情好斧散,可當(dāng)我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布供常。 她就那樣靜靜地躺著,像睡著了一般鸡捐。 火紅的嫁衣襯著肌膚如雪栈暇。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天箍镜,我揣著相機與錄音源祈,去河邊找鬼煎源。 笑死,一個胖子當(dāng)著我的面吹牛香缺,可吹牛的內(nèi)容都是我干的手销。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼图张,長吁一口氣:“原來是場噩夢啊……” “哼锋拖!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起祸轮,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤兽埃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后适袜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體柄错,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年苦酱,在試婚紗的時候發(fā)現(xiàn)自己被綠了售貌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡躏啰,死狀恐怖趁矾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情给僵,我是刑警寧澤毫捣,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站帝际,受9級特大地震影響蔓同,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蹲诀,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一斑粱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧脯爪,春花似錦则北、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至掖举,卻和暖如春快骗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工方篮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留名秀,地道東北人。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓藕溅,卻偏偏與公主長得像匕得,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蜈垮,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,685評論 2 360