手写简陋服务器

手写简陋服务器,服务器的作用就是接收请求,解析请求,处理请求

🧩 v1:最原始的 HTTP Server ------ "我能说话了"

你做了什么

ServerSocket.accept()

每来一个连接就:

读 socket 字节

手写解析 HTTP 请求行 / Header

返回一个 HTTP 响应

一次请求 → 一次连接 → 立刻关闭

这一版的本质

这是一个"协议学习型服务器"

你在 v1 学到的不是性能,而是HTTP 的本质:

HTTP 不是魔法,是纯文本协议

TCP 只是字节流

\r\n\r\n 才是请求边界

Content-Length 决定 body 怎么读

v1 的致命问题

一个请求 = 一次 TCP 连接

大量:

三次握手

四次挥手

资源浪费极其严重

📌 v1 能跑,但一跑就废

java 复制代码
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.*;

public class MiniHttpServer_v1 {

    private static final ExecutorService POOL = new ThreadPoolExecutor(
            8, 64,
            60, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(200),
            r -> {
                Thread t = new Thread(r);
                t.setName("mini-http-" + t.getId());
                t.setDaemon(true);
                return t;
            },
            new ThreadPoolExecutor.CallerRunsPolicy()
    );

    public static void main(String[] args) throws Exception {
        int port = args.length > 0 ? Integer.parseInt(args[0]) : 8080;

        try (ServerSocket server = new ServerSocket()) {
            server.bind(new InetSocketAddress("0.0.0.0", port));
            System.out.println("MiniHttpServer_v2 listening on 0.0.0.0:" + port);
//            打印ipv4地址
            printAllIpv4();

            while (true) {
                Socket client = server.accept();
                client.setSoTimeout(15_000);
                POOL.execute(() -> handleClient(client));
            }
        }
    }

    private static void handleClient(Socket client) {
        String remote = String.valueOf(client.getRemoteSocketAddress());
//        当 try 块结束时,自动调用 c.close()
        try (Socket c = client) {
            InputStream inRaw = c.getInputStream();
//            只能从Socket里面获取输出流,因为这里包含一套协议
            OutputStream outRaw = c.getOutputStream();

            // 只用字节流解析 HTTP(避免 BufferedReader 和 InputStream 混用的坑)
            HttpRequest req = readHttpRequest(inRaw);

            if (req == null) return;

            System.out.println("[" + remote + "] " + req.method + " " + req.target +
                    " len=" + req.body.length + " ct=" + req.headers.getOrDefault("content-type", ""));

            // 路由
            String path = req.path;
            Map<String, String> queryMap = req.query;

            if ("GET".equalsIgnoreCase(req.method) && "/".equals(path)) {
                String html = "<!doctype html><html><head><meta charset='utf-8'/>"
                        + "<title>MiniHttpServer</title></head><body>"
                        + "<h1>It works ✅</h1>"
                        + "<p>Time: " + escapeHtml(now()) + "</p>"
                        + "<ul>"
                        + "<li><a href='/hello?name=Frank'>/hello?name=Frank</a></li>"
                        + "<li><a href='/api/time'>/api/time</a></li>"
                        + "</ul>"
                        + "<p>Try POST /echo</p>"
                        + "</body></html>";
                writeResponse(outRaw, 200, "OK", "text/html; charset=utf-8", html.getBytes(StandardCharsets.UTF_8));
                return;
            }

            if ("GET".equalsIgnoreCase(req.method) && "/hello".equals(path)) {
                String name = queryMap.getOrDefault("name", "world");
                String html = "<!doctype html><html><head><meta charset='utf-8'/></head><body>"
                        + "<h2>Hello, " + escapeHtml(name) + " 👋</h2>"
                        + "<p><a href='/'>Back</a></p>"
                        + "</body></html>";
                writeResponse(outRaw, 200, "OK", "text/html; charset=utf-8", html.getBytes(StandardCharsets.UTF_8));
                return;
            }

            if ("GET".equalsIgnoreCase(req.method) && "/api/time".equals(path)) {
                String json = "{\"time\":\"" + now() + "\"}";
                writeResponse(outRaw, 200, "OK", "application/json; charset=utf-8", json.getBytes(StandardCharsets.UTF_8));
                return;
            }

            if ("POST".equalsIgnoreCase(req.method) && "/echo".equals(path)) {
                // 原样回显 body(字节级)
                writeResponse(outRaw, 200, "OK", "text/plain; charset=utf-8", req.body);
                return;
            }

            writeResponse(outRaw, 404, "Not Found", "text/plain; charset=utf-8",
                    ("No route: " + req.method + " " + path).getBytes(StandardCharsets.UTF_8));

        } catch (SocketTimeoutException e) {
            System.out.println("[" + remote + "] timeout");
        } catch (Exception e) {
            System.out.println("[" + remote + "] ERROR: " + e);
            e.printStackTrace();
            try {
                OutputStream out = client.getOutputStream();
                writeResponse(out, 500, "Internal Server Error", "text/plain; charset=utf-8",
                        (e.toString()).getBytes(StandardCharsets.UTF_8));
            } catch (Exception ignore) {}
        }
    }

    // ====== HTTP parsing (byte-based) ======

    private static class HttpRequest {
        String method;
        String target;
        String path;
        Map<String, String> query = new HashMap<>();
        String version;
        Map<String, String> headers = new HashMap<>();
        byte[] body = new byte[0];
    }

    private static HttpRequest readHttpRequest(InputStream in) throws IOException {
        // 读请求头(直到 \r\n\r\n)
//        HTTP 规定:头部结束的标志就是"空行",也就是 \r\n\r\n
//        "请求头到这里结束,后面(如果有)就是 body。"
        ByteArrayOutputStream headerBuf = new ByteArrayOutputStream();
        int state = 0;
        while (true) {
            int b = in.read();
            if (b == -1) return null;
            headerBuf.write(b);

            // 检测 \r\n\r\n
            if (state == 0 && b == '\r') state = 1;
            else if (state == 1 && b == '\n') state = 2;
            else if (state == 2 && b == '\r') state = 3;
            else if (state == 3 && b == '\n') break;
            else state = 0;

            if (headerBuf.size() > 64 * 1024) throw new IOException("Header too large");
        }

        String headerText = headerBuf.toString(StandardCharsets.UTF_8.name());
        String[] lines = headerText.split("\r\n");
        if (lines.length < 1) return null;

        String[] parts = lines[0].split(" ");
        if (parts.length < 3) throw new IOException("Bad request line: " + lines[0]);

        HttpRequest req = new HttpRequest();
        req.method = parts[0].trim();
        req.target = parts[1].trim();
        req.version = parts[2].trim();

        for (int i = 1; i < lines.length; i++) {
            String line = lines[i];
            if (line.isEmpty()) continue;
            int idx = line.indexOf(':');
            if (idx > 0) {
                String k = line.substring(0, idx).trim().toLowerCase(Locale.ROOT);
                String v = line.substring(idx + 1).trim();
                req.headers.put(k, v);
            }
        }

        // path + query
        String target = req.target;
        String path = target;
        String query = "";
        int qIdx = target.indexOf('?');
        if (qIdx >= 0) {
            path = target.substring(0, qIdx);
            query = target.substring(qIdx + 1);
        }
        req.path = path;
        req.query = parseQuery(query);

        int contentLength = 0;
        if (req.headers.containsKey("content-length")) {
            try { contentLength = Integer.parseInt(req.headers.get("content-length")); } catch (Exception ignore) {}
        }

        if (contentLength > 0) {
            req.body = readExactly(in, contentLength);
        } else {
            req.body = new byte[0];
        }
        return req;
    }

//    readExactly 用来按照 HTTP 的 Content-Length,
//    从 TCP 字节流中精确读取指定数量的字节,
//    解决 TCP 读操作"可能读不满"的问题。
    private static byte[] readExactly(InputStream in, int len) throws IOException {
        byte[] buf = new byte[len];
        int off = 0;
        while (off < len) {
            int n = in.read(buf, off, len - off);
            if (n < 0) throw new EOFException("Unexpected EOF");
            off += n;
        }
        return buf;
    }

