暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

http client 实现 keep-alive 源码探究

程序员跑路笔记 2019-10-04
794


前天在分享"实现自己的wget"的时候,因为我们的请求是一次性的,http 头里设置的 Connection:Close
。在 HTTP/1.1
为了提升 HTTP1.0
的网络性能,增加了 keepalive
的特性。那么浏览器在请求的时候都会加上 Connection:Keep-Alive
的头信息,是如何实现的呢?我们知道在服务端(nginx)可以通过设置 keepalive_timeout
来控制连接保持时间,那么 http
连接的保持需要浏览器(客户端)支持吗?那么今天咱们一起来通过 java.net.HttpURLConnection
源码看看客户端是如何维护这些 http
连接的。

测试代码

  1. package net.mengkang.demo;


  2. import java.io.*;

  3. import java.net.HttpURLConnection;

  4. import java.net.URL;


  5. public class Demo {

  6. public static void main(String[] args) throws IOException {

  7. test();

  8. test();

  9. }


  10. private static void test() throws IOException {

  11. URL url = new URL("http://static.mengkang.net/upload/image/2019/0921/1569075837628814.jpeg");


  12. HttpURLConnection connection = (HttpURLConnection) url.openConnection();

  13. connection.setRequestProperty("Charset", "UTF-8");

  14. connection.setRequestProperty("Connection", "Keep-Alive");

  15. connection.setRequestMethod("GET");

  16. connection.connect();


  17. BufferedInputStream bufferedInputStream = new BufferedInputStream(connection.getInputStream());


  18. File file = new File("./xxx.jpeg");

  19. OutputStream out = new FileOutputStream(file);

  20. int size;

  21. byte[] buf = new byte[1024];

  22. while ((size = bufferedInputStream.read(buf)) != -1) {

  23. out.write(buf, 0, size);

  24. }


  25. connection.disconnect();

  26. }

  27. }

解析返回的头信息

当客户端从服务端获取返回的字节流时

  1. connection.getInputStream()

HttpClient
会对返回的头信息进行解析,我简化了摘取了最重要的逻辑代码

  1. private boolean parseHTTPHeader(MessageHeader var1, ProgressSource var2, HttpURLConnection var3) throws IOException {

  2. String var15 = var1.findValue("Connection");

  3. ...

  4. if (var15 != null && var15.toLowerCase(Locale.US).equals("keep-alive")) {

  5. HeaderParser var11 = new HeaderParser(var1.findValue("Keep-Alive"));

  6. this.keepAliveConnections = var11.findInt("max", this.usingProxy ? 50 : 5);

  7. this.keepAliveTimeout = var11.findInt("timeout", this.usingProxy ? 60 : 5);

  8. }

  9. ...

  10. }


是否需要保持长连接,是客户端申请,服务端决定,所以要以服务端返回的头信息为准。比如客户端发送的请求是 Connection:Keep-Alive
,服务端返回的是 Connection:Close
那也得以服务端为准。

客户端请求完成

当第一次执行时 bufferedInputStream.read(buf)
时, HttpClient
会执行 finished()
方法

  1. public void finished() {

  2. if (!this.reuse) {

  3. --this.keepAliveConnections;

  4. this.poster = null;

  5. if (this.keepAliveConnections > 0 && this.isKeepingAlive() && !this.serverOutput.checkError()) {

  6. this.putInKeepAliveCache();

  7. } else {

  8. this.closeServer();

  9. }


  10. }

  11. }


加入到 http 长连接缓存

  1. protected static KeepAliveCache kac = new KeepAliveCache();


  2. protected synchronized void putInKeepAliveCache() {

  3. if (this.inCache) {

  4. assert false : "Duplicate put to keep alive cache";


  5. } else {

  6. this.inCache = true;

  7. kac.put(this.url, (Object)null, this);

  8. }

  9. }

  1. public class KeepAliveCache extends HashMap<KeepAliveKey, ClientVector> implements Runnable {

  2. ...

  3. public synchronized void put(URL var1, Object var2, HttpClient var3) {

  4. KeepAliveKey var5 = new KeepAliveKey(var1, var2); // var2 null

  5. ClientVector var6 = (ClientVector)super.get(var5);

  6. if (var6 == null) {

  7. int var7 = var3.getKeepAliveTimeout();

  8. var6 = new ClientVector(var7 > 0 ? var7 * 1000 : 5000);

  9. var6.put(var3);

  10. super.put(var5, var6);

  11. } else {

  12. var6.put(var3);

  13. }

  14. }

  15. ...

  16. }

