A session as a finite-state machine
◈ 4 cardsStates, events, transitions and actions used precisely: name a state for what the server is waiting for, allow exactly one terminal state, and model a PoD session as CONNECTED → LISTED → SENT or REJECTED → CLOSED.
Four words, used precisely
An FSM description of a session uses four words, and Assignment 3 is marked partly on whether you use them correctly.
- A state is a situation the connection can be in. Nothing more — not a variable, not a step of your code. A situation.
- An event is something that arrives: a connection is accepted, a line of input is read, the client disconnects, a timer expires. Events are inputs. You do not choose them; the world hands them to you.
- An action is something the server does in response: write a line, close the socket, append a log record. Actions are outputs. You choose them entirely.
- A transition binds the two: in state S, when event E occurs, perform action A and move to state S'.
Swapping event and action is the commonest error in a submitted specification, and it is easy to spot from the outside: if a box in your diagram reads 'send the poem', you have drawn an action where a state belongs. Actions belong on the arrows.
A state is what the server is waiting for
Here is the naming rule that makes a session FSM fall out almost mechanically: name each state for what the server is waiting for while it sits in it. The server is never 'sending the poem' for any length of time — sending is instantaneous relative to the session. What takes time is waiting: waiting for a connection, waiting for the client to type a choice, waiting for the socket to drain before it closes.
Apply the rule and the state set writes itself. A one-shot request/response protocol has one real waiting state — the one between the prompt and the reply. That is why PoD is small enough to specify completely in an afternoon, and why a marker can check it exhaustively.
Exactly one terminal state
A session FSM needs one initial state (entered when accept() returns) and exactly one terminal state — the state in which the socket is closed and the handler returns. One, not several: if two different places in your code close the socket, you have two definitions of 'done' and they will drift. Make CLOSED the single exit and route everything through it, and the loop condition in your handler becomes literally while (state != CLOSED).
The reachability check is worth doing on paper: from the initial state, can you get to the terminal state from every other state? If some state has no outgoing path to CLOSED, you have specified a place where the connection wedges.
Worked example — the Poem-of-the-Day session
The assignment fixes the interaction: connect, receive a welcome plus a list of poems plus instructions, send one selection, receive the poem or an error, session over. Model it.
- CONNECTED —
accept()has returned; nothing has been written yet. The server is waiting on nothing, so it acts immediately: greet, list the poems, prompt. Transition toLISTED. - LISTED — the listing and the prompt are on the wire and flushed. The server is waiting for exactly one line: the choice. This is the only state where a real wait happens, and therefore the only state that needs a rich error catalogue.
- SENT — a valid choice arrived and the poem has been written.
- REJECTED — an invalid choice arrived and the error line has been written.
- CLOSED — terminal. The socket is closed, the thread is returned to the pool.
Five states, five transitions, one terminal. SENT and REJECTED are distinct even though both go straight to CLOSED, because they log differently and because a future version might add a retry from one and not the other — keeping them apart costs nothing and states the intent.