    private static Map<String, String> parseQuery(String q) {
        Map<String, String> map = new HashMap<>();
        if (q == null || q.isEmpty()) return map;
        for (String pair : q.split("&")) {
            int idx = pair.indexOf('=');
            String k, v;
            if (idx >= 0) {
                k = urlDecode(pair.substring(0, idx));
                v = urlDecode(pair.substring(idx + 1));
            } else {
                k = urlDecode(pair);
                v = "";
            }
            map.put(k, v);
        }
        return map;
    }

    private static String urlDecode(String s) {
        try { return URLDecoder.decode(s, "UTF-8"); } catch (Exception e) { return s; }
    }

    // ====== HTTP write ======

    private static void writeResponse(OutputStream out, int code, String reason, String contentType, byte[] body) throws IOException {
        String headers =
                "HTTP/1.1 " + code + " " + reason + "\r\n" +
                        "Date: " + httpDate() + "\r\n" +
                        "Server: MiniHttpServer_v2/0.1\r\n" +
                        "Content-Type: " + contentType + "\r\n" +
                        "Content-Length: " + body.length + "\r\n" +
                        "Connection: close\r\n" +
                        "\r\n";
        out.write(headers.getBytes(StandardCharsets.UTF_8));
        out.write(body);
        out.flush();
    }

    private static String now() {
        return ZonedDateTime.now().toString();
    }

    private static String httpDate() {
        return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now());
    }

    private static String escapeHtml(String s) {
        return s.replace("&","&amp;").replace("<","&lt;").replace(">","&gt;")
                .replace("\"","&quot;").replace("'","&#39;");
    }

    private static void printAllIpv4() {
        try {
            System.out.println("IPv4 addresses you can try from another PC (same LAN):");
            Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
            while (networkInterfaces.hasMoreElements()) {
                NetworkInterface ni = networkInterfaces.nextElement();
                try {
                    if (ni.isUp() && !ni.isLoopback()) {
                        Enumeration<InetAddress> addresses = ni.getInetAddresses();
                        while (addresses.hasMoreElements()) {
                            InetAddress a = addresses.nextElement();
                            if (a instanceof Inet4Address) {
                                System.out.println("  " + ni.getDisplayName() + " -> " + a.getHostAddress());
                            }
                        }
                    }
                } catch (Exception e) {
                    // 忽略无法访问的网络接口
                }
            }
        } catch (Exception e) {
            System.out.println("Cannot list interfaces: " + e);
        }
    }

}

v2版本,每次请求都要重新连接,http1.1加入了一个keep-alive,连接过的请求,可以一直保存连接

🔁 v2:引入 keep-alive ------ "别老断线"

你做了什么

同一个 socket:

while(true) 循环读请求

识别:

HTTP/1.1 默认 keep-alive

Connection: close

响应里不再总是 close

v2 解决了什么问题

连接复用,性能提升一个量级

你第一次直观看到:

客户端端口固定

TCP 连接被复用

浏览器/客户端"真的在复用连接"

你在 v2 真正学到的

HTTP 是"多请求跑在一条 TCP 上"的协议

keep-alive 是性能的生命线

端口变化 = 连接变化(你已经能读日志判断协议行为)

v2 新问题(非常关键)

keep-alive = 连接不关 = 线程被占着

在阻塞 IO + 线程池模型下:

一个连接 ≈ 一个线程

空等的 keep-alive 连接:

不干活

占线程

📌 v2 快,但不公平、不安全。

java 复制代码
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;


//加一个keep-alive的实现
public class MiniHttpServer_v2 {


    private static final ExecutorService POOL = new ThreadPoolExecutor(
            8, 64,
            60, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(200),
            r -> {
                Thread t = new Thread(r);
                t.setName("mini-http-" + t.getId());
                t.setDaemon(true);
                return t;
            },
            new ThreadPoolExecutor.CallerRunsPolicy()
    );

    public static void main(String[] args) throws Exception {
        int port = args.length > 0 ? Integer.parseInt(args[0]) : 8080;

        try (ServerSocket server = new ServerSocket()) {
            server.bind(new InetSocketAddress("0.0.0.0", port));
            System.out.println("MiniHttpServer_v2 listening on 0.0.0.0:" + port);
//            打印ipv4地址
            printAllIpv4();

            while (true) {
                Socket client = server.accept();
                client.setSoTimeout(15_000);
                POOL.execute(() -> handleClient(client));
            }
        }
    }
    private static void handleClient(Socket client) {
        String remote = String.valueOf(client.getRemoteSocketAddress());
        try (Socket c = client) {

            InputStream inRaw = c.getInputStream();
            OutputStream outRaw = c.getOutputStream();

            while (true) {
                HttpRequest req= readHttpRequest(inRaw);

                if (req == null) return; // 客户端关闭连接 / EOF

                boolean keepAlive = shouldKeepAlive(req);

                System.out.println("[" + remote + "] " + req.method + " " + req.target +
                        " len=" + req.body.length + " ct=" + req.headers.getOrDefault("content-type", "") +
                        " keepAlive=" + keepAlive);

                // 路由
                String path = req.path;
                Map<String, String> queryMap = req.query;

                if ("GET".equalsIgnoreCase(req.method) && "/".equals(path)) {
                    String html = "<!doctype html><html><head><meta charset='utf-8'/>"
                            + "<title>MiniHttpServer</title></head><body>"
                            + "<h1>It works ✅</h1>"
                            + "<p>Time: " + escapeHtml(now()) + "</p>"
                            + "<ul>"
                            + "<li><a href='/hello?name=Frank'>/hello?name=Frank</a></li>"
                            + "<li><a href='/api/time'>/api/time</a></li>"
                            + "</ul>"
                            + "<p>Try POST /echo</p>"
                            + "</body></html>";
                    writeResponse(outRaw, 200, "OK", "text/html; charset=utf-8",
                            html.getBytes(StandardCharsets.UTF_8), keepAlive);
                } else if ("GET".equalsIgnoreCase(req.method) && "/hello".equals(path)) {
                    String name = queryMap.getOrDefault("name", "world");
                    String html = "<!doctype html><html><head><meta charset='utf-8'/></head><body>"
                            + "<h2>Hello, " + escapeHtml(name) + " 👋</h2>"
                            + "<p><a href='/'>Back</a></p>"
                            + "</body></html>";
                    writeResponse(outRaw, 200, "OK", "text/html; charset=utf-8",
                            html.getBytes(StandardCharsets.UTF_8), keepAlive);
                } else if ("GET".equalsIgnoreCase(req.method) && "/api/time".equals(path)) {
                    String json = "{\"time\":\"" + now() + "\"}";
                    writeResponse(outRaw, 200, "OK", "application/json; charset=utf-8",
                            json.getBytes(StandardCharsets.UTF_8), keepAlive);
                } else if ("POST".equalsIgnoreCase(req.method) && "/echo".equals(path)) {
                    writeResponse(outRaw, 200, "OK", "text/plain; charset=utf-8",
                            req.body, keepAlive);
                } else {
                    writeResponse(outRaw, 404, "Not Found", "text/plain; charset=utf-8",
                            ("No route: " + req.method + " " + path).getBytes(StandardCharsets.UTF_8),
                            keepAlive);
                }

                if (!keepAlive) {
                    return; // 按协议关闭
                }
                // keep-alive:继续 while 读取下一个请求

            }

        } catch (SocketTimeoutException e) {
            System.out.println("[" + remote + "] timeout");
        } catch (Exception e) {
            System.out.println("[" + remote + "] ERROR: " + e);
            e.printStackTrace();
            try {
                OutputStream out = client.getOutputStream();
                // 出错时一般直接 close 更合理
                writeResponse(out, 500, "Internal Server Error", "text/plain; charset=utf-8",
                        (e.toString()).getBytes(StandardCharsets.UTF_8), false);
            } catch (Exception ignore) {}
        }
    }


