In the previous lesson you designed BTCP/1, BiblioTech's catalogue query protocol: a text protocol, over TCP, on port 9090, with messages delimited by a line break and replies carrying three-digit codes. Now it is time to implement it. And the good news is that you already know how to do most of the work.

A socket is one endpoint of a conversation between two programs. Once established, a Java Socket hands you an InputStream and an OutputStream. Exactly the same ones you used in module 7 to read and write files. The same decorators, the same InputStreamReader with an explicit charset, the same BufferedReader, the same try-with-resources. Once connected, the network is I/O you already know how to do.

What is new, and what this lesson is about, is everything surrounding that I/O: how the connection is established and with what time limit, what options a socket has and when they matter, how to close it properly —and what closing only half of it means—, what each network exception tells you about what really happened, and one particular mistake that absolutely everybody makes at the start and that leaves the other end waiting forever.

By the end you will have the BiblioTech client working against a server that you will play by hand with nc. The real server arrives in 09-03.

Contents

  1. The socket and the streams of module 7
  2. The Socket class: connecting
  3. connect with a time limit and InetSocketAddress
  4. Writing and reading text over the socket
  5. The number-one bug: forgetting to flush
  6. The line protocol and the delimiter problem
  7. Closing properly and try-with-resources
  8. Half-close: shutdownOutput and shutdownInput
  9. Socket options
  10. setSoTimeout and SocketTimeoutException
  11. setTcpNoDelay and Nagle's algorithm
  12. The network exceptions and what each one means
  13. Binary data with DataOutputStream and DataInputStream
  14. Why not to send serialised objects to an untrusted client
  15. BiblioTech: the complete catalogue client
  16. Common Mistakes and Tips
  17. Exercises

  1. The socket and the streams of module 7

First of all, the idea that structures the whole lesson:

graph LR
    subgraph CLIENT
    A["Your code"] --> B["PrintWriter"]
    B --> C["BufferedWriter"]
    C --> D["OutputStreamWriter<br/>UTF-8"]
    D --> E["socket.getOutputStream()"]
    end
    E -->|bytes over the network| F
    subgraph SERVER
    F["socket.getInputStream()"] --> G["InputStreamReader<br/>UTF-8"]
    G --> H["BufferedReader"]
    H --> I["Your code"]
    end

That diagram is exactly the one from module 7, with a single difference: where there used to be a FileOutputStream and a FileInputStream, there is now a socket. Everything else —the Decorator pattern, the OutputStreamWriter bridge between bytes and characters, the buffer, the explicit charset— is identical.

Concept from module 7 Its network equivalent
new FileInputStream(file) socket.getInputStream()
new FileOutputStream(file) socket.getOutputStream()
The file exists or it does not The server accepts or refuses
read() returns -1 at end of file read() returns -1 when the other end closes
The file is complete when you open it The data arrives bit by bit
Closing releases a descriptor Closing ends the conversation

The last two rows are the ones that make the real difference. A file is complete when you open it; a socket hands you bytes as they arrive, and one read may give you less than you asked for simply because the rest is still travelling down the wire. That is the root of the delimiter problem you will see in section 6.

  1. The Socket class: connecting

java.net.Socket represents a client-side TCP socket. The most direct way to use it is its constructor, which connects during construction:

// The constructor CONNECTS. If it returns, you are already connected.
// If it cannot, it throws and the object never comes into existence.
Socket socket = new Socket("localhost", 9090);

That constructor does three things in one go:

  1. It resolves the name localhost to an address (DNS or /etc/hosts).
  2. It creates the socket and assigns it a local ephemeral port.
  3. It runs the three-way handshake against port 9090 of the destination.

And it can fail at any of the three, always with an IOException or a subclass:

import java.io.IOException;
import java.net.ConnectException;
import java.net.Socket;
import java.net.UnknownHostException;

try (Socket socket = new Socket("localhost", 9090)) {

    System.out.println("Connected to      : " + socket.getInetAddress().getHostAddress());
    System.out.println("Remote port       : " + socket.getPort());
    System.out.println("Local address     : " + socket.getLocalAddress().getHostAddress());
    System.out.println("Local port        : " + socket.getLocalPort());

} catch (UnknownHostException e) {
    // Failure at step 1: the name could not be resolved.
    System.err.println("No such host: " + e.getMessage());
} catch (ConnectException e) {
    // Failure at step 3: the machine replied RST. Nobody is listening.
    System.err.println("Connection refused: there is no server on that port");
} catch (IOException e) {
    // Any other network failure.
    System.err.println("Network failure: " + e.getMessage());
}

Output if a server is listening:

Connected to      : 127.0.0.1
Remote port       : 9090
Local address     : 127.0.0.1
Local port        : 51422

There you have the four-tuple from the previous lesson made code: (127.0.0.1:51422) talking to (127.0.0.1:9090). The local port was not chosen by you: the operating system assigned it from its ephemeral range.

Useful query methods

Method Returns
getInetAddress() The address of the other end
getPort() The port of the other end
getLocalAddress() / getLocalPort() Yours
getRemoteSocketAddress() Both of the other end, as a SocketAddress
isConnected() Whether it ever connected — not whether it is still alive
isClosed() Whether you closed it — not whether the other end did
isInputShutdown() / isOutputShutdown() Whether you have half-closed the connection

Important warning about isConnected(). It is the most quoted trap of the API. isConnected() returns true from the moment the connection was established, and it goes on returning true even if the other end has gone down, because TCP tells you nothing until you try to use the socket. There is no method that answers "is the other end still there?". The only way to know is to try to read or write and see what happens: a read() returning -1 means the other end closed in an orderly way; a SocketException means it broke. Any code that relies on isConnected() to decide whether there is still a conversation is wrong.

  1. connect with a time limit and InetSocketAddress

The constructor new Socket(host, port) has a serious problem: it accepts no connection time limit. If the destination machine does not respond —it is switched off, or a firewall is silently dropping the packets— the constructor blocks until the operating system gives up, and that default deadline can be over a minute on Linux, even several minutes.

A minute blocking BiblioTech's menu thread is not acceptable. The solution is to separate creation from connection:

import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;

// 1. The socket is created UNCONNECTED (no-argument constructor).
Socket socket = new Socket();

// 2. The destination is described with an InetSocketAddress: the (host, port) pair.
InetSocketAddress destination = new InetSocketAddress("192.168.1.50", 9090);

// 3. Connect with an explicit time limit in milliseconds.
try {
    socket.connect(destination, 3000);  // 3 seconds at most
} catch (SocketTimeoutException e) {
    // The deadline expired: the machine has not replied to the SYN.
    // TRANSIENT failure: retrying may make sense.
    System.err.println("The server is not responding within 3 s");
}

InetSocketAddress is simply the (address, port) pair we talked about in 09-01. There are two ways to build one, and the difference matters:

// Resolves the name NOW. If it does not resolve, isUnresolved() will be true
// (careful: it does NOT throw here; connect() will).
InetSocketAddress a = new InetSocketAddress("server.nexussoftware.local", 9090);

// Resolves nothing: it leaves the name as it is so that whoever connects
// resolves it (useful with SOCKS proxies, which resolve it themselves).
InetSocketAddress b = InetSocketAddress.createUnresolved("server.local", 9090);

// With an already-resolved InetAddress, without querying DNS.
InetSocketAddress c = new InetSocketAddress(InetAddress.getLoopbackAddress(), 9090);

A firm rule for the rest of the module: in production code, never use the constructor that connects. Always use new Socket() + connect(destination, timeLimit). And do not confuse the two time limits that exist, because they are different and you need both:

Time limit Set with Covers
Connection connect(addr, ms) How long to wait for the connection to be established
Read setSoTimeout(ms) How long to wait for data to arrive on each read()

A socket can connect in 5 ms and then sit for an hour receiving nothing. The first does not protect you from the second.

  1. Writing and reading text over the socket

With the connection made, we are on familiar ground. For BTCP/1, a line-based text protocol in UTF-8, the correct stack is this:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

try (Socket socket = new Socket()) {
    socket.connect(new InetSocketAddress("localhost", 9090), 3000);
    socket.setSoTimeout(10_000);

    // OUTPUT: your text -> characters -> UTF-8 bytes -> network
    //   PrintWriter        gives println() and formatting
    //   BufferedWriter     accumulates so as not to do one network write per character
    //   OutputStreamWriter is the BRIDGE characters -> bytes, with an explicit charset
    PrintWriter writer = new PrintWriter(
            new BufferedWriter(
                    new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8)),
            true);                      // <-- autoFlush: flushes on every println()

    // INPUT: network -> UTF-8 bytes -> characters -> lines
    BufferedReader reader = new BufferedReader(
            new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));

    String greeting = reader.readLine();        // "200 BIBLIOTECH BTCP/1"
    System.out.println("S: " + greeting);

    writer.println("QUERY 978-0000000001");
    System.out.println("C: QUERY 978-0000000001");

    String reply = reader.readLine();           // "200 OK 978-...|Effective Java|BOOK|true"
    System.out.println("S: " + reply);

    writer.println("QUIT");
    System.out.println("S: " + reader.readLine());   // "221 BYE"
}

Every layer is there for a specific reason:

Class Why it is there
OutputStreamWriter(..., UTF_8) The bridge from characters to bytes. The explicit charset is mandatory: without it the platform's own is used, and client and server may not agree
BufferedWriter Without it, every character would potentially be a network packet. With it, characters accumulate and go out in one piece
PrintWriter Provides println(), which adds the protocol's line delimiter, and printf()
InputStreamReader(..., UTF_8) The reverse bridge, with the same charset
BufferedReader Provides readLine(), which accumulates bytes until it finds the line break. It is the piece that solves the message-boundary problem

