Skip to content
wannacode

Programs on the network

Problem

The checkout and the warehouse

The checkout in the shop has to ask the warehouse how much coffee is left, and the warehouse program runs on another computer. A file does not help: the two programs cannot see each other's disks. One of them has to ask and the other one has to answer, right away, over the network.

That is how almost everything around you works: the browser asks the server for a page, the bank app asks the bank for your balance, a game sends your moves to the other players.

The idea

Address, port and two ways to send

For data to reach a program on another computer, you need two things:

  • An IP address: the number of the computer on the network, for example 192.168.1.20 or 142.250.185.78. Your own computer is always 127.0.0.1, with the name localhost.
  • A port: a number from 0 to 65,535 for the program on that computer. The address is the building, the port is the apartment. HTTPS uses 443, and we will use 5401 and up.

The program that waits for someone to reach it is the server. The one that does the reaching is the client. How the data travels is decided by the protocol:

TCP UDP
Connection they connect first, then they talk no connection, every packet is a separate letter
Delivery everything arrives, in the same order, or you find out it did not a packet can get lost or come in a different order
Speed slower faster
Used for the web, email, payments, files live voice and video, games, sensors
In Java the end points of a connection are called sockets.
More precisely, if you are curious
  • The network is described in layers. The OSI model has seven: physical (bits over the cable or the air), data link (the connection to the next device), network (IP addresses and routes), transport (TCP, UDP), session, presentation and application (HTTP, email). In practice the internet uses the simpler TCP/IP model with four layers. Your program works on the top one: Java takes care of everything below.
  • IP delivers separate packets to an address, but it does not promise that they will arrive. TCP is built on top of IP and numbers the packets, so it can put them in order and ask again for the lost ones.
  • Protocols you will come across: HTTP and HTTPS (the web; HTTPS is HTTP encrypted with TLS, whose older name is SSL), WebSocket (a two-way connection for chat and notifications in the browser), FTP (files), SSH (secure access to a remote computer).

See it

The server

The warehouse server gets the name of a product and answers with its price:

static final Map<String, Double> PRICES = Map.of("coffee", 7.50, "tea", 3.20);
static void serve(ServerSocket server) {
try (Socket client = server.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream(), true)) {
String product = in.readLine();
System.out.println("Server received: " + product);
Double price = PRICES.get(product);
out.println(price == null ? "no such product" : product + ": €" + price);
} catch (IOException e) {
System.out.println("Server: " + e.getMessage());
}
}
  • ServerSocket listens on a port. server.accept() waits until a client connects and returns a Socket for the connection with it.
  • The socket has two streams: getInputStream() for what comes in, and getOutputStream() for what you send. We wrap them, as with files: BufferedReader reads lines, PrintWriter writes lines.
  • true in PrintWriter means "send after every println". You will see why that matters.
  • try-with-resources closes the connection and the streams at the end.

See it

The client

The client connects with new Socket(address, port) and uses the same two streams. Here the server and the client are in one program: the server runs in its own thread, and main is the client. That way you can try them without a second computer:
ServerSocket server = new ServerSocket(5401);
new Thread(() -> serve(server)).start();
try (Socket socket = new Socket("localhost", 5401);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
out.println("coffee");
System.out.println("Client received: " + in.readLine());
}
server.close();
Output
Server received: coffee
Client received: coffee: €7.5
The port is opened with new ServerSocket(5401) in main, before the thread starts. That way, when the client connects, the server is already listening.

The two sides take turns: the client writes a line and waits for an answer with readLine(), and the server reads the line and answers. If they swap places and both wait to read, they will wait forever.

ServerSocket server = new ServerSocket(5401);
new Thread(() -> serve(server)).start();
try (Socket socket = new Socket("localhost", 5401);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
out.println("coffee");
System.out.println("Client received: " + in.readLine());
}
server.close();
Step by step, in words
  1. The server opens port 5401 and starts listening.
  2. In its own thread, the server calls accept() and waits for a client.
  3. The client connects. accept() returns a Socket, and each side has two streams: one for writing and one for reading.
  4. The client writes a line: "coffee". It travels over the connection to the input stream of the server.
  5. The client waits in readLine(). The server reads the line and prints it.
  6. The server finds the price and writes back: "coffee: €7.5".
  7. The client's readLine() returns the answer, and the client prints it.
  8. try closes the socket, and server.close() closes the port.