    private static boolean shouldKeepAlive(HttpRequest req) {
        // 取 Connection 头(可能没有)
        String conn = req.headers.getOrDefault("connection", "").toLowerCase(Locale.ROOT);

        // HTTP/1.1 默认 keep-alive,除非明确 close
        if ("HTTP/1.1".equalsIgnoreCase(req.version)) {
            return !conn.contains("close");
        }

        // HTTP/1.0 默认 close,只有明确 keep-alive 才保持
        if ("HTTP/1.0".equalsIgnoreCase(req.version)) {
            return conn.contains("keep-alive");
        }

        // 其它版本保守:不 keep
        return false;
    }

    // ====== HTTP parsing (byte-based) ======

    private static class HttpRequest {
        String method;
        String target;
        String path;
        Map<String, String> query = new HashMap<>();
        String version;
        Map<String, String> headers = new HashMap<>();
        byte[] body = new byte[0];
    }

    private static HttpRequest readHttpRequest(InputStream in) throws IOException {
        // 读请求头(直到 \r\n\r\n)
//        HTTP 规定:头部结束的标志就是"空行",也就是 \r\n\r\n
//        "请求头到这里结束,后面(如果有)就是 body。"
        ByteArrayOutputStream headerBuf = new ByteArrayOutputStream();
        int state = 0;
        while (true) {
            int b = in.read();
            if (b == -1) return null;
            headerBuf.write(b);

            // 检测 \r\n\r\n
            if (state == 0 && b == '\r') state = 1;
            else if (state == 1 && b == '\n') state = 2;
            else if (state == 2 && b == '\r') state = 3;
            else if (state == 3 && b == '\n') break;
            else state = 0;

            if (headerBuf.size() > 64 * 1024) throw new IOException("Header too large");
        }

        String headerText = headerBuf.toString(StandardCharsets.UTF_8.name());
        String[] lines = headerText.split("\r\n");
        if (lines.length < 1) return null;

        String[] parts = lines[0].split(" ");
        if (parts.length < 3) throw new IOException("Bad request line: " + lines[0]);

        HttpRequest req = new HttpRequest();
        req.method = parts[0].trim();
        req.target = parts[1].trim();
        req.version = parts[2].trim();

        for (int i = 1; i < lines.length; i++) {
            String line = lines[i];
            if (line.isEmpty()) continue;
            int idx = line.indexOf(':');
            if (idx > 0) {
                String k = line.substring(0, idx).trim().toLowerCase(Locale.ROOT);
                String v = line.substring(idx + 1).trim();
                req.headers.put(k, v);
            }
        }

        // path + query
        String target = req.target;
        String path = target;
        String query = "";
        int qIdx = target.indexOf('?');
        if (qIdx >= 0) {
            path = target.substring(0, qIdx);
            query = target.substring(qIdx + 1);
        }
        req.path = path;
        req.query = parseQuery(query);

        int contentLength = 0;
        if (req.headers.containsKey("content-length")) {
            try { contentLength = Integer.parseInt(req.headers.get("content-length")); } catch (Exception ignore) {}
        }

        if (contentLength > 0) {
            req.body = readExactly(in, contentLength);
        } else {
            req.body = new byte[0];
        }
        return req;
    }

//    readExactly 用来按照 HTTP 的 Content-Length,
//    从 TCP 字节流中精确读取指定数量的字节,
//    解决 TCP 读操作"可能读不满"的问题。
    private static byte[] readExactly(InputStream in, int len) throws IOException {
        byte[] buf = new byte[len];
        int off = 0;
        while (off < len) {
            int n = in.read(buf, off, len - off);
            if (n < 0) throw new EOFException("Unexpected EOF");
            off += n;
        }
        return buf;
    }

    private static Map<String, String> parseQuery(String q) {
        Map<String, String> map = new HashMap<>();
        if (q == null || q.isEmpty()) return map;
        for (String pair : q.split("&")) {
            int idx = pair.indexOf('=');
            String k, v;
            if (idx >= 0) {
                k = urlDecode(pair.substring(0, idx));
                v = urlDecode(pair.substring(idx + 1));
            } else {
                k = urlDecode(pair);
                v = "";
            }
            map.put(k, v);
        }
        return map;
    }

    private static String urlDecode(String s) {
        try { return URLDecoder.decode(s, "UTF-8"); } catch (Exception e) { return s; }
    }

    // ====== HTTP write ======

    private static void writeResponse(OutputStream out, int code, String reason,
                                      String contentType, byte[] body, boolean keepAlive) throws IOException {
        String headers =
                "HTTP/1.1 " + code + " " + reason + "\r\n" +
                        "Date: " + httpDate() + "\r\n" +
                        "Server: MiniHttpServer_v2/0.2\r\n" +
                        "Content-Type: " + contentType + "\r\n" +
                        "Content-Length: " + body.length + "\r\n" +
                        "Connection: " + (keepAlive ? "keep-alive" : "close") + "\r\n" +
                        "\r\n";
        out.write(headers.getBytes(StandardCharsets.UTF_8));
        out.write(body);
        out.flush();
    }


    private static String now() {
        return ZonedDateTime.now().toString();
    }

    private static String httpDate() {
        return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now());
    }

    private static String escapeHtml(String s) {
        return s.replace("&","&amp;").replace("<","&lt;").replace(">","&gt;")
                .replace("\"","&quot;").replace("'","&#39;");
    }

    private static void printAllIpv4() {
        try {
            System.out.println("IPv4 addresses you can try from another PC (same LAN):");
            Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
            while (networkInterfaces.hasMoreElements()) {
                NetworkInterface ni = networkInterfaces.nextElement();
                try {
                    if (ni.isUp() && !ni.isLoopback()) {
                        Enumeration<InetAddress> addresses = ni.getInetAddresses();
                        while (addresses.hasMoreElements()) {
                            InetAddress a = addresses.nextElement();
                            if (a instanceof Inet4Address) {
                                System.out.println("  " + ni.getDisplayName() + " -> " + a.getHostAddress());
                            }
                        }
                    }
                } catch (Exception e) {
                    // 忽略无法访问的网络接口
                }
            }
        } catch (Exception e) {
            System.out.println("Cannot list interfaces: " + e);
        }
    }

}

