Java

Networking Basics in Java: Sockets and HTTP Clients

Executive Summary

Networking basics in Java start with the TCP socket, a bidirectional byte stream between two endpoints. Java models it exactly as the IO article taught. ServerSocket.accept blocks until a client connects, Socket gives you an InputStream and an OutputStream, and try-with-resources closes both ends. However, the blocking model means one connected client occupies one thread. That is why socket servers scale through the concurrency tools of Part 5 and the NIO selectors of the non-blocking article.

The framing rule is the pitfall that bites everyone. TCP delivers a byte stream with no message boundaries, and read may return any prefix of what was sent. As a result, protocols need explicit framing, a length prefix or a delimiter. At the high layer sits the java.net.http HttpClient: one reusable, immutable, builder-configured client. It offers synchronous send and CompletableFuture-returning sendAsync, per-request timeouts, status codes, headers, and TLS handled for you per RFC 9110 semantics. In practice, choose HTTP for service-to-service communication where you want a defined protocol with semantics everyone shares. Choose raw sockets when you control both ends and need a custom, minimal protocol, and inherit the framing and protocol design burden knowingly.

Networking Basics in Java: The Layered Picture

Every networked Java program sits on the same stack. Knowing which layer you are programming decides which class you reach for:

  your application          "transfer this book record", "GET /books"
       |
  HTTP  (HttpClient)         methods, headers, status codes, TLS
       |                     a defined protocol: RFC 9110 semantics
  TCP   (Socket)             a reliable, ordered, bidirectional byte stream
       |                     connections between endpoints (host, port)
  IP + the network           packets, routing, best effort
Layer Java class It gives you It leaves to you
Application protocol your code The semantics: requests, responses, meaning Everything: format, errors, versioning
HTTP java.net.http.HttpClient Methods, headers, status codes, chunking, TLS REST conventions, payloads, error design (Part 9)
TCP Socket, ServerSocket Connections, ordered reliable bytes, ports Framing, request/response structure, concurrency

In short, the trade is consistency for control: each layer down removes guarantees and adds freedom. Most working code lives at the HTTP layer and never sees a socket. However, the layers below explain the failure modes, timeouts, partial data, connection resets, that surface anyway.

TCP Sockets: ServerSocket and Socket

A socket server binds a port, accepts connections one at a time, and speaks streams. The complete echo server and client, small enough to run in two terminals:

// EchoServer.java
import java.io.*;
import java.net.*;
import java.util.*;

public class EchoServer {
    public static void main(String[] args) throws IOException {
        try (var server = new ServerSocket(8080)) {
            System.out.println("listening on 8080");
            while (true) {
                try (var socket = server.accept();                       // blocks until connect
                     var in   = new BufferedReader(
                                 new InputStreamReader(socket.getInputStream()));
                     var out  = new PrintWriter(socket.getOutputStream(), true)) {

                    String line;
                    while ((line = in.readLine()) != null) {             // null = peer closed
                        out.println("echo: " + line);                     // newline = our frame
                    }
                }
            }
        }
    }
}
// EchoClient.java
import java.io.*;
import java.net.*;

public class EchoClient {
    public static void main(String[] args) throws IOException {
        try (var socket = new Socket("localhost", 8080);                 // connect: host, port
             var in   = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             var out  = new PrintWriter(socket.getOutputStream(), true)) {

            out.println("hello");
            System.out.println(in.readLine());                          // "echo: hello"
        }
    }
}

Now read the structure against what you already know. First, accept blocks until a client arrives, so this server serves one client at a time. Meanwhile, a second client waits in the kernel’s connection backlog. The streams are the IO article’s streams and readers, wrapped the same way, closed by the try-with-resources you have used since the file tools. Finally, readLine returning null is EOF: the peer closed the connection. Treating that as a normal loop exit rather than an error is the first protocol decision every socket program makes.

The Framing Pitfall: TCP Has No Message Boundaries

Here is the bug that has bitten every socket programmer, usually within a week of the first server. TCP gives you a byte stream, not a sequence of messages. As a result, a single read can return any prefix of what the other side sent:

// WRONG: assuming one read returns one whole message
byte[] buffer = new byte[1024];
int n = input.read(buffer);                 // may return a PART of the message
String message = new String(buffer, 0, n);  // TCP never promised the whole thing

// RIGHT: the protocol defines the frame, and you read the frame
// length-prefix framing: 4-byte big-endian length, then exactly that many bytes
var length = readFully(input, 4);
int size = ((length[0] & 0xFF) << 24) | ((length[1] & 0xFF) << 16)
         | ((length[2] & 0xFF) << 8)  |  (length[3] & 0xFF);