More precisely, if you are curious
  • The whole program, with the imports:
Main.java
import java.io.*;
import java.net.*;
import java.util.Map;
public class Main {
static final Map<String, Double> PRICES = Map.of("coffee", 7.50, "tea", 3.20);
static void serve(ServerSocket server) {
try (Socket client = server.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream(), true)) {
String product = in.readLine();
System.out.println("Server received: " + product);
Double price = PRICES.get(product);
out.println(price == null ? "no such product" : product + ": €" + price);
} catch (IOException e) {
System.out.println("Server: " + e.getMessage());
}
}
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(5401);
new Thread(() -> serve(server)).start();
try (Socket socket = new Socket("localhost", 5401);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
out.println("coffee");
System.out.println("Client received: " + in.readLine());
}
server.close();
}
}
  • In two separate files and two terminals it is the same: you start the server first, then the client. If there is no server on the port, new Socket throws ConnectException: Connection refused. If the port is already taken by another program, new ServerSocket throws BindException: Address already in use.
  • Text travels over the network as bytes. Java 18 and newer turn text into UTF-8 by default, so non-ASCII characters such as € arrive intact.

Try it

Wait for a client

Which method waits until a client connects?

ServerSocket server = new ServerSocket(5401);
Socket client = server.();

The idea

Many clients at once

While the server talks to one client, accept() is not called and the others wait. So a real server runs an endless loop: it accepts a client, gives it to a new thread and goes right back to accept():
static void acceptAll(ServerSocket server) {
int number = 0;
while (true) {
try {
Socket client = server.accept();
number++;
int n = number;
new Thread(() -> handle(client, n)).start();
} catch (IOException e) {
return;
}
}
}
static void handle(Socket client, int number) {
try (client;
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream(), true)) {
String line;
while ((line = in.readLine()) != null) {
System.out.println("Client " + number + ": " + line);
out.println("Echo: " + line);
}
} catch (IOException e) {
System.out.println("Client " + number + " disconnected: " + e.getMessage());
}
}
handle answers every line until the client closes the connection: then readLine() returns null. Two clients, one after the other:
ServerSocket server = new ServerSocket(5402);
Thread acceptor = new Thread(() -> acceptAll(server));
acceptor.setDaemon(true);
acceptor.start();
for (String order : new String[] { "table 4: two coffees", "table 7: the check" }) {
try (Socket socket = new Socket("localhost", 5402);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
out.println(order);
System.out.println(in.readLine());
}
}
server.close();
Output
Client 1: table 4: two coffees
Echo: table 4: two coffees
Client 2: table 7: the check
Echo: table 7: the check
More precisely, if you are curious
  • setDaemon(true) makes the thread a "background" thread: the program ends when main ends, without waiting for the endless loop. A real server does not make it a background thread: it has to run until you stop it.
  • try (client; ...) puts the socket that already exists into try-with-resources, so it is closed at the end.
  • With thousands of clients, a thread for each one is expensive. Then the accept loop gives the clients to a thread pool from the previous lesson: pool.execute(() -> handle(client, n)).

Try it

Both of them wait

The client sends "coffee" and waits for an answer. The server is the same, but the program hangs and prints nothing. Which line is the bug on?

Pick the line with the bug.

The idea

UDP: letters without a connection

The sensors in the shop send the temperature every minute. If one reading gets lost, the next one comes a minute later, so UDP is handy here. There is no connection and there are no streams: you send a DatagramPacket (bytes plus an address and a port) through a DatagramSocket:
DatagramSocket receiver = new DatagramSocket(5403);
try (DatagramSocket sensor = new DatagramSocket()) {
InetAddress address = InetAddress.getByName("localhost");
for (String reading : new String[] { "kitchen 21.5", "storeroom 4.0", "fridge 3.2" }) {
byte[] data = reading.getBytes();
sensor.send(new DatagramPacket(data, data.length, address, 5403));
}
}
byte[] buffer = new byte[256];
for (int i = 0; i < 3; i++) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
receiver.receive(packet);
String text = new String(packet.getData(), 0, packet.getLength());
System.out.println("Temperature: " + text + " °C");
}
receiver.close();
Output
Temperature: kitchen 21.5 °C
Temperature: storeroom 4.0 °C
Temperature: fridge 3.2 °C
  • The receiver listens on port 5403 with new DatagramSocket(5403). The sender uses new DatagramSocket() without a port: the system gives it a free one.
  • receive waits for a packet and writes it into the buffer. getLength() tells you how many bytes came.
  • Nobody confirms that the packet arrived. On localhost they do not get lost, but on a real network they can.