v3版本,加了keep-alive以后,会出现有的请求一直霸占着线程,解决办法,加入一次连接最求最多处理MAX_REQUESTS_PER_CONNECTION个请求,再加一个空闲x秒直接断开

🛑 v3:连接治理 ------ "别霸占资源"

你加了两把"工程级护栏"

1️⃣ 空闲超时(idle timeout)

连接 X 秒没新请求 → 服务器主动断开

2️⃣ 最大请求数(max requests per connection)

一个连接最多处理 N 次请求

达到上限 → Connection: close

v3 的本质

这是"资源治理",不是性能优化

你第一次像真正的服务器那样思考:

防慢连接

防连接霸占

防恶意或"守规矩但很黏"的客户端

v3 你学到的最重要一课

keep-alive 是优化,不是承诺

服务器必须:

主动回收资源

为新连接让路

📌 到 v3,你已经写出了**"早期生产级服务器骨架"**。

java 复制代码
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;


//加一个keep-alive的实现 + 5秒自动关闭 + 一个连接最多请求N次

public class MiniHttpServer_v3 {

    // keep-alive 空闲超时(毫秒)
    private static final int KEEP_ALIVE_IDLE_TIMEOUT_MS = 5_000; // 5 秒
    // 一个 TCP 连接最多处理多少个 HTTP 请求
    private static final int MAX_REQUESTS_PER_CONNECTION = 100;


    private static final ExecutorService POOL = new ThreadPoolExecutor(
            8, 64,
            60, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(200),
            r -> {
                Thread t = new Thread(r);
                t.setName("mini-http-" + t.getId());
                t.setDaemon(true);
                return t;
            },
            new ThreadPoolExecutor.CallerRunsPolicy()
    );


    public static void main(String[] args) throws Exception {
        int port = args.length > 0 ? Integer.parseInt(args[0]) : 8080;

        try (ServerSocket server = new ServerSocket()) {
            server.bind(new InetSocketAddress("0.0.0.0", port));
            System.out.println("MiniHttpServer_v2 listening on 0.0.0.0:" + port);
//            打印ipv4地址
            printAllIpv4();

            while (true) {
                Socket client = server.accept();
                client.setSoTimeout(15_000);
                POOL.execute(() -> handleClient(client));
            }
        }
    }
    private static void handleClient(Socket client) {
        String remote = String.valueOf(client.getRemoteSocketAddress());
        try (Socket c = client) {
            // 初始超时:用于首次请求
            c.setSoTimeout(15_000);
            InputStream inRaw = c.getInputStream();
            OutputStream outRaw = c.getOutputStream();
            int requestCount = 0; // ⭐ 新增:请求计数器
            while (true) {
                HttpRequest req;
                try {
                    req = readHttpRequest(inRaw);
                } catch (SocketTimeoutException e) {
                    // ⭐ keep-alive 空闲超时
                    System.out.println("[" + remote + "] keep-alive idle timeout, closing");
                    return;
                }
                if (req == null) return; // 客户端关闭连接 / EOF
                requestCount++; // ⭐ 新增:每来一个请求就 +1

                boolean keepAlive = shouldKeepAlive(req);
                // ⭐ 新增:超过最大请求数,强制关闭
                if (requestCount >= MAX_REQUESTS_PER_CONNECTION) {
                    keepAlive = false;
                }

                System.out.println("[" + remote + "] " + req.method + " " + req.target +
                        " len=" + req.body.length + " ct=" + req.headers.getOrDefault("content-type", "") +
                        " keepAlive=" + keepAlive);

                // 路由
                String path = req.path;
                Map<String, String> queryMap = req.query;

                if ("GET".equalsIgnoreCase(req.method) && "/".equals(path)) {
                    String html = "<!doctype html><html><head><meta charset='utf-8'/>"
                            + "<title>MiniHttpServer</title></head><body>"
                            + "<h1>It works ✅</h1>"
                            + "<p>Time: " + escapeHtml(now()) + "</p>"
                            + "<ul>"
                            + "<li><a href='/hello?name=Frank'>/hello?name=Frank</a></li>"
                            + "<li><a href='/api/time'>/api/time</a></li>"
                            + "</ul>"
                            + "<p>Try POST /echo</p>"
                            + "</body></html>";
                    writeResponse(outRaw, 200, "OK", "text/html; charset=utf-8",
                            html.getBytes(StandardCharsets.UTF_8), keepAlive);
                } else if ("GET".equalsIgnoreCase(req.method) && "/hello".equals(path)) {
                    String name = queryMap.getOrDefault("name", "world");
                    String html = "<!doctype html><html><head><meta charset='utf-8'/></head><body>"
                            + "<h2>Hello, " + escapeHtml(name) + " 👋</h2>"
                            + "<p><a href='/'>Back</a></p>"
                            + "</body></html>";
                    writeResponse(outRaw, 200, "OK", "text/html; charset=utf-8",
                            html.getBytes(StandardCharsets.UTF_8), keepAlive);
                } else if ("GET".equalsIgnoreCase(req.method) && "/api/time".equals(path)) {
                    String json = "{\"time\":\"" + now() + "\"}";
                    writeResponse(outRaw, 200, "OK", "application/json; charset=utf-8",
                            json.getBytes(StandardCharsets.UTF_8), keepAlive);
                } else if ("POST".equalsIgnoreCase(req.method) && "/echo".equals(path)) {
                    writeResponse(outRaw, 200, "OK", "text/plain; charset=utf-8",
                            req.body, keepAlive);
                } else {
                    writeResponse(outRaw, 404, "Not Found", "text/plain; charset=utf-8",
                            ("No route: " + req.method + " " + path).getBytes(StandardCharsets.UTF_8),
                            keepAlive);
                }

                if (!keepAlive) {
                    return; // 按协议关闭
                }
                // keep-alive:继续 while 读取下一个请求

                // ⭐ 进入 keep-alive 状态:设置较短的 idle timeout
                c.setSoTimeout(KEEP_ALIVE_IDLE_TIMEOUT_MS);
            }

        } catch (SocketTimeoutException e) {
            System.out.println("[" + remote + "] timeout");
        } catch (Exception e) {
            System.out.println("[" + remote + "] ERROR: " + e);
            e.printStackTrace();
            try {
                OutputStream out = client.getOutputStream();
                // 出错时一般直接 close 更合理
                writeResponse(out, 500, "Internal Server Error", "text/plain; charset=utf-8",
                        (e.toString()).getBytes(StandardCharsets.UTF_8), false);
            } catch (Exception ignore) {}
        }
    }


    private static boolean shouldKeepAlive(HttpRequest req) {
        // 取 Connection 头(可能没有)
        String conn = req.headers.getOrDefault("connection", "").toLowerCase(Locale.ROOT);

        // HTTP/1.1 默认 keep-alive,除非明确 close
        if ("HTTP/1.1".equalsIgnoreCase(req.version)) {
            return !conn.contains("close");
        }

        // HTTP/1.0 默认 close,只有明确 keep-alive 才保持
        if ("HTTP/1.0".equalsIgnoreCase(req.version)) {
            return conn.contains("keep-alive");
        }

        // 其它版本保守:不 keep
        return false;
    }