byte[] body = readFully(input, size);       // loop until every byte arrived

The delta: the wrong version trusts the network to respect intent. By contrast, the right version defines where a message ends and enforces it in code. Text protocols use a delimiter instead, the echo server’s newline, and readLine exists precisely because that convention is common. However, what you cannot do is skip the decision: no framing, no protocol, just bytes that mean different things on different days. This pitfall, plus the one-thread-per-client blocking model, is why the next steps exist. Part 5’s thread pools and the NIO article’s non-blocking channels are how production servers handle many clients at once.

The Modern HTTP Client: java.net.http

HTTP is the protocol the world already speaks, and since Java 11 the JDK ships a client worthy of it. HttpURLConnection, the pre-2018 API, is officially legacy: usable, but verbose, awkward to test, and effectively frozen. By contrast, the new HttpClient is immutable, reusable, and builder-configured, with synchronous and asynchronous modes in one class per the HttpClient API:

import java.net.URI;
import java.net.http.*;
import java.time.Duration;

var client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(5))
        .build();                                        // create once, reuse everywhere

var request = HttpRequest.newBuilder(URI.create("https://httpbin.org/get"))
        .timeout(Duration.ofSeconds(10))                  // per-request, not per-client
        .header("Accept", "application/json")
        .build();

// synchronous: blocks this thread until the response arrives
HttpResponse<String> response =
        client.send(request, HttpResponse.BodyHandlers.ofString());

if (response.statusCode() == 200) {
    System.out.println(response.body());
}

// asynchronous: returns a CompletableFuture immediately, non-blocking
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
      .thenApply(HttpResponse::body)
      .thenAccept(body -> System.out.println("async: " + body))
      .exceptionally(ex -> { System.err.println("failed: " + ex.getMessage()); return null; });

In particular, three design choices deserve attention. First, the client is created once and shared. It owns a connection pool internally, so a client per request wastes connections. That is the first performance mistake HTTP client code makes. Second, timeouts are explicit at two levels, connect and request, because the network’s default failure mode is silence. Without a timeout, a dead peer hangs your thread forever. Third, sendAsync returns the CompletableFuture you will meet properly in Part 5. You can already read the pattern, a running operation that will deliver a value later, with composition and error handling built in.

Sockets or HTTP: The Decision

Reach for When What you inherit
HttpClient Service-to-service calls, public APIs, anything where a defined protocol, TLS, proxies, and status semantics help The protocol’s overhead: headers, text framing, connection management, all handled by the client and RFC 9110
ServerSocket and Socket Custom protocols where you control both ends: databases, brokers, game servers, telemetry feeds Every protocol decision: framing, errors, versioning, concurrency, TLS if needed
A framework on top of HTTP Production services with REST contracts (Part 9’s stack) Framework conventions, plus the HTTP layer underneath, which is why this article’s model still applies

The pattern in real infrastructure: the clients around you are sockets all the way down. For example, JDBC drivers speak a binary TCP protocol to the database, Redis and message broker clients handshake over raw TCP, and game servers hand-roll protocols for latency. Even so, HTTP dominates between services precisely because its costs, more bytes and more round trips, buy semantics. You get methods that mean something, status codes, headers, a whole industry of proxies, caches, and load balancers that understand the conversation.

How Real Systems Do This

Production networking is overwhelmingly HTTP for service-to-service traffic, HTTP/2 by default in the JDK client where servers support it. Meanwhile, sockets survive wherever latency or custom semantics justify the protocol ownership: databases, brokers, caches, games, and internal high-volume pipelines. The Socket API itself has been stable for decades. That tells you the fundamentals in this article have not moved: the blocking stream model, the framing question, the timeout discipline.

My framing war story is a telemetry feed that worked perfectly in test and corrupted in production. Two services exchanged length-prefixed records over a raw socket, and the reading code called read once per record and moved on. On localhost the whole record always arrived in one packet, so tests never saw the flaw. Across a real network, however, under load, TCP delivered records in pieces. The parser consumed bytes mid-record, then “resynchronized” onto garbage, silently corrupting stream positions for hours. The bug was not in Java, whose read contract documents partial returns clearly. Instead, it was in an assumption the test environment made true by accident. The fix was a readFully loop with a deadline, the same shape as this article’s RIGHT block. Since then, I treat “works on localhost” as a hypothesis, not evidence.

