美文网首页
HttpURLConnection 测试

HttpURLConnection 测试

作者: 蓝笔头 | 来源:发表于2021-07-26 13:05 被阅读0次

实验环境

  • JDK: adopt-openjdk-1.8.0_292
  • SpringBoot:2.5.3
  • SpringCloud:2020.0.3

准备条件

(1)Maven 依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

(2)接收请求的 DemoController

@RestController
@Slf4j
public class DemoController {

    @GetMapping("/test")
    public String testGet(HttpServletRequest request) {
        log.info("remote ip:port = {}:{}", request.getRemoteHost(), request.getRemotePort());
        return "testGet";
    }

    @PostMapping("/test")
    public String testPost(HttpServletRequest request) throws IOException {
        log.info("remote ip:port = {}:{}", request.getRemoteHost(), request.getRemotePort());
        // org.apache.commons.io.IOUtils
        List<String> lines = IOUtils.readLines(request.getInputStream());
        lines.forEach(System.out::println);
        return "testPost";
    }
}

实验

(一)调用 Get 请求

@Slf4j
public class DemoTest {

    public static void main(String[] args) throws Exception {
        String requestUrl = "http://127.0.0.1:8080/test";
        testGet(requestUrl);
    }

    @SneakyThrows
    public static void testGet(String requestUrl) {
        final URL url = new URL(requestUrl);
        final HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        log.info("sleep begin");
        Thread.sleep(3 * 1000);
        log.info("sleep end");

        // 建立 socket 连接,并发送 http 请求
        List<String> lines = IOUtils.readLines(connection.getInputStream());
        lines.forEach(System.out::println);
    }

客户端控制台输出:

11:19:39.047 [main] INFO com.example.demo.DemoTest - sleep begin
11:19:42.047 [main] INFO com.example.demo.DemoTest - sleep end
testGet

服务端控制台输出:

2021-07-26 11:19:42.047  INFO 11832 --- [nio-8080-exec-3] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:7091

Wireshark 网络抓包显示:


Wireshark 抓包

可以看到,在调用 connection.getInputStream() 后才建立 socket 连接,并发送 http 请求。

(二)调用 Post 请求

@Slf4j
public class DemoTest {

    public static void main(String[] args) throws Exception {
        String requestUrl = "http://127.0.0.1:8080/test";
        testPost(requestUrl);
    }

    @SneakyThrows
    public static void testPost(String requestUrl) {
        final URL url = new URL(requestUrl);
        final HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        log.info("getOutputStream() sleep begin");
        Thread.sleep(3 * 1000);
        log.info("getOutputStream() sleep end");

        String text = "你好啊";
        connection.setDoOutput(true);
        // 建立 socket 连接
        OutputStream outputStream = connection.getOutputStream();

        log.info("write() sleep begin");
        Thread.sleep(3 * 1000);
        log.info("write() sleep end");

        outputStream.write(text.getBytes(StandardCharsets.UTF_8));

        log.info("getInputStream sleep begin");
        Thread.sleep(3 * 1000);
        log.info("getInputStream sleep end");

        // 发送 http 请求
        List<String> lines = IOUtils.readLines(connection.getInputStream());
        lines.forEach(System.out::println);
    }

}

客户端控制台输出:

11:26:19.815 [main] INFO com.example.demo.DemoTest - getOutputStream() sleep begin
11:26:22.830 [main] INFO com.example.demo.DemoTest - getOutputStream() sleep end
11:26:22.832 [main] INFO com.example.demo.DemoTest - write() sleep begin
11:26:25.840 [main] INFO com.example.demo.DemoTest - write() sleep end
11:26:25.840 [main] INFO com.example.demo.DemoTest - getInputStream sleep begin
11:26:28.847 [main] INFO com.example.demo.DemoTest - getInputStream sleep end
testPost

服务端控制台输出:

2021-07-26 11:26:28.848  INFO 11832 --- [nio-8080-exec-7] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:11500
你好啊

Wireshark 网络抓包显示:


可以看到:

  • 在调用 connection.getOutputStream() 后会建立 socket 连接。
  • write() 只是把数据写入到 OutputStream 中的缓存,没有调用 flush() 刷新到远程。
  • 在调用 connection.getInputStream() 后才发送 http 请求。

(三)多次调用 Get 请求

@Slf4j
public class DemoTest {

    public static void main(String[] args) throws Exception {
        String requestUrl = "http://127.0.0.1:8080/test";
        IntStream.range(0, 5).forEach(v -> testGet(requestUrl));
    }