More precisely, if you are curious

If the receiver has to answer, it takes the address and the port of the sender from the packet itself, packet.getAddress() and packet.getPort(), and sends a new packet there. A packet has a size limit (under 64 KB, and over the internet it is sensible to stay at about 1,400 bytes), so UDP is no good for files.

The idea

HTTP: the language of the web

When you open a page, the browser makes a TCP connection to the server and sends a text HTTP request: "give me /". The server sends back a response: a status, headers and a body (HTML, JSON, an image). In Java this is done by HttpClient:

import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create("https://example.com")).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
System.out.println("Type: " + response.headers().firstValue("content-type").orElse("?"));
String body = response.body();
System.out.println("Title: " + body.substring(body.indexOf("<title>") + 7, body.indexOf("</title>")));
Output (with internet)
Status: 200
Type: text/html; charset=utf-8
Title: Example Domain
  • HttpRequest.newBuilder(URI) describes the request, GET by default.
  • send sends the request and waits for the response. BodyHandlers.ofString() says "give me the body as text".
  • The status tells you how it went: 200 is success, 404 means there is no such page, 500 is an error on the server.

The site's code check runs without internet, so this program is shown with the output from a run with a network. Without a network, send throws IOException.

More precisely, if you are curious
Java itself has an HTTP server too (com.sun.net.httpserver). That way you can try HttpClient without internet, on localhost:
HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 5404), 0);
server.createContext("/stock", exchange -> {
byte[] answer = "coffee: 12 in stock".getBytes();
exchange.sendResponseHeaders(200, answer.length);
exchange.getResponseBody().write(answer);
exchange.close();
});
server.start();
HttpClient client = HttpClient.newHttpClient();
for (String path : new String[] { "/stock", "/prices" }) {
HttpRequest request = HttpRequest.newBuilder(URI.create("http://localhost:5404" + path)).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(path + " -> " + response.statusCode() + " " + response.body());
}
server.stop(0);
Output
/stock -> 200 coffee: 12 in stock
/prices -> 404 <h1>404 Not Found</h1>No context found for request
The address /prices has no handler, so the server returns 404 on its own. In real projects, web servers are built with libraries such as Spring Boot. They are built on the same sockets and threads.

Exercise

Servers on localhost

Solve these in your editor. The server and the client can be in one program (the server in a thread) or in two separate ones, run in two terminals.

1. Echo server. The server sends back every line with "Echo: " in front, until it gets "end". Then it answers "Goodbye" and closes the connection. The client sends "hello", "how are you" and "end", and prints the answers:

Output
Echo: hello
Echo: how are you
Goodbye

2. A server with commands. UPCASE text returns the text in uppercase, TIME returns the current time as 14:05 (LocalTime.now() and DateTimeFormatter.ofPattern("HH:mm")), and anything else returns "Unknown command: " and the command. The time changes, so the client checks only its shape with a regular expression:

Output
ORDER 42 IS READY
The time looks like HH:MM: true
Unknown command: HOUR
3. A chat for two. Ana opens the server on port 5407, Boris connects. Each one is in their own thread and sends their prepared lines, waiting for an answer after each one:
Output
Boris received: Hi, Boris! Are you free at 6 PM?
Ana received: Yes, I will be there.
Boris received: Great, see you soon.
Ana received: Bye!
Then make it real: two programs, each with two threads. One thread reads from the keyboard and sends, and the other one reads from the socket and prints. That way you can type while you wait for an answer.

Test

Check yourself

1. What does the port point to?
2. When is UDP the better choice over TCP?
3. What does server.accept() do?
4. The client writes with println, but the server gets nothing, and both of them wait. What is the most common cause?
5. HttpResponse.statusCode() returns 404. What does it mean?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·