Memra

`ServerSocket` and the accept loop

◈ 5 cards

Why servers need a second class; the six-step server life cycle; accept() returns an ordinary Socket; and the two nested try blocks that keep one bad client from killing the whole server.

The asymmetry that needs a second class

A client knows two things before it starts: which host it wants, and which port. new Socket(host, port) consumes both and the conversation begins. A server knows neither. It cannot name its callers in advance and it cannot say when they will arrive. All it can do is claim a port and wait — a different job from moving bytes, so Java gives it a different class: ServerSocket.

A ServerSocket never carries application data. It has no getInputStream() and no getOutputStream(); look for them and you will not find them. Its one productive method is accept(), which blocks until some client completes a TCP handshake against the port you bound, then returns an ordinary java.net.Socket — the very class you drove as a client last module, already connected, already carrying its two streams. Every byte the server sends or receives travels over that socket. The ServerSocket goes straight back to waiting.

The six steps of a server's life

  1. Construct a ServerSocket on a port. Construction binds the port and puts the kernel into listening state.
  2. Accept. The calling thread stops here — for a millisecond or for a week — until a client connects.
  3. Get streams from the returned Socket: input, output, or both, depending on which way the protocol talks.
  4. Converse according to the protocol.
  5. Close the Socket. That conversation is over; the port is still yours.
  6. Go back to step 2.

Steps 2–5 wrapped in a while (true) is the accept loop. A server that runs it on a single thread is an iterative server: it finishes one connection before it even looks at the next. For a protocol that writes thirty bytes and hangs up, that is genuinely enough — and it is where you should start, because the concurrency in the next two lessons is a modification of this shape, not a replacement for it.

Two scopes, two try blocks

Something goes wrong on every busy server, and where you catch it decides whether one client suffers or all of them do. There are exactly two scopes:

  • Connection scope — a client that vanishes mid-write, a malformed request, a read that times out. Abandon that connection and loop round. Catch it inside the loop.
  • Server scope — the port is already taken, the process may not bind it, the ServerSocket itself has been closed. There is nothing left to serve on. Catch it outside the loop and stop.

So the shape is one try around the ServerSocket and a second try around the body of the loop. Collapsing them into one is the commonest server bug in this course: a single client's IOException propagates past the loop and the process exits, silently, some hours after you started it.

Worked example — a daytime server you can telnet

The daytime protocol is the smallest useful server there is: the client connects, the server writes a human-readable timestamp, the server closes. Nothing is read.

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;

public final class DaytimeServer {

    private static final int PORT = 1313;

    public static void main(String[] args) throws IOException {
        try (ServerSocket listener = new ServerSocket(PORT)) {
            System.out.println("daytime listening on " + listener.getLocalPort());
            while (true) {
                try (Socket client = listener.accept()) {
                    Writer out = new OutputStreamWriter(
                            client.getOutputStream(), StandardCharsets.US_ASCII);
                    out.write(ZonedDateTime.now() + "\r\n");
                    out.flush();
                } catch (IOException ex) {
                    System.err.println("dropped: " + ex.getMessage());   // this client only
                }
            }
        }
    }
}

Four details are doing real work. The charset is explicitUS_ASCII, not the platform default, because a wire format that changes with the machine you deploy on is not a protocol. The line ends \r\n, written by hand rather than by println(), for the same reason: println() emits whatever the host OS prefers, and almost every text protocol on the internet specifies CRLF. flush() precedes the close because the Writer buffers, and an unflushed buffer discarded at close is a client that hangs waiting for bytes you thought you sent. And the port is 1313, not the registered daytime port 13, because on Unix, macOS and Linux binding anything below 1024 requires root.

Run it and connect with telnet localhost 1313. You get one line and a closed connection — the whole protocol.

a client connectstwo streamsresponse sentloopnew ServerSocket(1313)bind + listenaccept()blocks; returns a Socketwrite + flushover the Socketclose the Socketthe port stays boundback to accept()next clientThe ServerSocket nevercarries data.
One pass of the loop is one client, start to finish. Only the third and fourth stages touch the Socket; the ServerSocket is occupied purely with stages one, two and five. Closing the Socket ends a conversation — closing the ServerSocket retires the port.
NORMAL ~/memra/learn/comp-348/server-socket-accept-loop utf-8 LF