    @SneakyThrows
    public static void testGet(String requestUrl) {
        final URL url = new URL(requestUrl);
        final HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        // 如果还没有建立过socket 连接,则建立 socket 连接
        // 如果建立过 socket 连接,则直接从缓存中取出一个可用的
        // 发送 http 请求
        List<String> lines = IOUtils.readLines(connection.getInputStream());
        lines.forEach(System.out::println);
    }
}

客户端控制台输出:

testGet
testGet
testGet
testGet
testGet

服务端控制台输出:

2021-07-26 11:31:56.864  INFO 11832 --- [nio-8080-exec-5] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9611
2021-07-26 11:31:56.873  INFO 11832 --- [nio-8080-exec-6] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9611
2021-07-26 11:31:56.875  INFO 11832 --- [nio-8080-exec-6] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9611
2021-07-26 11:31:56.876  INFO 11832 --- [nio-8080-exec-7] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9611
2021-07-26 11:31:56.878  INFO 11832 --- [nio-8080-exec-8] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9611

Wireshark 网络抓包显示:


可以看到,只建立了一个 socket 连接,后续的请求都是走的同一个 socket 连接。

(四)多线程调用 Get 请求

@Slf4j
public class DemoTest {

    public static void main(String[] args) throws Exception {
        String requestUrl = "http://127.0.0.1:8080/test";
        IntStream.range(0, 6).forEach(v -> testGetWithThread(requestUrl));

        // 睡眠 1s,从而让上一轮执行的 socket 连接被放入缓存池中
        Thread.sleep(1 * 1000);

        IntStream.range(0, 6).forEach(v -> testGetWithThread(requestUrl));
    }

    public static void testGetWithThread(String requestUrl) {
        new Thread(() -> {
            testGet(requestUrl);
        }).start();
    }