    // ====== HTTP parsing (byte-based) ======

    private static class HttpRequest {
        String method;
        String target;
        String path;
        Map<String, String> query = new HashMap<>();
        String version;
        Map<String, String> headers = new HashMap<>();
        byte[] body = new byte[0];
    }

    private static HttpRequest readHttpRequest(InputStream in) throws IOException {
        // 读请求头(直到 \r\n\r\n)
//        HTTP 规定:头部结束的标志就是"空行",也就是 \r\n\r\n
//        "请求头到这里结束,后面(如果有)就是 body。"
        ByteArrayOutputStream headerBuf = new ByteArrayOutputStream();
        int state = 0;
        while (true) {
            int b = in.read();
            if (b == -1) return null;
            headerBuf.write(b);

            // 检测 \r\n\r\n
            if (state == 0 && b == '\r') state = 1;
            else if (state == 1 && b == '\n') state = 2;
            else if (state == 2 && b == '\r') state = 3;
            else if (state == 3 && b == '\n') break;
            else state = 0;

            if (headerBuf.size() > 64 * 1024) throw new IOException("Header too large");
        }

        String headerText = headerBuf.toString(StandardCharsets.UTF_8.name());
        String[] lines = headerText.split("\r\n");
        if (lines.length < 1) return null;

        String[] parts = lines[0].split(" ");
        if (parts.length < 3) throw new IOException("Bad request line: " + lines[0]);

        HttpRequest req = new HttpRequest();
        req.method = parts[0].trim();
        req.target = parts[1].trim();
        req.version = parts[2].trim();

        for (int i = 1; i < lines.length; i++) {
            String line = lines[i];
            if (line.isEmpty()) continue;
            int idx = line.indexOf(':');
            if (idx > 0) {
                String k = line.substring(0, idx).trim().toLowerCase(Locale.ROOT);
                String v = line.substring(idx + 1).trim();
                req.headers.put(k, v);
            }
        }

        // path + query
        String target = req.target;
        String path = target;
        String query = "";
        int qIdx = target.indexOf('?');
        if (qIdx >= 0) {
            path = target.substring(0, qIdx);
            query = target.substring(qIdx + 1);
        }
        req.path = path;
        req.query = parseQuery(query);

        int contentLength = 0;
        if (req.headers.containsKey("content-length")) {
            try { contentLength = Integer.parseInt(req.headers.get("content-length")); } catch (Exception ignore) {}
        }

        if (contentLength > 0) {
            req.body = readExactly(in, contentLength);
        } else {
            req.body = new byte[0];
        }
        return req;
    }

//    readExactly 用来按照 HTTP 的 Content-Length,
//    从 TCP 字节流中精确读取指定数量的字节,
//    解决 TCP 读操作"可能读不满"的问题。
    private static byte[] readExactly(InputStream in, int len) throws IOException {
        byte[] buf = new byte[len];
        int off = 0;
        while (off < len) {
            int n = in.read(buf, off, len - off);
            if (n < 0) throw new EOFException("Unexpected EOF");
            off += n;
        }
        return buf;
    }

    private static Map<String, String> parseQuery(String q) {
        Map<String, String> map = new HashMap<>();
        if (q == null || q.isEmpty()) return map;
        for (String pair : q.split("&")) {
            int idx = pair.indexOf('=');
            String k, v;
            if (idx >= 0) {
                k = urlDecode(pair.substring(0, idx));
                v = urlDecode(pair.substring(idx + 1));
            } else {
                k = urlDecode(pair);
                v = "";
            }
            map.put(k, v);
        }
        return map;
    }

    private static String urlDecode(String s) {
        try { return URLDecoder.decode(s, "UTF-8"); } catch (Exception e) { return s; }
    }

    // ====== HTTP write ======

    private static void writeResponse(OutputStream out, int code, String reason,
                                      String contentType, byte[] body, boolean keepAlive) throws IOException {
        String headers =
                "HTTP/1.1 " + code + " " + reason + "\r\n" +
                        "Date: " + httpDate() + "\r\n" +
                        "Server: MiniHttpServer_v2/0.2\r\n" +
                        "Content-Type: " + contentType + "\r\n" +
                        "Content-Length: " + body.length + "\r\n" +
                        "Connection: " + (keepAlive ? "keep-alive" : "close") + "\r\n" +
                        "\r\n";
        out.write(headers.getBytes(StandardCharsets.UTF_8));
        out.write(body);
        out.flush();
    }


    private static String now() {
        return ZonedDateTime.now().toString();
    }

    private static String httpDate() {
        return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now());
    }

    private static String escapeHtml(String s) {
        return s.replace("&","&amp;").replace("<","&lt;").replace(">","&gt;")
                .replace("\"","&quot;").replace("'","&#39;");
    }

    private static void printAllIpv4() {
        try {
            System.out.println("IPv4 addresses you can try from another PC (same LAN):");
            Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
            while (networkInterfaces.hasMoreElements()) {
                NetworkInterface ni = networkInterfaces.nextElement();
                try {
                    if (ni.isUp() && !ni.isLoopback()) {
                        Enumeration<InetAddress> addresses = ni.getInetAddresses();
                        while (addresses.hasMoreElements()) {
                            InetAddress a = addresses.nextElement();
                            if (a instanceof Inet4Address) {
                                System.out.println("  " + ni.getDisplayName() + " -> " + a.getHostAddress());
                            }
                        }
                    }
                } catch (Exception e) {
                    // 忽略无法访问的网络接口
                }
            }
        } catch (Exception e) {
            System.out.println("Cannot list interfaces: " + e);
        }
    }

}

v4版本,用Java21的虚拟线程,解决上面因为线程不够的问题

🧵 v4:Java 21 虚拟线程 ------ "换一条赛道"

你做了什么

把平台线程池:

ThreadPoolExecutor(...)

换成:

Executors.newVirtualThreadPerTaskExecutor()

业务代码 一行没改

v4 解决的不是"某一个问题"

而是换掉了"线程=资源瓶颈"的假设

你亲眼看到的事实

通过压测你已经验证:

平台线程:

connectFail 极高

大量连接连不上

任务提前结束

虚拟线程:

连接存活率极高

95%+ 请求跑完

吞吐稳定、延迟可控

v4 的本质

阻塞 IO 不再等于占 OS 线程

read() 阻塞时:

虚拟线程挂起

平台线程被释放

keep-alive 不再"占坑"

📌 v4 不是"更快",而是**"更能扛"**。

阻塞 IO 在网络服务中是常态,问题不在阻塞本身,而在阻塞是否占用 OS 线程;
虚拟线程通过在阻塞点挂起执行,使平台线程得以复用,从而显著提升高并发 IO 场景下的连接承载能力;
但虚拟线程并不消除资源治理的需求,线程池原理、限流、超时和下游池控制仍然是系统设计的核心

java 复制代码
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//import java.util.concurrent.TimeUnit;


//Java21虚拟线程

public class MiniHttpServer_v4 {

    // keep-alive 空闲超时(毫秒)
    private static final int KEEP_ALIVE_IDLE_TIMEOUT_MS = 5_000; // 5 秒
    // 一个 TCP 连接最多处理多少个 HTTP 请求
    private static final int MAX_REQUESTS_PER_CONNECTION = 100;




