skip to content

In the TCP socket API, what do socket, bind, listen, accept and connect each do, and where does the three-way handshake actually happen?

level: middleimportance: must knowfreq 60%

answer

  1. passive open versus active open
  2. listen puts it in LISTEN
  3. handshake runs without the application
  4. accept dequeues a finished connection
  5. backlog is not in the standard

basics

~20 s

socket creates an endpoint, bind names its local address and port, listen makes it a passive open in LISTEN, and connect sends the SYN. The stack completes the handshake and queues it; accept hands it over as a new socket.

solid answer

~40 s

`socket()` creates an unbound endpoint; `bind()` attaches a local IP address and port (the wildcard address means any local address); `listen()` makes it a **passive open** in RFC 9293's terms, so it sits in LISTEN. A client's `connect()` is the **active open**: its stack picks an ephemeral port if none was bound and sends the SYN. The server's stack answers with SYN-ACK and, when the final ACK arrives, holds an ESTABLISHED connection in a queue — no application code runs during the handshake. `accept()` removes one completed connection from that queue and returns a **new** socket for it, while the listening socket stays in LISTEN. The queue length is suggested by `listen`'s backlog argument; RFC 4987 notes the backlog is not described in the standards, so behaviour when it is full varies by stack.

go deeper

for a junior

Recall the order on each side — socket, bind, listen, accept for the server; socket, connect for the client — and that accept returns a new socket per client.

for a middle

Explain passive versus active open and place the handshake correctly: the stack completes it without the application, and accept only dequeues finished connections.

for a senior

Show you can read a symptom from the queue: clients that connect but wait for a first response point at a slow accept loop or a full backlog, whose overflow behaviour varies by stack.

for a principal

Treat the accept queue as a buffer between network arrival and application capacity: size it deliberately, and decide whether overload should look like slow connects or fast refusals.

## Two ways to open a connection RFC 9293 describes an abstract user interface whose `OPEN` call is either **passive** or **active**: - A **passive open** waits for someone to connect. With an unspecified remote socket it waits "for any call"; the connection record sits in **LISTEN**. - An **active open** starts the handshake "at once" by sending a SYN toward a specified remote socket. The Berkeley/POSIX socket API splits these into separate calls. `listen` is the passive open; `connect` is the active open. `socket`, `bind` and `accept` are the API's own plumbing around them. ## The server side, call by call 1. **`socket()`** creates an endpoint handle. Nothing is bound and nothing is sent. 2. **`bind(address, port)`** attaches a local socket. The wildcard address (`0.0.0.0` or `::`) means "any local address"; RFC 9293 says such a passive open "will await an incoming connection request to any local IP address" and then binds the connection to whichever address was used. 3. **`listen(backlog)`** turns the endpoint into a passive open. The record enters LISTEN. **Nothing is transmitted** — clients learn nothing until they send a SYN. 4. **`accept()`** takes one *already established* connection off the listener's queue and returns a **new handle** for it. The listener stays in LISTEN and can be accepted from again. 5. **`close()`** on the connection handle starts that connection's teardown; closing the listener stops new connections without touching accepted ones. ## The client side 1. **`socket()`** creates the handle. 2. **`bind()`** is optional. Without it, the stack chooses the local address from the route toward the destination and an ephemeral port that keeps the 5-tuple unique. 3. **`connect(server address, port)`** performs the active open: the SYN goes out, and a blocking `connect` returns once the handshake completes or fails. ```pseudocode // server ls = socket(TCP) bind(ls, 0.0.0.0, 8080) listen(ls, backlog = 128) // ls enters LISTEN; nothing is sent loop: c = accept(ls) // waits for a completed connection serve(c) // c: local 8080, remote = client's socket close(c) // ls stays in LISTEN throughout // client s = socket(TCP) connect(s, 192.0.2.10, 8080) // ephemeral port chosen, SYN sent ``` ## Where the handshake happens | Event | Who acts | Application involved? | |---|---|---| | SYN sent | client stack, triggered by `connect` | client is inside `connect` | | SYN arrives at LISTEN, SYN-ACK sent | server stack | **no** | | Final ACK arrives, connection ESTABLISHED | server stack | **no** | | Connection handed to the program | `accept` | yes | The server's stack completes the handshake on its own. RFC 7413 (Appendix A.2) states the traditional rule directly: "accept() returns only after a socket is connected". Its one exception is TCP Fast Open, where `accept` can return as soon as a SYN carrying a valid cookie and data arrives. A consequence worth saying in an interview: a client's `connect` can **succeed** while the server program is busy and has not called `accept` yet. The client may even send its request; the bytes wait in the server's receive buffer until the program accepts and reads. ## The accept queue and the backlog Between the handshake and `accept`, connections wait in the listener's queue. Many stacks keep two stages: connections still in SYN-RECEIVED, and completed connections waiting for `accept`. The `backlog` argument to `listen` suggests a size, and the stack may adjust it. - RFC 4987 (Section 2.2) is explicit that "the concept of using a backlog is not described in the standards documents". - When the limit is reached, "either incoming SYN segments are ignored, or uncompleted connections in the backlog are replaced", and the RFC adds that some stacks might generate RSTs instead. The behaviour differs between stacks. - If SYNs are ignored, clients retransmit their SYN and see a slow connect; if RSTs are sent, they see an immediate failure. - The backlog bounds **waiting** connections, not the total the server can hold once accepted. A slow accept loop therefore usually shows up at the client as latency before the first response rather than as an error. ## Common mistakes - Believing `accept` sends the SYN-ACK. It does not; the stack already did. - Believing `listen` announces the port to the network. - Treating the backlog as a maximum connection count. - Expecting `accept` to return the listening handle itself. - Thinking a client must `bind` first.

  • Why can bind() on a restarted TCP server's port fail while its previous connections sit in TIME-WAIT, and does TCP require that refusal?
    The old connections still exist as TIME-WAIT records that use the local port, and typical socket implementations make `bind` refuse a port that such records still use unless `SO_REUSEADDR` is set. TCP itself only protects the exact old 5-tuples for 2×MSL; a new listener's connections will be new tuples, and RFC 9293 even allows (MAY-2) a TIME-WAIT record to accept a fresh SYN from the same peer under conditions. So the refusal is API policy, not a protocol rule.
  • Why does accept() return a new socket instead of turning the listening socket into the connection?
    Because the listener must keep accepting. The listening socket is a passive open with an unspecified remote socket; each accepted connection is fully specified — both addresses and both ports — with its own sequence numbers, windows and buffers. Keeping them as separate handles lets the program keep calling `accept` on the listener while other code serves the connections.

saying these in an interview costs you the question

  • accept() performs the three-way handshake with the client.
  • listen() sends a message announcing the port to the network.
  • The listen backlog is the maximum number of connections the server can hold.
  • accept() converts the listening socket into the client connection.
  • A TCP client must call bind() before it can call connect().