About PrintWriter and exceptions. PrintWriter does not throw IOException: it swallows errors and exposes them through checkError(). That is convenient for writing to the console and dangerous on a network, because a write failure can go unnoticed. If you need to know, check writer.checkError() after writing, or use BufferedWriter directly with write() and newLine(), which do throw. In the BiblioTech client we will check checkError().

About the line delimiter. println() writes the platform's separator: \n on Linux and macOS, \r\n on Windows. A network protocol cannot depend on which system each end runs. The good news is that readLine() accepts \n, \r and \r\n interchangeably, so in practice it interoperates; but if the protocol demands strict \r\n (HTTP does), you have to write it by hand with print("...\r\n"). In BTCP/1 we specified \n, and to be rigorous the client will write it explicitly.

  1. The number-one bug: forgetting to flush

This is the mistake everybody makes the first time, and it deserves a full demonstration because its symptom is deceptive: there is no exception, no error, nothing in the log. The program simply sits there forever.

The broken code

// BROKEN CLIENT - do not copy it, it is the example of what NOT to do.
try (Socket socket = new Socket()) {
    socket.connect(new InetSocketAddress("localhost", 9090), 3000);

    PrintWriter writer = new PrintWriter(
            new BufferedWriter(
                    new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8)));
    //          ^^^^^ the autoFlush "true" is missing

    BufferedReader reader = new BufferedReader(
            new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));

    writer.println("QUERY 978-0000000001");    // Stays in the BUFFER.

    String reply = reader.readLine();          // <-- BLOCKS HERE FOREVER
    System.out.println(reply);
}

What has happened exactly

sequenceDiagram
    participant App as Client code
    participant Buf as BufferedWriter
    participant Net as Network
    participant Srv as Server
    App->>Buf: println("QUERY ...")
    Note over Buf: 21 bytes held in memory.<br/>The buffer is 8192 bytes: more still fits,<br/>so NOTHING is sent.
    App->>Net: readLine()
    Note over App,Net: The client waits for a reply...
    Note over Srv: ...to a request that never arrived.
    Note over App,Srv: DEADLOCK. Each waits for the other.<br/>No exception. No trace. Forever.

The BufferedWriter has a buffer of 8192 characters by default. Its 21 characters fit with room to spare, so it does what it was asked to do: wait until it has more before spending a network operation. In a file that is a consequence-free optimisation, because closing the file flushes it and everything arrives. On a network it is a deadlock, because the other end is waiting for those bytes in order to reply.

And notice the cruel detail: the same code would work with a message longer than 8192 characters, because the buffer would overflow and send itself. It is the kind of bug that "works on my machine" with large data and fails in production with small data.

The three ways of fixing it

// WAY 1 (recommended for line protocols): autoFlush on PrintWriter.
// The second argument true makes println(), printf() and format()
// flush automatically. CAREFUL: print() with no line break does NOT flush.
PrintWriter writer = new PrintWriter(
        new BufferedWriter(
                new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8)),
        true);
writer.println("QUERY 978-0000000001");        // sends itself

// WAY 2: explicit flush. Mandatory if you write with print() and no line break,
// or if the protocol uses \r\n and you write the terminator by hand.
writer.print("QUERY 978-0000000001\n");
writer.flush();                                  // <-- indispensable