    @SneakyThrows
    public static void testGet(String requestUrl) {
        final URL url = new URL(requestUrl);
        final HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        // 如果还没有建立过socket 连接,则建立 socket 连接
        // 如果建立过 socket 连接,则直接从缓存中取出一个可用的
        // 发送 http 请求
        List<String> lines = IOUtils.readLines(connection.getInputStream());
        lines.forEach(System.out::println);
    }

}

客户端控制台输出:

testGet
testGet
testGet
testGet
testGet
testGet
testGet
testGet
testGet
testGet
testGet
testGet

服务端控制台输出:

2021-07-26 11:41:45.812  INFO 11832 --- [io-8080-exec-10] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9099
2021-07-26 11:41:45.812  INFO 11832 --- [nio-8080-exec-6] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9095
2021-07-26 11:41:45.812  INFO 11832 --- [nio-8080-exec-2] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9096
2021-07-26 11:41:45.812  INFO 11832 --- [nio-8080-exec-9] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9097
2021-07-26 11:41:45.812  INFO 11832 --- [nio-8080-exec-7] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9094
2021-07-26 11:41:45.812  INFO 11832 --- [nio-8080-exec-3] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9098
2021-07-26 11:41:46.808  INFO 11832 --- [nio-8080-exec-2] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9097
2021-07-26 11:41:46.808  INFO 11832 --- [nio-8080-exec-5] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9094
2021-07-26 11:41:46.808  INFO 11832 --- [nio-8080-exec-6] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9099
2021-07-26 11:41:46.808  INFO 11832 --- [nio-8080-exec-1] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9095
2021-07-26 11:41:46.808  INFO 11832 --- [nio-8080-exec-4] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9098
2021-07-26 11:41:46.808  INFO 11832 --- [nio-8080-exec-7] com.example.demo.DemoController          : remote ip:port = 127.0.0.1:9103

可以看到,除了 90969103 端口,其他端口的 socket 连接都被使用了两次。

源码解析

HttpURLConnection 的 connect() 方法

public void connect() throws IOException {
    synchronized (this) {
        connecting = true;
    }
    plainConnect();
}

protected void plainConnect()  throws IOException {
    synchronized (this) {
        // 如果已经连接过了,则直接返回
        if (connected) {
            return;
        }
    }
    
    // run without additional permission
    plainConnect0();
}

protected void plainConnect0()  throws IOException {
    try {
        // 1. 获取 HttpClient
        http = getNewHttpClient(url, null, connectTimeout);
        http.setReadTimeout(readTimeout);
    } catch (IOException e) {
        throw e;
    }
    // 设置为已连接状态
    connected = true;
}

protected HttpClient getNewHttpClient(URL url, Proxy p, int connectTimeout)
    throws IOException {
    return HttpClient.New(url, p, connectTimeout, this);
}

// HttpClient class
public static HttpClient New(URL url, Proxy p, int to,
    HttpURLConnection httpuc) throws IOException
{
    return New(url, p, to, true, httpuc);
}

public static HttpClient New(URL url, Proxy p, int to, boolean useCache,
    HttpURLConnection httpuc) throws IOException
{

    HttpClient ret = null;
    // 1. 从缓存中获取 HttpClient
    if (useCache) {
        ret = kac.get(url, null);
    }
    // 2. 缓存中没有,则创建一个新的 HttpClient,并建立 socket 连接
    if (ret == null) {
        ret = new HttpClient(url, p, to);
    }
    return ret;
}

HttpClient 的 finished() 方法

public void finished() {
    if (reuse) /* will be reused */
        return;
    // keep-alive 的连接数减一
    keepAliveConnections--;
    // OutputStream 的 buffer 设置为 null
    poster = null;
    if (keepAliveConnections > 0 && isKeepingAlive() &&
           !(serverOutput.checkError())) {
        // 如果可以,则放入缓存
        putInKeepAliveCache();
    } else {
        // 否则,关闭 socket 连接
        closeServer();
    }
}

protected synchronized void putInKeepAliveCache() {
    if (inCache) {
        assert false : "Duplicate put to keep alive cache";
        return;
    }
    inCache = true;
    kac.put(url, null, this);
}

// KeepAliveCache
public synchronized void put(final URL url, Object obj, HttpClient http) {
    // 1. 创建缓存 key
    KeepAliveKey key = new KeepAliveKey(url, obj);
    
    // 2. 把当前 HttpClient 放入缓存中
    ClientVector v = super.get(key);
    v.put(http);    
}

class KeepAliveKey {
    public KeepAliveKey(URL url, Object obj) {
        // obj 一般为 null
        // 以 protocol:host:port 为 key
        // host:port 表示一个远程服务
        // protocol 表示当前服务支持的协议
        this.protocol = url.getProtocol();
        this.host = url.getHost();
        this.port = url.getPort();
        this.obj = obj;
    }
}

synchronized void put(HttpClient h) {
    // KeepAliveCache.getMaxConnections() 的默认值为 5
    // 表示最多缓存 5 个 HttpClient
    if (size() >= KeepAliveCache.getMaxConnections()) {
        // 1. 如果已经缓存了 5 个 HttpClient,则关闭当前 HttpClient 的 socket 连接
        h.closeServer(); // otherwise the connection remains in limbo
    } else {
        // 2. 如果还没有缓存 5 个 HttpClient,则将当前 HttpClient 加入缓存
        push(new KeepAliveEntry(h, System.currentTimeMillis()));
    }
}

HttpURLConnection 的 getOutputStream() 方法

/*
 * Allowable input/output sequences:
 * [interpreted as request entity]
 * - get output, [write output,] get input, [read input]
 * - get output, [write output]
 * [interpreted as GET]
 * - get input, [read input]
 * Disallowed:
 * - get input, [read input,] get output, [write output]
 */

@Override
public synchronized OutputStream getOutputStream() throws IOException {
    connecting = true;
    return getOutputStream0();
}

private synchronized OutputStream getOutputStream0() throws IOException {
    try {
        // 没有设置 connection.setDoOutput(true); 则抛出异常
        if (!doOutput) {
            throw new ProtocolException("cannot write to a URLConnection"
                           + " if doOutput=false - call setDoOutput(true)");
        }

        // method 如果是 GET,则修改为 POST
        if (method.equals("GET")) {
            method = "POST"; // Backward compatibility
        }
        
        if (!checkReuseConnection())
            connect(); // 建立 socket 连接
        
        if (poster == null) {
            poster = new PosterOutputStream();
        }
        return poster;
    }
}

HttpURLConnection 的 getInputStream() 方法

@Override
public synchronized InputStream getInputStream() throws IOException {
    connecting = true;
    return getInputStream0();
}

private synchronized InputStream getInputStream0() throws IOException {

    // doInput 默认为 true
    if (!doInput) {
        throw new ProtocolException("Cannot read from URLConnection"
               + " if doInput=false (call setDoInput(true))");
    }

    // 已经获取过 InputStream,则直接返回
    if (inputStream != null) {
        return inputStream;
    }

    
    try {
        do {
            // 如果还没有建立 socket 连接,则建立 socket 连接
            if (!checkReuseConnection())
                connect();

            // 发送 http 请求
            if (!streaming()) {
                writeRequests();
            }
            
            // 获取 inputStream
            inputStream = http.getInputStream();
            
            
            try {
                // 获取 response 的 content-length
                cl = Long.parseLong(responses.findValue("content-length"));
            } catch (Exception exc) { };

            if (method.equals("HEAD") || cl == 0 ||
                respCode == HTTP_NOT_MODIFIED ||
                respCode == HTTP_NO_CONTENT) {

                // 如果长度为 0,或其他 case
                // 说明不用通过说明不用通过 InputStream 获取数据,则直接调用 http.finished() 方法释放 HttpClient
                http.finished();
                http = null;
                inputStream = new EmptyInputStream();
                connected = false;
            }

            return inputStream;
        } while (redirects < maxRedirects);
    }
}

参考

相关文章

网友评论

      本文标题:HttpURLConnection 测试

      本文链接:https://www.haomeiwen.com/subject/bkibmltx.html