Decision Framework

  1. Does the other side already speak HTTP? Use HttpClient and inherit the protocol’s semantics: status codes, headers, TLS, proxies.
  2. Are you building a service others will call? It will speak HTTP; plan for Part 9’s server stack rather than hand-rolling sockets.
  3. Do you control both ends and need minimal latency or a compact binary protocol? Raw sockets, with framing designed before the first message is sent.
  4. Is a socket in play? Set connect and read timeouts explicitly; the network’s default failure mode is silence, and a missing timeout converts silence into a hung thread.
  5. Are multiple clients connecting at once? Decide the concurrency model deliberately, thread per connection for now, Part 5’s pools and virtual threads next.
  6. Are you writing new client code against HttpURLConnection? Stop: it is legacy, and java.net.http is already in your JDK.

When NOT to Use This

  • Do not hand-roll an HTTP server from sockets. The protocol has enough edge cases, such as chunking, keep-alive, and status semantics, that Part 9’s framework exists to own them.
  • Do not choose raw sockets because they look lighter. In reality, you are volunteering for framing, versioning, error handling, and TLS, a full protocol design your HTTP alternative gets for free.
  • Do not create an HttpClient per request: it owns connection pools and configuration, both wasted by per-request construction.
  • Do not use sockets for cross-organization integration. Parties who do not share a codebase need a shared protocol, which is the exact situation HTTP was standardized for.

Common Mistakes

  • Assuming one read returns one message: TCP has no message boundaries, and read may return any prefix; frame explicitly or loop to a boundary.
  • Skipping timeouts: connect, read, request. Every missing timeout is a thread that can hang forever on a silent peer.
  • One thread per socket with no limit: the echo server’s model is correct for learning but a denial-of-service vector in production. Each connection pins a thread until Part 5 fixes that.
  • Ignoring EOF: readLine returning null or read returning -1 means the peer closed. Treating it as an exception instead of a protocol event produces noisy logs and missed shutdown paths.
  • Not closing: sockets are scarce OS resources; a missing try-with-resources leaks file descriptors until accepts start failing.
  • Testing only on localhost: localhost makes partial reads, latency, and reordering invisible. That is the exact gap the framing pitfall lives in.

Key Takeaways

  • TCP sockets are byte streams: ServerSocket.accept blocks for a connection, Socket gives you the IO article’s streams, and try-with-resources closes both ends.
  • TCP has no message boundaries: a single read may return part of a message. Therefore, protocols need framing, a length prefix or a delimiter, enforced in code.
  • The blocking model means one connected client occupies one thread. Meanwhile, Part 5’s concurrency and the NIO article’s non-blocking channels are the production answers.
  • java.net.http.HttpClient replaces HttpURLConnection: one immutable reusable client, builder configuration, explicit timeouts, and synchronous or CompletableFuture-based asynchronous calls.
  • HTTP’s overhead buys semantics: methods, status codes, headers, TLS, and an ecosystem of proxies and load balancers that understand the protocol.
  • Raw sockets survive where you control both ends and need a custom protocol: databases, brokers, games, and telemetry. They come with the protocol design burden owned knowingly.
  • Timeouts at every level are not optional: the network’s default failure mode is silence, and every missing timeout is a future hang.

FAQ

How do I create a server socket in Java?

new ServerSocket(8080) binds the port. Then accept blocks until a client connects and returns a Socket whose streams you wrap and use. Finally, close the socket with try-with-resources when the conversation ends.

What is the Java HttpClient?

java.net.http.HttpClient, added in Java 11, is an immutable, reusable HTTP client with builder configuration and explicit timeouts. It offers synchronous send and CompletableFuture-returning sendAsync, and support for HTTP/2 where available.

Should I still use HttpURLConnection?

No, not in new code. It works and remains in the JDK. However, it is legacy: verbose, hard to test, and effectively frozen since the java.net.http client replaced it in Java 11.

What is TCP framing in Java?

It means defining where one message ends and the next begins over TCP’s boundary-free byte stream. Typically that is a length prefix, a 4-byte size before the body, or a delimiter like a newline. You then read exactly that frame in a loop.

Is the Java HttpClient asynchronous?

Both. send blocks the calling thread until the response arrives. By contrast, sendAsync returns immediately with a CompletableFuture that completes with the response, enabling the composition and error handling Part 5 covers.

Conclusion

You now hold the networking basics in Java at both layers. First come the socket fundamentals: blocking accepts, stream reads, framing, and EOF. Then comes the HTTP client that carries modern traffic with timeouts, status codes, and an asynchronous mode that previews Part 5. The layers below explain the failure modes you will debug even when working entirely at the HTTP layer.

Next comes a consolidation pause before concurrency: the modern Java language features, var, text blocks, and pattern matching. The code in the remaining 36 articles uses them constantly, and they deserve their own focused tour.

Frame your messages, time your reads, and close your sockets. The network is reliable only up to the protocol you design above it.

Last updated on 1 September 2026.

Share this article

Leave a Reply

Your email address will not be published. Required fields are marked *