// WAY 3: BufferedWriter directly, which additionally DOES throw IOException.
BufferedWriter bw = new BufferedWriter(
        new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
bw.write("QUERY 978-0000000001");
bw.write('\n');
bw.flush();

The rule to memorise: on a network, after sending something that expects a reply, you must flush. No exceptions. If you use autoFlush, make sure you always write with println and not with print, because autoFlush only acts on println, printf and format — a print("QUIT\n") with autoFlush enabled does not flush, and you are back to the same deadlock with code that apparently does it right.

The recognisable symptom. If your network program "hangs" with no error, suspect this first. The second suspect is not having set setSoTimeout, which turns the eternal block into a diagnosable SocketTimeoutException. With both things done properly, this bug becomes impossible to suffer in silence.

  1. The line protocol and the delimiter problem

In 09-01 we saw that TCP is a byte stream with no message boundaries. It is worth seeing now exactly what that means in code.

// What the client does:
writer.println("QUERY 111");
writer.println("LIST");

// What the server may read with a read() over raw bytes:
//   Case A: "QUERY 111\nLIST\n"      (both messages together)
//   Case B: "QUERY 111\n"            then "LIST\n"      (one and one)
//   Case C: "QUER"                   then "Y 111\nLI"   then "ST\n"

All three cases are legal and really do happen, depending on buffer sizes, the network MTU and Nagle's algorithm. A server that assumes case B works on localhost and fails in production.

BufferedReader.readLine() solves this for you. Internally it keeps its own buffer, and when you ask it for a line it keeps reading from the socket as many times as needed until it finds a line break. It returns the line without the terminator.

String line;
while ((line = reader.readLine()) != null) {
    // Here 'line' is ALWAYS a complete protocol message.
    // readLine() has already resolved the fragmentation for you.
    process(line);
}
// We leave the loop when readLine() returns null:
// the other end has closed its side of the connection.

Two warnings about readLine():

  1. It returns null at end of stream, which on a network means "the other end closed". Not checking it produces a NullPointerException with the first client that disconnects, and it is an absolute classic. The loop while ((line = reader.readLine()) != null) is not a stylistic flourish.
  2. It blocks until it finds the line break. If the other end sends "QUERY 111" with no \n and goes quiet, your readLine() never returns. Hence setSoTimeout.

And a design limitation: readLine() has no length limit. A malicious client can send a hundred megabytes with not one line break and your server will accumulate them in memory until it bursts. It is a real denial-of-service vector. In the 09-03 server we will add a bounded read.

When the delimiter does not work

The line delimiter works if the delimiter cannot appear in the data. For BTCP/1 that is true: neither an ISBN nor a title carries line breaks. But if you had to send a multi-line text —a book review, for example— you would have two ways out: escape the breaks (\n as two characters) or switch to a length prefix:

// Length prefix: first how many bytes are coming, then the bytes.
byte[] data = review.getBytes(StandardCharsets.UTF_8);
dataOutput.writeInt(data.length);        // 4 bytes in big-endian
dataOutput.write(data);                  // the data, with no restrictions
dataOutput.flush();

// When reading:
int length = dataInput.readInt();
if (length < 0 || length > MAX_ALLOWED) {
    // ESSENTIAL: otherwise a malicious client asks for a 2 GB array
    // and causes an OutOfMemoryError with a 4-byte request.
    throw new ProtocolException("Invalid declared length: " + length);
}
byte[] data = dataInput.readNBytes(length);
String review = new String(data, StandardCharsets.UTF_8);

Notice the bounds check. Whenever you read a length that comes from the network, validate it before allocating memory with it. It is one of the oldest and most repeated vulnerabilities in existence.

  1. Closing properly and try-with-resources

Socket implements Closeable, so it goes in a try-with-resources without discussion:

try (Socket socket = new Socket()) {
    socket.connect(destination, 3000);
    // ... conversation ...
}   // socket.close() guaranteed, including if an exception is thrown

A detail that saves code: closing the Socket also closes its streams, and closing any of its streams closes the socket. They are bound together. That is why there is no need —nor is it advisable— to declare the PrintWriter and the BufferedReader in the try-with-resources:

// UNNECESSARY and also misleading: it suggests they are independent
// resources when in reality all three are the same socket.
try (Socket socket = new Socket();
     PrintWriter writer = new PrintWriter(...);
     BufferedReader reader = new BufferedReader(...)) {

Worse still, that code has a real ordering problem: try-with-resources closes in reverse order, so it would close the reader, then the writer —which will try to flush its buffer onto an already-closed socket— and then the socket. Declare only the Socket.

That said, there is one thing automatic closing does not do properly for you: flushing what is pending before closing when the order matters. close() on a BufferedWriter does flush; but if the socket is closed first by another route, what is pending is lost. With autoFlush and println you do not have that problem, which is another reason to use it.

  1. Half-close: shutdownOutput and shutdownInput

A TCP connection has two independent directions, and they can be closed separately. It is what we saw reflected in 09-01 in the fact that teardown is four messages and not three.

Operation What it does What the other end sees
socket.shutdownOutput() Closes your write direction. Sends FIN Its read() returns -1; its readLine() returns null
socket.shutdownInput() Closes your read direction. Whatever arrives is discarded Nothing immediately; if it keeps writing, it will end in an error
socket.close() Closes both directions and releases the descriptor Its read() returns -1, and its writes will fail

The classic use case for shutdownOutput is a protocol in which the client sends everything, then the server replies with everything, and the "I have finished sending" signal is the end of the stream:

// Client that uploads a file to the server and then waits for the summary.
try (Socket socket = new Socket()) {
    socket.connect(destination, 3000);

    // 1. Send the complete content.
    try (OutputStream output = socket.getOutputStream()) {
        // CAREFUL: this try-with-resources would close the whole socket. Wrong.
    }
}

Written that way it is wrong, precisely because of what we saw: closing the stream closes the socket. The correct form is:

try (Socket socket = new Socket()) {
    socket.connect(destination, 3000);

    // 1. Send the whole content and FLUSH.
    OutputStream output = socket.getOutputStream();
    Files.copy(Path.of("catalog.csv"), output);
    output.flush();

    // 2. Say "I have finished talking" WITHOUT closing the connection.
    //    The server will see end of stream and know it can process.
    socket.shutdownOutput();

    // 3. Carry on listening: the read direction is still open.
    BufferedReader reader = new BufferedReader(
            new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
    String summary;
    while ((summary = reader.readLine()) != null) {
        System.out.println("S: " + summary);
    }
}   // now yes, a complete close

Without shutdownOutput(), the server would wait indefinitely for more data and the client would wait for a reply: the same deadlock as in section 5, from a different cause.

In BTCP/1 we do not need it, because the protocol has an explicit farewell command (QUIT) and delimited replies. But it is worth knowing, because many real protocols —and several exercises in this module— use it.

  1. Socket options

A socket has options that modify the behaviour of the operating system's TCP stack. These are the ones that really matter:

Option Method Typical value What for
Read time limit setSoTimeout(int ms) 5,000-30,000 The most important one. Prevents eternal blocks in read()
No delay (Nagle) setTcpNoDelay(boolean) true in interactive protocols Sends small messages immediately
Keep alive setKeepAlive(boolean) true on long connections Detects dead peers on idle connections
Reuse address setReuseAddress(boolean) true on servers Allows listening again on a port in TIME_WAIT
Receive buffer setReceiveBufferSize(int) default Fine tuning for high-latency links
Send buffer setSendBufferSize(int) default The same
Linger on close setSoLinger(boolean, int) disabled Controls what happens to pending data on close
Traffic priority setTrafficClass(int) default A quality-of-service hint to the network

Three warnings:

  • Almost all of them have to be set before using the socket, and some before connecting. setSoTimeout can be changed at any moment and affects subsequent reads.
  • Buffer sizes are suggestions. The operating system may give you something else; check with getReceiveBufferSize() what you actually have. Unless you are tuning a specific high-latency, high-bandwidth link, do not touch them.
  • setSoLinger is a trap. With setSoLinger(true, 0) the close sends an RST instead of a FIN, discarding pending data. It is sometimes used to free ports quickly, but it causes SocketException: Connection reset at the other end and data loss. Do not use it unless you know exactly why.

  1. setSoTimeout and SocketTimeoutException

It is the option that will save you most often, so it deserves a section of its own.

By default, a read() on a socket blocks indefinitely. If the other end sends nothing and does not close either —because the laptop's battery ran out, or because an intermediate firewall dropped the connection without warning— your thread sits there. Forever. With no exception.

socket.setSoTimeout(10_000);       // 10 seconds per read

try {
    String line = reader.readLine();
    if (line == null) {
        System.out.println("The other end closed in an orderly way");
    } else {
        process(line);
    }
} catch (SocketTimeoutException e) {
    // 10 s have gone by without a single byte arriving.
    // VERY IMPORTANT: the socket is STILL VALID. The read can be retried.
    System.out.println("No data in 10 s; the socket is still usable");
}

Three properties to be clear about:

  1. The deadline is per read operation, not total. If you ask for a long line that arrives bit by bit, the counter resets with every chunk that arrives. Ten seconds means "ten seconds without receiving anything", not "ten seconds at most".
  2. After the exception, the socket is still valid. Unlike almost any other network exception, SocketTimeoutException does not invalidate the connection: you can call readLine() again. This enables the "wait with periodic check" pattern:
socket.setSoTimeout(1000);          // poll once a second
while (!Thread.currentThread().isInterrupted() && running) {
    try {
        String line = reader.readLine();
        if (line == null) {
            break;                  // the other end closed
        }
        process(line);
    } catch (SocketTimeoutException e) {
        // One second with no data: we take the chance to check
        // the cancellation flag of module 8 and go back to waiting.
        continue;
    }
}

This pattern is what makes a blocking socket cancellable, and it connects directly with the interruption protocol of 08-02: without it, a thread blocked in read() ignores interruptions —read() is not interruptible— and your application's graceful shutdown waits for a thread that is never coming back.

  1. A value of 0 means infinite, which is the default. setSoTimeout(0) is not "no wait": it is "wait forever".

  1. setTcpNoDelay and Nagle's algorithm

In 1984 John Nagle observed that interactive sessions (typing on a remote console) generated a 41-byte packet for every keystroke: 1 byte of data and 40 of headers. A 97 % waste.

His solution, Nagle's algorithm, is enabled by default in every TCP stack and works like this: if there is small data already sent and unacknowledged, do not send more small data; accumulate it until the acknowledgement arrives or until a full segment is filled.

It is a good idea for bulk transfers. It is a problem for request-reply protocols with small messages, especially combined with another optimisation called delayed acknowledgement (the receiver waits up to 200 ms before acknowledging, in case it can acknowledge several things together). The two together produce artificial delays of tens or hundreds of milliseconds:

Without TCP_NODELAY, interactive protocol:

  Client: sends "QUERY 111\n"   (10 bytes)  --> goes out at once (nothing pending)
  Server: receives, processes, replies
  Client: sends "LIST\n"        (5 bytes)   --> WAITS: there is an unacknowledged send
          ...                                   up to 200 ms of artificial delay

For BTCP/1, which is exactly an interactive protocol of short messages, the right thing is to disable it:

socket.setTcpNoDelay(true);     // true = NO Nagle algorithm = send now

The name is confusing: setTcpNoDelay(true) means "enable the NODELAY option", that is, disable Nagle's algorithm. true = send immediately.

Type of traffic setTcpNoDelay Reason
Interactive request-reply (BTCP, Redis, games) true Latency matters more than efficiency
Large file transfer false (default) The segments are already full; Nagle does not get in the way
Real-time streaming true The same as interactive

Note. Nagle only acts on data you have not grouped. If your code already writes each message in one go with one flush() per message —as autoFlush does—, and the messages are large, Nagle is barely noticeable. The painful case is writing one message in several small pieces with several flush(). The practical conclusion: group what you can yourself and set setTcpNoDelay(true) in interactive protocols.

  1. The network exceptions and what each one means

They all descend from IOException, so a single catch (IOException e) catches them all. But treating them alike wastes extremely valuable information: each one tells you something different about what happened, and about whether retrying makes sense.

graph TD
    A["IOException"] --> B["SocketException"]
    A --> C["UnknownHostException"]
    A --> D["EOFException"]
    A --> E["InterruptedIOException"]
    B --> F["ConnectException"]
    B --> G["BindException"]
    B --> H["NoRouteToHostException"]
    E --> I["SocketTimeoutException"]
Exception What really happened Retry? Recommended reaction
UnknownHostException The name does not resolve: it does not exist or DNS is failing No (except on a temporary DNS failure) Configuration error. Log it and tell the user
ConnectException You reached the machine and it replied RST: nobody is listening on that port No, not immediately The server is not running, or it is another port
SocketTimeoutException (connecting) Nobody replied to the SYN: machine down or a firewall dropping Yes, with increasing backoff Transient. Retry 2-3 times
SocketTimeoutException (reading) Connection alive but the other end sends nothing It depends The socket is still valid: retry the read or give up
NoRouteToHostException There is no path to that network No Routing or firewall problem
BindException The local port is already taken No ss -tlnp to see who has it (typical on servers, 09-03)
SocketException: Connection reset The other end sent RST: it went down, or closed abruptly, or refused what was sent Yes, once The connection is dead: another one must be opened
SocketException: Broken pipe You wrote to a connection the other end had already closed Yes, once The same
EOFException Unexpected end of stream while reading with DataInputStream No on that socket The other end closed mid-message: incomplete data

And a distinction that must be very clear:

Situation How it shows up
The other end closed in an orderly way read() returns -1; readLine() returns null. There is no exception
The other end went down or closed abruptly SocketException: Connection reset
The other end is alive but silent SocketTimeoutException, if you set setSoTimeout
The other end is alive but silent and you did not set a time limit Nothing. Silence. Your thread blocked forever

Complete handling, applying the layered strategy of 06-07:

package com.nexussoftware.bibliotech.network;

import com.nexussoftware.bibliotech.exception.BiblioTechException;

import java.io.IOException;
import java.net.ConnectException;
import java.net.NoRouteToHostException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Translates network exceptions into BiblioTech domain exceptions
 * and decides whether the failure is transient (worth a retry) or permanent.
 * This is the ERROR BOUNDARY of 06-07 applied to the network.
 */
public final class NetworkErrorTranslator {

    private static final Logger LOG = Logger.getLogger(NetworkErrorTranslator.class.getName());

    private NetworkErrorTranslator() {
    }

    /** The result of classifying a network failure. */
    public record Classification(String userMessage, boolean transientFailure) {
    }

    public static Classification classify(IOException e, String destination) {
        if (e instanceof UnknownHostException) {
            LOG.severe("Unresolvable name: " + destination);
            return new Classification(
                    "The catalogue server cannot be found. Check the configuration.", false);
        }
        if (e instanceof ConnectException) {
            // Mind the order: ConnectException is a subclass of SocketException,
            // so it must be checked FIRST.
            LOG.severe("Connection refused by " + destination + ": the server is not running");
            return new Classification(
                    "The catalogue server is unavailable.", false);
        }
        if (e instanceof SocketTimeoutException) {
            LOG.warning("Timed out with " + destination);
            return new Classification(
                    "The catalogue server is taking too long to reply.", true);
        }
        if (e instanceof NoRouteToHostException) {
            LOG.severe("No route to " + destination);
            return new Classification(
                    "There is no connection to the server's network.", false);
        }
        if (e instanceof SocketException) {
            // "Connection reset", "Broken pipe" and the like.
            LOG.warning("Broken connection with " + destination + ": " + e.getMessage());
            return new Classification(
                    "The connection with the catalogue has been lost.", true);
        }
        LOG.log(Level.SEVERE, "Unclassified network failure with " + destination, e);
        return new Classification("Communication error with the catalogue.", false);
    }

    /** Turns the failure into the domain exception, preserving the cause. */
    public static BiblioTechException translate(IOException e, String destination) {
        Classification c = classify(e, destination);
        return new BiblioTechException(c.userMessage(), e);
    }
}

Notice the order of the checks: ConnectException and BindException are subclasses of SocketException, so if you checked SocketException first you would catch them all there and lose the distinction. The same would happen with a catch written in the wrong order — with the difference that there the compiler would warn you; here it does not.

  1. Binary data with DataOutputStream and DataInputStream

BTCP/1 is textual, but not every protocol is. When numbers, booleans or binary blocks have to be sent, module 7's DataOutputStream and DataInputStream work over a socket exactly as they do over a file, with an added advantage: they write in big-endian, the network byte order, so they interoperate with programs written in other languages without conversion.

package com.nexussoftware.bibliotech.network;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
 * Binary variant of sending a material card.
 * Used when volume matters more than readability.
 */
public final class BinaryProtocol {

    /** Defensive limit: no legitimate text field exceeds this. */
    private static final int MAX_TEXT = 4096;

    private BinaryProtocol() {
    }

    public static void sendCard(Socket socket, String isbn, String title,
                                int pages, boolean available) throws IOException {
        // We buffer: without BufferedOutputStream, each writeInt would be
        // potentially an independent network operation.
        DataOutputStream output = new DataOutputStream(
                new BufferedOutputStream(socket.getOutputStream()));

        writeText(output, isbn);            // length (int) + UTF-8 bytes
        writeText(output, title);
        output.writeInt(pages);             // 4 bytes, big-endian
        output.writeBoolean(available);     // 1 byte
        output.flush();                     // indispensable, as always
    }

    public static String receiveTitle(Socket socket) throws IOException {
        DataInputStream input = new DataInputStream(
                new BufferedInputStream(socket.getInputStream()));

        String isbn = readText(input);
        String title = readText(input);
        int pages = input.readInt();
        boolean available = input.readBoolean();

        return title + " (" + isbn + ", " + pages + " pp., "
                + (available ? "available" : "on loan") + ")";
    }

    /** Writes a text with a length prefix: 4 bytes of size + UTF-8 bytes. */
    private static void writeText(DataOutputStream output, String text)
            throws IOException {
        byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
        output.writeInt(bytes.length);
        output.write(bytes);
    }

    /** Reads a text with a length prefix, VALIDATING the declared length. */
    private static String readText(DataInputStream input) throws IOException {
        int length = input.readInt();

        // Without this check, a malicious client sends the int 2_000_000_000
        // and we cause an OutOfMemoryError with a 4-byte request.
        if (length < 0 || length > MAX_TEXT) {
            throw new IOException("Invalid text length: " + length);
        }

        // readNBytes reads EXACTLY n bytes or throws EOFException if the
        // stream ends first. A bare read(byte[]) could read less and
        // leave you with incomplete data unnoticed: a classic mistake.
        byte[] bytes = input.readNBytes(length);
        if (bytes.length < length) {
            throw new java.io.EOFException("Stream ended in the middle of a text");
        }
        return new String(bytes, StandardCharsets.UTF_8);
    }
}

The DataOutputStream methods write fixed, known sizes, which makes the protocol predictable:

Method Bytes Notes
writeByte 1
writeShort 2 big-endian
writeInt 4 big-endian
writeLong 8 big-endian
writeFloat / writeDouble 4 / 8 IEEE 754
writeBoolean 1 0 or 1
writeUTF 2 + n modified UTF, maximum 65535 bytes

About writeUTF/readUTF. They are convenient because they do the length prefix for you, but they use a proprietary variant of UTF-8 ("modified UTF") that is not standard UTF-8, and they are limited to 65535 bytes. They interoperate well between Java programs, and badly with any other language. If the other end may not be Java, do the length prefix by hand as in the example.

  1. Why not to send serialised objects to an untrusted client

In 07-05 you learned Serializable, serialVersionUID and the risks of deserialising untrusted data. It is tempting to apply serialisation to sockets, because it seems to solve everything at a stroke:

// TEMPTING AND DANGEROUS. Do not do this in a network server.
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
output.writeObject(book);
output.flush();

// And on the other side:
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
Book book = (Book) input.readObject();           // <-- HERE IS THE HOLE

The problem is that readObject() builds arbitrary objects before you can check what type they are. The (Book) cast happens after deserialising. If an attacker sends a carefully crafted stream, readObject will instantiate the classes they name and run their deserialisation code (private readObject, readResolve, finalize) — using classes that are already on your classpath. It is the family of vulnerabilities known as gadget chains, and it has caused remote-code-execution holes in very well-known products.

Risk Description
Code execution Chains of classpath classes strung together to run system commands
Denial of service A small stream that expands into giant structures or objects with quadratic hashCode
Coupling Both ends must have the same classes and compatible versions
Not interoperable Only Java speaks that format

Practical rules:

  1. Never deserialise Java objects coming from an untrusted network. Not even from an internal network: Nexus Software's internal network includes the laptop somebody plugged into the guest Wi-Fi.
  2. Use a data format, not an object format: delimited text (like BTCP/1), CSV, JSON or binary with a length prefix. All of them produce data, which you then turn into objects with your own validated code.
  3. If you are stuck with it for legacy reasons, use ObjectInputFilter (Java 9+) to restrict which classes may be deserialised:
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
// Strict allow list: only these classes, at most 1000 objects,
// at most 100 KB, maximum depth 10. Everything else, rejected.
input.setObjectInputFilter(ObjectInputFilter.Config.createFilter(
        "com.nexussoftware.bibliotech.domain.Book;"
        + "java.lang.String;"
        + "maxdepth=10;maxarray=1000;maxbytes=102400;"
        + "!*"));         // the trailing !* rejects everything not listed

Even so, the official recommendation from Oracle and from the Java team itself is clear: native serialisation must not be used as a network protocol. BiblioTech will not use it. BTCP/1 sends text, and the text becomes a Book through a method of yours that validates every field.

  1. BiblioTech: the complete catalogue client

It is time to put it all together. We are going to write CatalogClient, the class that lets any Nexus Software workstation speak BTCP/1 with the server.

Design

sequenceDiagram
    participant M as BiblioTechMenu
    participant C as CatalogClient
    participant S as BTCP/1 server
    M->>C: connect()
    C->>S: TCP connect (3 s limit)
    S-->>C: 200 BIBLIOTECH BTCP/1
    C->>C: checks the greeting and the version
    M->>C: query("978-0000000001")
    C->>S: QUERY 978-0000000001
    S-->>C: 200 OK 978-...|Effective Java|BOOK|true
    C-->>M: Result.success(Card)
    M->>C: close()
    C->>S: QUIT
    S-->>C: 221 BYE
    C->>C: socket.close()

CatalogClient implements AutoCloseable, so it is used in a try-with-resources like any other BiblioTech resource, and its close() is polite: it says goodbye with QUIT before closing.

package com.nexussoftware.bibliotech.network;

import com.nexussoftware.bibliotech.exception.BiblioTechException;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Client for BiblioTech's BTCP/1 protocol.
 *
 * Talks to the catalogue server defined in 09-01 and implemented in 09-03.
 * Implements AutoCloseable: it says goodbye with QUIT and closes the socket.
 *
 * It is NOT thread-safe: a socket is a conversation, and two threads
 * writing at once would interleave their requests. One thread, one client.
 */
public class CatalogClient implements AutoCloseable {

    private static final Logger LOG = Logger.getLogger(CatalogClient.class.getName());

    private static final int CONNECT_TIMEOUT_MS = 3_000;
    private static final int READ_TIMEOUT_MS = 10_000;
    private static final String EXPECTED_VERSION = "BTCP/1";
    /** Cap on the lines of a multi-line reply: a defence against a hostile server. */
    private static final int MAX_LINES = 10_000;

    private final String host;
    private final int port;

    private Socket socket;
    private BufferedReader reader;
    private PrintWriter writer;

    public CatalogClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    /** The card of a material exactly as the protocol returns it. */
    public record ProtocolCard(String isbn, String title, String type, boolean available) {

        /** Turns a line "isbn|title|type|available" into a card. */
        static ProtocolCard fromLine(String line) throws BiblioTechException {
            // -1 in split keeps the trailing empty fields;
            // without it, "111|Title|BOOK|" would give 3 fields instead of 4.
            String[] fields = line.split("\\|", -1);
            if (fields.length != 4) {
                throw new BiblioTechException(
                        "Server reply with an invalid format: " + line);
            }
            return new ProtocolCard(fields[0], fields[1], fields[2],
                    Boolean.parseBoolean(fields[3]));
        }
    }

    // ---------------------------------------------------------------
    // Connection
    // ---------------------------------------------------------------

    /** Connects and verifies the server's greeting. */
    public void connect() throws BiblioTechException {
        try {
            socket = new Socket();

            // Interactive protocol of short messages: no Nagle.
            socket.setTcpNoDelay(true);

            // CONNECTION time limit: never the constructor that connects.
            socket.connect(new InetSocketAddress(host, port), CONNECT_TIMEOUT_MS);

            // READ time limit: different from the previous one, and just as mandatory.
            socket.setSoTimeout(READ_TIMEOUT_MS);

            // The stream stack of module 7, with an EXPLICIT charset in both directions.
            writer = new PrintWriter(
                    new BufferedWriter(
                            new OutputStreamWriter(socket.getOutputStream(),
                                    StandardCharsets.UTF_8)),
                    true);      // autoFlush: every println() is sent
            reader = new BufferedReader(
                    new InputStreamReader(socket.getInputStream(),
                            StandardCharsets.UTF_8));

            // The server speaks first: we check it is who we say
            // and that it speaks our version before sending anything.
            String greeting = reader.readLine();
            if (greeting == null) {
                throw new BiblioTechException(
                        "The server closed the connection without greeting");
            }
            if (!greeting.startsWith("200 ") || !greeting.contains(EXPECTED_VERSION)) {
                throw new BiblioTechException(
                        "Unexpected greeting from the server: '" + greeting + "'");
            }
            LOG.info(() -> "Connected to the catalogue " + host + ":" + port
                    + " (" + greeting + ")");

        } catch (IOException e) {
            closeSocketQuietly();
            throw NetworkErrorTranslator.translate(e, host + ":" + port);
        }
    }

    // ---------------------------------------------------------------
    // Protocol operations
    // ---------------------------------------------------------------

    /** QUERY <isbn>. Returns null if the material does not exist (404). */
    public ProtocolCard query(String isbn) throws BiblioTechException {
        validateArgument(isbn, "the ISBN");
        String reply = exchange("QUERY " + isbn);

        if (reply.startsWith("200 OK ")) {
            return ProtocolCard.fromLine(reply.substring("200 OK ".length()));
        }
        if (reply.startsWith("404 ")) {
            return null;        // "not found" is a result, not an error
        }
        throw protocolError(reply);
    }

    /** LIST. Returns every card in the catalogue. */
    public List<ProtocolCard> list() throws BiblioTechException {
        String header = exchange("LIST");

        if (!header.startsWith("201 LIST ")) {
            throw protocolError(header);
        }

        int announced = intOf(header.substring("201 LIST ".length()).strip(), header);
        if (announced < 0 || announced > MAX_LINES) {
            throw new BiblioTechException(
                    "The server announces a number of lines out of range: " + announced);
        }

        List<ProtocolCard> cards = new ArrayList<>(announced);
        try {
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.equals(".")) {
                    break;      // end of the multi-line reply
                }
                if (cards.size() >= MAX_LINES) {
                    throw new BiblioTechException(
                            "The server is sending more lines than allowed");
                }
                cards.add(ProtocolCard.fromLine(line));
            }
            if (line == null) {
                throw new BiblioTechException(
                        "The server closed the connection in the middle of the list");
            }
        } catch (SocketTimeoutException e) {
            throw new BiblioTechException(
                    "The server stopped replying in the middle of the list", e);
        } catch (IOException e) {
            throw NetworkErrorTranslator.translate(e, host + ":" + port);
        }

        // The announced number is redundant on purpose: if it does not match,
        // there is a problem and it is better to know than to ignore it.
        if (cards.size() != announced) {
            LOG.warning("The server announced " + announced + " lines and sent "
                    + cards.size());
        }
        return cards;
    }

    /** LEND <isbn> <employee>. Returns true if the loan was registered. */
    public boolean lend(String isbn, String employee) throws BiblioTechException {
        validateArgument(isbn, "the ISBN");
        validateArgument(employee, "the employee");

        String reply = exchange("LEND " + isbn + " " + employee);

        if (reply.startsWith("200 ")) {
            return true;
        }
        if (reply.startsWith("409 ") || reply.startsWith("404 ")) {
            LOG.info(() -> "Loan rejected: " + reply);
            return false;
        }
        throw protocolError(reply);
    }

    // ---------------------------------------------------------------
    // Core: send one line and read one reply
    // ---------------------------------------------------------------

    private String exchange(String request) throws BiblioTechException {
        if (socket == null || socket.isClosed()) {
            throw new BiblioTechException("The client is not connected");
        }
        try {
            // The protocol specifies \n, not the platform separator:
            // that is why we write it by hand instead of using println(String).
            writer.print(request + "\n");
            writer.flush();         // MANDATORY: print() does not trigger autoFlush

            // PrintWriter swallows IOException; checkError() is the only
            // way of finding out that the write failed.
            if (writer.checkError()) {
                throw new BiblioTechException(
                        "Failed to send the request to the catalogue server");
            }

            String reply = reader.readLine();
            if (reply == null) {
                throw new BiblioTechException(
                        "The server closed the connection without replying to: " + request);
            }
            LOG.fine(() -> "C: " + request + "  ->  S: " + reply);
            return reply;

        } catch (SocketTimeoutException e) {
            throw new BiblioTechException(
                    "The server did not reply within " + READ_TIMEOUT_MS + " ms", e);
        } catch (IOException e) {
            throw NetworkErrorTranslator.translate(e, host + ":" + port);
        }
    }

    // ---------------------------------------------------------------
    // Validation and utilities
    // ---------------------------------------------------------------

    /**
     * An argument with a line break breaks the protocol: it would inject
     * an extra request. Validating the OUTPUT is as important as
     * validating the input.
     */
    private void validateArgument(String value, String name) throws BiblioTechException {
        if (value == null || value.isBlank()) {
            throw new BiblioTechException("Missing " + name);
        }
        if (value.indexOf('\n') >= 0 || value.indexOf('\r') >= 0) {
            throw new BiblioTechException(
                    "Invalid value for " + name + ": it contains line breaks");
        }
    }

    private int intOf(String text, String context) throws BiblioTechException {
        try {
            return Integer.parseInt(text);
        } catch (NumberFormatException e) {
            throw new BiblioTechException(
                    "Invalid number in the server reply: '" + context + "'", e);
        }
    }

    private BiblioTechException protocolError(String reply) {
        return new BiblioTechException(
                "Unexpected reply from the catalogue server: " + reply);
    }

    private void closeSocketQuietly() {
        if (socket != null) {
            try {
                socket.close();
            } catch (IOException ignored) {
                // While closing there is nothing left to save.
            }
        }
    }

    // ---------------------------------------------------------------
    // Closing
    // ---------------------------------------------------------------

    @Override
    public void close() {
        if (socket == null || socket.isClosed()) {
            return;
        }
        try {
            // A polite farewell: the server can tell an orderly close
            // from a crash and release resources calmly.
            writer.print("QUIT\n");
            writer.flush();

            // We give it little leeway: if it does not answer, we close anyway.
            socket.setSoTimeout(1_000);
            String bye = reader.readLine();
            LOG.fine(() -> "Farewell from the server: " + bye);

        } catch (IOException e) {
            // While closing, a failure changes nothing: log it and carry on.
            LOG.log(Level.FINE, "Failure during the farewell", e);
        } finally {
            closeSocketQuietly();
            LOG.info(() -> "Connection with " + host + ":" + port + " closed");
        }
    }
}

Test program

package com.nexussoftware.bibliotech.presentation;

import com.nexussoftware.bibliotech.exception.BiblioTechException;
import com.nexussoftware.bibliotech.network.CatalogClient;
import com.nexussoftware.bibliotech.network.CatalogClient.ProtocolCard;

import java.util.List;

/** Manual test of the BTCP/1 client against a server. */
public class CatalogClientTest {

    public static void main(String[] args) {
        String host = args.length > 0 ? args[0] : "localhost";
        int port = args.length > 1 ? Integer.parseInt(args[1]) : 9090;

        // CatalogClient is AutoCloseable: try-with-resources as always.
        try (CatalogClient client = new CatalogClient(host, port)) {
            client.connect();

            System.out.println("--- QUERY for an existing ISBN ---");
            ProtocolCard card = client.query("978-0000000001");
            if (card == null) {
                System.out.println("Not found");
            } else {
                System.out.printf("%s - %s [%s] %s%n",
                        card.isbn(), card.title(), card.type(),
                        card.available() ? "available" : "on loan");
            }

            System.out.println();
            System.out.println("--- QUERY for a non-existent ISBN ---");
            System.out.println(client.query("000-0000000000") == null
                    ? "Not found (correct)" : "Unexpected");

            System.out.println();
            System.out.println("--- LIST ---");
            List<ProtocolCard> catalog = client.list();
            for (ProtocolCard c : catalog) {
                System.out.println("  " + c.isbn() + "  " + c.title());
            }
            System.out.println("Total: " + catalog.size());

            System.out.println();
            System.out.println("--- LEND ---");
            boolean ok = client.lend("978-0000000002", "Diego_Alonso");
            System.out.println(ok ? "Loan registered" : "Loan rejected");

        } catch (BiblioTechException e) {
            // Error boundary: here there are no sockets left, only the domain.
            System.err.println("ERROR: " + e.getMessage());
            if (e.getCause() != null) {
                System.err.println("Technical cause: " + e.getCause());
            }
        }
    }
}

How to test it with no server: nc as a manual server

The real server is written in 09-03. In the meantime, be the server yourself. This is the best way to understand a protocol, because you see the conversation letter by letter.

Terminal 1 — acts as the server on port 9090:

nc -l 9090

(In some versions of netcat you need nc -l -p 9090. If you do not have nc, Nmap's ncat or socat -v TCP-LISTEN:9090,reuseaddr - do the same job.)

Terminal 2 — launch the client:

javac -d classes $(find src -name "*.java")
java -cp classes com.nexussoftware.bibliotech.presentation.CatalogClientTest

Now, in terminal 1, you will see what the client sends appear and you will be able to type the replies by hand. The complete session is this (what you type is marked):

TERMINAL 1 (you, playing the server with nc -l 9090)
-----------------------------------------------------
200 BIBLIOTECH BTCP/1                                   <-- YOU TYPE (and Enter)
QUERY 978-0000000001                                        (sent by the client)
200 OK 978-0000000001|Effective Java|BOOK|true           <-- YOU TYPE
QUERY 000-0000000000
404 NOT FOUND 000-0000000000                            <-- YOU TYPE
LIST
201 LIST 3                                              <-- YOU TYPE
978-0000000001|Effective Java|BOOK|true                 <-- YOU TYPE
978-0000000002|Design Patterns|BOOK|false               <-- YOU TYPE
978-0000000003|Refactoring|BOOK|true                    <-- YOU TYPE
.                                                       <-- YOU TYPE
LEND 978-0000000002 Diego_Alonso
409 NOT AVAILABLE 978-0000000002                        <-- YOU TYPE
QUIT
221 BYE                                                 <-- YOU TYPE
TERMINAL 2 (the Java client)
-----------------------------------------------------
--- QUERY for an existing ISBN ---
978-0000000001 - Effective Java [BOOK] available

--- QUERY for a non-existent ISBN ---
Not found (correct)

--- LIST ---
  978-0000000001  Effective Java
  978-0000000002  Design Patterns
  978-0000000003  Refactoring
Total: 3

--- LEND ---
Loan rejected

Experiments worth doing right now, because they teach more than ten pages of theory:

  1. Do not type the greeting. The client will wait 10 seconds and fail with "The server did not reply". That is setSoTimeout doing its job: without it, it would wait forever.
  2. Type a wrong greeting, for example HELLO. The client rejects the connection with "Unexpected greeting". Checking the version before talking avoids absurd conversations with the wrong server.
  3. Close nc with Ctrl+C in the middle of the list. The client detects the null from readLine() and reports "The server closed the connection in the middle of the list", not a NullPointerException.
  4. Do not start nc at all. You get "The catalogue server is unavailable", which is the translation of ConnectException. Compare it with connecting to a non-existent IP on your network (192.168.1.250), which times out after 3 seconds: refused and no reply are different things, and now you can tell them apart.
  5. Type a title with accented characters, such as Naïve Set Theory. It arrives correctly because both ends use UTF-8. Try starting the JVM with -Dfile.encoding=ISO-8859-1 and you will see that it still arrives correctly, precisely because the charset is explicit in the code and does not depend on the platform. That is the reason for module 7's rule.

Common Mistakes and Tips

Not flushing the buffer after sending. The number-one bug, already demonstrated. Symptom: the program hangs with no error. Use autoFlush with println, or an explicit flush(). And remember that autoFlush does not act with print().

Using new Socket(host, port) in production. It accepts no connection time limit and can block for over a minute. Always use new Socket() + connect(addr, ms).

Confusing the connection time limit with the read one. They are two different things and you need both. connect(addr, 3000) does not protect you from an eternal read().

Not setting setSoTimeout. It turns any problem at the other end into a thread blocked forever. On a server with a bounded pool, a few zombie clients exhaust the pool and the service dies in silence, without a single line of error.

Trusting isConnected(). It returns true from the moment it connected, even if the other end has been switched off for hours. The only way to know whether it is still alive is to try to use the socket.

Not checking readLine() for null. It produces a NullPointerException with the first client that disconnects. The while ((line = reader.readLine()) != null) is mandatory.

Using the default charset. new InputStreamReader(socket.getInputStream()) with no charset works on your machine and corrupts accents as soon as the other end has a different configuration. Always StandardCharsets.UTF_8.

Assuming that a read() returns the complete message. TCP has no message boundaries. Either you use readLine(), which already solves it, or you implement a length prefix. Never assume that one send equals one read.

Reading with no size limit. A readLine() on a hundred-megabyte line exhausts memory, and a readInt() for the length followed by new byte[length] without validation is a four-byte denial of service. Always validate lengths that come from the network.

Declaring the streams in the try-with-resources alongside the socket. It closes in reverse order and the writer tries to flush onto an already-closed socket. Declare only the Socket.

Sending serialised objects over the network. readObject() builds objects before you can validate them. Send data, not objects.

Not validating what you write into the protocol. If the employee name carries a line break, it injects an extra request. Validating the output is as necessary as validating the input — it is the same kind of flaw as an SQL injection.

Golden tip for debugging. When something does not work, put yourself in the middle. nc -l 9090 plays the server and shows you exactly what your client sends, byte by byte. If your request does not show up there, you have not flushed it. If it shows up deformed, you have a charset or delimiter problem. Two minutes with nc save two hours of System.out.println.

Exercises

Exercise 1: Echo client with latency measurement

Write a class EchoClient that connects to an echo server (one that returns exactly what it receives), sends it N lines and measures the round-trip time of each one.

Requirements:

  • Connection time limit of 2 s and read time limit of 5 s.
  • setTcpNoDelay(true), and an alternative mode with false for comparison.
  • It must report minimum, average and maximum latency in milliseconds with two decimal places.
  • It must verify that what is received matches what was sent and count the mismatches.
  • Differentiated handling of ConnectException, SocketTimeoutException and the rest.

Test it against nc -l 9090 (you will have to echo by hand) or, better, against a one-line echo server: while true; do nc -l 9090 -c cat; done on Linux.

Exercise 2: Service detector

Write a class ServiceDetector with a method scan(String host, int from, int to, int timeoutMs) that checks which ports in a range have something listening, and tries to identify the service.

Requirements:

  • For each port, try to connect with a short time limit (200-500 ms).
  • Classify the result into three states: OPEN (connected), CLOSED (ConnectException) and FILTERED (timed out, typical of a firewall that drops).
  • For the open ones, try to read a greeting line for 500 ms; if the service greets (SMTP, FTP, SSH and BTCP do), show it.
  • Show the usual name of the service according to the well-known ports table of 09-01.
  • Print a table and a summary.

Test it against localhost with an nc -l 9090 running, and explain in a comment why scanning ports of machines that are not yours without permission is, besides discourteous, illegal in many countries.

Exercise 3: Binary transfer with shutdownOutput

Write a minimal client/server pair to transfer BiblioTech's catalog.csv file:

  • FileSender: connects, sends the file name and its size with DataOutputStream (length prefix, validated), then the content in 8 KB blocks, calls shutdownOutput() and waits for a summary line from the receiver.
  • FileReceiver: with a minimal ServerSocket (you may anticipate just enough of 09-03: new ServerSocket(9091) and accept()), reads the name and the size, validates that the size is reasonable (maximum 10 MB) and that the name contains neither / nor .. (path traversal), saves the content in received/ with NIO.2 and replies with a summary.

Requirements: an explicit charset wherever there is text, strict validation of everything that arrives, try-with-resources and logging with java.util.logging. Explain in a comment why the shutdownOutput() is indispensable.

Solutions

Solution 1

package com.nexussoftware.bibliotech.network;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;

/**
 * Echo client that measures round-trip latency.
 * Useful for comparing the effect of setTcpNoDelay and for measuring the network.
 */
public class EchoClient {

    private final String host;
    private final int port;
    private final boolean noNagle;

    public EchoClient(String host, int port, boolean noNagle) {
        this.host = host;
        this.port = port;
        this.noNagle = noNagle;
    }

    /** The result of one round of measurements. */
    public record Measurement(int sent, int mismatches,
                              double minMs, double avgMs, double maxMs) {
    }

    public Measurement measure(int repetitions) throws IOException {
        try (Socket socket = new Socket()) {

            // Options BEFORE using the socket.
            socket.setTcpNoDelay(noNagle);
            socket.connect(new InetSocketAddress(host, port), 2_000);
            socket.setSoTimeout(5_000);

            PrintWriter writer = new PrintWriter(
                    new BufferedWriter(
                            new OutputStreamWriter(socket.getOutputStream(),
                                    StandardCharsets.UTF_8)),
                    true);
            BufferedReader reader = new BufferedReader(
                    new InputStreamReader(socket.getInputStream(),
                            StandardCharsets.UTF_8));

            double min = Double.MAX_VALUE;
            double max = 0;
            double sum = 0;
            int mismatches = 0;
            int completed = 0;

            for (int i = 1; i <= repetitions; i++) {
                String message = "PING-" + i + "-BiblioTech";

                long t0 = System.nanoTime();
                writer.println(message);            // autoFlush: sent right away
                String echo = reader.readLine();    // we wait for the return
                long t1 = System.nanoTime();

                if (echo == null) {
                    System.out.println("The server closed after " + completed + " echoes");
                    break;
                }

                double ms = (t1 - t0) / 1_000_000.0;
                min = Math.min(min, ms);
                max = Math.max(max, ms);
                sum += ms;
                completed++;

                // The echo must be identical: if it is not, there is a problem
                // of charset, of delimiter or of protocol desynchronisation.
                if (!message.equals(echo)) {
                    mismatches++;
                    System.out.println("  MISMATCH: I sent '" + message
                            + "' and received '" + echo + "'");
                }
                System.out.printf("  %2d) %.2f ms%n", i, ms);
            }

            if (completed == 0) {
                return new Measurement(0, 0, 0, 0, 0);
            }
            return new Measurement(completed, mismatches, min, sum / completed, max);
        }
    }

    public static void main(String[] args) {
        String host = args.length > 0 ? args[0] : "localhost";
        int port = args.length > 1 ? Integer.parseInt(args[1]) : 9090;

        for (boolean noNagle : new boolean[]{true, false}) {
            System.out.println("=== setTcpNoDelay(" + noNagle + ") ===");
            try {
                Measurement m = new EchoClient(host, port, noNagle).measure(10);
                System.out.printf(
                        "  Echoes: %d   Mismatches: %d%n"
                        + "  min %.2f ms   avg %.2f ms   max %.2f ms%n%n",
                        m.sent(), m.mismatches(), m.minMs(), m.avgMs(), m.maxMs());

            } catch (ConnectException e) {
                // Nobody is listening: there is no point in retrying.
                System.err.println("  There is no echo server on " + host + ":" + port);
                System.err.println("  Start it with:  while true; do nc -l "
                        + port + " -c cat; done");
                return;

            } catch (SocketTimeoutException e) {
                // Transient: the machine does not reply in time.
                System.err.println("  Timed out talking to " + host);

            } catch (IOException e) {
                System.err.println("  Network failure: " + e);
            }
        }
    }
}

Output on localhost against a real echo server:

=== setTcpNoDelay(true) ===
   1) 1.84 ms
   2) 0.21 ms
   ...
  Echoes: 10   Mismatches: 0
  min 0.14 ms   avg 0.31 ms   max 1.84 ms

