先介紹一下我研究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()
方法會創(chuàng)建URLConnection
實例哆键,但是URLConnection
實例并不代表真正的TCP連接,只有當(dāng)調(diào)用URLConnection.connect()
方法才會創(chuàng)建TCP連接瘦锹,接下來我們看一下這個方法的注釋籍嘹。下圖是
connect()
方法的注釋,可以看出調(diào)用此方法便會建立連接此時建立連接弯院,那么如果是長連接那是不是在這里就會獲取緩存里的連接呢辱士?抱著疑問,開始斷點抽兆。
圖中可以看出识补,確實是去緩存中獲取了連接,不過這個連接不是
URLConnection
辫红,而是HttpClient
凭涂。那么問題來了,這個緩存是在什么時候存儲的呢贴妻?
何時緩存
斷點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);
}
}
}
HttpInputStream
是HttpURLConnection
的內(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()
方法中的三個判斷都會返回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)雏赦。