这里涉及了 KeepAliveKey
ClientVector

  1. class KeepAliveKey {

  2. private String protocol = null;

  3. private String host = null;

  4. private int port = 0;

  5. private Object obj = null;

  6. }

设计这个对象呢,是因为只有 protocol
+ host
+ port
才能确定为同一个连接。所以用 KeepAliveKey
作为 KeepAliveCache
key
ClientVector
则是一个栈,每次有同一个域下的请求都入栈。

  1. class ClientVector extends Stack<KeepAliveEntry> {

  2. private static final long serialVersionUID = -8680532108106489459L;

  3. int nap;


  4. ClientVector(int var1) {

  5. this.nap = var1;

  6. }


  7. synchronized void put(HttpClient var1) {

  8. if (this.size() >= KeepAliveCache.getMaxConnections()) {

  9. var1.closeServer();

  10. } else {

  11. this.push(new KeepAliveEntry(var1, System.currentTimeMillis()));

  12. }

  13. }

  14. ...

  15. }

“断开”连接

  1. connection.disconnect();


如果是保持长连接的,实际只是关闭了一些流,socket 并没有关闭。

  1. public void disconnect() {

  2. ...

  3. boolean var2 = var1.isKeepingAlive();

  4. if (var2) {

  5. var1.closeIdleConnection();

  6. }

  7. ...

  8. }

  1. public void closeIdleConnection() {

  2. HttpClient var1 = kac.get(this.url, (Object)null);

  3. if (var1 != null) {

  4. var1.closeServer();

  5. }

  6. }

连接的复用

  1. public static HttpClient New(URL var0, Proxy var1, int var2, boolean var3, HttpURLConnection var4) throws IOException {

  2. ...

  3. HttpClient var5 = null;

  4. if (var3) {

  5. var5 = kac.get(var0, (Object)null);

  6. ...

  7. }


  8. if (var5 == null) {

  9. var5 = new HttpClient(var0, var1, var2);

  10. } else {

  11. ...

  12. var5.url = var0;

  13. }


  14. return var5;

  15. }

  1. public class KeepAliveCache extends HashMap<KeepAliveKey, ClientVector> implements Runnable {

  2. ...

  3. public synchronized HttpClient get(URL var1, Object var2) {

  4. KeepAliveKey var3 = new KeepAliveKey(var1, var2);

  5. ClientVector var4 = (ClientVector)super.get(var3);

  6. return var4 == null ? null : var4.get();

  7. }

  8. ...

  9. }

ClientVector
取的时候则出栈,出栈过程中如果该连接已经超时,则关闭与服务端的连接,继续执行出栈操作。

  1. class ClientVector extends Stack<KeepAliveEntry> {

  2. private static final long serialVersionUID = -8680532108106489459L;

  3. int nap;


  4. ClientVector(int var1) {

  5. this.nap = var1;

  6. }


  7. synchronized HttpClient get() {

  8. if (this.empty()) {

  9. return null;

  10. } else {

  11. HttpClient var1 = null;

  12. long var2 = System.currentTimeMillis();


  13. do {

  14. KeepAliveEntry var4 = (KeepAliveEntry)this.pop();

  15. if (var2 - var4.idleStartTime > (long)this.nap) {

  16. var4.hc.closeServer();

  17. } else {

  18. var1 = var4.hc;

  19. }

  20. } while(var1 == null && !this.empty());


  21. return var1;

  22. }

  23. }

  24. ...

  25. }

这样就实现了客户端 http
连接的复用。

小结

存储结构如下复用 tcp
的连接标准是 protocol
+ host
+ port
,客户端连接与服务端维持的连接数也不宜过多, HttpURLConnection
默认只能存5个不同的连接,再多则直接断开连接(见上面 HttpClient#finished
方法),保持连接数过多对客户端和服务端都会增加不小的压力。同时 KeepAliveCache
也每隔5秒钟扫描检测一次,清除过期的 httpClient

文章转载自程序员跑路笔记,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论