Comments. Three observations. The first measurement is always the slowest (1.84 ms against 0.2 ms): it is the cost of the JVM loading the classes and of the first send travelling paths that are not yet warm; any network measurement must discard the first iterations or at least not take them as representative. The second: on localhost the difference between setTcpNoDelay(true) and false is practically nil, because loopback does not apply Nagle the way a real link does; to see Nagle's effect you need two machines, and there the differences can be 40 ms per message. And the third: checking that the echo matches what was sent is not paranoia; it is the way to detect that the protocol has gone out of sync, which is the hardest failure to diagnose when it appears.

Solution 2

package com.nexussoftware.bibliotech.network;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * Service detector: checks which ports in a range have something listening.
 *
 * LEGAL AND ETHICAL WARNING: scanning ports of machines that are not yours
 * without written authorisation is, in many countries, an offence of
 * unauthorised access to computer systems, and in practically all of them a
 * reason to be blocked by your provider. This tool is intended ONLY for
 * localhost and for machines belonging to Nexus Software with authorisation
 * from the systems department. A scan generates traffic and alerts in any modern IDS.
 */
public class ServiceDetector {

    /** The state of a port. An enum like those of 04-07. */
    public enum State {
        OPEN("there is a service listening"),
        CLOSED("the machine replies RST: nobody is on that port"),
        FILTERED("no reply: a firewall dropping, or a machine down");

