前幾天在分享"實作自己的wget"的時候,因為我們的請求是一次性的,http 頭裡設定的 Connection: Close HTTP/1.1 HTTP 1.0 keepalive Connection: Keep-Alive
。在
為了提升
的網絡性能,增加了
的特性。那麼浏覽器在請求的時候都會加上
的頭資訊,是如何實作的呢?
我們知道在服務端(nginx)可以通過設定
keepalive_timeout
來控制連接配接保持時間,那麼 http
連接配接的保持需要浏覽器(用戶端)支援嗎?今天咱們一起來通過 java.net.HttpURLConnection
源碼看看用戶端是如何維護這些 http
連接配接的。 測試代碼
package net.mengkang.demo;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
public class Demo {
public static void main(String[] args) throws IOException {
test();
test();
}
private static void test() throws IOException {
URL url = new URL("http://static.mengkang.net/upload/image/2019/0921/1569075837628814.jpeg");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestProperty("Charset", "UTF-8");
connection.setRequestProperty("Connection", "Keep-Alive");
connection.setRequestMethod("GET");
connection.connect();
BufferedInputStream bufferedInputStream = new BufferedInputStream(connection.getInputStream());
File file = new File("./xxx.jpeg");
OutputStream out = new FileOutputStream(file);
int size;
byte[] buf = new byte[1024];
while ((size = bufferedInputStream.read(buf)) != -1) {
out.write(buf, 0, size);
}
connection.disconnect();
}
}
解析傳回的頭資訊
當用戶端從服務端擷取傳回的位元組流時
connection.getInputStream()
HttpClient
會對傳回的頭資訊進行解析,我簡化了摘取了最重要的邏輯代碼
private boolean parseHTTPHeader(MessageHeader var1, ProgressSource var2, HttpURLConnection var3) throws IOException {
String var15 = var1.findValue("Connection");
...
if (var15 != null && var15.toLowerCase(Locale.US).equals("keep-alive")) {
HeaderParser var11 = new HeaderParser(var1.findValue("Keep-Alive"));
this.keepAliveConnections = var11.findInt("max", this.usingProxy ? 50 : 5);
this.keepAliveTimeout = var11.findInt("timeout", this.usingProxy ? 60 : 5);
}
...
}
是否需要保持長連接配接,是用戶端申請,服務端決定,是以要以服務端傳回的頭資訊為準。比如用戶端發送的請求是
Connection: Keep-Alive
,服務端傳回的是
Connection: Close
那也得以服務端為準。
用戶端請求完成
當第一次執行時
bufferedInputStream.read(buf)
時,
HttpClient
會執行
finished()
方法
public void finished() {
if (!this.reuse) {
--this.keepAliveConnections;
this.poster = null;
if (this.keepAliveConnections > 0 && this.isKeepingAlive() && !this.serverOutput.checkError()) {
this.putInKeepAliveCache();
} else {
this.closeServer();
}
}
}
加入到 http 長連接配接緩存
protected static KeepAliveCache kac = new KeepAliveCache();
protected synchronized void putInKeepAliveCache() {
if (this.inCache) {
assert false : "Duplicate put to keep alive cache";
} else {
this.inCache = true;
kac.put(this.url, (Object)null, this);
}
}
public class KeepAliveCache extends HashMap<KeepAliveKey, ClientVector> implements Runnable {
...
public synchronized void put(URL var1, Object var2, HttpClient var3) {
KeepAliveKey var5 = new KeepAliveKey(var1, var2); // var2 null
ClientVector var6 = (ClientVector)super.get(var5);
if (var6 == null) {
int var7 = var3.getKeepAliveTimeout();
var6 = new ClientVector(var7 > 0 ? var7 * 1000 : 5000);
var6.put(var3);
super.put(var5, var6);
} else {
var6.put(var3);
}
}
...
}
這裡涉及了
KeepAliveKey
和
ClientVector
class KeepAliveKey {
private String protocol = null;
private String host = null;
private int port = 0;
private Object obj = null;
}
設計這個對象呢,是因為隻有
protocol
+
host
port
才能确定為同一個連接配接。是以用
KeepAliveKey
作為
KeepAliveCache
的
key
。
ClientVector
則是一個棧,每次有同一個域下的請求都入棧。
class ClientVector extends Stack<KeepAliveEntry> {
private static final long serialVersionUID = -8680532108106489459L;
int nap;
ClientVector(int var1) {
this.nap = var1;
}
synchronized void put(HttpClient var1) {
if (this.size() >= KeepAliveCache.getMaxConnections()) {
var1.closeServer();
} else {
this.push(new KeepAliveEntry(var1, System.currentTimeMillis()));
}
}
...
}
“斷開”連接配接
connection.disconnect();
如果是保持長連接配接的,實際隻是關閉了一些流,socket 并沒有關閉。
public void disconnect() {
...
boolean var2 = var1.isKeepingAlive();
if (var2) {
var1.closeIdleConnection();
}
...
}
public void closeIdleConnection() {
HttpClient var1 = kac.get(this.url, (Object)null);
if (var1 != null) {
var1.closeServer();
}
}
連接配接的複用
public static HttpClient New(URL var0, Proxy var1, int var2, boolean var3, HttpURLConnection var4) throws IOException {
...
HttpClient var5 = null;
if (var3) {
var5 = kac.get(var0, (Object)null);
...
}
if (var5 == null) {
var5 = new HttpClient(var0, var1, var2);
} else {
...
var5.url = var0;
}
return var5;
}
public class KeepAliveCache extends HashMap<KeepAliveKey, ClientVector> implements Runnable {
...
public synchronized HttpClient get(URL var1, Object var2) {
KeepAliveKey var3 = new KeepAliveKey(var1, var2);
ClientVector var4 = (ClientVector)super.get(var3);
return var4 == null ? null : var4.get();
}
...
}
ClientVector
取的時候則出棧,出棧過程中如果該連接配接已經逾時,則關閉與服務端的連接配接,繼續執行出棧操作。
class ClientVector extends Stack<KeepAliveEntry> {
private static final long serialVersionUID = -8680532108106489459L;
int nap;
ClientVector(int var1) {
this.nap = var1;
}
synchronized HttpClient get() {
if (this.empty()) {
return null;
} else {
HttpClient var1 = null;
long var2 = System.currentTimeMillis();
do {
KeepAliveEntry var4 = (KeepAliveEntry)this.pop();
if (var2 - var4.idleStartTime > (long)this.nap) {
var4.hc.closeServer();
} else {
var1 = var4.hc;
}
} while(var1 == null && !this.empty());
return var1;
}
}
...
}
這樣就實作了用戶端
http
連接配接的複用。
小結
存儲結構如下
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcuMTO3AjNxYDNykzNxAzN1EzLcRDMwEzLclTMwIzLcV2Zh1WavwFZh9GbwV3LcRXZu5yZuF2an5WZt5yYpRXY0N3Lc9CX6MHc0RHaiojIsJye.png)
複用
tcp
的連接配接标準是
protocol
host
port
,用戶端連接配接與服務端維持的連接配接數也不宜過多,
HttpURLConnection
預設隻能存5個不同的連接配接,再多則直接斷開連接配接(見上面
HttpClient#finished
方法),保持連接配接數過多對用戶端和服務端都會增加不小的壓力。
同時
KeepAliveCache
也每隔5秒鐘掃描檢測一次,清除過期的
httpClient