    private static final ExecutorService POOL = Executors.newVirtualThreadPerTaskExecutor();


    public static void main(String[] args) throws Exception {
        int port = args.length > 0 ? Integer.parseInt(args[0]) : 8080;

        try (ServerSocket server = new ServerSocket()) {
            server.bind(new InetSocketAddress("0.0.0.0", port));
            System.out.println("MiniHttpServer_v2 listening on 0.0.0.0:" + port);
//            打印ipv4地址
            printAllIpv4();

            while (true) {
                Socket client = server.accept();
                client.setSoTimeout(15_000);
                POOL.execute(() -> handleClient(client));
            }
        }
    }
    private static void handleClient(Socket client) {
        String remote = String.valueOf(client.getRemoteSocketAddress());
        try (Socket c = client) {
            // 初始超时:用于首次请求
            c.setSoTimeout(15_000);
            InputStream inRaw = c.getInputStream();
            OutputStream outRaw = c.getOutputStream();
            int requestCount = 0; // ⭐ 新增:请求计数器
            while (true) {
                HttpRequest req;
                try {
                    req = readHttpRequest(inRaw);
                } catch (SocketTimeoutException e) {
                    // ⭐ keep-alive 空闲超时
                    System.out.println("[" + remote + "] keep-alive idle timeout, closing");
                    return;
                }
                if (req == null) return; // 客户端关闭连接 / EOF
                requestCount++; // ⭐ 新增:每来一个请求就 +1

                boolean keepAlive = shouldKeepAlive(req);
                // ⭐ 新增:超过最大请求数,强制关闭
                if (requestCount >= MAX_REQUESTS_PER_CONNECTION) {
                    keepAlive = false;
                }

                System.out.println("[" + remote + "] " + req.method + " " + req.target +
                        " len=" + req.body.length + " ct=" + req.headers.getOrDefault("content-type", "") +
                        " keepAlive=" + keepAlive);

                // 路由
                String path = req.path;
                Map<String, String> queryMap = req.query;

                if ("GET".equalsIgnoreCase(req.method) && "/".equals(path)) {
                    String html = "<!doctype html><html><head><meta charset='utf-8'/>"
                            + "<title>MiniHttpServer</title></head><body>"
                            + "<h1>It works ✅</h1>"
                            + "<p>Time: " + escapeHtml(now()) + "</p>"
                            + "<ul>"
                            + "<li><a href='/hello?name=Frank'>/hello?name=Frank</a></li>"
                            + "<li><a href='/api/time'>/api/time</a></li>"
                            + "</ul>"
                            + "<p>Try POST /echo</p>"
                            + "</body></html>";
                    writeResponse(outRaw, 200, "OK", "text/html; charset=utf-8",
                            html.getBytes(StandardCharsets.UTF_8), keepAlive);
                } else if ("GET".equalsIgnoreCase(req.method) && "/hello".equals(path)) {
                    String name = queryMap.getOrDefault("name", "world");
                    String html = "<!doctype html><html><head><meta charset='utf-8'/></head><body>"
                            + "<h2>Hello, " + escapeHtml(name) + " 👋</h2>"
                            + "<p><a href='/'>Back</a></p>"
                            + "</body></html>";
                    writeResponse(outRaw, 200, "OK", "text/html; charset=utf-8",
                            html.getBytes(StandardCharsets.UTF_8), keepAlive);
                } else if ("GET".equalsIgnoreCase(req.method) && "/api/time".equals(path)) {
                    String json = "{\"time\":\"" + now() + "\"}";
                    writeResponse(outRaw, 200, "OK", "application/json; charset=utf-8",
                            json.getBytes(StandardCharsets.UTF_8), keepAlive);
                } else if ("POST".equalsIgnoreCase(req.method) && "/echo".equals(path)) {
                    writeResponse(outRaw, 200, "OK", "text/plain; charset=utf-8",
                            req.body, keepAlive);
                } else {
                    writeResponse(outRaw, 404, "Not Found", "text/plain; charset=utf-8",
                            ("No route: " + req.method + " " + path).getBytes(StandardCharsets.UTF_8),
                            keepAlive);
                }

                if (!keepAlive) {
                    return; // 按协议关闭
                }
                // keep-alive:继续 while 读取下一个请求

                // ⭐ 进入 keep-alive 状态:设置较短的 idle timeout
                c.setSoTimeout(KEEP_ALIVE_IDLE_TIMEOUT_MS);
            }

        } catch (SocketTimeoutException e) {
            System.out.println("[" + remote + "] timeout");
        } catch (Exception e) {
            System.out.println("[" + remote + "] ERROR: " + e);
            e.printStackTrace();
            try {
                OutputStream out = client.getOutputStream();
                // 出错时一般直接 close 更合理
                writeResponse(out, 500, "Internal Server Error", "text/plain; charset=utf-8",
                        (e.toString()).getBytes(StandardCharsets.UTF_8), false);
            } catch (Exception ignore) {}
        }
    }


    private static boolean shouldKeepAlive(HttpRequest req) {
        // 取 Connection 头(可能没有)
        String conn = req.headers.getOrDefault("connection", "").toLowerCase(Locale.ROOT);

        // HTTP/1.1 默认 keep-alive,除非明确 close
        if ("HTTP/1.1".equalsIgnoreCase(req.version)) {
            return !conn.contains("close");
        }

        // HTTP/1.0 默认 close,只有明确 keep-alive 才保持
        if ("HTTP/1.0".equalsIgnoreCase(req.version)) {
            return conn.contains("keep-alive");
        }

        // 其它版本保守:不 keep
        return false;
    }

    // ====== HTTP parsing (byte-based) ======

    private static class HttpRequest {
        String method;
        String target;
        String path;
        Map<String, String> query = new HashMap<>();
        String version;
        Map<String, String> headers = new HashMap<>();
        byte[] body = new byte[0];
    }

    private static HttpRequest readHttpRequest(InputStream in) throws IOException {
        // 读请求头(直到 \r\n\r\n)
//        HTTP 规定:头部结束的标志就是"空行",也就是 \r\n\r\n
//        "请求头到这里结束,后面(如果有)就是 body。"
        ByteArrayOutputStream headerBuf = new ByteArrayOutputStream();
        int state = 0;
        while (true) {
            int b = in.read();
            if (b == -1) return null;
            headerBuf.write(b);

            // 检测 \r\n\r\n
            if (state == 0 && b == '\r') state = 1;
            else if (state == 1 && b == '\n') state = 2;
            else if (state == 2 && b == '\r') state = 3;
            else if (state == 3 && b == '\n') break;
            else state = 0;

            if (headerBuf.size() > 64 * 1024) throw new IOException("Header too large");
        }

        String headerText = headerBuf.toString(StandardCharsets.UTF_8.name());
        String[] lines = headerText.split("\r\n");
        if (lines.length < 1) return null;

        String[] parts = lines[0].split(" ");
        if (parts.length < 3) throw new IOException("Bad request line: " + lines[0]);

        HttpRequest req = new HttpRequest();
        req.method = parts[0].trim();
        req.target = parts[1].trim();
        req.version = parts[2].trim();

        for (int i = 1; i < lines.length; i++) {
            String line = lines[i];
            if (line.isEmpty()) continue;
            int idx = line.indexOf(':');
            if (idx > 0) {
                String k = line.substring(0, idx).trim().toLowerCase(Locale.ROOT);
                String v = line.substring(idx + 1).trim();
                req.headers.put(k, v);
            }
        }

        // path + query
        String target = req.target;
        String path = target;
        String query = "";
        int qIdx = target.indexOf('?');
        if (qIdx >= 0) {
            path = target.substring(0, qIdx);
            query = target.substring(qIdx + 1);
        }
        req.path = path;
        req.query = parseQuery(query);

        int contentLength = 0;
        if (req.headers.containsKey("content-length")) {
            try { contentLength = Integer.parseInt(req.headers.get("content-length")); } catch (Exception ignore) {}
        }

        if (contentLength > 0) {
            req.body = readExactly(in, contentLength);
        } else {
            req.body = new byte[0];
        }
        return req;
    }

//    readExactly 用来按照 HTTP 的 Content-Length,
//    从 TCP 字节流中精确读取指定数量的字节,
//    解决 TCP 读操作"可能读不满"的问题。
    private static byte[] readExactly(InputStream in, int len) throws IOException {
        byte[] buf = new byte[len];
        int off = 0;
        while (off < len) {
            int n = in.read(buf, off, len - off);
            if (n < 0) throw new EOFException("Unexpected EOF");
            off += n;
        }
        return buf;
    }