        private final String description;

        State(String description) {
            this.description = description;
        }

        public String description() {
            return description;
        }
    }

    private static final Map<Integer, String> SERVICES = new HashMap<>();

    static {
        SERVICES.put(21, "FTP");
        SERVICES.put(22, "SSH");
        SERVICES.put(25, "SMTP");
        SERVICES.put(53, "DNS");
        SERVICES.put(80, "HTTP");
        SERVICES.put(443, "HTTPS");
        SERVICES.put(3306, "MySQL");
        SERVICES.put(5432, "PostgreSQL");
        SERVICES.put(6379, "Redis");
        SERVICES.put(8080, "Alternative HTTP");
        SERVICES.put(9090, "BiblioTech BTCP/1");
        SERVICES.put(9091, "BiblioTech discovery");
    }

    public void scan(String host, int from, int to, int timeoutMs) {
        System.out.printf("Scanning %s ports %d-%d (timeout %d ms)%n%n",
                host, from, to, timeoutMs);
        System.out.printf("%-8s %-10s %-24s %s%n", "PORT", "STATE", "SERVICE", "GREETING");
        System.out.println("-".repeat(90));

        int open = 0, closed = 0, filtered = 0;

        for (int port = from; port <= to; port++) {
            State state = probe(host, port, timeoutMs);

            switch (state) {
                case OPEN -> open++;
                case CLOSED -> closed++;
                case FILTERED -> filtered++;
            }

            // We only show the interesting ones: a table with 65535 CLOSED
            // helps nobody.
            if (state == State.OPEN) {
                String greeting = readGreeting(host, port, 500);
                System.out.printf("%-8d %-10s %-24s %s%n",
                        port, state,
                        SERVICES.getOrDefault(port, "(unknown)"),
                        greeting == null ? "(no greeting)" : greeting);
            } else if (state == State.FILTERED) {
                System.out.printf("%-8d %-10s %-24s %s%n",
                        port, state,
                        SERVICES.getOrDefault(port, "(unknown)"), "");
            }
        }

        System.out.println("-".repeat(90));
        System.out.printf("Open: %d   Closed: %d   Filtered: %d%n",
                open, closed, filtered);
        for (State s : State.values()) {
            System.out.printf("  %-10s %s%n", s, s.description());
        }
    }

