Networking & HTTP
Microservices spend most wall-clock time on the network—TCP connections, TLS handshakes, HTTP retries, and backpressure. Java gives you blocking sockets for learning and custom protocols, NIO channels for scalable servers, and java.net.http.HttpClient (Java 11+) for production HTTP and WebSocket clients. This chapter walks each layer and steers you away from legacy APIs that still compile but hurt operability.
Choosing a networking API
Match transport to requirements: reliable ordered bytes (TCP), fire-and-forget datagrams (UDP), or application-layer HTTP when interoperability matters more than raw latency.
| API | Transport | Typical use |
|---|---|---|
ServerSocket / Socket | TCP | Custom protocols, learning, legacy services |
DatagramSocket | UDP | DNS-like queries, gaming, metrics, multicast |
SocketChannel + Selector | TCP (NIO) | Many concurrent connections, proxies |
HttpClient | TCP + HTTP/1.1 or HTTP/2 | REST, downloads, webhooks, WebSocket client |
For REST microservices, default to HttpClient (or framework clients built on it). Drop to sockets when you own the wire format end-to-end.
Socket programming
Sockets are the BSD-style endpoint abstraction: IP address + port, byte streams (TCP) or datagrams (UDP). All higher protocols—including HTTP—eventually sit on these primitives.
TCP — ServerSocket and Socket
TCP provides reliable, ordered, connection-oriented delivery. The server binds a port with ServerSocket,
calls accept() to obtain a connected Socket per client, then reads/writes via input/output streams.
The client creates a Socket(host, port) and uses the same stream API.
Each accepted socket is usually handled on its own thread in simple servers—or registered with a Selector for NIO. Always set read timeouts (socket.setSoTimeout) and close sockets in try-with-resources. InetAddress resolves hostnames to IPs—cache carefully in hot paths; prefer explicit connection timeouts over infinite blocking on misconfigured firewalls.
// Server — echo one line per connection
try (var server = new ServerSocket(9000)) {
server.setSoTimeout(30_000);
while (true) {
try (Socket client = server.accept();
var in = new BufferedReader(new InputStreamReader(
client.getInputStream(), StandardCharsets.UTF_8));
var out = new PrintWriter(client.getOutputStream(), true, StandardCharsets.UTF_8)) {
String line = in.readLine();
if (line != null) out.println("echo: " + line);
}
}
}
// Client
try (var socket = new Socket("localhost", 9000);
var out = new PrintWriter(socket.getOutputStream(), true, StandardCharsets.UTF_8);
var in = new BufferedReader(new InputStreamReader(
socket.getInputStream(), StandardCharsets.UTF_8))) {
out.println("hello");
System.out.println(in.readLine());
}
SO_REUSEADDR, backlog queue sizing, and TLS (SSLSocket) matter in production servers—frameworks like Netty and embedded Tomcat wrap these details.
UDP — DatagramSocket and DatagramPacket
UDP sends self-contained datagrams without connection setup—no guarantee of delivery, order, or duplicate suppression. Lower latency and simpler fan-out; you implement reliability in the app layer if needed.
DatagramSocket sends/receives DatagramPacket objects wrapping byte arrays and SocketAddress endpoints.
// UDP server
byte[] buf = new byte[512];
try (var socket = new DatagramSocket(9001)) {
var packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
String msg = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
byte[] reply = ("ack: " + msg).getBytes(StandardCharsets.UTF_8);
socket.send(new DatagramPacket(reply, reply.length, packet.getSocketAddress()));
}
// UDP client
try (var socket = new DatagramSocket()) {
byte[] data = "ping".getBytes(StandardCharsets.UTF_8);
socket.send(new DatagramPacket(data, data.length,
InetAddress.getByName("localhost"), 9001));
}
Non-blocking TCP with NIO SocketChannel
SocketChannel.open() and ServerSocketChannel.open() integrate with Selector—
configure configureBlocking(false), register for OP_ACCEPT / OP_READ, and multiplex many connections on few threads.
Same model as the echo server in I/O: Selector & NIO server.
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(9002));
server.configureBlocking(false);
server.register(selector, SelectionKey.OP_ACCEPT);
// On accept:
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
// read/write via ByteBuffer on client.read(buf)
With virtual threads, blocking Socket code scales further without selectors—still cap connection counts and use timeouts. Selectors remain relevant for custom gateways and Netty-style event loops.
HTTP Client (Java 11+)
java.net.http.HttpClient replaces Apache HttpClient and HttpURLConnection for new code:
HTTP/2, async API, WebSocket, and immutable request/response types.
Create one shared instance per application (connection pooling inside).
HttpClient — builder, executor, redirects, authenticator
HttpClient.newBuilder() configures defaults for all requests: connect timeout, redirect policy, proxy, SSL context, HTTP version preference, and the Executor used by sendAsync.
| Builder method | Effect |
|---|---|
connectTimeout | Max wait to establish TCP connection |
followRedirects | ALWAYS, NEVER, or NORMAL (no https→http downgrade) |
version | HTTP_2 preferred with HTTP/1.1 fallback |
proxy | ProxySelector for corporate egress |
authenticator | Supply credentials when server returns 401 |
executor | Thread pool for async callbacks; default is executor service |
cookieHandler | Optional CookieManager for session cookies |
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NORMAL)
.version(HttpClient.Version.HTTP_2)
.executor(Executors.newFixedThreadPool(8))
.authenticator(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
if (getRequestorType() == RequestorType.SERVER) {
return new PasswordAuthentication("user", "secret".toCharArray());
}
return null;
}
})
.build();
HttpRequest — GET, POST, headers, body
HttpRequest.newBuilder(URI) builds immutable requests. Set method (default GET), headers, timeout, and body publisher for POST/PUT/PATCH.
BodyPublishers.ofString, ofByteArray, ofFile, and ofInputStream supply bodies; set Content-Type explicitly.
HttpRequest get = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.header("Accept", "application/json")
.GET()
.build();
String json = "{\"name\":\"Ada\"}";
HttpRequest post = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.timeout(Duration.ofSeconds(30))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse — BodyHandlers
client.send(request, responseBodyHandler) pairs the response with a typed body.
Common handlers: BodyHandlers.ofString(), ofByteArray(), ofFile(path), ofInputStream() (stream must be consumed or closed),
ofLines() for line-delimited text, discarding() when you only care about status headers.
HttpResponse<String> response = client.send(get, HttpResponse.BodyHandlers.ofString());
int status = response.statusCode();
HttpHeaders headers = response.headers();
String body = response.body();
// Stream large download — consumer must read and close
HttpResponse<InputStream> fileResp = client.send(get,
HttpResponse.BodyHandlers.ofInputStream());
try (InputStream in = fileResp.body()) {
in.transferTo(Files.newOutputStream(Path.of("out.bin")));
}
Check statusCode() and handle 4xx/5xx—HttpClient does not throw on HTTP error status by default (unlike some REST clients).
Custom types use HttpResponse.BodyHandlers.mapping(handler, mapper)—e.g. map JSON string to a record with Jackson in the mapper after ofString().
HttpRequest upload = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/upload"))
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofFile(Path.of("report.pdf")))
.build();
Synchronous vs asynchronous — sendAsync and CompletableFuture
send blocks the calling thread until the response is complete—fine for CLI tools and request-per-thread servers.
sendAsync returns CompletableFuture<HttpResponse<T>>—compose non-blocking pipelines, fan out parallel requests, or bridge to reactive stacks.
Uses the client’s executor; avoid blocking inside thenApply on a small pool.
CompletableFuture<HttpResponse<String>> future =
client.sendAsync(get, HttpResponse.BodyHandlers.ofString());
HttpResponse<String> response = future.join(); // or get with timeout
// Parallel fetches
List<CompletableFuture<HttpResponse<String>>> futures = ids.stream()
.map(id -> client.sendAsync(
HttpRequest.newBuilder(URI.create(base + "/items/" + id)).build(),
HttpResponse.BodyHandlers.ofString()))
.toList();
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).join();
See Concurrency: CompletableFuture for composition, exception handling, and executor choice.
WebSocket support
The same HttpClient exposes newWebSocketBuilder() for RFC 6455 handshakes.
Provide a WebSocket.Listener for open, text/binary messages, ping/pong, error, and close.
Use wss:// in production; handle backpressure—sendText returns a completion stage.
WebSocket.Listener listener = new WebSocket.Listener() {
@Override
public void onOpen(WebSocket webSocket) {
webSocket.sendText("subscribe:metrics", true);
WebSocket.Listener.super.onOpen(webSocket);
}
@Override
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
process(data.toString());
return WebSocket.Listener.super.onText(webSocket, data, last);
}
};
WebSocket ws = client.newWebSocketBuilder()
.header("Authorization", "Bearer " + token)
.buildAsync(URI.create("wss://stream.example.com/v1"), listener)
.join();
Reuse one HttpClient instance—construction is relatively expensive and pooling is per client. For Spring Boot 3+, RestClient and WebClient wrap similar ideas; know the JDK API underneath.
URL & URI
Addresses and resource identifiers appear throughout HTTP configuration, redirects, and classpath loading—use the right type and avoid the legacy connection API.
URI vs URL
URI (java.net.URI) is the general identifier: scheme, authority, path, query, fragment—parsing and resolution without requiring network access.
Immutable and strict about encoding; preferred for HttpRequest and configuration strings.
URL (java.net.URL) is a legacy combination of identifier and legacy connection machinery—can open streams via handlers.
URLs are tied to stream handler protocols registered on the JVM; parsing is lenient compared to URI.
Convert with uri.toURL() only when an older API requires URL; prefer URI at boundaries.
| Concern | URI | URL |
|---|---|---|
| Immutability | Immutable | Mutable (hashCode cache quirk) |
| HttpClient | Native | Convert first |
| Equality | Structure-based | Opens connection semantics historically |
URI base = URI.create("https://api.example.com/v1/");
URI item = base.resolve("users/42"); // https://api.example.com/v1/users/42
// Encode query values — never concatenate raw user input
String q = URLEncoder.encode("java networking", StandardCharsets.UTF_8);
URI withQuery = URI.create("https://api.example.com/search?q=" + q + "&page=2");
// Prefer URI for HttpClient
HttpRequest req = HttpRequest.newBuilder(item).GET().build();
URLConnection — legacy, why to avoid
URL.openConnection() returns HttpURLConnection (or HTTPS variant)—blocking, mutable, easy to misuse:
must call setRequestMethod, handle redirects manually, read error streams on 4xx, and tune disconnect flags.
No HTTP/2, awkward timeout API, and inconsistent behavior across JDK versions compared to HttpClient.
Third-party stacks (Apache HttpClient 5, OkHttp) remain valid; for JDK-only dependencies, standardize on HttpClient since Java 11.
// Legacy — avoid for new code
URL url = URI.create("https://api.example.com/data").toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(10_000);
int code = conn.getResponseCode();
// read conn.getInputStream() or getErrorStream()
// Modern replacement
HttpResponse<String> resp = HttpClient.newHttpClient()
.send(HttpRequest.newBuilder(URI.create("https://api.example.com/data")).build(),
HttpResponse.BodyHandlers.ofString());
Building query strings with string concatenation breaks encoding—use URI builders, libraries, or URLEncoder.encode for form bodies vs path segments (different rules).
Contrast TCP vs UDP. Sketch client-server flow with ServerSocket.accept. Name why HttpClient supersedes HttpURLConnection and how sendAsync fits CompletableFuture composition.