天天看點

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

連接配接的。

測試代碼

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

連接配接的複用。

小結

存儲結構如下

http client 實作 keep-alive 源碼探究

複用

tcp

的連接配接标準是

protocol

host

port

,用戶端連接配接與服務端維持的連接配接數也不宜過多,

HttpURLConnection

預設隻能存5個不同的連接配接,再多則直接斷開連接配接(見上面

HttpClient#finished

方法),保持連接配接數過多對用戶端和服務端都會增加不小的壓力。

同時

KeepAliveCache

也每隔5秒鐘掃描檢測一次,清除過期的

httpClient

繼續閱讀