    /** Tries to connect and classifies the result by the TYPE of exception. */
    private State probe(String host, int port, int timeoutMs) {
        try (Socket socket = new Socket()) {
            socket.connect(new InetSocketAddress(host, port), timeoutMs);
            return State.OPEN;

        } catch (SocketTimeoutException e) {
            // Nobody replied to the SYN: usually a firewall with a DROP policy.
            return State.FILTERED;

        } catch (ConnectException e) {
            // The machine replied RST: it is alive, but that port has no service.
            // THIS IS VALUABLE INFORMATION: it tells "machine down" from "port closed".
            return State.CLOSED;

        } catch (IOException e) {
            // NoRouteToHost and the like.
            return State.FILTERED;
        }
    }

    /**
     * Many protocols greet on connect (SMTP, FTP, SSH, BTCP/1).
     * A timeout here is NOT a failure: it means the service expects
     * you to speak first (like HTTP).
     */
    private String readGreeting(String host, int port, int timeoutMs) {
        try (Socket socket = new Socket()) {
            socket.connect(new InetSocketAddress(host, port), timeoutMs);
            socket.setSoTimeout(timeoutMs);

            BufferedReader reader = new BufferedReader(
                    new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
            String line = reader.readLine();

            if (line == null) {
                return null;
            }
            // We truncate: a huge greeting does not fit in the table, and besides
            // we do not want to dump arbitrary network data onto the console.
            return line.length() > 45 ? line.substring(0, 45) + "..." : line;

        } catch (IOException e) {
            return null;
        }
    }

    public static void main(String[] args) {
        ServiceDetector detector = new ServiceDetector();
        // ONLY localhost by default: scanning another machine requires authorisation.
        detector.scan("localhost", 9080, 9100, 300);
    }
}

Output with an nc -l 9090 running:

Scanning localhost ports 9080-9100 (timeout 300 ms)

PORT     STATE      SERVICE                  GREETING
------------------------------------------------------------------------------------------
9090     OPEN       BiblioTech BTCP/1        (no greeting)
------------------------------------------------------------------------------------------
Open: 1   Closed: 20   Filtered: 0

Comments. The teaching value of this exercise lies in the classification into three states, which is exactly what nmap does. ConnectException and SocketTimeoutException mean very different things and confusing them is the most common diagnostic mistake: "refused" proves the machine is alive and that the packet arrived and came back; "timed out" proves nothing, because it may be a machine switched off, a broken route or a firewall with a silent-drop policy. Notice too that nc -l does not greet —it expects you to speak— and that is why readGreeting returns null without that being an error: a timeout while reading the greeting is information, not a failure. And observe the detail of truncating the greeting to 45 characters: never dump data coming from the network onto the console without a limit, because it may contain terminal escape sequences.

Solution 3

package com.nexussoftware.bibliotech.network;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Logger;

/**
 * Sender of a catalogue file over TCP.
 *
 * Protocol (binary, with length prefix):
 *   [int  ] length of the name in UTF-8 bytes
 *   [bytes] name
 *   [long ] size of the content in bytes
 *   [bytes] content
 *   -- shutdownOutput --
 *   [line ] summary from the receiver, as UTF-8 text
 */
public class FileSender {

