一个简单的 Java HTTP 服务器在 ApacheBench 上失败但在浏览器上运行良好

A simple Java HTTP server fails with ApacheBench but works fine on a browser

作为并发博客系列的一部分,我用不同的语言(Java、Kotlin、Rust、Go、JS、TS)构建了最简单的 HTTP 服务器,除了 [=52] =],又名在 JVM 上。所有代码都可以找到here。下面是 Java 中的服务器代码,我尝试了一个基于传统线程的代码和一个基于 AsynchronousServerSocketChannel 的代码,但是无论我何时 运行 使用 ApacheBench 进行基准测试,它都会失败 Broken pipeapr_socket_recv: Connection reset by peer (104) 这很奇怪,因为其他语言中的类似设置工作正常。这里的问题只发生在 ApacheBench 上,因为当我在浏览器中访问 URL 时它工作正常。所以我正在努力弄清楚发生了什么。我试着玩 keep-alive 等,但似乎没有帮助。我看了一堆类似的例子,但我没有看到任何地方有什么特别的地方。我希望有人能弄清楚这里出了什么问题,因为它看起来肯定与 JVM + APacheBench 有关。我用 Java 11 和 15 试过了,但结果是一样的。

Java 线程示例hello.html 可以是任何 HTML 文件)

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class JavaHTTPServerCopy {
    public static void main(String[] args) {
        int port = 8080;
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("Server is listening on port " + port);
            while (true) {
                new ServerThreadCopy(serverSocket.accept()).start();
            }
        } catch (IOException ex) {
            System.out.println("Server exception: " + ex.getMessage());
        }
    }
}

class ServerThreadCopy extends Thread {

    private final Socket socket;

    public ServerThreadCopy(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        var file = new File("hello.html");
        try (
                // we get character output stream to client (for headers)
                var out = new PrintWriter(socket.getOutputStream());
                // get binary output stream to client (for requested data)
                var dataOut = new BufferedOutputStream(socket.getOutputStream());
                var fileIn = new FileInputStream(file)
        ) {
            var fileLength = (int) file.length();
            var fileData = new byte[fileLength];
            int read = fileIn.read(fileData);
            System.out.println("Responding with Content-length: " + read);
            var contentMimeType = "text/html";
            // send HTTP Headers
            out.println("HTTP/1.1 200 OK");
            out.println("Connection: keep-alive");
            out.println("Content-type: " + contentMimeType);
            out.println("Content-length: " + fileLength);
            out.println(); // blank line between headers and content, very important !
            out.flush(); // flush character output stream buffer

            dataOut.write(fileData, 0, fileLength);
            dataOut.flush();
        } catch (Exception ex) {
            System.err.println("Error with exception : " + ex);
        } finally {
            try {
                socket.close(); // we close socket connection
            } catch (Exception e) {
                System.err.println("Error closing stream : " + e.getMessage());
            }
        }
    }
}

控制台错误

Responding with Content-length: 176
Error with exception : java.net.SocketException: Broken pipe (Write failed)
Error with exception : java.net.SocketException: Broken pipe (Write failed)
Error with exception : java.net.SocketException: Broken pipe (Write failed)
Error with exception : java.net.SocketException: Broken pipe (Write failed)
Error with exception : java.net.SocketException: Broken pipe (Write failed)

ApacheBench 输出

ab -c 100 -n 1000 http://localhost:8080/ 

This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
apr_socket_recv: Connection reset by peer (104)

Java 异步样本

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

public class JavaAsyncHTTPServer {

    public static void main(String[] args) throws Exception {
        new JavaAsyncHTTPServer().go();
        Thread.currentThread().join();//Wait forever
    }

    private void go() throws IOException {
        AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();
        InetSocketAddress hostAddress = new InetSocketAddress("localhost", 8080);
        server.bind(hostAddress);
        server.setOption(StandardSocketOptions.SO_REUSEADDR, true);
        System.out.println("Server channel bound to port: " + hostAddress.getPort());

        if (server.isOpen()) {
            server.accept(null, new CompletionHandler<>() {
                @Override
                public void completed(final AsynchronousSocketChannel result, final Object attachment) {
                    if (server.isOpen()) {
                        server.accept(null, this);
                    }
                    handleAcceptConnection(result);
                }

                @Override
                public void failed(final Throwable exc, final Object attachment) {
                    if (server.isOpen()) {
                        server.accept(null, this);
                        System.out.println("Connection handler error: " + exc);
                    }
                }
            });
        }
    }

