Java I/O, NIO &
Network Programming
A comprehensive technical reference covering the full Java I/O stack — from classic byte streams to non-blocking channels, memory-mapped files, TCP/UDP sockets, the modern HttpClient API, SSL/TLS, Asynchronous I/O, and Virtual Thread integration.
Classic I/O — java.io
The java.io package, introduced in Java 1.0, provides blocking, stream-oriented I/O. Every operation blocks the calling thread until data is available or written. Despite its age, it remains widely used for simplicity and compatibility.
Stream Hierarchy
All I/O in java.io descends from four abstract roots:
java.io uses the Decorator (Wrapper) pattern. You wrap a raw stream with filtering streams to add capabilities (buffering, data conversion, compression) without modifying the underlying stream.
Byte Streams vs. Character Streams
| Aspect | InputStream / OutputStream | Reader / Writer |
|---|---|---|
| Unit | byte (8-bit) | char (16-bit Unicode) |
| Encoding | None — raw bytes | Uses Charset (UTF-8, ISO-8859-1…) |
| Bridge | — | InputStreamReader / OutputStreamWriter |
| Use case | Binary data, images, serialization | Text files, XML, JSON |
// Always specify charset explicitly — never rely on platform default try (BufferedReader reader = new BufferedReader( new InputStreamReader( new FileInputStream("data.txt"), StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } // AutoCloseable — stream closed automatically Java
Buffered I/O
Unbuffered reads/writes invoke a system call per byte — catastrophic for performance. BufferedInputStream and BufferedReader maintain an in-memory buffer (default 8 KB) and fill it in bulk.
// Benchmark: Unbuffered vs Buffered file copy // Unbuffered: ~50ms for 10MB Buffered: ~3ms (~16x faster) public static void bufferedCopy(Path src, Path dst) throws IOException { try (var in = new BufferedInputStream(new FileInputStream(src.toFile()), 65536); var out = new BufferedOutputStream(new FileOutputStream(dst.toFile()), 65536)) { byte[] buf = new byte[65536]; int n; while ((n = in.read(buf)) != -1) { out.write(buf, 0, n); } } } Java
BufferedOutputStream does not auto-flush on every write. Call flush() or close the stream (which calls flush internally) to ensure all data is written to disk. Failure to flush can silently lose data.
Object Serialization
// Serializing an object graph to bytes record Point(double x, double y) implements Serializable { @Serial private static final long serialVersionUID = 1L; } try (var oos = new ObjectOutputStream( new BufferedOutputStream( new FileOutputStream("point.ser")))) { oos.writeObject(new Point(3.0, 4.0)); } // Deserializing — restrict classes to prevent gadget attacks try (var ois = new ObjectInputStream( new BufferedInputStream( new FileInputStream("point.ser"))) { @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws ClassNotFoundException, IOException { if (!desc.getName().equals(Point.class.getName())) throw new InvalidClassException("Unauthorized: ", desc.getName()); return super.resolveClass(desc); } }) { Point p = (Point) ois.readObject(); } Java
ObjectInputFilter (JEP 290) to allowlist deserializable types.
NIO — java.nio
Introduced in Java 1.4 (NIO) and extended in Java 7 (NIO.2), the java.nio package provides buffer-oriented, channel-based, optionally non-blocking I/O. The core abstraction shift: data moves to/from Buffers through Channels, enabling zero-copy transfers and selector-based multiplexing.
Buffers
Fixed-capacity containers for primitive data. The unit of all NIO data transfer.
Channels
Bidirectional conduits to I/O resources (files, sockets). Support scatter/gather.
Selectors
Multiplex many channels on a single thread. Foundation of async servers.
NIO.2 Path API
Modern file system API. Replaces legacy java.io.File.
Buffers — Deep Dive
A Buffer is a fixed-capacity array plus three bookmarks: position, limit, and capacity.
// Buffer lifecycle: allocate → write → flip → read → clear/compact ByteBuffer buf = ByteBuffer.allocate(1024); // heap buffer ByteBuffer dir = ByteBuffer.allocateDirect(1024); // direct (off-heap) buffer // Writing data into buffer buf.put((byte) 65); // absolute buf.putInt(42); // typed put buf.put("hello".getBytes()); // bulk // Switch to read mode buf.flip(); // limit=position, position=0 while (buf.hasRemaining()) { byte b = buf.get(); // reads one byte, advances position } // Partial reads: compact() shifts unread data to beginning buf.compact(); // unread bytes → [0..n], position=n, limit=capacity buf.clear(); // full reset: position=0, limit=capacity (data untouched!) Java
Heap vs. Direct Buffers
| Property | Heap Buffer | Direct Buffer |
|---|---|---|
| Memory location | JVM heap | OS native memory (off-heap) |
| GC managed | Yes | No (Cleaner / PhantomReference) |
| Allocation cost | Low | High |
| I/O throughput | Lower (extra copy) | Higher (zero-copy path) |
| Best use | Short-lived, small data | Long-lived, large I/O buffers |
Channels
Channels replace streams for NIO I/O. Unlike streams they are bidirectional, support scatter/gather I/O, and can be placed in non-blocking mode.
// FileChannel — zero-copy transfer between channels try (var in = FileChannel.open(src, StandardOpenOption.READ); var out = FileChannel.open(dst, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { long transferred = 0, size = in.size(); while (transferred < size) { transferred += in.transferTo(transferred, size - transferred, out); // transferTo uses sendfile(2) on Linux — true zero-copy! } } // Scatter read — fill multiple buffers in one syscall ByteBuffer header = ByteBuffer.allocate(12); ByteBuffer body = ByteBuffer.allocate(4096); channel.read(new ByteBuffer[]{ header, body }); // scatter // Gather write — drain multiple buffers in one syscall channel.write(new ByteBuffer[]{ header, body }); // gather Java
Selectors — Event-Driven Multiplexing
A Selector monitors multiple SelectableChannels and blocks until at least one is ready for I/O. This enables a single thread to manage thousands of connections — the core of Reactor-pattern servers.
// Non-blocking server skeleton using Selector Selector selector = Selector.open(); ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.configureBlocking(false); ssc.bind(new InetSocketAddress(8080)); ssc.register(selector, SelectionKey.OP_ACCEPT); while (true) { selector.select(); // blocks until ≥1 channel ready var keys = selector.selectedKeys().iterator(); while (keys.hasNext()) { SelectionKey key = keys.next(); keys.remove(); // MUST remove manually if (key.isAcceptable()) { SocketChannel client = ssc.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ, new ByteBuffer[]{ByteBuffer.allocate(8192)}); } else if (key.isReadable()) { SocketChannel ch = (SocketChannel) key.channel(); ByteBuffer[] bufs = (ByteBuffer[]) key.attachment(); long n = ch.read(bufs); if (n == -1) { ch.close(); continue; } processRequest(ch, bufs[0]); } } } Java
SelectionKey. Use key.attach(obj) to associate per-connection state (buffers, protocol state machines) with its key — no need for external Maps.
NIO.2 — Modern File API
java.nio.file.Path, Files, and FileSystem (Java 7) replace the error-prone java.io.File. All methods throw proper checked exceptions instead of silently returning false.
Path p = Path.of("/var/data", "report.csv"); // Atomic read / write String text = Files.readString(p, StandardCharsets.UTF_8); List<String> lines = Files.readAllLines(p, StandardCharsets.UTF_8); Files.writeString(p, "new content", StandardOpenOption.APPEND); // Stream-based directory walk (lazy, no memory explosion) try (Stream<Path> walk = Files.walk(p.getParent(), 3)) { walk.filter(Files::isRegularFile) .filter(f -> f.toString().endsWith(".csv")) .forEach(System.out::println); } // WatchService — filesystem events WatchService watcher = FileSystems.getDefault().newWatchService(); p.getParent().register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE); WatchKey key = watcher.take(); // blocks for (WatchEvent<?> event : key.pollEvents()) { System.out.printf("%s: %s%n", event.kind(), event.context()); } key.reset(); // MUST reset to receive future events Java
Memory-Mapped Files
Memory-mapped files expose a file as a ByteBuffer backed by OS virtual memory pages. The OS handles paging — no explicit read/write syscalls needed. Ideal for random access to large files.
try (var fc = FileChannel.open(path, StandardOpenOption.READ)) { MappedByteBuffer mmap = fc.map( FileChannel.MapMode.READ_ONLY, 0, // offset fc.size() // length ); // Access bytes directly — OS pages in on demand while (mmap.hasRemaining()) { process(mmap.get()); } // Force dirty pages to storage mmap.force(); } // Note: MappedByteBuffer is NOT automatically unmapped on GC. // Use sun.misc.Cleaner (or MethodHandles on Java 9+) for explicit unmap. Java
TCP Sockets
TCP provides reliable, ordered, stream-oriented communication. Java exposes it via java.net.Socket (client) and java.net.ServerSocket (server).
// ─── TCP Server ──────────────────────────────────────────────── try (var serverSocket = new ServerSocket()) { serverSocket.setReuseAddress(true); // SO_REUSEADDR serverSocket.setReceiveBufferSize(65536); serverSocket.bind(new InetSocketAddress(8080), 128); // backlog=128 ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor(); // Java 21 while (!serverSocket.isClosed()) { Socket client = serverSocket.accept(); // blocks pool.submit(() -> handleClient(client)); } } static void handleClient(Socket socket) { try (socket; var in = new BufferedInputStream(socket.getInputStream()); var out = new BufferedOutputStream(socket.getOutputStream())) { socket.setSoTimeout(30_000); // 30 s read timeout socket.setTcpNoDelay(true); // disable Nagle for latency socket.setKeepAlive(true); // detect dead peers byte[] buf = new byte[8192]; int n; while ((n = in.read(buf)) != -1) { out.write(buf, 0, n); // echo out.flush(); } } catch (SocketTimeoutException e) { System.err.println("Client timed out"); } catch (IOException e) { System.err.println("Client error: " + e.getMessage()); } } Java
Important Socket Options
| Option | Default | Purpose |
|---|---|---|
| SO_REUSEADDR | false | Reuse TIME_WAIT addresses — essential for servers |
| SO_KEEPALIVE | false | Send TCP keepalives to detect dead peers |
| TCP_NODELAY | false | Disable Nagle's algorithm — reduces latency for small writes |
| SO_RCVBUF / SO_SNDBUF | OS default | Socket buffer size — tune for high-bandwidth links |
| SO_TIMEOUT | 0 (infinite) | Read timeout in milliseconds |
| SO_LINGER | disabled | Block on close until data sent or timeout |
UDP Datagrams
UDP is connectionless, unreliable, unordered — but with lower overhead. Use for DNS lookups, real-time media (VoIP, games), and QUIC-based protocols.
// UDP Server try (var socket = new DatagramSocket(9090)) { byte[] buf = new byte[65507]; // max UDP payload DatagramPacket pkt = new DatagramPacket(buf, buf.length); while (true) { socket.receive(pkt); // blocks until datagram arrives String msg = new String(pkt.getData(), 0, pkt.getLength(), StandardCharsets.UTF_8); // Echo back to sender byte[] reply = ("Echo: " + msg).getBytes(); socket.send(new DatagramPacket(reply, reply.length, pkt.getAddress(), pkt.getPort())); } } // Multicast group join (NIO DatagramChannel) DatagramChannel mc = DatagramChannel.open(StandardProtocolFamily.INET); mc.setOption(StandardSocketOptions.SO_REUSEADDR, true); mc.bind(new InetSocketAddress(5353)); NetworkInterface ni = NetworkInterface.getByName("eth0"); mc.join(InetAddress.getByName("224.0.0.251"), ni); // mDNS group Java
High-Performance Non-Blocking Server
Combining ServerSocketChannel, Selector, and a Reactor pattern yields a server that handles thousands of concurrent connections with a small, fixed thread pool.
// Production-grade Reactor — single-threaded event loop with worker pool public class ReactorServer { private final Selector selector; private final ServerSocketChannel ssc; private final ExecutorService workers = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); public ReactorServer(int port) throws IOException { selector = Selector.open(); ssc = ServerSocketChannel.open(); ssc.configureBlocking(false); ssc.setOption(StandardSocketOptions.SO_REUSEADDR, true); ssc.bind(new InetSocketAddress(port), 1024); ssc.register(selector, SelectionKey.OP_ACCEPT); } public void run() throws IOException { while (!Thread.currentThread().isInterrupted()) { if (selector.select(1000) == 0) continue; // timeout, check shutdown var it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey k = it.next(); it.remove(); try { if (k.isAcceptable()) onAccept(k); else if (k.isReadable()) onRead(k); else if (k.isWritable()) onWrite(k); } catch (IOException e) { closeKey(k); } } } } private void onAccept(SelectionKey k) throws IOException { SocketChannel ch = ((ServerSocketChannel) k.channel()).accept(); if (ch == null) return; ch.configureBlocking(false); ch.setOption(StandardSocketOptions.TCP_NODELAY, true); ch.register(selector, SelectionKey.OP_READ, new ConnectionCtx()); } private void onRead(SelectionKey k) throws IOException { SocketChannel ch = (SocketChannel) k.channel(); ConnectionCtx ctx = (ConnectionCtx) k.attachment(); int n = ch.read(ctx.readBuf); if (n == -1) { closeKey(k); return; } if (ctx.isRequestComplete()) { var snapshot = ctx.drainRequest(); workers.submit(() -> { ByteBuffer resp = processRequest(snapshot); ctx.enqueueResponse(resp); k.interestOpsOr(SelectionKey.OP_WRITE); selector.wakeup(); // wake selector from worker thread }); } } } Java
HttpClient API (Java 11+)
java.net.http.HttpClient is the modern, built-in HTTP client supporting HTTP/1.1, HTTP/2, WebSocket, synchronous and asynchronous modes, and reactive streaming via Flow.
// Build once, reuse for all requests (thread-safe) HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_2) .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NORMAL) .executor(Executors.newVirtualThreadPerTaskExecutor()) .build(); // ── Synchronous GET ────────────────────────────────────────── HttpRequest req = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/items")) .header("Accept", "application/json") .timeout(Duration.ofSeconds(10)) .GET() .build(); HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString()); System.out.println(resp.statusCode() + " " + resp.body()); // ── Async POST with JSON body ───────────────────────────────── CompletableFuture<HttpResponse<String>> future = client.sendAsync( HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/items")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"widget\"}")) .build(), HttpResponse.BodyHandlers.ofString() ).thenApply(r -> { if (r.statusCode() != 201) throw new RuntimeException("Unexpected status: " + r.statusCode()); return r; }); // ── Streaming response body via BodyHandler.ofInputStream ──── HttpResponse<InputStream> streamResp = client.send(req, HttpResponse.BodyHandlers.ofInputStream()); try (var is = streamResp.body()) { // process in chunks — no full body buffering in memory is.transferTo(System.out); } Java
WebSocket
WebSocket ws = client.newWebSocketBuilder() .header("Authorization", "Bearer token") .buildAsync(URI.create("wss://echo.example.com"), new WebSocket.Listener() { private final StringBuilder sb = new StringBuilder(); @Override public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) { sb.append(data); if (last) { System.out.println("Received: " + sb); sb.setLength(0); } ws.request(1); // flow control return null; } }).join(); ws.sendText("Hello WebSocket", true).join(); ws.sendClose(WebSocket.NORMAL_CLOSURE, "done"); Java
SSL / TLS
Java's javax.net.ssl package wraps Socket and SocketChannel with TLS. The core components are SSLContext, SSLEngine, and SSLSocket.
// ── Mutual TLS (mTLS) Server ────────────────────────────────── // 1. Load server keystore (certificate + private key) KeyStore ks = KeyStore.getInstance("PKCS12"); try (var fis = new FileInputStream("server.p12")) { ks.load(fis, "changeit".toCharArray()); } KeyManagerFactory kmf = KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm()); kmf.init(ks, "changeit".toCharArray()); // 2. Load truststore (trusted CA certs) KeyStore ts = KeyStore.getInstance("JKS"); try (var fis = new FileInputStream("truststore.jks")) { ts.load(fis, "trustpass".toCharArray()); } TrustManagerFactory tmf = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); tmf.init(ts); // 3. Build SSLContext with TLS 1.3 SSLContext ctx = SSLContext.getInstance("TLSv1.3"); ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); SSLServerSocketFactory ssf = ctx.getServerSocketFactory(); try (var sss = (SSLServerSocket) ssf.createServerSocket(8443)) { sss.setNeedClientAuth(true); // require client certificate (mTLS) sss.setEnabledProtocols(new String[]{ "TLSv1.3" }); while (true) { SSLSocket client = (SSLSocket) sss.accept(); // Kick off handshake on virtual thread Thread.startVirtualThread(() -> handleTlsClient(client)); } } Java
SSLEngine — Non-Blocking TLS
SSLEngine decouples TLS handshaking from transport, enabling use with NIO channels.
SSLEngine engine = ctx.createSSLEngine("peer.example.com", 443); engine.setUseClientMode(true); engine.setEnabledProtocols(new String[]{ "TLSv1.3" }); engine.setEnabledCipherSuites(new String[]{ "TLS_AES_256_GCM_SHA384" }); engine.beginHandshake(); // Handshake loop — pump wrap/unwrap until FINISHED while (engine.getHandshakeStatus() != SSLEngineResult.HandshakeStatus.FINISHED) { switch (engine.getHandshakeStatus()) { case NEED_WRAP -> doWrap(engine, channel, netOutBuf); case NEED_UNWRAP -> doUnwrap(engine, channel, netInBuf, appInBuf); case NEED_TASK -> engine.getDelegatedTask().run(); // offload to thread pool default -> throw new IllegalStateException(); } } Java
Asynchronous I/O (AIO)
Introduced in Java 7, AsynchronousFileChannel and AsynchronousSocketChannel provide true kernel-level async I/O (IOCP on Windows, epoll/io_uring on Linux) via CompletionHandlers or Futures.
// Async file read with CompletionHandler try (var afc = AsynchronousFileChannel.open( path, StandardOpenOption.READ)) { ByteBuffer buf = ByteBuffer.allocate((int) afc.size()); afc.read(buf, 0, buf, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer attachment) { attachment.flip(); System.out.println("Read " + result + " bytes"); } @Override public void failed(Throwable exc, ByteBuffer attachment) { exc.printStackTrace(); } }); Thread.sleep(Duration.ofSeconds(1)); // keep JVM alive } // Async TCP client with Future-based API AsynchronousSocketChannel asc = AsynchronousSocketChannel.open(); Future<Void> connected = asc.connect(new InetSocketAddress("host", 8080)); connected.get(); // block until connected ByteBuffer data = ByteBuffer.wrap("GET / HTTP/1.1\r\n\r\n".getBytes()); Future<Integer> written = asc.write(data); System.out.println("Wrote: " + written.get() + " bytes"); Java
Virtual Threads & I/O (Java 21)
Project Loom's Virtual Threads (JEP 444, Java 21) are cheap, JVM-managed threads that automatically unmount from the carrier thread when blocked on I/O. This eliminates the reactive programming complexity of Selector-based code while achieving comparable throughput.
// Virtual thread per connection — dead simple, high concurrency try (var server = new ServerSocket(8080)) { try (var exec = Executors.newVirtualThreadPerTaskExecutor()) { while (true) { Socket s = server.accept(); // blocking — fine on VT! exec.submit(() -> { try (s; var in = s.getInputStream()) { // blocking I/O here — VT unmounts, doesn't waste OS thread byte[] data = in.readAllBytes(); process(data); } }); } } } // Virtual threads + HttpClient: maximum simplicity + throughput HttpClient client = HttpClient.newBuilder() .executor(Executors.newVirtualThreadPerTaskExecutor()) .build(); // Structured Concurrency (JEP 453) — fan out to 100 endpoints try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { List<StructuredTaskScope.Subtask<String>> tasks = urls.stream() .map(url -> scope.fork(() -> fetchUrl(client, url))) .toList(); scope.join().throwIfFailed(); List<String> results = tasks.stream().map(StructuredTaskScope.Subtask::get).toList(); } Java
- All
java.ioandjava.netblocking operations automatically support unmounting on VTs. - Avoid
synchronizedblocks that contain I/O — they pin the VT to its carrier thread. UseReentrantLockinstead (Java 21 unfixes this for most cases automatically). ThreadLocalin VTs works but is expensive at scale — preferScopedValue(JEP 446).
Performance Patterns
Buffer Sizing Strategy
| Scenario | Recommended Size | Rationale |
|---|---|---|
| Network socket reads | 8 – 64 KB | Matches typical MTU multiples |
| File copy | 64 – 256 KB | Amortizes syscall overhead |
| Memory-mapped access | Full file or page-aligned chunks | OS paging efficiency |
| SSL/TLS records | ≥ 16 KB + overhead | Max TLS record = 16384 bytes |
Object Pooling for Buffers
// Avoid DirectByteBuffer allocation churn with a pool public class BufferPool { private final ArrayDeque<ByteBuffer> pool = new ArrayDeque<>(); private final int capacity; public BufferPool(int capacity) { this.capacity = capacity; } public synchronized ByteBuffer acquire() { ByteBuffer buf = pool.poll(); return buf != null ? buf.clear() : ByteBuffer.allocateDirect(capacity); } public synchronized void release(ByteBuffer buf) { if (pool.size() < 64) pool.push(buf); // else discard — let GC reclaim } } Java
Zero-Copy Transfer
// Serve a file over socket without copying to user-space try (var fileChannel = FileChannel.open(filePath, StandardOpenOption.READ); var socketChannel = SocketChannel.open(serverAddr)) { long size = fileChannel.size(); long transferred = 0; while (transferred < size) { long n = fileChannel.transferTo(transferred, size - transferred, socketChannel); if (n <= 0) break; // connection closed transferred += n; } // Linux: sendfile(2) — data never enters user-space heap // macOS: sendfile(2) with header/trailer support // Windows: TransmitFile } Java
Common Pitfalls & Best Practices
selector.selectedKeys(), you must call iterator.remove(). The selector never removes keys itself. Failure causes infinite re-processing of the same ready key.
channel.read(buf) may read fewer bytes than buf.remaining(). channel.write(buf) may write fewer bytes than the buffer contains. Always loop until the buffer is drained or the channel signals end-of-stream.
new InputStreamReader(is) and new String(bytes) use the platform default charset (varies by OS). Always specify StandardCharsets.UTF_8 explicitly.
Too many open files errors under load.
synchronized blocks pin virtual threads to carrier threads. A pinned VT doing blocking I/O defeats the purpose of virtual threads. Replace with java.util.concurrent.locks.ReentrantLock.
socket.connect(addr, timeout)) and read timeout (socket.setSoTimeout(ms)). A missing read timeout causes a thread to block permanently on a stalled peer — a classic cause of thread pool exhaustion.
Quick Reference Cheat Sheet
| Task | Best API (Java 21) |
|---|---|
| Read a small text file | Files.readString(path, UTF_8) |
| Stream a large file line-by-line | Files.lines(path, UTF_8) (try-with-resources) |
| Copy a file efficiently | Files.copy(src, dst, REPLACE_EXISTING) |
| Watch a directory for changes | WatchService via FileSystems.getDefault() |
| Simple HTTP GET | HttpClient.send(req, BodyHandlers.ofString()) |
| High-concurrency TCP server | ServerSocket + newVirtualThreadPerTaskExecutor() |
| 100k+ concurrent connections | NIO Selector + SocketChannel reactor |
| Zero-copy file serving | FileChannel.transferTo() |
| Secure socket | SSLContext → SSLSocket / SSLEngine |
| Non-blocking TLS | SSLEngine + SocketChannel |