http client 實現 keep-alive 源碼探究

前幾天在分享"實現本身的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();
        }

    }
}

加入到 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);
        }
    }
    ...
}

這裏涉及了KeepAliveKeyClientVectorsocket

class KeepAliveKey {
    private String protocol = null;
    private String host = null;
    private int port = 0;
    private Object obj = null;
}

設計這個對象呢,是由於只有protocol+host+port才能肯定爲同一個鏈接。因此用KeepAliveKey做爲KeepAliveCachekey
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鏈接的複用。測試

小結

存儲結構以下
image.png
複用tcp的鏈接標準是protocol+host+port,客戶端鏈接與服務端維持的鏈接數也不宜過多,HttpURLConnection默認只能存5個不一樣的鏈接,再多則直接斷開鏈接(見上面HttpClient#finished方法),保持鏈接數過多對客戶端和服務端都會增長不小的壓力。
同時KeepAliveCache也每隔5秒鐘掃描檢測一次,清除過時的httpClient

相關文章
相關標籤/搜索