    private static Map<String, String> parseQuery(String q) {
        Map<String, String> map = new HashMap<>();
        if (q == null || q.isEmpty()) return map;
        for (String pair : q.split("&")) {
            int idx = pair.indexOf('=');
            String k, v;
            if (idx >= 0) {
                k = urlDecode(pair.substring(0, idx));
                v = urlDecode(pair.substring(idx + 1));
            } else {
                k = urlDecode(pair);
                v = "";
            }
            map.put(k, v);
        }
        return map;
    }

    private static String urlDecode(String s) {
        try { return URLDecoder.decode(s, "UTF-8"); } catch (Exception e) { return s; }
    }

    // ====== HTTP write ======

    private static void writeResponse(OutputStream out, int code, String reason,
                                      String contentType, byte[] body, boolean keepAlive) throws IOException {
        String headers =
                "HTTP/1.1 " + code + " " + reason + "\r\n" +
                        "Date: " + httpDate() + "\r\n" +
                        "Server: MiniHttpServer_v2/0.2\r\n" +
                        "Content-Type: " + contentType + "\r\n" +
                        "Content-Length: " + body.length + "\r\n" +
                        "Connection: " + (keepAlive ? "keep-alive" : "close") + "\r\n" +
                        "\r\n";
        out.write(headers.getBytes(StandardCharsets.UTF_8));
        out.write(body);
        out.flush();
    }


    private static String now() {
        return ZonedDateTime.now().toString();
    }

    private static String httpDate() {
        return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now());
    }

    private static String escapeHtml(String s) {
        return s.replace("&","&amp;").replace("<","&lt;").replace(">","&gt;")
                .replace("\"","&quot;").replace("'","&#39;");
    }

    private static void printAllIpv4() {
        try {
            System.out.println("IPv4 addresses you can try from another PC (same LAN):");
            Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
            while (networkInterfaces.hasMoreElements()) {
                NetworkInterface ni = networkInterfaces.nextElement();
                try {
                    if (ni.isUp() && !ni.isLoopback()) {
                        Enumeration<InetAddress> addresses = ni.getInetAddresses();
                        while (addresses.hasMoreElements()) {
                            InetAddress a = addresses.nextElement();
                            if (a instanceof Inet4Address) {
                                System.out.println("  " + ni.getDisplayName() + " -> " + a.getHostAddress());
                            }
                        }
                    }
                } catch (Exception e) {
                    // 忽略无法访问的网络接口
                }
            }
        } catch (Exception e) {
            System.out.println("Cannot list interfaces: " + e);
        }
    }

}

适用场景

  • 虚拟线程适用于执行阻塞式任务,在阻塞期间,可以将CPU资源让渡给其他任务
  • 虚拟线程不适合CPU密集计算或非阻塞任务,虚拟线程并不会运行的更快,而是增加了规模虚拟线程是轻量级资源,用完即抛,不需要池化
  • 通常我们不需要直接使用虚拟线程,像Tomcat、Jetty、Netty、Spring boot等都已支持虚拟线程

压力测试

java 复制代码
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

public class LoadClient_v2 {

    // ====== counters ======
    static final AtomicLong okReq = new AtomicLong();        // 成功请求数
    static final AtomicLong failReq = new AtomicLong();      // 失败请求数(某次请求没完成)
    static final AtomicLong okConn = new AtomicLong();       // 成功完成 loops 的连接数
    static final AtomicLong failConn = new AtomicLong();     // 中途失败退出的连接数

    // 失败原因分类(连接/读写层面)
    static final AtomicLong connectFail = new AtomicLong();
    static final AtomicLong timeoutFail = new AtomicLong();
    static final AtomicLong eofFail = new AtomicLong();
    static final AtomicLong ioFail = new AtomicLong();
    static final AtomicLong otherFail = new AtomicLong();

    // 延迟(粗略)
    static final AtomicLong lastMs = new AtomicLong();
    static final AtomicLong maxMs = new AtomicLong();

