手写简陋服务器,服务器的作用就是接收请求,解析请求,处理请求
🧩 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("&","&").replace("<","<").replace(">",">")
.replace("\"",""").replace("'","'");
}
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("&","&").replace("<","<").replace(">",">")
.replace("\"",""").replace("'","'");
}
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("&","&").replace("<","<").replace(">",">")
.replace("\"",""").replace("'","'");
}
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("&","&").replace("<","<").replace(">",">")
.replace("\"",""").replace("'","'");
}
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();
}
}
}