Lesson 44 • Advanced
Networking & HTTP
After this lesson you'll be able to open raw TCP/UDP sockets, talk to REST APIs with the modern HttpClient (sync and async, GET and POST), and handle the timeouts and errors that real networks throw at you.
Before You Start
You should know IO & NIO (sockets read and write streams), CompletableFuture (used for async HTTP), and JSON processing (for parsing API responses). A little background on what HTTP is — requests, responses, status codes — also helps.
What You'll Learn in This Lesson
- ✓Build a TCP client and server with Socket and ServerSocket
- ✓Send connectionless UDP packets with DatagramSocket
- ✓Make REST calls with the Java 11+ HttpClient (GET and POST)
- ✓Add headers and JSON bodies, and read status codes
- ✓Fire requests in parallel with sendAsync and CompletableFuture
- ✓Set timeouts, close sockets, and handle network errors safely
1️⃣ The Networking Mental Model
Networking just means two programs sending bytes to each other across a connection. Java gives you tools at different levels — pick the lowest one that does the job, and the highest one that's still convenient.
💡 Analogy: Think of networking like a phone system. A ServerSocket is a receptionist sitting by a phone (a port), waiting for calls. A Socket is the live phone line once a call connects — both sides can talk. DatagramSocket (UDP) is more like dropping a postcard in the mailbox: you send it and hope it arrives, with no call and no confirmation. And HttpClient is a smart assistant who already speaks HTTP — you just say "GET me this page" and it dials, talks, and hands you the answer.
| Level | API | Protocol | Use Case |
|---|---|---|---|
| Low-level, reliable | Socket / ServerSocket | TCP | Custom protocols, chat servers |
| Low-level, fast | DatagramSocket | UDP | Gaming, voice, video streaming |
| High-level | HttpClient | HTTP/1.1, HTTP/2 | REST APIs, downloading pages |
A port is just a numbered door on a machine (0–65535). A server "listens" on a port; a client connects to host:port. localhost (address 127.0.0.1) means "this same machine".
2️⃣ TCP Sockets — A Reliable Connection
TCP (Transmission Control Protocol) gives you a reliable, ordered pipe of bytes: everything you send arrives, in order, or you get an error. The server creates a ServerSocket and calls accept(), which blocks until a client connects. The client creates a Socket pointed at host:port. After that, both sides read and write through ordinary streams — exactly the BufferedReader / PrintWriter from the IO lesson.
The worked example below runs a tiny "echo" server (it replies with whatever you send) and a client, in one program. Notice try-with-resources on every socket and stream — that's how you avoid leaks. Read the inline comments and the request/response trace in the output panel.
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws Exception {
int port = 8080;
// ServerSocket "listens" on a port — like a receptionist waiting for calls.
ExecutorService pool = Executors.newCachedThreadPool();
ServerSocket server = new ServerSocket(port);
pool.submit(() -> {
try {
while (!server.isClosed()) {
Socket client = server.accept(); // blocks until a client connects
pool.submit(() -> handleClient(client));
}
} catch (IOException ignored) { /* server was closed */ }
});
System.out.println("Server listening on port " + port);
// A Socket is the phone line connecting client to server.
// try-with-resources closes the socket + streams automatically.
try (Socket socket = new Socket("localhost", port);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
for (String msg : new String[]{"Hello Server!", "Goodbye!"}) {
out.println(msg); // send a line
System.out.println("Client -> Server: " + msg);
System.out.println("Server -> Client: " + in.readLine()); // read reply
}
}
server.close(); // free the port
pool.shutdownNow(); // stop the background threads
}
// Each connected client gets its own thread; we echo its lines back.
static void handleClient(Socket client) {
try (client;
BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream(), true)) {
String line;
while ((line = in.readLine()) != null) {
out.println("Echo: " + line); // reply to the client
}
} catch (IOException ignored) { }
}
}Server listening on port 8080
Client -> Server: Hello Server!
Server -> Client: Echo: Hello Server!
Client -> Server: Goodbye!
Server -> Client: Echo: Goodbye!ServerSocket and connects over the loopback interface, so it needs a local JDK allowed to bind a port — most online sandboxes block sockets. Run it in your own editor or terminal to see the echo conversation.3️⃣ UDP — Fire and Forget with DatagramSocket
UDP (User Datagram Protocol) has no connection and no delivery guarantee. You wrap your bytes in a DatagramPacket addressed to a host and port, and send it through a DatagramSocket. The packet might arrive, arrive twice, or vanish — UDP won't tell you. In exchange you get very low overhead, which is why live games, voice, and video use it: a dropped frame matters less than a delayed one.
The receiver binds a DatagramSocket to a port and calls receive() (which blocks for one packet). The sender just builds a packet and calls send() — no handshake.
import java.net.*;
import java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) throws Exception {
int port = 9000;
// UDP is "fire and forget" — no connection, no delivery guarantee.
// Receiver: bind a DatagramSocket to a port and wait for a packet.
DatagramSocket receiver = new DatagramSocket(port);
new Thread(() -> {
try {
byte[] buf = new byte[1024]; // inbox buffer
DatagramPacket packet = new DatagramPacket(buf, buf.length);
receiver.receive(packet); // blocks for one packet
String text = new String(
packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
System.out.println("Receiver got: " + text);
} catch (Exception ignored) { }
}).start();
// Sender: wrap bytes in a packet addressed to host:port and send.
try (DatagramSocket sender = new DatagramSocket()) {
byte[] data = "ping".getBytes(StandardCharsets.UTF_8);
InetAddress host = InetAddress.getByName("localhost");
DatagramPacket packet = new DatagramPacket(data, data.length, host, port);
sender.send(packet); // no handshake, just send
System.out.println("Sender sent: ping");
}
Thread.sleep(200); // give the receiver a moment
receiver.close();
}
}Sender sent: ping
Receiver got: ping4️⃣ HttpClient — Modern REST Calls
Most of the time you're not inventing a protocol — you're talking to a web API over HTTP. Java 11 introduced java.net.http.HttpClient, a fluent, modern API that replaces the clunky old HttpURLConnection. It speaks HTTP/2, supports async calls, and pools connections for you.
HttpClient.newHttpClient() — create a client (build once, reuse everywhere)
HttpRequest.newBuilder().uri(...).GET().build() — describe the request
client.send(req, BodyHandlers.ofString()) — send it and block for the response
res.statusCode() / res.body() — read the result (check status first!)
A BodyHandler decides what to turn the response into — ofString() for text/JSON, ofFile(path) to stream straight to disk. To POST data, attach a BodyPublisher like BodyPublishers.ofString(json). The worked example below does a GET with an Accept header and a JSON POST, then prints both status codes and a slice of the response body.
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
public class Main {
public static void main(String[] args) throws Exception {
// Reuse ONE client — it manages a connection pool internally.
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10)) // cap time to open a connection
.build();
// --- GET with a header ---
HttpRequest getReq = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get"))
.header("Accept", "application/json") // ask for JSON back
.timeout(Duration.ofSeconds(30)) // cap the whole request
.GET()
.build();
HttpResponse<String> getRes = client.send(
getReq, HttpResponse.BodyHandlers.ofString());
System.out.println("GET -> status " + getRes.statusCode());
// --- POST a JSON body ---
HttpRequest postReq = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/post"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"Charlie\"}"))
.build();
HttpResponse<String> postRes = client.send(
postReq, HttpResponse.BodyHandlers.ofString());
System.out.println("POST -> status " + postRes.statusCode());
// ALWAYS check the status before trusting the body. 2xx = success.
if (getRes.statusCode() == 200) {
String body = getRes.body();
System.out.println("Body starts: "
+ body.substring(0, Math.min(40, body.length())));
} else {
System.out.println("Request failed: " + getRes.statusCode());
}
}
}GET -> status 200
POST -> status 200
Body starts: {
"args": {},
"headers": {
"Acc5️⃣ URI vs URL — Addressing a Resource
You pass HttpClient a URI, not a URL. The difference trips people up, so here it is in one line each:
- URI — identifies a resource. It parses the string into scheme / host / path / query but does no network work. Create one with
URI.create("https://api.example.com/users?id=5"). - URL — a URI that also knows how to open a connection. You rarely need it directly anymore;
HttpClienthandles connecting.
URI uri = URI.create("https://api.example.com/users?id=5");
uri.getHost(); // "api.example.com"
uri.getPath(); // "/users"
uri.getQuery(); // "id=5"URI for HttpClient. Reach for URL only when an old API specifically demands one.6️⃣ Async Requests — Don't Block the Thread
client.send(...) blocks the calling thread until the response comes back. If you have ten URLs to fetch, doing them one after another is slow. client.sendAsync(...) returns immediately with a CompletableFuture, so you can launch many requests at once and they run in parallel.
You chain .thenApply(...) to transform each response, then use CompletableFuture.allOf(...) to wait for every request to finish. The worked example fires three requests together and prints each status code.
import java.net.URI;
import java.net.http.*;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class Main {
static final HttpClient client = HttpClient.newHttpClient();
public static void main(String[] args) throws Exception {
// sendAsync returns immediately with a CompletableFuture — the main
// thread is NOT blocked, so all three requests run in parallel.
List<String> urls = List.of(
"https://httpbin.org/get",
"https://httpbin.org/headers",
"https://httpbin.org/ip");
List<CompletableFuture<Integer>> futures = urls.stream()
.map(u -> HttpRequest.newBuilder(URI.create(u)).build())
.map(req -> client
.sendAsync(req, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::statusCode)) // map response -> status
.toList();
// Wait for every future to finish, then read each status code.
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
for (int i = 0; i < urls.size(); i++) {
System.out.println(urls.get(i) + " -> " + futures.get(i).join());
}
}
}https://httpbin.org/get -> 200
https://httpbin.org/headers -> 200
https://httpbin.org/ip -> 200🎯 Your Turn #1 — Complete the GET request
Fill in the three ___ blanks to build a request, send it, and print the status code. The expected output is in the comments so you can check yourself.
import java.net.URI;
import java.net.http.*;
public class Main {
public static void main(String[] args) throws Exception {
// 🎯 YOUR TURN — finish this GET request (fill in the ___ blanks)
HttpClient client = HttpClient.newHttpClient();
// 1) Build a request to https://httpbin.org/get
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(___)) // 👉 put the URL string in here
.GET()
.build();
// 2) Send it synchronously and capture the response as a String
HttpResponse<String> res = client.send(
req, HttpResponse.BodyHandlers.___()); // 👉 use ofString
// 3) Print the status code (a number like 200)
System.out.println("Status: " + res.___()); // 👉 the method is statusCode
// ✅ Expected output:
// Status: 200
}
}___, then run it on a JDK 11+ with internet access. You should see Status: 200.🎯 Your Turn #2 — Read a line from a socket
The server is written for you. Fill in the port and the method that reads one line from the socket's input stream.
import java.io.*;
import java.net.*;
public class Main {
public static void main(String[] args) throws Exception {
int port = 7000;
// A tiny server thread that sends one greeting, then closes.
new Thread(() -> {
try (ServerSocket server = new ServerSocket(port);
Socket client = server.accept();
PrintWriter out = new PrintWriter(client.getOutputStream(), true)) {
out.println("Welcome!");
} catch (IOException ignored) { }
}).start();
Thread.sleep(100); // let the server start listening
// 🎯 YOUR TURN — connect and read the one line the server sends
// 1) Open a Socket to "localhost" on the port above
try (Socket socket = new Socket("localhost", ___); // 👉 which port?
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
// 2) Read a single line from the server and print it
String line = in.___(); // 👉 the method that reads one line
System.out.println("Server said: " + line);
}
// Note: try-with-resources closes the socket for you — no leak.
// ✅ Expected output:
// Server said: Welcome!
}
}Server said: Welcome!.🏆 Mini-Challenge — POST JSON (no scaffolding)
Now write it yourself. The starter has only a comment outline — no filled-in logic. Follow the steps and match the expected output.
import java.net.URI;
import java.net.http.*;
public class Main {
public static void main(String[] args) throws Exception {
// 🎯 MINI-CHALLENGE: POST JSON and report success/failure
// 1. Create ONE HttpClient (reuse it)
// 2. Build a POST request to https://httpbin.org/post
// - set header "Content-Type" to "application/json"
// - body: BodyPublishers.ofString("{\"city\":\"London\"}")
// 3. Send it synchronously (BodyHandlers.ofString)
// 4. If statusCode() is 200, print "Saved!" — otherwise print
// "Failed: " followed by the status code
//
// ✅ Expected output (httpbin returns 200):
// Saved!
// your code here
}
}main from the outline, then run it on a JDK 11+ with internet access. httpbin returns 200, so you should see Saved!.Common Errors (and the Fix)
- ❌ No timeout — program hangs forever: a slow or dead server makes
send()orreadLine()block with no end. Fix: set.connectTimeout(Duration.ofSeconds(10))on the client and.timeout(...)on the request; for raw sockets usesocket.setSoTimeout(ms)so reads throwSocketTimeoutExceptioninstead of freezing. - ❌ Not closing sockets —
Too many open files: leakedSocket/ServerSocketobjects exhaust the OS file descriptors. Fix: wrap every socket and stream in try-with-resources so they close even when an exception is thrown. - ❌ Blocking the main thread: a UI or server freezes because it's stuck in a synchronous
send(). Fix: usesendAsync(...)(returns aCompletableFuture) or run the call on a background thread. - ❌ Ignoring the status code: parsing
res.body()as JSON when the server returned a404or500error page gives a confusing crash. Fix: always checkres.statusCode()is in the 2xx range before trusting the body. - ❌
ConnectException: Connection refused: nothing is listening on that host/port. Fix: confirm the server is running and you used the right port — and that the server started before the client connected.
Pro Tips
- 💡 Reuse one HttpClient — it pools connections internally. Create it once, share it everywhere; making a new one per request is wasteful.
- 💡 HTTP/2 is automatic —
HttpClientnegotiates HTTP/2 with servers that support it, falling back to HTTP/1.1. - 💡 Stream big downloads to disk — use
BodyHandlers.ofFile(path)instead ofofString()to avoid loading a huge file into memory. - 💡 Retry transient failures with backoff — on a 5xx or timeout, wait 1s, then 2s, then 4s before retrying, so you don't hammer a struggling server.
📋 Quick Reference
| Task | Code | Notes |
|---|---|---|
| TCP server | new ServerSocket(port) | accept() blocks for a client |
| TCP client | new Socket(host, port) | read/write via streams |
| UDP socket | new DatagramSocket(port) | send/receive DatagramPacket |
| HTTP client | HttpClient.newHttpClient() | create once, reuse |
| GET (sync) | client.send(req, ofString()) | blocks for the response |
| Async | client.sendAsync(req, h) | returns CompletableFuture |
| POST JSON | BodyPublishers.ofString(json) | + Content-Type header |
| Add header | .header("Accept", "...") | on the request builder |
| Status / body | res.statusCode() / res.body() | check status first |
| Timeout | .timeout(Duration.ofSeconds(30)) | avoids hanging forever |
❓ Frequently Asked Questions
What is the difference between TCP and UDP?
TCP (Socket / ServerSocket) opens a reliable, ordered connection — every byte arrives, in order, or you get an error. UDP (DatagramSocket) is connectionless 'fire and forget': packets may arrive out of order, duplicated, or not at all, but it has far less overhead. Use TCP for web, files, and chat; use UDP for live gaming, voice, and video where a dropped packet matters less than low latency.
Should I still use URL/URLConnection or the new HttpClient?
Use java.net.http.HttpClient (Java 11+). It is the modern, fluent replacement for the old HttpURLConnection: it supports HTTP/2, async requests via CompletableFuture, timeouts, and connection pooling out of the box. The older URL/URLConnection API still works and is fine for a quick one-off read, but HttpClient is the recommended default for new code.
What is the difference between URI and URL in Java?
A URI is an identifier — it just parses and represents the string (scheme, host, path, query) without doing any network work. A URL additionally knows how to open a connection. With HttpClient you pass a URI via URI.create("https://..."); the client handles the connection, so you rarely touch URL directly anymore.
Why does my socket program hang forever?
Blocking calls like accept(), readLine(), and HttpClient.send() wait until data arrives. If the other side never responds and you set no timeout, your thread blocks indefinitely. Always set connectTimeout on the client and timeout on the request, and consider setSoTimeout on a raw Socket so a stalled peer eventually throws instead of freezing.
Do I need to close sockets and HttpClient?
Always close Socket, ServerSocket, and DatagramSocket — leaking them exhausts the operating system's file descriptors. The cleanest way is try-with-resources, which closes them automatically even when an exception is thrown. HttpClient is different: it is meant to be created once and reused; in Java 11–20 you do not close it explicitly (it has no close() method until Java 21's AutoCloseable support).
🎉 Lesson Complete!
You can now work at every layer of Java networking: reliable TCP with Socket / ServerSocket, fast UDP with DatagramSocket, and clean REST calls with the modern HttpClient — sync and async, GET and POST, with headers and JSON. Just as important, you know to set timeouts, close sockets with try-with-resources, and check status codes before trusting a response.
Next up: Thread Pools — managing concurrent work efficiently with ExecutorService, the engine behind handling many client connections at once.
Sign up for free to track which lessons you've completed and get learning reminders.