From 63516c40aa8993cb0fe4512dbd1d4cba13909faa Mon Sep 17 00:00:00 2001 From: doxlik Date: Fri, 26 Dec 2025 14:33:09 +0400 Subject: [PATCH 1/4] netty-fast --- frameworks/Java/netty-fast/.sdkmanrc | 3 + frameworks/Java/netty-fast/README.md | 60 ++++ .../Java/netty-fast/benchmark_config.json | 24 ++ frameworks/Java/netty-fast/config.toml | 15 + frameworks/Java/netty-fast/netty.dockerfile | 13 + frameworks/Java/netty-fast/pom.xml | 101 ++++++ frameworks/Java/netty-fast/run_netty.sh | 14 + .../src/main/java/hello/Constants.java | 19 + .../main/java/hello/HelloServerHandler.java | 336 ++++++++++++++++++ .../java/hello/HelloServerInitializer.java | 15 + .../src/main/java/hello/HelloWebServer.java | 84 +++++ .../src/main/java/hello/HttpResponses.java | 39 ++ .../src/main/java/hello/IoMultiplexer.java | 54 +++ .../src/main/java/hello/JsonUtils.java | 13 + .../src/main/java/hello/Message.java | 15 + 15 files changed, 805 insertions(+) create mode 100644 frameworks/Java/netty-fast/.sdkmanrc create mode 100644 frameworks/Java/netty-fast/README.md create mode 100644 frameworks/Java/netty-fast/benchmark_config.json create mode 100644 frameworks/Java/netty-fast/config.toml create mode 100644 frameworks/Java/netty-fast/netty.dockerfile create mode 100644 frameworks/Java/netty-fast/pom.xml create mode 100755 frameworks/Java/netty-fast/run_netty.sh create mode 100644 frameworks/Java/netty-fast/src/main/java/hello/Constants.java create mode 100644 frameworks/Java/netty-fast/src/main/java/hello/HelloServerHandler.java create mode 100644 frameworks/Java/netty-fast/src/main/java/hello/HelloServerInitializer.java create mode 100644 frameworks/Java/netty-fast/src/main/java/hello/HelloWebServer.java create mode 100644 frameworks/Java/netty-fast/src/main/java/hello/HttpResponses.java create mode 100644 frameworks/Java/netty-fast/src/main/java/hello/IoMultiplexer.java create mode 100644 frameworks/Java/netty-fast/src/main/java/hello/JsonUtils.java create mode 100644 frameworks/Java/netty-fast/src/main/java/hello/Message.java diff --git a/frameworks/Java/netty-fast/.sdkmanrc b/frameworks/Java/netty-fast/.sdkmanrc new file mode 100644 index 00000000000..b68ac92e765 --- /dev/null +++ b/frameworks/Java/netty-fast/.sdkmanrc @@ -0,0 +1,3 @@ +# Enable auto-env through the sdkman_auto_env config +# Add key=value pairs of SDKs to use below +java=25-oracle diff --git a/frameworks/Java/netty-fast/README.md b/frameworks/Java/netty-fast/README.md new file mode 100644 index 00000000000..d771217fcb7 --- /dev/null +++ b/frameworks/Java/netty-fast/README.md @@ -0,0 +1,60 @@ +# Netty (Fast / Minimal) + +This is a Netty-based implementation for the TechEmpower Framework Benchmarks. +It is a minimal, high-performance HTTP/1.1 server built directly on Netty +primitives with a focus on correctness and low overhead. + +The implementation uses real HTTP parsing, proper response headers, and +standards-compliant behavior while aggressively minimizing allocations and +framework abstractions. + +## Tests + +### Plaintext Test +Responds with a static plaintext message. + +GET /plaintext + +Response body: +Hello, World! + +Content-Type: +text/plain + +### JSON Test +Responds with a JSON-encoded object using a real JSON serializer. + +GET /json + +Response body: +{"message":"Hello, World!"} + +Content-Type: +application/json + +JSON serialization is performed using fastjson2. + +## Implementation Notes + +- HTTP/1.1 only +- Pipelining supported +- Uses Netty event loops and pooled buffers +- Minimal channel pipeline +- No framework abstractions beyond Netty itself +- Optimized for low allocation rate and high throughput +- Designed to match TechEmpower benchmark rules and expectations + +This implementation is intended to demonstrate the maximum achievable +performance of Netty when used as a low-level HTTP server. + +## Versions + +- Java 24+ (tested with Java 24 / 25) +- Netty 4.2.x +- fastjson2 + +## References + +- https://netty.io/ +- https://github.com/netty/netty +- https://github.com/TechEmpower/FrameworkBenchmarks diff --git a/frameworks/Java/netty-fast/benchmark_config.json b/frameworks/Java/netty-fast/benchmark_config.json new file mode 100644 index 00000000000..14dd7c21495 --- /dev/null +++ b/frameworks/Java/netty-fast/benchmark_config.json @@ -0,0 +1,24 @@ +{ + "framework": "netty-fast", + "tests": [{ + "default": { + "json_url": "/json", + "plaintext_url": "/plaintext", + "port": 8080, + "approach": "Realistic", + "classification": "Platform", + "database": "None", + "framework": "netty-fast", + "language": "Java", + "flavor": "None", + "orm": "Raw", + "platform": "Netty", + "webserver": "None", + "os": "Linux", + "database_os": "Linux", + "display_name": "netty-fast", + "notes": "", + "versus": "netty-fast" + } + }] +} diff --git a/frameworks/Java/netty-fast/config.toml b/frameworks/Java/netty-fast/config.toml new file mode 100644 index 00000000000..a7ff0829c33 --- /dev/null +++ b/frameworks/Java/netty-fast/config.toml @@ -0,0 +1,15 @@ +[framework] +name = "netty-fast" + +[main] +urls.plaintext = "/plaintext" +urls.json = "/json" +approach = "Realistic" +classification = "Platform" +database = "None" +database_os = "Linux" +os = "Linux" +orm = "Raw" +platform = "Netty" +webserver = "None" +versus = "netty-fast" diff --git a/frameworks/Java/netty-fast/netty.dockerfile b/frameworks/Java/netty-fast/netty.dockerfile new file mode 100644 index 00000000000..1b7545d40da --- /dev/null +++ b/frameworks/Java/netty-fast/netty.dockerfile @@ -0,0 +1,13 @@ +FROM maven:3.9-eclipse-temurin-25-noble as maven +WORKDIR /netty-fast +COPY pom.xml pom.xml +COPY src src +RUN mvn -q -DskipTests package + +FROM eclipse-temurin:25-jre-noble +WORKDIR /netty-fast +COPY --from=maven /netty-fast/target/app.jar ./app.jar +COPY run_netty.sh ./run_netty.sh +RUN chmod +x ./run_netty.sh +EXPOSE 8080 +ENTRYPOINT ["./run_netty.sh"] diff --git a/frameworks/Java/netty-fast/pom.xml b/frameworks/Java/netty-fast/pom.xml new file mode 100644 index 00000000000..a1c2cad444a --- /dev/null +++ b/frameworks/Java/netty-fast/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + + com.techempower + netty-example + 0.1 + + + 24 + 24 + 4.2.0.Final + + + jar + + + + + io.netty + netty-all + ${netty.version} + + + + io.netty + netty-transport-native-epoll + ${netty.version} + linux-x86_64 + + + + io.netty + netty-transport-native-kqueue + ${netty.version} + osx-x86_64 + + + + io.netty + netty-transport-native-kqueue + ${netty.version} + osx-aarch_64 + + + + io.netty + netty-transport-native-io_uring + ${netty.version} + linux-x86_64 + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.53 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + false + + + + + maven-assembly-plugin + + app + + + hello.HelloWebServer + + + + jar-with-dependencies + + false + + + + make-assembly + package + + single + + + + + + + + diff --git a/frameworks/Java/netty-fast/run_netty.sh b/frameworks/Java/netty-fast/run_netty.sh new file mode 100755 index 00000000000..41972b17d50 --- /dev/null +++ b/frameworks/Java/netty-fast/run_netty.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# PROFILING: -XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints +JAVA_OPTIONS="--enable-native-access=ALL-UNNAMED \ + -Dio.netty.noUnsafe=false \ + --sun-misc-unsafe-memory-access=allow \ + --add-opens=java.base/java.lang=ALL-UNNAMED \ + -XX:+UseNUMA \ + -XX:+UseParallelGC \ + -Dio.netty.buffer.checkBounds=false \ + -Dio.netty.buffer.checkAccessible=false \ + $@" + +exec java $JAVA_OPTIONS -jar app.jar \ No newline at end of file diff --git a/frameworks/Java/netty-fast/src/main/java/hello/Constants.java b/frameworks/Java/netty-fast/src/main/java/hello/Constants.java new file mode 100644 index 00000000000..1b1438cbf11 --- /dev/null +++ b/frameworks/Java/netty-fast/src/main/java/hello/Constants.java @@ -0,0 +1,19 @@ +package hello; + +import io.netty.util.AsciiString; +import io.netty.util.CharsetUtil; + +public final class Constants { + + public static final byte[] STATIC_PLAINTEXT = "Hello, World!".getBytes(CharsetUtil.UTF_8); + public static final int STATIC_PLAINTEXT_LEN = STATIC_PLAINTEXT.length; + public static final CharSequence PLAINTEXT_CLHEADER_VALUE = + AsciiString.cached(String.valueOf(STATIC_PLAINTEXT_LEN)); + + public static final CharSequence SERVER_NAME = AsciiString.cached("Netty"); + + public static final Message STATIC_MESSAGE = new Message("Hello, World!"); + + private Constants() { + } +} diff --git a/frameworks/Java/netty-fast/src/main/java/hello/HelloServerHandler.java b/frameworks/Java/netty-fast/src/main/java/hello/HelloServerHandler.java new file mode 100644 index 00000000000..2b8c834b405 --- /dev/null +++ b/frameworks/Java/netty-fast/src/main/java/hello/HelloServerHandler.java @@ -0,0 +1,336 @@ +package hello; + +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.AttributeKey; +import io.netty.util.concurrent.FastThreadLocal; + +public class HelloServerHandler extends ChannelInboundHandlerAdapter { + + private static final AttributeKey STATE_KEY = + AttributeKey.valueOf("hello.state"); + + // One cache per EventLoop thread + private static final FastThreadLocal CACHE_TL = new FastThreadLocal<>(); + + private static final byte[] NOT_FOUND_BYTES = + "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n" + .getBytes(StandardCharsets.US_ASCII); + + private static final byte CR = (byte) '\r'; + private static final byte LF = (byte) '\n'; + + private static final byte[] GET = "GET".getBytes(StandardCharsets.US_ASCII); + + private static final byte[] PATH_PLAINTEXT = "/plaintext".getBytes(StandardCharsets.US_ASCII); + private static final byte[] PATH_JSON = "/json".getBytes(StandardCharsets.US_ASCII); + + // JSON is static in TFB, so compute once + private static final byte[] JSON_BODY = JsonUtils.serializeMsg(Constants.STATIC_MESSAGE); + + private final ResponseCache cache; + + public HelloServerHandler(ScheduledExecutorService scheduledExecutor) { + ResponseCache c = CACHE_TL.get(); + if (c == null) { + c = new ResponseCache(scheduledExecutor); + CACHE_TL.set(c); + } + this.cache = c; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.channel().attr(STATE_KEY).set(new PerChannelState()); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + PerChannelState state = ctx.channel().attr(STATE_KEY).get(); + if (state != null && state.inbound != null) { + state.inbound.release(); + state.inbound = null; + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (!(msg instanceof ByteBuf)) { + io.netty.util.ReferenceCountUtil.release(msg); + return; + } + ByteBuf in = (ByteBuf) msg; + PerChannelState state = state(ctx); + + try { + if (state.inbound == null) { + state.inbound = in; + } else { + state.inbound.writeBytes(in); + in.release(); + } + + parseRequests(ctx, state); + + } catch (Throwable t) { + if (state.inbound != null) { + state.inbound.release(); + state.inbound = null; + } + ctx.close(); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + PerChannelState state = state(ctx); + if (state.hadBatched && state.outAggregate != null) { + ByteBuf out = state.outAggregate; + state.outAggregate = null; + state.hadBatched = false; + ctx.writeAndFlush(out).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + } else { + ctx.flush(); + } + } + + private PerChannelState state(ChannelHandlerContext ctx) { + PerChannelState st = ctx.channel().attr(STATE_KEY).get(); + if (st == null) { + st = new PerChannelState(); + ctx.channel().attr(STATE_KEY).set(st); + } + return st; + } + + private ByteBuf ensureAggregate(ChannelHandlerContext ctx, PerChannelState state) { + if (state.outAggregate == null) { + state.outAggregate = ctx.alloc().buffer(256); + } + return state.outAggregate; + } + + private void parseRequests(ChannelHandlerContext ctx, PerChannelState state) { + ByteBuf in = state.inbound; + if (in == null) { + return; + } + + while (true) { + int start = in.readerIndex(); + int endOfHeaders = findEndOfHeaders(in, start, in.writerIndex()); + if (endOfHeaders == -1) { + return; + } + + if (!parseOneRequestAndRespond(ctx, state, in, start, endOfHeaders)) { + return; + } + + in.readerIndex(endOfHeaders); + + if (in.readableBytes() == 0) { + in.release(); + state.inbound = null; + return; + } + + in.discardSomeReadBytes(); + } + } + + private boolean parseOneRequestAndRespond(ChannelHandlerContext ctx, PerChannelState state, ByteBuf in, int start, int endOfHeaders) { + int lineEnd = findCrlf(in, start, endOfHeaders); + if (lineEnd == -1) { + return false; + } + + int sp1 = findByte(in, start, lineEnd, (byte) ' '); + if (sp1 == -1) { + return false; + } + int sp2 = findByte(in, sp1 + 1, lineEnd, (byte) ' '); + if (sp2 == -1) { + return false; + } + + if (!equalsAscii(in, start, sp1, GET)) { + writeNotFound(ctx, state); + return true; + } + + int pathStart = sp1 + 1; + int pathEnd = sp2; + + if (equalsAscii(in, pathStart, pathEnd, PATH_PLAINTEXT)) { + encodePlaintext(ctx, state); + return true; + } + + if (equalsAscii(in, pathStart, pathEnd, PATH_JSON)) { + encodeJson(ctx, state); + return true; + } + + writeNotFound(ctx, state); + return true; + } + + private void encodePlaintext(ChannelHandlerContext ctx, PerChannelState state) { + ByteBuf agg = ensureAggregate(ctx, state); + agg.writeBytes(cache.plaintextResponseBytes()); + state.hadBatched = true; + } + + private void encodeJson(ChannelHandlerContext ctx, PerChannelState state) { + ByteBuf agg = ensureAggregate(ctx, state); + agg.writeBytes(cache.jsonResponseBytes()); + state.hadBatched = true; + } + + private void writeNotFound(ChannelHandlerContext ctx, PerChannelState state) { + if (state.outAggregate != null) { + ctx.writeAndFlush(state.outAggregate).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + state.outAggregate = null; + state.hadBatched = false; + } + ctx.writeAndFlush(Unpooled.wrappedBuffer(NOT_FOUND_BYTES)).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + } + + private static int findEndOfHeaders(ByteBuf buf, int from, int to) { + for (int i = from + 3; i < to; i++) { + if (buf.getByte(i - 3) == CR && buf.getByte(i - 2) == LF && + buf.getByte(i - 1) == CR && buf.getByte(i) == LF) { + return i + 1; + } + } + return -1; + } + + private static int findCrlf(ByteBuf buf, int from, int to) { + for (int i = from + 1; i < to; i++) { + if (buf.getByte(i - 1) == CR && buf.getByte(i) == LF) { + return i - 1; + } + } + return -1; + } + + private static int findByte(ByteBuf buf, int from, int to, byte b) { + for (int i = from; i < to; i++) { + if (buf.getByte(i) == b) { + return i; + } + } + return -1; + } + + private static boolean equalsAscii(ByteBuf buf, int start, int end, byte[] ascii) { + int len = end - start; + if (len != ascii.length) { + return false; + } + for (int i = 0; i < len; i++) { + if (buf.getByte(start + i) != ascii[i]) { + return false; + } + } + return true; + } + + private static final class PerChannelState { + ByteBuf inbound; + ByteBuf outAggregate; + boolean hadBatched; + } + + /** + * Whole-response cache, updated once/sec, one instance per EventLoop thread. + * Hot path: just read volatile byte[] and writeBytes(). + */ + private static final class ResponseCache implements Runnable { + private static final DateTimeFormatter RFC_1123 = + DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC); + + private static final byte[] PLAIN_PREFIX = + ("HTTP/1.1 200 OK\r\n" + + "Server: Netty\r\n" + + "Date: ").getBytes(StandardCharsets.US_ASCII); + + private static final byte[] PLAIN_MID = + ("\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Length: " + Constants.STATIC_PLAINTEXT_LEN + "\r\n" + + "\r\n").getBytes(StandardCharsets.US_ASCII); + + private static final byte[] JSON_PREFIX = + ("HTTP/1.1 200 OK\r\n" + + "Server: Netty\r\n" + + "Date: ").getBytes(StandardCharsets.US_ASCII); + + private static final byte[] JSON_MID = + ("\r\n" + + "Content-Type: application/json\r\n" + + "Content-Length: " + JSON_BODY.length + "\r\n" + + "\r\n").getBytes(StandardCharsets.US_ASCII); + + private final ScheduledExecutorService scheduler; + + private volatile byte[] plaintextResponse; + private volatile byte[] jsonResponse; + + ResponseCache(ScheduledExecutorService scheduler) { + this.scheduler = scheduler; + rebuildNow(); // first responses already have Date + this.scheduler.scheduleAtFixedRate(this, 1, 1, TimeUnit.SECONDS); + } + + byte[] plaintextResponseBytes() { + return plaintextResponse; + } + + byte[] jsonResponseBytes() { + return jsonResponse; + } + + @Override + public void run() { + rebuildNow(); + } + + private void rebuildNow() { + // "Tue, 3 Jun 2008 11:05:30 GMT" + String date = RFC_1123.format(Instant.now()); + byte[] dateBytes = date.getBytes(StandardCharsets.US_ASCII); + + // plaintext: prefix + date + mid + body + byte[] plain = new byte[PLAIN_PREFIX.length + dateBytes.length + PLAIN_MID.length + Constants.STATIC_PLAINTEXT_LEN]; + int p = 0; + System.arraycopy(PLAIN_PREFIX, 0, plain, p, PLAIN_PREFIX.length); p += PLAIN_PREFIX.length; + System.arraycopy(dateBytes, 0, plain, p, dateBytes.length); p += dateBytes.length; + System.arraycopy(PLAIN_MID, 0, plain, p, PLAIN_MID.length); p += PLAIN_MID.length; + System.arraycopy(Constants.STATIC_PLAINTEXT, 0, plain, p, Constants.STATIC_PLAINTEXT_LEN); + + // json: prefix + date + mid + body + byte[] json = new byte[JSON_PREFIX.length + dateBytes.length + JSON_MID.length + JSON_BODY.length]; + int j = 0; + System.arraycopy(JSON_PREFIX, 0, json, j, JSON_PREFIX.length); j += JSON_PREFIX.length; + System.arraycopy(dateBytes, 0, json, j, dateBytes.length); j += dateBytes.length; + System.arraycopy(JSON_MID, 0, json, j, JSON_MID.length); j += JSON_MID.length; + System.arraycopy(JSON_BODY, 0, json, j, JSON_BODY.length); + + plaintextResponse = plain; + jsonResponse = json; + } + } +} diff --git a/frameworks/Java/netty-fast/src/main/java/hello/HelloServerInitializer.java b/frameworks/Java/netty-fast/src/main/java/hello/HelloServerInitializer.java new file mode 100644 index 00000000000..dfa636b6282 --- /dev/null +++ b/frameworks/Java/netty-fast/src/main/java/hello/HelloServerInitializer.java @@ -0,0 +1,15 @@ +package hello; + +import java.util.concurrent.ScheduledExecutorService; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.SocketChannel; + +public class HelloServerInitializer extends ChannelInitializer { + + @Override + protected void initChannel(SocketChannel ch) { + ScheduledExecutorService service = ch.eventLoop(); + ch.pipeline().addLast("handler", new HelloServerHandler(service)); + } +} diff --git a/frameworks/Java/netty-fast/src/main/java/hello/HelloWebServer.java b/frameworks/Java/netty-fast/src/main/java/hello/HelloWebServer.java new file mode 100644 index 00000000000..41743e50406 --- /dev/null +++ b/frameworks/Java/netty-fast/src/main/java/hello/HelloWebServer.java @@ -0,0 +1,84 @@ +package hello; + +import java.net.InetSocketAddress; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.epoll.EpollChannelOption; +import io.netty.channel.uring.IoUringChannelOption; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetector.Level; + +public class HelloWebServer { + + private static final IoMultiplexer PREFERRED_TRANSPORT; + + static { + ResourceLeakDetector.setLevel(Level.DISABLED); + String transportName = System.getProperty("hello.transport"); + if (transportName != null) { + try { + PREFERRED_TRANSPORT = IoMultiplexer.valueOf(transportName); + } catch (IllegalArgumentException e) { + System.err.println("Invalid transport name: " + transportName); + throw e; + } + } else { + PREFERRED_TRANSPORT = IoMultiplexer.type(); + } + } + + private final int port; + + public HelloWebServer(int port) { + this.port = port; + } + + public void run() throws Exception { + final var preferredTransport = PREFERRED_TRANSPORT; + System.out.printf("Using %s IoMultiplexer%n", preferredTransport); + final int coreCount = Runtime.getRuntime().availableProcessors(); + final var group = preferredTransport.newEventLoopGroup(coreCount); + + try { + final var serverChannelClass = preferredTransport.serverChannelClass(); + var inet = new InetSocketAddress(port); + var b = new ServerBootstrap(); + + b.option(ChannelOption.SO_BACKLOG, 8192); + b.option(ChannelOption.SO_REUSEADDR, true); + switch (preferredTransport) { + case EPOLL: + b.option(EpollChannelOption.SO_REUSEPORT, true); + break; + case IO_URING: + b.option(IoUringChannelOption.SO_REUSEPORT, true); + break; + } + var channelB = b.group(group).channel(serverChannelClass); + channelB.childHandler(new HelloServerInitializer()); + b.childOption(ChannelOption.SO_REUSEADDR, true); + + Channel ch = b.bind(inet).sync().channel(); + + System.out.printf("Httpd started. Listening on: %s%n", inet); + + ch.closeFuture().sync(); + } finally { + group.shutdownGracefully().sync(); + } + } + + public static void main(String[] args) throws Exception { + int port; + if (args.length > 0) { + port = Integer.parseInt(args[0]); + } else { + port = 8080; + } + new HelloWebServer(port).run(); + + + } +} diff --git a/frameworks/Java/netty-fast/src/main/java/hello/HttpResponses.java b/frameworks/Java/netty-fast/src/main/java/hello/HttpResponses.java new file mode 100644 index 00000000000..a6505dd11db --- /dev/null +++ b/frameworks/Java/netty-fast/src/main/java/hello/HttpResponses.java @@ -0,0 +1,39 @@ +package hello; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.util.AsciiString; + +public final class HttpResponses { + + private static final CharSequence SERVER_NAME = AsciiString.cached("Netty"); + + private HttpResponses() { + } + + public static FullHttpResponse plaintext() { + ByteBuf buf = Unpooled.wrappedBuffer(Constants.STATIC_PLAINTEXT); + HttpHeaders headers = new io.netty.handler.codec.http.DefaultHttpHeaders(); + headers.set(HttpHeaderNames.SERVER, SERVER_NAME); + headers.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN); + headers.setInt(HttpHeaderNames.CONTENT_LENGTH, Constants.STATIC_PLAINTEXT_LEN); + return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf, headers, HttpHeaders.EMPTY_HEADERS); + } + + public static FullHttpResponse json() { + byte[] body = JsonUtils.serializeMsg(Constants.STATIC_MESSAGE); + ByteBuf buf = Unpooled.wrappedBuffer(body); + HttpHeaders headers = new io.netty.handler.codec.http.DefaultHttpHeaders(); + headers.set(HttpHeaderNames.SERVER, SERVER_NAME); + headers.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON); + headers.setInt(HttpHeaderNames.CONTENT_LENGTH, body.length); + return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf, headers, HttpHeaders.EMPTY_HEADERS); + } +} diff --git a/frameworks/Java/netty-fast/src/main/java/hello/IoMultiplexer.java b/frameworks/Java/netty-fast/src/main/java/hello/IoMultiplexer.java new file mode 100644 index 00000000000..5797bbe14f5 --- /dev/null +++ b/frameworks/Java/netty-fast/src/main/java/hello/IoMultiplexer.java @@ -0,0 +1,54 @@ +package hello; + +import io.netty.channel.IoHandlerFactory; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollIoHandler; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.kqueue.KQueue; +import io.netty.channel.kqueue.KQueueIoHandler; +import io.netty.channel.kqueue.KQueueServerSocketChannel; +import io.netty.channel.nio.NioIoHandler; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.uring.IoUring; +import io.netty.channel.uring.IoUringIoHandler; +import io.netty.channel.uring.IoUringServerSocketChannel; + +public enum IoMultiplexer { + EPOLL, KQUEUE, JDK, IO_URING; + + public Class serverChannelClass() { + return switch (this) { + case EPOLL -> EpollServerSocketChannel.class; + case KQUEUE -> KQueueServerSocketChannel.class; + case JDK -> NioServerSocketChannel.class; + case IO_URING -> IoUringServerSocketChannel.class; + }; + } + + public IoHandlerFactory newIoHandlerFactory() { + return switch (this) { + case EPOLL -> EpollIoHandler.newFactory(); + case KQUEUE -> KQueueIoHandler.newFactory(); + case JDK -> NioIoHandler.newFactory(); + case IO_URING -> IoUringIoHandler.newFactory(); + }; + } + + public MultiThreadIoEventLoopGroup newEventLoopGroup(int nThreads) { + return new MultiThreadIoEventLoopGroup(nThreads, newIoHandlerFactory()); + } + + public static IoMultiplexer type() { + if (IoUring.isAvailable()) { + return IO_URING; + } else if (Epoll.isAvailable()) { + return EPOLL; + } else if (KQueue.isAvailable()) { + return KQUEUE; + } else { + return JDK; + } + } +} \ No newline at end of file diff --git a/frameworks/Java/netty-fast/src/main/java/hello/JsonUtils.java b/frameworks/Java/netty-fast/src/main/java/hello/JsonUtils.java new file mode 100644 index 00000000000..4b6db13e619 --- /dev/null +++ b/frameworks/Java/netty-fast/src/main/java/hello/JsonUtils.java @@ -0,0 +1,13 @@ +package hello; + +import com.alibaba.fastjson2.JSON; + +public final class JsonUtils { + + private JsonUtils() { + } + + public static byte[] serializeMsg(Message obj) { + return JSON.toJSONBytes(obj); + } +} diff --git a/frameworks/Java/netty-fast/src/main/java/hello/Message.java b/frameworks/Java/netty-fast/src/main/java/hello/Message.java new file mode 100644 index 00000000000..13698780e9f --- /dev/null +++ b/frameworks/Java/netty-fast/src/main/java/hello/Message.java @@ -0,0 +1,15 @@ +package hello; + +public class Message { + + private final String message; + + public Message(String message) { + super(); + this.message = message; + } + + public String getMessage() { + return message; + } +} \ No newline at end of file From eaa522f58a2c48371c7412a880382aee61f95d50 Mon Sep 17 00:00:00 2001 From: doxlik Date: Fri, 26 Dec 2025 14:35:36 +0400 Subject: [PATCH 2/4] netty-fast --- .../Java/netty-fast/{netty.dockerfile => netty-fast.dockerfile} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frameworks/Java/netty-fast/{netty.dockerfile => netty-fast.dockerfile} (100%) diff --git a/frameworks/Java/netty-fast/netty.dockerfile b/frameworks/Java/netty-fast/netty-fast.dockerfile similarity index 100% rename from frameworks/Java/netty-fast/netty.dockerfile rename to frameworks/Java/netty-fast/netty-fast.dockerfile From 5617dc7606c6831bea4f0131fe2a87ea3d84ef1b Mon Sep 17 00:00:00 2001 From: doxlik Date: Sat, 27 Dec 2025 01:31:48 +0400 Subject: [PATCH 3/4] netty-fast --- .../main/java/hello/HelloServerHandler.java | 203 +++++++----------- .../java/hello/HelloServerInitializer.java | 5 + 2 files changed, 78 insertions(+), 130 deletions(-) diff --git a/frameworks/Java/netty-fast/src/main/java/hello/HelloServerHandler.java b/frameworks/Java/netty-fast/src/main/java/hello/HelloServerHandler.java index 2b8c834b405..b95c8dda774 100644 --- a/frameworks/Java/netty-fast/src/main/java/hello/HelloServerHandler.java +++ b/frameworks/Java/netty-fast/src/main/java/hello/HelloServerHandler.java @@ -12,6 +12,10 @@ import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.AttributeKey; import io.netty.util.concurrent.FastThreadLocal; @@ -27,14 +31,6 @@ public class HelloServerHandler extends ChannelInboundHandlerAdapter { "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n" .getBytes(StandardCharsets.US_ASCII); - private static final byte CR = (byte) '\r'; - private static final byte LF = (byte) '\n'; - - private static final byte[] GET = "GET".getBytes(StandardCharsets.US_ASCII); - - private static final byte[] PATH_PLAINTEXT = "/plaintext".getBytes(StandardCharsets.US_ASCII); - private static final byte[] PATH_JSON = "/json".getBytes(StandardCharsets.US_ASCII); - // JSON is static in TFB, so compute once private static final byte[] JSON_BODY = JsonUtils.serializeMsg(Constants.STATIC_MESSAGE); @@ -57,35 +53,39 @@ public void channelActive(ChannelHandlerContext ctx) { @Override public void channelInactive(ChannelHandlerContext ctx) { PerChannelState state = ctx.channel().attr(STATE_KEY).get(); - if (state != null && state.inbound != null) { - state.inbound.release(); - state.inbound = null; + if (state != null && state.outAggregate != null) { + state.outAggregate.release(); + state.outAggregate = null; + state.hadBatched = false; } } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { - if (!(msg instanceof ByteBuf)) { - io.netty.util.ReferenceCountUtil.release(msg); - return; - } - ByteBuf in = (ByteBuf) msg; - PerChannelState state = state(ctx); - + // We expect HttpRequest objects from HttpRequestDecoder. + // But be defensive: release HttpContent ByteBufs if any appear (e.g. weird clients). try { - if (state.inbound == null) { - state.inbound = in; - } else { - state.inbound.writeBytes(in); - in.release(); + if (msg instanceof HttpRequest req) { + handleRequest(ctx, state(ctx), req); + return; } - parseRequests(ctx, state); + if (msg instanceof HttpContent content) { + // Discard any body content; TFB doesn't send it. + content.release(); + if (msg instanceof LastHttpContent) { + return; + } + return; + } + io.netty.util.ReferenceCountUtil.release(msg); } catch (Throwable t) { - if (state.inbound != null) { - state.inbound.release(); - state.inbound = null; + PerChannelState st = ctx.channel().attr(STATE_KEY).get(); + if (st != null && st.outAggregate != null) { + st.outAggregate.release(); + st.outAggregate = null; + st.hadBatched = false; } ctx.close(); } @@ -104,6 +104,24 @@ public void channelReadComplete(ChannelHandlerContext ctx) { } } + private void handleRequest(ChannelHandlerContext ctx, PerChannelState state, HttpRequest req) { + // Minimal keep-alive handling (mostly irrelevant for TFB but "correct enough"). + state.closeAfterFlush |= !HttpUtil.isKeepAlive(req); + + // Fast path route by URI. (TFB uses exact "/plaintext" and "/json") + final String uri = req.uri(); + if ("/plaintext".equals(uri)) { + encodePlaintext(ctx, state); + return; + } + if ("/json".equals(uri)) { + encodeJson(ctx, state); + return; + } + + writeNotFound(ctx, state); + } + private PerChannelState state(ChannelHandlerContext ctx) { PerChannelState st = ctx.channel().attr(STATE_KEY).get(); if (st == null) { @@ -120,138 +138,64 @@ private ByteBuf ensureAggregate(ChannelHandlerContext ctx, PerChannelState state return state.outAggregate; } - private void parseRequests(ChannelHandlerContext ctx, PerChannelState state) { - ByteBuf in = state.inbound; - if (in == null) { - return; - } - - while (true) { - int start = in.readerIndex(); - int endOfHeaders = findEndOfHeaders(in, start, in.writerIndex()); - if (endOfHeaders == -1) { - return; - } - - if (!parseOneRequestAndRespond(ctx, state, in, start, endOfHeaders)) { - return; - } - - in.readerIndex(endOfHeaders); - - if (in.readableBytes() == 0) { - in.release(); - state.inbound = null; - return; - } - - in.discardSomeReadBytes(); - } - } - - private boolean parseOneRequestAndRespond(ChannelHandlerContext ctx, PerChannelState state, ByteBuf in, int start, int endOfHeaders) { - int lineEnd = findCrlf(in, start, endOfHeaders); - if (lineEnd == -1) { - return false; - } - - int sp1 = findByte(in, start, lineEnd, (byte) ' '); - if (sp1 == -1) { - return false; - } - int sp2 = findByte(in, sp1 + 1, lineEnd, (byte) ' '); - if (sp2 == -1) { - return false; - } - - if (!equalsAscii(in, start, sp1, GET)) { - writeNotFound(ctx, state); - return true; - } - - int pathStart = sp1 + 1; - int pathEnd = sp2; - - if (equalsAscii(in, pathStart, pathEnd, PATH_PLAINTEXT)) { - encodePlaintext(ctx, state); - return true; - } - - if (equalsAscii(in, pathStart, pathEnd, PATH_JSON)) { - encodeJson(ctx, state); - return true; - } - - writeNotFound(ctx, state); - return true; - } - private void encodePlaintext(ChannelHandlerContext ctx, PerChannelState state) { ByteBuf agg = ensureAggregate(ctx, state); agg.writeBytes(cache.plaintextResponseBytes()); state.hadBatched = true; + maybeCloseOnReadComplete(ctx, state); } private void encodeJson(ChannelHandlerContext ctx, PerChannelState state) { ByteBuf agg = ensureAggregate(ctx, state); agg.writeBytes(cache.jsonResponseBytes()); state.hadBatched = true; + maybeCloseOnReadComplete(ctx, state); } private void writeNotFound(ChannelHandlerContext ctx, PerChannelState state) { + // Preserve your “flush aggregate first” behavior. if (state.outAggregate != null) { - ctx.writeAndFlush(state.outAggregate).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + ByteBuf out = state.outAggregate; state.outAggregate = null; state.hadBatched = false; - } - ctx.writeAndFlush(Unpooled.wrappedBuffer(NOT_FOUND_BYTES)).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); - } - private static int findEndOfHeaders(ByteBuf buf, int from, int to) { - for (int i = from + 3; i < to; i++) { - if (buf.getByte(i - 3) == CR && buf.getByte(i - 2) == LF && - buf.getByte(i - 1) == CR && buf.getByte(i) == LF) { - return i + 1; + if (state.closeAfterFlush) { + // write aggregated + 404 and close + ctx.write(out); + ctx.write(Unpooled.wrappedBuffer(NOT_FOUND_BYTES)); + ctx.flush(); + ctx.close(); + state.closeAfterFlush = false; + return; } - } - return -1; - } - private static int findCrlf(ByteBuf buf, int from, int to) { - for (int i = from + 1; i < to; i++) { - if (buf.getByte(i - 1) == CR && buf.getByte(i) == LF) { - return i - 1; - } + ctx.writeAndFlush(out).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); } - return -1; - } - private static int findByte(ByteBuf buf, int from, int to, byte b) { - for (int i = from; i < to; i++) { - if (buf.getByte(i) == b) { - return i; - } + if (state.closeAfterFlush) { + ctx.writeAndFlush(Unpooled.wrappedBuffer(NOT_FOUND_BYTES)) + .addListener(ChannelFutureListener.CLOSE_ON_FAILURE) + .addListener(ChannelFutureListener.CLOSE); + state.closeAfterFlush = false; + } else { + ctx.writeAndFlush(Unpooled.wrappedBuffer(NOT_FOUND_BYTES)) + .addListener(ChannelFutureListener.CLOSE_ON_FAILURE); } - return -1; } - private static boolean equalsAscii(ByteBuf buf, int start, int end, byte[] ascii) { - int len = end - start; - if (len != ascii.length) { - return false; - } - for (int i = 0; i < len; i++) { - if (buf.getByte(start + i) != ascii[i]) { - return false; - } + private void maybeCloseOnReadComplete(ChannelHandlerContext ctx, PerChannelState state) { + // We close only after flushing whatever we batched in channelReadComplete(). + // So we just mark here; actual close is done in channelReadComplete flush path. + if (state.closeAfterFlush) { + // If you want “strict” close behavior, you could flush immediately, + // but that would break batching. We keep batching. } - return true; } private static final class PerChannelState { - ByteBuf inbound; ByteBuf outAggregate; boolean hadBatched; + boolean closeAfterFlush; } /** @@ -309,7 +253,6 @@ public void run() { } private void rebuildNow() { - // "Tue, 3 Jun 2008 11:05:30 GMT" String date = RFC_1123.format(Instant.now()); byte[] dateBytes = date.getBytes(StandardCharsets.US_ASCII); diff --git a/frameworks/Java/netty-fast/src/main/java/hello/HelloServerInitializer.java b/frameworks/Java/netty-fast/src/main/java/hello/HelloServerInitializer.java index dfa636b6282..6ca37a5789d 100644 --- a/frameworks/Java/netty-fast/src/main/java/hello/HelloServerInitializer.java +++ b/frameworks/Java/netty-fast/src/main/java/hello/HelloServerInitializer.java @@ -4,12 +4,17 @@ import io.netty.channel.ChannelInitializer; import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpDecoderConfig; +import io.netty.handler.codec.http.HttpRequestDecoder; public class HelloServerInitializer extends ChannelInitializer { @Override protected void initChannel(SocketChannel ch) { ScheduledExecutorService service = ch.eventLoop(); + var config = new HttpDecoderConfig().setMaxInitialLineLength(4096).setMaxHeaderSize(8192).setMaxChunkSize(8192); + + ch.pipeline().addLast("httpDecoder", new HttpRequestDecoder(config)); ch.pipeline().addLast("handler", new HelloServerHandler(service)); } } From 76b6ccffe3178010c52168b8fcb69f06689dc0af Mon Sep 17 00:00:00 2001 From: doxlik Date: Sat, 27 Dec 2025 02:32:36 +0400 Subject: [PATCH 4/4] remove some unnecessary comments --- .../main/java/hello/HelloServerHandler.java | 25 +----------- .../src/main/java/hello/HttpResponses.java | 39 ------------------- 2 files changed, 1 insertion(+), 63 deletions(-) delete mode 100644 frameworks/Java/netty-fast/src/main/java/hello/HttpResponses.java diff --git a/frameworks/Java/netty-fast/src/main/java/hello/HelloServerHandler.java b/frameworks/Java/netty-fast/src/main/java/hello/HelloServerHandler.java index b95c8dda774..c356bd2fa55 100644 --- a/frameworks/Java/netty-fast/src/main/java/hello/HelloServerHandler.java +++ b/frameworks/Java/netty-fast/src/main/java/hello/HelloServerHandler.java @@ -24,14 +24,12 @@ public class HelloServerHandler extends ChannelInboundHandlerAdapter { private static final AttributeKey STATE_KEY = AttributeKey.valueOf("hello.state"); - // One cache per EventLoop thread private static final FastThreadLocal CACHE_TL = new FastThreadLocal<>(); private static final byte[] NOT_FOUND_BYTES = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n" .getBytes(StandardCharsets.US_ASCII); - // JSON is static in TFB, so compute once private static final byte[] JSON_BODY = JsonUtils.serializeMsg(Constants.STATIC_MESSAGE); private final ResponseCache cache; @@ -62,8 +60,6 @@ public void channelInactive(ChannelHandlerContext ctx) { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { - // We expect HttpRequest objects from HttpRequestDecoder. - // But be defensive: release HttpContent ByteBufs if any appear (e.g. weird clients). try { if (msg instanceof HttpRequest req) { handleRequest(ctx, state(ctx), req); @@ -71,7 +67,6 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { } if (msg instanceof HttpContent content) { - // Discard any body content; TFB doesn't send it. content.release(); if (msg instanceof LastHttpContent) { return; @@ -105,10 +100,8 @@ public void channelReadComplete(ChannelHandlerContext ctx) { } private void handleRequest(ChannelHandlerContext ctx, PerChannelState state, HttpRequest req) { - // Minimal keep-alive handling (mostly irrelevant for TFB but "correct enough"). state.closeAfterFlush |= !HttpUtil.isKeepAlive(req); - // Fast path route by URI. (TFB uses exact "/plaintext" and "/json") final String uri = req.uri(); if ("/plaintext".equals(uri)) { encodePlaintext(ctx, state); @@ -142,25 +135,21 @@ private void encodePlaintext(ChannelHandlerContext ctx, PerChannelState state) { ByteBuf agg = ensureAggregate(ctx, state); agg.writeBytes(cache.plaintextResponseBytes()); state.hadBatched = true; - maybeCloseOnReadComplete(ctx, state); } private void encodeJson(ChannelHandlerContext ctx, PerChannelState state) { ByteBuf agg = ensureAggregate(ctx, state); agg.writeBytes(cache.jsonResponseBytes()); state.hadBatched = true; - maybeCloseOnReadComplete(ctx, state); } private void writeNotFound(ChannelHandlerContext ctx, PerChannelState state) { - // Preserve your “flush aggregate first” behavior. if (state.outAggregate != null) { ByteBuf out = state.outAggregate; state.outAggregate = null; state.hadBatched = false; if (state.closeAfterFlush) { - // write aggregated + 404 and close ctx.write(out); ctx.write(Unpooled.wrappedBuffer(NOT_FOUND_BYTES)); ctx.flush(); @@ -183,25 +172,13 @@ private void writeNotFound(ChannelHandlerContext ctx, PerChannelState state) { } } - private void maybeCloseOnReadComplete(ChannelHandlerContext ctx, PerChannelState state) { - // We close only after flushing whatever we batched in channelReadComplete(). - // So we just mark here; actual close is done in channelReadComplete flush path. - if (state.closeAfterFlush) { - // If you want “strict” close behavior, you could flush immediately, - // but that would break batching. We keep batching. - } - } - private static final class PerChannelState { ByteBuf outAggregate; boolean hadBatched; boolean closeAfterFlush; } - /** - * Whole-response cache, updated once/sec, one instance per EventLoop thread. - * Hot path: just read volatile byte[] and writeBytes(). - */ + private static final class ResponseCache implements Runnable { private static final DateTimeFormatter RFC_1123 = DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC); diff --git a/frameworks/Java/netty-fast/src/main/java/hello/HttpResponses.java b/frameworks/Java/netty-fast/src/main/java/hello/HttpResponses.java deleted file mode 100644 index a6505dd11db..00000000000 --- a/frameworks/Java/netty-fast/src/main/java/hello/HttpResponses.java +++ /dev/null @@ -1,39 +0,0 @@ -package hello; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaderValues; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpVersion; -import io.netty.util.AsciiString; - -public final class HttpResponses { - - private static final CharSequence SERVER_NAME = AsciiString.cached("Netty"); - - private HttpResponses() { - } - - public static FullHttpResponse plaintext() { - ByteBuf buf = Unpooled.wrappedBuffer(Constants.STATIC_PLAINTEXT); - HttpHeaders headers = new io.netty.handler.codec.http.DefaultHttpHeaders(); - headers.set(HttpHeaderNames.SERVER, SERVER_NAME); - headers.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN); - headers.setInt(HttpHeaderNames.CONTENT_LENGTH, Constants.STATIC_PLAINTEXT_LEN); - return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf, headers, HttpHeaders.EMPTY_HEADERS); - } - - public static FullHttpResponse json() { - byte[] body = JsonUtils.serializeMsg(Constants.STATIC_MESSAGE); - ByteBuf buf = Unpooled.wrappedBuffer(body); - HttpHeaders headers = new io.netty.handler.codec.http.DefaultHttpHeaders(); - headers.set(HttpHeaderNames.SERVER, SERVER_NAME); - headers.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON); - headers.setInt(HttpHeaderNames.CONTENT_LENGTH, body.length); - return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf, headers, HttpHeaders.EMPTY_HEADERS); - } -}