Skip to main content

    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.

    LevelAPIProtocolUse Case
    Low-level, reliableSocket / ServerSocketTCPCustom protocols, chat servers
    Low-level, fastDatagramSocketUDPGaming, voice, video streaming
    High-levelHttpClientHTTP/1.1, HTTP/2REST 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.

    TCP echo server & client (request/response trace)
    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) { }
        }
    }
    Output
    Server listening on port 8080
    Client -> Server: Hello Server!
    Server -> Client: Echo: Hello Server!
    Client -> Server: Goodbye!
    Server -> Client: Echo: Goodbye!
    This opens a real 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.

    UDP sender & receiver
    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();
        }
    }
    Output
    Sender sent: ping
    Receiver got: ping
    This binds a real UDP port on the loopback interface, which most online sandboxes block. Run it locally. The two print lines can appear in either order — UDP gives no timing guarantees.

    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.

    HttpClient GET + JSON POST (with headers)
    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());
            }
        }
    }
    Output
    GET  -> status 200
    POST -> status 200
    Body starts: {
      "args": {},
      "headers": {
        "Acc
    This makes real HTTP requests to httpbin.org, so it needs internet access and a JDK 11+. The status codes and body reflect the live response — the body slice shown is representative. Run it in your own editor.

    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:

    • URIidentifies 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.

    Parallel requests with sendAsync
    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());
            }
        }
    }
    Output
    https://httpbin.org/get -> 200
    https://httpbin.org/headers -> 200
    https://httpbin.org/ip -> 200
    This fires real parallel requests against httpbin.org, so timings depend on the live network and a JDK 11+. The three lines may print in any order. Run it in your own editor with internet access.

    🎯 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.

    Your Turn #1 — finish the HttpClient GET
    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
        }
    }
    Replace each ___, 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.

    Your Turn #2 — connect and read one line
    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!
        }
    }
    Fill the two blanks, then run it locally (sockets are usually blocked in online sandboxes). You should see 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.

    Mini-Challenge — POST a JSON body and report the result
    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
        }
    }
    Write the body of 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() 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 automaticHttpClient 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

    TaskCodeNotes
    TCP servernew ServerSocket(port)accept() blocks for a client
    TCP clientnew Socket(host, port)read/write via streams
    UDP socketnew DatagramSocket(port)send/receive DatagramPacket
    HTTP clientHttpClient.newHttpClient()create once, reuse
    GET (sync)client.send(req, ofString())blocks for the response
    Asyncclient.sendAsync(req, h)returns CompletableFuture
    POST JSONBodyPublishers.ofString(json)+ Content-Type header
    Add header.header("Accept", "...")on the request builder
    Status / bodyres.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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service

    Install LearnCodingFast

    Learn faster with the app on your home screen.