    public static void main(String[] args) throws Exception {
        String host = args.length > 0 ? args[0] : "127.0.0.1";
        int port = args.length > 1 ? Integer.parseInt(args[1]) : 8080;

        int connections = args.length > 2 ? Integer.parseInt(args[2]) : 2000;
        int loopsPerConn = args.length > 3 ? Integer.parseInt(args[3]) : 50;
        int thinkMs = args.length > 4 ? Integer.parseInt(args[4]) : 0;

        // ⭐ v2 新增:连接爬坡参数(默认就给你一个温和的坡)
        int rampBatch = args.length > 5 ? Integer.parseInt(args[5]) : 50; // 每多少个连接 sleep 一次
        int rampSleepMs = args.length > 6 ? Integer.parseInt(args[6]) : 10; // sleep 多久

        // ⭐ v2 新增:超时更宽松,避免过载时误判
        int connectTimeoutMs = args.length > 7 ? Integer.parseInt(args[7]) : 10_000;
        int readTimeoutMs = args.length > 8 ? Integer.parseInt(args[8]) : 60_000;

        long expectedTotalReq = 1L * connections * loopsPerConn;

        System.out.printf("Target http://%s:%d, connections=%d loopsPerConn=%d thinkMs=%d%n",
                host, port, connections, loopsPerConn, thinkMs);
        System.out.printf("Ramp: batch=%d sleep=%dms | timeouts: connect=%dms read=%dms%n",
                rampBatch, rampSleepMs, connectTimeoutMs, readTimeoutMs);
        System.out.printf("Expected total requests: %d%n", expectedTotalReq);

        ExecutorService es = Executors.newVirtualThreadPerTaskExecutor(); // 客户端用虚拟线程方便拉高并发
        CountDownLatch latch = new CountDownLatch(connections);

        long startMs = System.currentTimeMillis();

        ScheduledExecutorService printer = Executors.newSingleThreadScheduledExecutor();
        printer.scheduleAtFixedRate(() -> {
            long elapsed = System.currentTimeMillis() - startMs;
            long ok = okReq.get();
            long fr = failReq.get();
            double rps = elapsed > 0 ? ok * 1000.0 / elapsed : 0;

            System.out.printf(
                    "[%s] okReq=%d failReq=%d okConn=%d failConn=%d rps=%.1f last=%dms max=%dms | " +
                            "connectFail=%d timeout=%d eof=%d io=%d other=%d%n",
                    Instant.now(),
                    ok, fr,
                    okConn.get(), failConn.get(),
                    rps, lastMs.get(), maxMs.get(),
                    connectFail.get(), timeoutFail.get(), eofFail.get(), ioFail.get(), otherFail.get()
            );
        }, 1, 1, TimeUnit.SECONDS);

        for (int i = 0; i < connections; i++) {
            es.execute(() -> {
                try {
                    runOneConnection(host, port, loopsPerConn, thinkMs, connectTimeoutMs, readTimeoutMs);
                    okConn.incrementAndGet();
                } catch (Exception e) {
                    failConn.incrementAndGet();
                    classifyFailure(e);
                } finally {
                    latch.countDown();
                }
            });

            // ⭐ 连接爬坡:避免瞬间洪峰把 accept/backlog/网络打爆
            if (rampBatch > 0 && rampSleepMs > 0 && i > 0 && (i % rampBatch == 0)) {
                Thread.sleep(rampSleepMs);
            }
        }

        latch.await();
        printer.shutdownNow();
        es.shutdown();

        long elapsed = System.currentTimeMillis() - startMs;
        long ok = okReq.get();
        long fr = failReq.get();
        double rps = elapsed > 0 ? ok * 1000.0 / elapsed : 0;

        System.out.println("==== SUMMARY ====");
        System.out.printf("Elapsed: %d ms | RPS: %.1f%n", elapsed, rps);
        System.out.printf("Expected total requests: %d%n", expectedTotalReq);
        System.out.printf("Completed requests: okReq=%d failReq=%d (ok+fail=%d)%n",
                ok, fr, ok + fr);
        System.out.printf("Connections: okConn=%d failConn=%d (total=%d)%n",
                okConn.get(), failConn.get(), okConn.get() + failConn.get());
        System.out.printf("Failures: connect=%d timeout=%d eof=%d io=%d other=%d%n",
                connectFail.get(), timeoutFail.get(), eofFail.get(), ioFail.get(), otherFail.get());
        System.out.println("DONE");
    }

    static void runOneConnection(String host, int port, int loops, int thinkMs,
                                 int connectTimeoutMs, int readTimeoutMs) throws Exception {
        Socket s = new Socket();
        s.connect(new InetSocketAddress(host, port), connectTimeoutMs);
        s.setSoTimeout(readTimeoutMs);

        try (Socket socket = s) {
            InputStream in = socket.getInputStream();
            OutputStream out = socket.getOutputStream();
            byte[] body = "ping".getBytes(StandardCharsets.UTF_8);

            for (int i = 0; i < loops; i++) {
                long t0 = System.nanoTime();
                try {
                    writePostEcho(out, host, port, body);
                    readHttpResponse(in); // 必须把响应读完整
                    long ms = (System.nanoTime() - t0) / 1_000_000;

                    okReq.incrementAndGet();
                    lastMs.set(ms);
                    maxMs.updateAndGet(prev -> Math.max(prev, ms));
                } catch (Exception e) {
                    failReq.incrementAndGet();
                    throw e; // 让连接任务终止,算作 failConn
                }

                if (thinkMs > 0) Thread.sleep(thinkMs);
            }
        }
    }

    static void writePostEcho(OutputStream out, String host, int port, byte[] body) throws IOException {
        String req =
                "POST /echo HTTP/1.1\r\n" +
                        "Host: " + host + ":" + port + "\r\n" +
                        "Content-Type: text/plain\r\n" +
                        "Content-Length: " + body.length + "\r\n" +
                        "Connection: keep-alive\r\n" +
                        "\r\n";
        out.write(req.getBytes(StandardCharsets.UTF_8));
        out.write(body);
        out.flush();
    }

    // 最小 HTTP 响应读取:读到 \r\n\r\n -> 解析 Content-Length -> 读 body
    static void readHttpResponse(InputStream in) throws IOException {
        ByteArrayOutputStream headerBuf = new ByteArrayOutputStream();
        int state = 0;
        while (true) {
            int b = in.read();
            if (b == -1) throw new EOFException("server closed while reading headers");
            headerBuf.write(b);

            if (state == 0 && b == '\r') state = 1;
            else if (state == 1 && b == '\n') state = 2;
            else if (state == 2 && b == '\r') state = 3;
            else if (state == 3 && b == '\n') break;
            else state = 0;

            if (headerBuf.size() > 64 * 1024) throw new IOException("resp header too large");
        }

        String headerText = headerBuf.toString(StandardCharsets.UTF_8);
        int contentLength = 0;

        // 顺便校验一下状态行(不是必须,但更直观)
        String[] lines = headerText.split("\r\n");
        if (lines.length > 0 && !lines[0].startsWith("HTTP/1.1 200")) {
            // 不是 200 也可以继续读 body,这里简单当成错误提示
            // throw new IOException("non-200: " + lines[0]);
        }

        for (String line : lines) {
            int idx = line.indexOf(':');
            if (idx > 0) {
                String k = line.substring(0, idx).trim().toLowerCase();
                String v = line.substring(idx + 1).trim();
                if (k.equals("content-length")) {
                    try { contentLength = Integer.parseInt(v); } catch (Exception ignore) {}
                }
            }
        }

        // 读 body
        byte[] buf = new byte[contentLength];
        int off = 0;
        while (off < contentLength) {
            int n = in.read(buf, off, contentLength - off);
            if (n < 0) throw new EOFException("server closed while reading body");
            off += n;
        }
    }

    static void classifyFailure(Exception e) {
        // 这部分只做"粗分类",足够你定位瓶颈来自哪一类
        Throwable t = e;
        while (t.getCause() != null) t = t.getCause();

        if (t instanceof ConnectException || t instanceof NoRouteToHostException) {
            connectFail.incrementAndGet();
        } else if (t instanceof SocketTimeoutException) {
            timeoutFail.incrementAndGet();
        } else if (t instanceof EOFException) {
            eofFail.incrementAndGet();
        } else if (t instanceof IOException) {
            ioFail.incrementAndGet();
        } else {
            otherFail.incrementAndGet();
        }
    }
}
相关推荐
萧曵 丶8 小时前
Linux 业务场景常用命令详解
linux·运维·服务器
乾元8 小时前
ISP 级别的异常洪泛检测与防护——大流量事件的 AI 自动识别与响应工程
运维·网络·人工智能·安全·web安全·架构
youxiao_909 小时前
kubernetes 概念与安装(一)
linux·运维·服务器
凡梦千华9 小时前
logrotate日志切割
linux·运维·服务器
ELI_He99910 小时前
Airflow docker 部署
运维·docker·容器
拜托啦!狮子10 小时前
安装和使用Homer(linux)
linux·运维·服务器
liulilittle11 小时前
XDP VNP虚拟以太网关(章节:一)
linux·服务器·开发语言·网络·c++·通信·xdp
剑之所向12 小时前
c# modbus大小端
linux·运维·网络
顶点多余12 小时前
Linux中的基本命令-2
linux·运维·服务器