    private void handleAcceptConnection(final AsynchronousSocketChannel ch) {
        var content = "Hello Java!";
        var message = ("HTTP/1.0 200 OK\n" +
                "Connection: keep-alive\n" +
                "Content-length: " + content.length() + "\n" +
                "Content-Type: text/html; charset=utf-8\r\n\r\n" +
                content).getBytes();
        var buffer = ByteBuffer.wrap(message);
        ch.write(buffer);
        try {
            ch.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

控制台没有错误

ApacheBench 输出

❯ ab -c 100 -n 1000 http://localhost:8080/

This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
apr_socket_recv: Connection reset by peer (104)

带 keep-alive 的 ApacheBench 输出

 ab -k -c 100 -n 1000 http://localhost:8080/
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
apr_socket_recv: Connection reset by peer (104)
Total of 37 requests completed

想问一下是否尝试过设置 127.0.0.1 而不是 localhost

InetAddress 地址 = InetAddress.getByName("127.0.0.1"); ServerSocket sock = new ServerSocket(1234, 50, addr);

我在 IntelliJ IDEA 中使用了你的“Java 线程示例”代码和 运行 它在 macOS 11.1 上,然后 运行 安装在我的机器上的默认 ab 二进制文件 - 的输出测试 运行 与您的相似:

➜ ab -c 100 -n 1000 http://127.0.0.1:8080/
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
apr_socket_recv: Connection reset by peer (54)

经过一番研究,我收集到的主要信息指出了 macOS 上的 ab 二进制文件可能已损坏的方向。我走上了 运行 在 docker 容器中安装 ab 二进制文件的道路。

➜ docker run --rm jordi/ab -k -c 100 -n 1000 http://host.docker.internal:8080/
This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking host.docker.internal (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:
Server Hostname:        host. docker.internal
Server Port:            8080

Document Path:          /
Document Length:        7 bytes

Concurrency Level:      100
Time taken for tests:   0.343 seconds
Complete requests:      1000
Failed requests:        495
   (Connect: 0, Receive: 0, Length: 495, Exceptions: 0)
Keep-Alive requests:    505
Total transferred:      45657 bytes
HTML transferred:       3591 bytes
Requests per second:    2913.39 [#/sec] (mean)
Time per request:       34.324 [ms] (mean)
Time per request:       0.343 [ms] (mean, across all concurrent requests)
Transfer rate:          129.90 [Kbytes/sec] received

Connection Times (ms)
               min  mean[+/-sd] median   max
Connect:        0   15  15.5     20      48
Processing:     0   15  11.1     11      43
Waiting:        0   13  13.6     10      43
Total:          0   31  25.4     33      75

Percentage of the requests served within a certain time (ms)
  50%     33
  66%     46
  75%     57
  80%     59
  90%     65
  95%     68
  98%     73
  99%     74
 100%     75 (longest request)

可能值得在您的测试中尝试不同的 version/revision ApacheBench。

编辑:在 运行 宁 ApacheBench 时省略 -k(保持活动标志)时,输出为

➜ docker run --rm jordi/ab -c 100 -n 1000 
http://host.docker.internal:8080/
This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, 
http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking host.docker.internal (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:
Server Hostname:        host.docker.internal
Server Port:            8080

Document Path:          /
Document Length:        7 bytes

Concurrency Level:      100
Time taken for tests:   0.934 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      90157 bytes
HTML transferred:       7091 bytes
Requests per second:    1070.17 [#/sec] (mean)
Time per request:       93.444 [ms] (mean)
Time per request:       0.934 [ms] (mean, across all concurrent requests)
Transfer rate:          94.22 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        2   40  15.1     35      85
Processing:    26   48  14.2     44     110
Waiting:       13   36  12.0     33      79
Total:         31   88  20.1     86     144

Percentage of the requests served within a certain time (ms)
  50%     86
  66%     92
  75%    101
  80%    105
  90%    123
  95%    129
  98%    131
  99%    138
 100%    144 (longest request)

所以没有失败的请求。

在我看来你这边有问题。

您的响应是 HTTP/1.0 +“连接:保持活动状态”,这意味着您在通知客户端它可以重用该连接来执行其他请求。然而,您在写入响应后立即关闭套接字。

因此,由于网络不是瞬时的,客户端试图重用套接字并编写第二个请求,只是为了让门砰的一声关上它的鼻子。

要么停止在每次响应时关闭套接字,要么停止强制执行“连接:保持活动”(关闭是 HTTP/1.0 上的默认设置)。

因此,感谢此处和 Twitter 上的评论和回答,第一个代码示例现已修复。问题是在读取之前写入 TCP 流。感谢Ganesh for the original solution on this here and the explanation is on this 所以回答

这里是适用于 Java 线程示例的更新代码

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class JavaHTTPServer {
    public static void main(String[] args) {
        var count = 0;
        var port = 8080;
        try (var serverSocket = new ServerSocket(port, 100)) {
            System.out.println("Server is listening on port " + port);
            while (true) {
                count++;
                new ServerThread(serverSocket.accept(), count).start();
            }
        } catch (IOException ex) {
            System.out.println("Server exception: " + ex.getMessage());
        }
    }
}

class ServerThread extends Thread {

    private final Socket socket;
    private final int count;

    public ServerThread(Socket socket, int count) {
        this.socket = socket;
        this.count = count;
    }

    @Override
    public void run() {
        var file = new File("hello.html");
        try (
                var in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                // we get character output stream to client (for headers)
                var out = new PrintWriter(socket.getOutputStream());
                // get binary output stream to client (for requested data)
                var dataOut = new BufferedOutputStream(socket.getOutputStream());
                var fileIn = new FileInputStream(file)
        ) {
            // add 2 second delay to every 10th request
            if (count % 10 == 0) {
                System.out.println("Adding delay. Count: " + count);
                Thread.sleep(2000);
            }

            // read the request fully to avoid connection reset errors and broken pipes
            while (true) {
                String requestLine = in.readLine();
                if (requestLine == null || requestLine.length() == 0) {
                    break;
                }
            }

            var fileLength = (int) file.length();
            var fileData = new byte[fileLength];
            fileIn.read(fileData);

            var contentMimeType = "text/html";
            // send HTTP Headers
            out.println("HTTP/1.1 200 OK");
            out.println("Content-type: " + contentMimeType);
            out.println("Content-length: " + fileLength);
            out.println("Connection: keep-alive");

            out.println(); // blank line between headers and content, very important !
            out.flush(); // flush character output stream buffer

            dataOut.write(fileData, 0, fileLength); // write the file data to output stream
            dataOut.flush();
        } catch (Exception ex) {
            System.err.println("Error with exception : " + ex);
        }
    }
}

和 apacheBench 输出

ab -r -c 100 -n 1000 http://127.0.0.1:8080/
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        
Server Hostname:        127.0.0.1
Server Port:            8080

Document Path:          /
Document Length:        176 bytes

Concurrency Level:      100
Time taken for tests:   2.385 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      260000 bytes
HTML transferred:       176000 bytes
Requests per second:    419.21 [#/sec] (mean)
Time per request:       238.546 [ms] (mean)
Time per request:       2.385 [ms] (mean, across all concurrent requests)
Transfer rate:          106.44 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   1.8      0       8
Processing:     0  221 600.7     21    2058
Waiting:        0  220 600.8     21    2057
Total:          0  221 600.8     21    2058

Percentage of the requests served within a certain time (ms)
  50%     21
  66%     33
  75%     38
  80%     43
  90%   2001
  95%   2020
  98%   2036
  99%   2044
 100%   2058 (longest request)

我将尝试以同样的方式修复第二个 Async 示例

编辑:同时修复了异步样本

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutionException;

public class JavaAsyncHTTPServer {

    public static void main(String[] args) throws Exception {
        new JavaAsyncHTTPServer().start();
        Thread.currentThread().join(); // Wait forever
    }

    private void start() throws IOException {
        // we shouldn't use try with resource here as it will kill the stream
        var server = AsynchronousServerSocketChannel.open();
        var hostAddress = new InetSocketAddress("127.0.0.1", 8080);
        server.bind(hostAddress, 100);   // bind listener
        server.setOption(StandardSocketOptions.SO_REUSEADDR, true);
        System.out.println("Server is listening on port 8080");

        final int[] count = {0}; // count used to introduce delays

        // listen to all incoming requests
        server.accept(null, new CompletionHandler<>() {
            @Override
            public void completed(final AsynchronousSocketChannel result, final Object attachment) {
                if (server.isOpen()) {
                    server.accept(null, this);
                }
                count[0]++;
                handleAcceptConnection(result, count[0]);
            }

            @Override
            public void failed(final Throwable exc, final Object attachment) {
                if (server.isOpen()) {
                    server.accept(null, this);
                    System.out.println("Connection handler error: " + exc);
                }
            }
        });
    }

    private void handleAcceptConnection(final AsynchronousSocketChannel ch, final int count) {
        var file = new File("hello.html");
        try (var fileIn = new FileInputStream(file)) {
            // add 2 second delay to every 10th request
            if (count % 10 == 0) {
                System.out.println("Adding delay. Count: " + count);
                Thread.sleep(2000);
            }
            if (ch != null && ch.isOpen()) {
                // Read the first 1024 bytes of data from the stream
                final ByteBuffer buffer = ByteBuffer.allocate(1024);
                // read the request fully to avoid connection reset errors
                ch.read(buffer).get();

                // read the HTML file
                var fileLength = (int) file.length();
                var fileData = new byte[fileLength];
                fileIn.read(fileData);

                // send HTTP Headers
                var message = ("HTTP/1.1 200 OK\n" +
                        "Connection: keep-alive\n" +
                        "Content-length: " + fileLength + "\n" +
                        "Content-Type: text/html; charset=utf-8\r\n\r\n" +
                        new String(fileData, StandardCharsets.UTF_8)
                ).getBytes();

                // write the to output stream
                ch.write(ByteBuffer.wrap(message)).get();

                buffer.clear();
                ch.close();
            }
        } catch (IOException | InterruptedException | ExecutionException e) {
            System.out.println("Connection handler error: " + e);
        }
    }
}