Networking
Reviewed & published by Brayan K
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.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
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) { }
}
}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;
import java.util.concurrent.atomic.AtomicReference;
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);
// The receiving thread HANDS BACK what it read instead of printing it
// itself. Two threads printing race each other, so the order of the two
// lines below would be decided by the scheduler rather than by the code
// — and a lesson cannot promise output that a race decides.
AtomicReference<String> received = new AtomicReference<>();
Thread listener = new Thread(() -> {
try {
byte[] buf = new byte[1024]; // inbox buffer
DatagramPacket packet = new DatagramPacket(buf, buf.length);
receiver.receive(packet); // blocks for one packet
received.set(new String(
packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8));
} catch (Exception ignored) { }
});
listener.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");
}
// join() waits for the listener to finish, so the read is complete
// before we print it — no sleep-and-hope, and a fixed line order.
listener.join(2000);
receiver.close();
System.out.println("Receiver got: " + received.get());
}
}4️⃣ 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());
}
}
}5️⃣ 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; HttpClient handles connecting.
URI uri = URI.create("https://api.example.com/users?id=5");
uri.getHost(); // "api.example.com"
uri.getPath(); // "/users"
uri.getQuery(); // "id=5"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());
}
}
}🎯 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
}
}🎯 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!
}
}🏆 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
}
}Common Errors (and the Fix)
- ❌ No timeout — program hangs forever: a slow or dead server makes send() or readLine() block with no end. Fix: set .connectTimeout(Duration.ofSeconds(10)) on the client and .timeout(...) on the request; for raw sockets use socket.setSoTimeout(ms) so reads throw SocketTimeoutException instead of freezing.
- ❌ Not closing sockets — Too many open files: leaked Socket / ServerSocket objects 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: use sendAsync(...) (returns a CompletableFuture) or run the call on a background thread.
- ❌ Ignoring the status code: parsing res.body() as JSON when the server returned a 404 or 500 error page gives a confusing crash. Fix: always check res.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 — HttpClient negotiates HTTP/2 with servers that support it, falling back to HTTP/1.1.
- 💡 Stream big downloads to disk — use BodyHandlers.ofFile(path) instead of ofString() 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 |
🎉 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.
Practice quiz
Which class does a TCP server use to listen for incoming connections?
- Socket
- DatagramSocket
- ServerSocket
- HttpClient
Answer: ServerSocket. A server creates a ServerSocket and calls accept(), which blocks until a client connects.
What is the key difference between TCP and UDP?
- TCP is a reliable, ordered connection; UDP is connectionless 'fire and forget'
- TCP is connectionless; UDP is reliable
- They are identical
- UDP guarantees delivery; TCP does not
Answer: TCP is a reliable, ordered connection; UDP is connectionless 'fire and forget'. TCP (Socket/ServerSocket) guarantees ordered, reliable delivery. UDP (DatagramSocket) is lower overhead but may drop, duplicate, or reorder packets.
Which API does UDP use to send a packet?
- Socket
- ServerSocket
- HttpRequest
- DatagramSocket with DatagramPacket
Answer: DatagramSocket with DatagramPacket. UDP wraps bytes in a DatagramPacket addressed to host:port and sends it through a DatagramSocket — no handshake.
Which Java 11+ class is the modern way to make HTTP/REST calls?
- HttpURLConnection
- HttpClient
- URLConnection
- Socket
Answer: HttpClient. java.net.http.HttpClient is the fluent, modern replacement that supports HTTP/2, async calls, and connection pooling.
How do you read the HTTP status code from an HttpResponse?
- res.statusCode()
- res.code()
- res.status()
- res.getStatus()
Answer: res.statusCode(). res.statusCode() returns the numeric status (e.g. 200). Always check it before trusting res.body().
What does client.sendAsync(...) return?
- An HttpResponse
- void
- A CompletableFuture
- A Socket
Answer: A CompletableFuture. sendAsync returns immediately with a CompletableFuture, so the calling thread is not blocked and requests can run in parallel.
Which BodyHandler turns the response into text or JSON you can read as a String?
- BodyHandlers.ofFile(path)
- BodyHandlers.ofString()
- BodyHandlers.discarding()
- BodyHandlers.ofByteArray()
Answer: BodyHandlers.ofString(). BodyHandlers.ofString() collects the response body into a String. ofFile streams it straight to disk instead.
What do you pass to HttpRequest.newBuilder().uri(...)?
- A URL object
- A plain String only
- A Socket
- A URI, e.g. URI.create("https://...")
Answer: A URI, e.g. URI.create("https://..."). HttpClient takes a URI (URI.create(...)). A URI just identifies the resource; HttpClient handles the connection.
What is the cleanest way to make sure sockets and streams are closed?
- Call System.gc()
- try-with-resources
- Leave them for the garbage collector
- Call finalize()
Answer: try-with-resources. try-with-resources closes Socket, ServerSocket, and streams automatically — even when an exception is thrown.
How do you stop an HttpClient.send() from hanging forever on a dead server?
- Nothing — it cannot hang
- Use a faster CPU
- Set connectTimeout on the client and timeout on the request
- Call close() repeatedly
Answer: Set connectTimeout on the client and timeout on the request. Set .connectTimeout(...) on the client and .timeout(...) on the request so a stalled peer eventually throws instead of freezing.
Continue this course
- Previous: JavaFX for Desktop UI Development
- Next: Working with Threads Pools & Scheduled Executors — Execute tasks concurrently with ScheduledExecutorService and ForkJoinPool
- Quick reference: Java cheat sheet
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).