    private static final Logger LOG = Logger.getLogger(FileSender.class.getName());
    private static final int BLOCK = 8192;

    public void send(String host, int port, Path file) throws IOException {
        if (!Files.isRegularFile(file)) {
            throw new IOException("Not a file: " + file);
        }
        long size = Files.size(file);

        try (Socket socket = new Socket()) {
            socket.connect(new InetSocketAddress(host, port), 3_000);
            socket.setSoTimeout(30_000);

            DataOutputStream output = new DataOutputStream(
                    new BufferedOutputStream(socket.getOutputStream()));

            // --- Header: name with a length prefix ---
            // We send ONLY the file name, never the full path:
            // the receiver decides where to store it.
            byte[] name = file.getFileName().toString()
                    .getBytes(StandardCharsets.UTF_8);
            output.writeInt(name.length);
            output.write(name);
            output.writeLong(size);

            // --- Content in blocks ---
            long sent = 0;
            try (InputStream fileInput =
                         new BufferedInputStream(Files.newInputStream(file))) {
                byte[] buffer = new byte[BLOCK];
                int read;
                while ((read = fileInput.read(buffer)) != -1) {
                    // read may return LESS than BLOCK: we must write
                    // exactly 'read' bytes, never buffer.length.
                    output.write(buffer, 0, read);
                    sent += read;
                }
            }
            output.flush();     // the usual flush, before closing the direction

            LOG.info(() -> "Sent " + size + " bytes of " + file.getFileName());

            // --- shutdownOutput: INDISPENSABLE ---
            // The receiver reads until the stream is exhausted. Without this
            // half-close, the receiver would go on waiting for more bytes
            // indefinitely and we would wait for its summary: a perfect deadlock.
            // We cannot use socket.close() because we still have to READ.
            socket.shutdownOutput();

            // --- Summary from the receiver (the read direction is still open) ---
            BufferedReader reader = new BufferedReader(
                    new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
            String summary = reader.readLine();
            System.out.println("Receiver: " + (summary == null ? "(no reply)" : summary));

            if (sent != size) {
                LOG.warning("The file changed size during the send");
            }
        }
    }

    public static void main(String[] args) throws IOException {
        Path file = Path.of(args.length > 0 ? args[0] : "catalog.csv");
        new FileSender().send("localhost", 9091, file);
    }
}
package com.nexussoftware.bibliotech.network;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Receiver of the file. A minimal single-connection server:
 * the concurrent, complete version is written in 09-03.
 */
public class FileReceiver {

    private static final Logger LOG = Logger.getLogger(FileReceiver.class.getName());

    private static final int MAX_NAME = 255;
    private static final long MAX_SIZE = 10L * 1024 * 1024;        // 10 MB
    private static final Path TARGET_DIR = Path.of("received");

