accept returned a different number
You create a socket. It is fd 3. You bind it, you listen
on it, you call accept. A client connects. accept returns
4. You now have two sockets, and if you read from fd 3 you get an error.
The data is on fd 4. Where did fd 4 come from, and what is fd 3 still for?
The answer is that TCP has two different kinds of socket, and accept
is the moment one of them manufactures the other. To see why, you need to see
what the kernel does in the gap between connect on one machine
and accept returning on the other.
Two kinds of socket
A listening socket is bound to a local address and port and does one job:
it waits for handshakes. It has no peer. You cannot send on it.
Its kernel object holds a queue of connections that have finished the handshake
but have not yet been handed to your program.
A connected socket is the connection itself. It is identified by a 5-tuple: protocol, local IP, local port, remote IP, remote port. It has its own send buffer, its own receive buffer, its own sequence numbers. Every client that connects to your server gets its own connected socket, and therefore its own fd. The listening socket stays where it is and keeps listening.
accept is how you pick one up. A server with 1000 clients has
1001 sockets: one listener and 1000 connections.
Three packets
When the client calls connect, its kernel sends a segment with the
SYN flag set and an initial sequence number, say 1000.
"I want to talk. My bytes will start counting from 1000."
The server kernel, if something is listening on that port, replies with a segment
that has both SYN and ACK set: its own initial sequence
number, say 5000, and an acknowledgement number of 1001.
"I heard your 1000. My bytes start at 5000." The server program is not
involved. It is asleep inside accept, or busy doing something else.
The kernel answers on its own.
The client kernel sends an ACK with acknowledgement 5001.
Now both sides know the other side can hear them, and both know where the other's
byte count starts. The connection is ESTABLISHED.
connect returns 0 on the client. On the server, the connection is placed
in the listening socket's queue, and the next accept returns a new fd for it.
The sequence numbers are the foundation of everything TCP promises. Every byte
sent has a number. The receiver acknowledges the next number it expects.
If a segment is lost, its ACK never comes, and the sender's timer fires and
retransmits. If segments arrive out of order, the receiver puts them back in
order by number. You will never see any of this from your program. It is all in
the kernel, and it is why recv on a TCP socket gives you bytes in the
order they were sent.
The server side
Read the loop carefully. accept is called on lfd and returns
cfd. Data moves on cfd. cfd is closed.
lfd is never closed; it lives for the life of the server.
Each trip around the loop is one client.
TIME_WAIT
for a minute or so to catch late packets. By default, bind refuses the port
while any of those linger. SO_REUSEADDR tells the kernel you know what you
are doing. Set it on every server socket before bind.
The client side
The client never calls bind. connect picks an ephemeral local port,
performs the handshake, and returns when the connection is established. The same fd
is used for the handshake and for data: a client socket goes straight from
"nothing" to "connected".
When nobody is home
Send a SYN to a port with no listening socket and the server kernel replies with
a segment whose RST flag is set. Reset: there is no such connection
and there never will be. The client's connect returns -1 with
errno == ECONNREFUSED. No program on the server ran. The last step of the
visualization shows it.
If the SYN gets no reply at all, which is what a firewall that silently drops
packets looks like, the client kernel retransmits the SYN a few times with
growing delays and eventually gives up with ETIMEDOUT.
On Linux that takes about two minutes. "Connection refused" is fast and honest.
"Connection timed out" means something ate your packets.
recv(lfd, ...) returns -1 with ENOTCONN. The listening socket has
no peer and no receive buffer for data. If your server "receives nothing",
check which fd you passed.
Closing is also a handshake
close on a connected socket sends a FIN: "I have no more bytes
to send." The peer ACKs it. The peer can keep sending; the connection is now
half-closed. When the peer closes too, it sends its own FIN and gets an ACK.
Four segments to tear down what three built. The side that closed first sits in
TIME_WAIT for a while in case a stray segment turns up late.
If you want the half-close without giving up the fd, shutdown(fd, SHUT_WR)
sends the FIN but lets you keep reading. That is how a client says "here is my whole
request" and then waits for the whole response.
accept needs a matching close.
Each connected socket is an fd, a kernel object, and two buffers.
Forget to close them and a long-running server leaks fds until accept
fails with EMFILE, the same leak you met in the file-descriptors lesson,
wearing a network costume.
Three packets between two kernels create a connection; accept hands it to you as a new fd, and the listening socket never carries a byte.