前幾天在分享"實現本身的wget"的時候,由於咱們的請求是一次性的,http 頭裏設置的Connection: Close
。在HTTP/1.1
爲了提高HTTP 1.0
的網絡性能,增長了keepalive
的特性。瀏覽器在請求的時候都會加上Connection: Keep-Alive
的頭信息,是如何實現的呢?
咱們知道在服務端(nginx)能夠經過設置keepalive_timeout
來控制鏈接保持時間,那麼http
鏈接的保持須要瀏覽器(客戶端)支持嗎?今天我們一塊兒來經過java.net.HttpURLConnection
源碼看看客戶端是如何維護這些http
鏈接的。java
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(); } }
當客戶端從服務端獲取返回的字節流時nginx
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(); } } }
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
socket
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
則是一個棧,每次有同一個域下的請求都入棧。tcp
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 並無關閉。post
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
鏈接的複用。測試
存儲結構以下
複用tcp
的鏈接標準是protocol
+host
+port
,客戶端鏈接與服務端維持的鏈接數也不宜過多,HttpURLConnection
默認只能存5個不一樣的鏈接,再多則直接斷開鏈接(見上面HttpClient#finished
方法),保持鏈接數過多對客戶端和服務端都會增長不小的壓力。
同時KeepAliveCache
也每隔5秒鐘掃描檢測一次,清除過時的httpClient
。