    public void listen(int port) throws IOException {
        Files.createDirectories(TARGET_DIR);

        try (ServerSocket server = new ServerSocket(port)) {
            LOG.info(() -> "Receiver listening on port " + port);

            // accept() blocks until a connection arrives and returns
            // an ALREADY connected Socket. All the detail, in 09-03.
            try (Socket socket = server.accept()) {
                socket.setSoTimeout(30_000);
                LOG.info(() -> "Connection from " + socket.getRemoteSocketAddress());
                serve(socket);
            }
        }
    }

    private void serve(Socket socket) throws IOException {
        DataInputStream input = new DataInputStream(socket.getInputStream());

        String summary;
        try {
            // --- Name, with a VALIDATED length prefix ---
            int nameLength = input.readInt();
            if (nameLength <= 0 || nameLength > MAX_NAME) {
                throw new IOException("Invalid name length: " + nameLength);
            }
            byte[] nameBytes = input.readNBytes(nameLength);
            if (nameBytes.length < nameLength) {
                throw new EOFException("Stream cut while reading the name");
            }
            String name = new String(nameBytes, StandardCharsets.UTF_8);

            // --- Sanitising the name: PATH TRAVERSAL ---
            // Without this, a malicious sender sends "../../etc/cron.d/backdoor"
            // and we write wherever they like. It is a classic vulnerability.
            if (name.contains("/") || name.contains("\\")
                    || name.contains("..") || name.isBlank()) {
                throw new IOException("File name not allowed: " + name);
            }

            // --- Size, also validated ---
            long size = input.readLong();
            if (size < 0 || size > MAX_SIZE) {
                throw new IOException("Size out of range: " + size);
            }

            // --- Content ---
            Path target = TARGET_DIR.resolve(name).normalize();
            // Belt and braces: we check that the result is still inside
            // the intended directory, in case something slipped past earlier.
            if (!target.startsWith(TARGET_DIR)) {
                throw new IOException("Resulting path outside the directory: " + target);
            }

            long received = 0;
            try (OutputStream fileOutput =
                         new BufferedOutputStream(Files.newOutputStream(target))) {
                byte[] buffer = new byte[8192];
                int read;
                // We read until end of stream: it is the sender's shutdownOutput()
                // that produces this -1. Without it, this loop would never end.
                while ((read = input.read(buffer)) != -1) {
                    if (received + read > size) {
                        throw new IOException(
                                "The sender is sending more bytes than announced");
                    }
                    fileOutput.write(buffer, 0, read);
                    received += read;
                }
            }

            if (received != size) {
                throw new EOFException(size
                        + " bytes were announced and " + received + " arrived");
            }

            long finalReceived = received;
            LOG.info(() -> "Saved " + target + " (" + finalReceived + " bytes)");
            summary = "200 OK " + name + " " + received + " bytes";

        } catch (IOException e) {
            LOG.log(Level.WARNING, "Transfer rejected", e);
            summary = "400 REJECTED " + e.getMessage();
        }

        // --- Text reply, with an explicit charset ---
        BufferedWriter writer = new BufferedWriter(
                new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
        writer.write(summary);
        writer.write('\n');
        writer.flush();         // without this, the sender would wait forever
    }

    public static void main(String[] args) throws IOException {
        new FileReceiver().listen(9091);
    }
}

Test in two terminals:

# Terminal 1
java -cp classes com.nexussoftware.bibliotech.network.FileReceiver

# Terminal 2
java -cp classes com.nexussoftware.bibliotech.network.FileSender catalog.csv
Terminal 1:
INFO: Receiver listening on port 9091
INFO: Connection from /127.0.0.1:52104
INFO: Saved received/catalog.csv (2847 bytes)

Terminal 2:
INFO: Sent 2847 bytes of catalog.csv
Receiver: 200 OK catalog.csv 2847 bytes

Comments. Four points worth keeping.

The shutdownOutput() is the heart of the exercise. The receiver reads with while ((read = input.read(buffer)) != -1), and that -1 only appears when the sender closes its write direction. If the sender did socket.close() instead, the -1 would arrive too, but the sender would also have closed its read direction and could never receive the summary. A half-close is exactly the tool for "I have finished talking, but I am still listening".

The validation of the name is not decoration. Files.newOutputStream(TARGET_DIR.resolve("../../../etc/passwd")) writes wherever the attacker wants if the process has permission. It is checked twice: rejecting the dangerous characters and then verifying with startsWith that the normalised path is still inside the intended directory. In security, redundancy is correct.

The validation of the size closes the other hole: without it, a sender announcing 500 GB fills the disk. And notice that it is checked twice: the limit before starting, and during the loop that no more bytes arrive than were announced. A sender can lie in the header.

And the read(buffer) that may return less than requested appears on both sides: the sender writes output.write(buffer, 0, read) and never output.write(buffer). Writing the whole buffer when only 300 bytes have been read sends 7892 bytes of rubbish. It is the same mistake as in module 7, and on a network it hurts more because the file arrives corrupt and you do not find out until somebody opens it.

Conclusion

You have opened your first connection from Java, and with it you have discovered what the previous lesson promised: once connected, the network is the I/O of module 7. getInputStream() and getOutputStream() hand you the same streams, with the same decorators, the same InputStreamReader with an explicit charset and the same try-with-resources. What is new is not reading and writing: it is everything surrounding that reading and writing.

You know how to connect properly: never with the new Socket(host, port) constructor, which can block for over a minute with no time limit, but with new Socket() followed by connect(new InetSocketAddress(host, port), ms). And you are clear that there are two different time limits and you need both: the connection one, which you set in connect, and the read one, which you set with setSoTimeout and which is what stops a zombie client leaving you with a thread blocked forever. You also know that SocketTimeoutException is the only network exception that does not invalidate the socket, and that this enables the polling-with-cancellation-check pattern that makes a blocking socket interruptible.

You have seen demonstrated the number-one bug of beginners: writing into a BufferedWriter and sitting there waiting for a reply that never comes, because your twenty-one characters are still in a buffer of eight thousand and have never reached the network. No exception, no trace, nothing. And you know the three ways of avoiding it, with the nuance that saves lives: autoFlush only acts with println, printf and format, never with print.

You understand why readLine() exists and what problem it solves: TCP is a byte stream with no message boundaries, and what you write in two sends may be read in one, in two or in seven. BufferedReader accumulates up to the delimiter and hands you complete messages. With the two warnings that go with it: it returns null when the other end closes —and not checking that is a guaranteed NullPointerException—, and it has no length limit, which makes it a denial-of-service vector if you do not bound it. And you know when the delimiter does not work and you have to move to a length prefix, always validating the declared length before allocating memory with it.

You know how to close properly: only the Socket in the try-with-resources, because closing any of its streams closes it; and you know the half-close, with shutdownOutput() to say "I have finished talking, but I am still listening", which is the only way of handling protocols that end the send with the end of the stream.

You can handle the socket options, and above all the two that matter: setSoTimeout, already discussed, and setTcpNoDelay(true) to disable Nagle's algorithm in interactive protocols of short messages, where the combination of Nagle with delayed acknowledgement produces artificial delays of up to two hundred milliseconds. And you know which ones not to touch: the buffer sizes and, above all, setSoLinger.

You can tell each network exception and what it means apart: UnknownHostException is configuration, ConnectException is that you reached the machine but nobody is on that port, SocketTimeoutException is that nobody replied, SocketException: Connection reset is that the other end went down, and an orderly close throws no exception at all: it shows up as -1 or null. With the rule that orders the catch: ConnectException and BindException are subclasses of SocketException and must be checked first. And you know how to translate all of that into BiblioTechException at the boundary, classifying between transient —worth a retry— and permanent —retrying only burns CPU—, exactly as you learned in 06-07.

You know how to send binary data with DataOutputStream/DataInputStream, which also write in big-endian and therefore interoperate; and you know why you must never send serialised objects to an untrusted client: readObject() builds arbitrary objects before your cast can say a word, and that is a route to remote code execution. The rule is to send data, not objects.

And BiblioTech has gained three classes: CatalogClient, which speaks full BTCP/1 —connects with a time limit, verifies the greeting and the version, queries, lists, lends, validates its own arguments against line-break injection, bounds the server's replies and says goodbye with QUIT in its close()—; NetworkErrorTranslator, the boundary that turns each network exception into a domain message with its transience classification; and NetworkDiagnostics from the previous lesson. You have tested it against nc -l 9090 playing the server by hand, which is the best way there is to understand a protocol, and you have checked live what happens when the server does not greet, when it greets badly, when it closes halfway and when it is not there.

But your client still has nobody real to talk to. So far the server was you, typing replies into a terminal.

In the next lesson, ServerSocket, you will write the other end. You will see ServerSocket and its bind to a port, the queue of pending connections, and accept() as the blocking method that hands you an already-connected Socket. You will start with a sequential server, check its limit live —the second client waits for the first to finish— and solve it with module 8's ExecutorService: a bounded pool, a ThreadFactory with names for the logs, a two-phase shutdown and closing the ServerSocket to unblock the accept. That is where all of module 8 really pays off, and where you will understand why the ConcurrentCatalog with its ConcurrentHashMap was already prepared for this without knowing it. By the end, BiblioTech's catalogue server will accept Marta, Diego and Nuria at the same time, will validate everything arriving over the network, will reply with codes, will evict idle clients and will shut down in an orderly way — and you will test it with telnet localhost 9090.

Java Programming Course

Module 1: Introduction to Java

Module 2: Control Flow

Module 3: Object-Oriented Programming

Module 4: Advanced Object-Oriented Programming

Module 5: Data Structures and Collections

Module 6: Exception Handling

Module 7: File Input/Output

Module 8: Multithreading and Concurrency

Module 9: Networking

Module 10: Advanced Topics

Module 11: Java Frameworks and Libraries

Module 12: Building Real-World Applications

© Copyright 2026. All rights reserved