Network Fundamentals and Java's Built-in Networking
Part 1 of 11 in Mastering Java Network Programming
The physical layer
Network communication starts with the medium. In wired Ethernet, signals travel as electrical pulses through copper — a switch reads voltage levels on the wire and forwards frames to the correct destination MAC address. Wireless (Wi‑Fi) modulates radio waves onto a carrier frequency; an access point translates between wireless frames on the air and wired frames on the backbone.
In both cases the physical layer doesn’t know anything about addresses above MAC — that’s handled by higher layers. The network layer (IP) assigns logical addresses, and the transport layer (TCP/UDP) breaks data into segments with port numbers.
Java abstracts all of this away. You never touch a raw socket fd or bind to an IP address manually — java.net does it in constructors.
TCP sockets: low-level networking
A TCP connection needs two endpoints. In C you’d call socket(), bind(), listen(), accept() — each with its own error handling, flag constants, and struct initialization. Java gives you ServerSocket and Socket as first-class objects.
The demo below shows both sides: a server listening on port 18900 in a background thread, then a client connecting and exchanging two messages before closing.
import java.io.*;
import java.net.*;
public class Demo {
public static void main(String[] args) throws Exception {
Thread serverThread = new Thread(() -> {
try (ServerSocket server = new ServerSocket(18900)) {
System.out.println("[Server] Listening on port 18900");
Socket client = server.accept();
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);
System.out.println("[Server] Received: " + line);
if ("quit".equalsIgnoreCase(line)) break;
}
} catch (IOException e) {
System.err.println("[Server] Error: " + e.getMessage());
}
});
serverThread.start();
Thread.sleep(1000);
System.out.println("\n[Client] Connecting...");
try (Socket socket = new Socket("localhost", 18900);
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
String[] messages = {"Hello from Java", "Sockets are simple"};
for (String msg : messages) {
out.println(msg);
System.out.println("[Client] Server says: " + in.readLine());
}
}
serverThread.join();
System.out.println("\nAll done.");
}
}
The key details: ServerSocket takes a port and starts listening immediately. accept() blocks until a client connects — no polling, no select(). Once connected, getInputStream() and getOutputStream() return plain Stream objects; wrapping them in BufferedReader and PrintWriter adds line-based I/O with zero extra code.
Every message sent over the wire is just bytes — TCP handles segmentation, retransmission, flow control, and ordering transparently. Java’s Socket doesn’t add a protocol layer on top; it’s an OS socket wrapped in an object that manages lifecycle and resource cleanup through try-with-resources.
HTTP client: high-level networking
Raw sockets are fine for custom protocols, but most applications just need to talk HTTP. Before Java 11 the standard way was Apache HttpClient or hand-rolling HttpURLConnection — neither particularly pleasant. Java 11 introduced java.net.http.HttpClient as a modern, fluent API in the standard library.
import java.net.http.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class HttpClientDemo {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
// GET request
var getReq = HttpRequest.newBuilder()
.uri(java.net.URI.create("https://httpbin.org/get"))
.header("User-Agent", "Java-Demo/1.0")
.GET()
.build();
var getResp = client.send(getReq, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + getResp.statusCode());
// POST with form body
String formBody = URLEncoder.encode("name", StandardCharsets.UTF_8) + "=" +
URLEncoder.encode("JavaDemo", StandardCharsets.UTF_8);
var postReq = HttpRequest.newBuilder()
.uri(java.net.URI.create("https://httpbin.org/post"))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(formBody))
.build();
var postResp = client.send(postReq, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + postResp.statusCode());
// Timeout
var timeoutReq = HttpRequest.newBuilder()
.uri(java.net.URI.create("https://httpbin.org/delay/1"))
.timeout(java.time.Duration.ofSeconds(5))
.GET()
.build();
var timeoutResp = client.send(timeoutReq, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + timeoutResp.statusCode());
// DNS resolution
java.net.InetAddress[] addresses = InetAddress.getAllByName("httpbin.org");
for (InetAddress addr : addresses) {
System.out.println(addr.getHostAddress() + " " + addr.getHostName());
}
}
}
HttpClient.newHttpClient() creates a reusable client with a built-in connection pool. HttpRequest.newBuilder() uses the builder pattern to set method, headers, and body — no more concatenating header strings manually or constructing multipart bodies by hand. .send() blocks synchronously (there’s also .sendAsync() for non-blocking usage) and returns an HttpResponse with typed accessors.
The DNS section shows another utility: InetAddress.getAllByName() resolves a hostname through the system resolver, returning all IP addresses — in this case eight for httpbin.org’s CDN-geolb. Java handles the full resolution chain (DNS → ARP) without exposing any of it.
Takeaway
The physical layer decides how bits move; higher layers decide what those bits mean. Java’s java.net package gives you raw sockets when you need custom protocols, a fluent HTTP client for web services, and DNS utilities — all without external dependencies or boilerplate that exists in lower-level languages.